ms-components 0.3.4__tar.gz → 0.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ms_components-0.3.4/src/ms_components.egg-info → ms_components-0.3.5}/PKG-INFO +1 -1
- {ms_components-0.3.4 → ms_components-0.3.5}/docs/index.md +1 -1
- {ms_components-0.3.4 → ms_components-0.3.5}/docs/ms_tutorial.md +20 -0
- ms_components-0.3.5/docs/project_form_events.md +17 -0
- ms_components-0.3.5/src/ms_components/ms_projects/__init__.py +4 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_projects/browser.py +13 -3
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_projects/widget.py +3 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/__init__.py +2 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/smart_table_view.py +15 -1
- ms_components-0.3.5/src/ms_components/ms_table/table_interaction.py +42 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_tutorial/bindings.py +1 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_tutorial/controller.py +30 -6
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_tutorial/overlay.py +18 -8
- {ms_components-0.3.4 → ms_components-0.3.5/src/ms_components.egg-info}/PKG-INFO +1 -1
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components.egg-info/SOURCES.txt +3 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components.egg-info/scm_file_list.json +3 -0
- ms_components-0.3.5/src/ms_components.egg-info/scm_version.json +8 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_ms_projects_widget.py +45 -0
- ms_components-0.3.5/tests/test_table_interaction.py +58 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_tutorial.py +80 -4
- ms_components-0.3.4/src/ms_components/ms_projects/__init__.py +0 -4
- ms_components-0.3.4/src/ms_components.egg-info/scm_version.json +0 -8
- {ms_components-0.3.4 → ms_components-0.3.5}/.github/workflows/docs.yml +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/.github/workflows/publish.yml +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/.gitignore +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/LICENSE +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/README.md +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/docs/api_reference.md +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/docs/examples.md +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/docs/usage.md +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/examples/tutorial_demo.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/examples/tutorial_demo.toml +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/pyproject.toml +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/setup.cfg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/__init__.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/backdrop.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/base.qss +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/chips.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/listview.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_dockwidget/__init__.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_dockwidget/widget.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_executors.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/__init__.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/bridge.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/cards.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/config.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/detail_panel.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/formatting.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/job_cards.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/models.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/palette.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/polling.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_monitor/widget.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_projects/app.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_projects/scratch.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/__init__.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/box.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/center.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/display.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/hydrogens-polar.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/hydrogens.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/plugins.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/widget.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_settings/__init__.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_settings/panel.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_stepper.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/action_header.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/cell_delegate.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/column_panel.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/example_usage.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/filter_panel.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/columns.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/export.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/filter.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/menu.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/reload.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/search.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/settings.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/icons/sort.svg +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/pagination_control.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/query_builder.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/session_provider.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/sort_panel.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/table_config.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/table_model.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/test_external_filters.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/test_toolbar.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_tutorial/__init__.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_tutorial/models.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/opengl.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/pagination.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/palettes.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/step_dialog.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/theme.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/tool_panel.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/wheel.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components.egg-info/dependency_links.txt +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components.egg-info/requires.txt +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components.egg-info/top_level.txt +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_app_settings.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_chips.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_dock_window.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_executors_ui.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_molsuite_monitor_bridge.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_molsuite_monitor_widgets.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_monitor_config.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_opengl.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_progress_column.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_project_browser.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_pymol_grid.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_pymol_toolbar.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_query_builder_keyset.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_shift_hscroll.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_split_button_style.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_step_dialog.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_table_toolbar.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_table_view_state.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/tests/test_theme_contrast.py +0 -0
- {ms_components-0.3.4 → ms_components-0.3.5}/zensical.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ms_components
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: PySide6 UI components for MolSuite: smart table, project browser, job monitor, PyMOL dock, steppers.
|
|
5
5
|
Author: Mario S. Valdés-Tresanco, Mario E. Valdés-Tresanco
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ pip install ms_components
|
|
|
21
21
|
| `ms_components.ms_table` | Declarative, paginated table over SQLModel |
|
|
22
22
|
| `ms_components.ms_monitor` | Job monitor for an `ms_flow` workspace: cancel, resume, move |
|
|
23
23
|
| `ms_components.ms_executors` | Hosts and executors: Settings pages and the panel that starts them |
|
|
24
|
-
| `ms_components.ms_projects` | Project browser
|
|
24
|
+
| [`ms_components.ms_projects`](project_form_events.md) | Project browser, project menu, and form lifecycle events |
|
|
25
25
|
| `ms_components.ms_settings` | Typed editor for application settings |
|
|
26
26
|
| `ms_components.ms_pymol` | PyMOL dock and control bar |
|
|
27
27
|
| `ms_components.ms_stepper` | Step-by-step wizard widget |
|
|
@@ -90,6 +90,14 @@ window.tutorials.start(overview)
|
|
|
90
90
|
without resetting application data. Multiple queued copies of an old event do
|
|
91
91
|
not complete a later step, even if both steps use the same signal.
|
|
92
92
|
|
|
93
|
+
For asynchronous operations that may finish while the user has gone Back, supply
|
|
94
|
+
`TutorialEvent(signal, accept=..., is_complete=lambda: cached_result_is_ready)`.
|
|
95
|
+
The controller checks `is_complete` when entering an unfinished step and advances
|
|
96
|
+
if it returns true. The application records successful completion before emitting
|
|
97
|
+
its signal; the callback reads that state, without blocking or querying a database.
|
|
98
|
+
`accept` validates incoming signal arguments; `is_complete` independently checks
|
|
99
|
+
the operation and result that the step requires.
|
|
100
|
+
|
|
93
101
|
`TutorialEvent.signal` also accepts a zero-argument resolver for signals belonging
|
|
94
102
|
to dialogs or controls that are recreated. It is resolved when entering the step.
|
|
95
103
|
|
|
@@ -118,6 +126,18 @@ the application automatically. Hidden targets temporarily hide the overlay; an
|
|
|
118
126
|
initial hidden target without an existing overlay fails with a diagnostic. Missing
|
|
119
127
|
or deleted targets and callback failures stop the tour and emit `failed`.
|
|
120
128
|
|
|
129
|
+
Call the public `controller.refresh()` after an application UI change when the
|
|
130
|
+
spotlight should update immediately. Normal tracking continues automatically.
|
|
131
|
+
Accepting a host `QDialog` preserves the tutorial so its target can move back to
|
|
132
|
+
the parent window; closing or rejecting the host still cancels the tutorial.
|
|
133
|
+
|
|
134
|
+
`TutorialController(dim_background=False)` preserves the application's background
|
|
135
|
+
colors and draws only the target outline. Input gating remains the same.
|
|
136
|
+
`instruction_provider=lambda step: current_instruction` supplies dynamic plain-text
|
|
137
|
+
instructions. They are measured again only when their text changes, without
|
|
138
|
+
resetting the reader's scroll position on every tracking tick. These options do not
|
|
139
|
+
depend on any application or project package.
|
|
140
|
+
|
|
121
141
|
Lifecycle signals:
|
|
122
142
|
|
|
123
143
|
| Signal | Arguments |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Project form events
|
|
2
|
+
|
|
3
|
+
`ProjectsMenuWidget` and `ProjectBrowserPanel` emit
|
|
4
|
+
`project_form_opened(ProjectFormDialog)` before entering a create/edit form's modal
|
|
5
|
+
loop. The form's `mode` is `"create"` or `"edit"`. Hosts can bind controls and
|
|
6
|
+
observe the normal `accepted`, `rejected`, and `finished` Qt signals without
|
|
7
|
+
searching the browser's widget tree or replacing its actions.
|
|
8
|
+
|
|
9
|
+
`ProjectFormDialog` is exported from `ms_components.ms_projects`. Its public
|
|
10
|
+
controls include `name_input`, `path_input`, `path_row`, `browse_button`, and
|
|
11
|
+
`save_button`. `name_committed(str)` emits the trimmed name when editing finishes.
|
|
12
|
+
`path_selected(str)` emits only when Browse confirms a folder, including selecting
|
|
13
|
+
the current folder again. Cancelling the chooser emits no selection event.
|
|
14
|
+
|
|
15
|
+
`project_created(project_id)` means that project creation succeeded. It remains
|
|
16
|
+
separate from opening the form or accepting its fields, which may still be followed
|
|
17
|
+
by a backend error. None of these APIs depend on `ms_tutorial` or a host application.
|
|
@@ -988,6 +988,9 @@ class StatsDialog(QDialog):
|
|
|
988
988
|
|
|
989
989
|
|
|
990
990
|
class ProjectFormDialog(QDialog):
|
|
991
|
+
name_committed = Signal(str)
|
|
992
|
+
path_selected = Signal(str)
|
|
993
|
+
|
|
991
994
|
def __init__(self, *, apps: list, mode: str,
|
|
992
995
|
initial_data: dict | None = None,
|
|
993
996
|
allow_app_selection: bool = True, parent=None):
|
|
@@ -1005,6 +1008,7 @@ class ProjectFormDialog(QDialog):
|
|
|
1005
1008
|
layout = QVBoxLayout(self)
|
|
1006
1009
|
form = QFormLayout()
|
|
1007
1010
|
self.name_input = QLineEdit(self.initial_data.get("name", ""))
|
|
1011
|
+
self.name_input.editingFinished.connect(lambda: self.name_committed.emit(self.name_input.text().strip()))
|
|
1008
1012
|
self.path_input = QLineEdit(str(self.initial_data.get("path", "")))
|
|
1009
1013
|
self.path_input.setReadOnly(True)
|
|
1010
1014
|
self.description_input = QTextEdit(self.initial_data.get("description", ""))
|
|
@@ -1022,11 +1026,11 @@ class ProjectFormDialog(QDialog):
|
|
|
1022
1026
|
idx = self.app_combo.findData(cid)
|
|
1023
1027
|
if idx >= 0: self.app_combo.setCurrentIndex(idx)
|
|
1024
1028
|
self.app_combo.setEnabled(self.allow_app_selection and not is_edit)
|
|
1025
|
-
path_row = QWidget()
|
|
1029
|
+
path_row = self.path_row = QWidget()
|
|
1026
1030
|
pl = QHBoxLayout(path_row)
|
|
1027
1031
|
pl.setContentsMargins(0,0,0,0)
|
|
1028
1032
|
pl.addWidget(self.path_input)
|
|
1029
|
-
browse_btn = QPushButton("Browse...")
|
|
1033
|
+
browse_btn = self.browse_button = QPushButton("Browse...")
|
|
1030
1034
|
browse_btn.clicked.connect(self._on_browse)
|
|
1031
1035
|
browse_btn.setEnabled(not is_edit)
|
|
1032
1036
|
pl.addWidget(browse_btn)
|
|
@@ -1037,6 +1041,7 @@ class ProjectFormDialog(QDialog):
|
|
|
1037
1041
|
form.addRow("Description:", self.description_input)
|
|
1038
1042
|
layout.addLayout(form)
|
|
1039
1043
|
btns = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
|
1044
|
+
self.save_button = btns.button(QDialogButtonBox.Save)
|
|
1040
1045
|
btns.accepted.connect(self.accept)
|
|
1041
1046
|
btns.rejected.connect(self.reject)
|
|
1042
1047
|
layout.addWidget(btns)
|
|
@@ -1045,7 +1050,9 @@ class ProjectFormDialog(QDialog):
|
|
|
1045
1050
|
folder = QFileDialog.getExistingDirectory(
|
|
1046
1051
|
self, "Select folder",
|
|
1047
1052
|
self.path_input.text().strip() or str(Path.home()))
|
|
1048
|
-
if folder:
|
|
1053
|
+
if folder:
|
|
1054
|
+
self.path_input.setText(folder)
|
|
1055
|
+
self.path_selected.emit(folder)
|
|
1049
1056
|
|
|
1050
1057
|
def accept(self):
|
|
1051
1058
|
if not self.name_input.text().strip():
|
|
@@ -1091,6 +1098,7 @@ class StatTile(QFrame):
|
|
|
1091
1098
|
class ProjectBrowserPanel(QWidget):
|
|
1092
1099
|
project_requested = Signal(str)
|
|
1093
1100
|
project_created = Signal(str)
|
|
1101
|
+
project_form_opened = Signal(object)
|
|
1094
1102
|
projects_changed = Signal()
|
|
1095
1103
|
|
|
1096
1104
|
def __init__(self, *, app_id: str | None = None, header_title: str,
|
|
@@ -1288,6 +1296,7 @@ class ProjectBrowserPanel(QWidget):
|
|
|
1288
1296
|
"description": raw.description, "tags": raw.tags,
|
|
1289
1297
|
"app_id": raw.app_id},
|
|
1290
1298
|
allow_app_selection=self._dialog_allows_app_selection(), parent=self)
|
|
1299
|
+
self.project_form_opened.emit(dlg)
|
|
1291
1300
|
if dlg.exec() != QDialog.Accepted:
|
|
1292
1301
|
return
|
|
1293
1302
|
data = dlg.get_data()
|
|
@@ -1355,6 +1364,7 @@ class ProjectBrowserPanel(QWidget):
|
|
|
1355
1364
|
initial_data={"path": str(Path.home()/"molsuite_projects"),
|
|
1356
1365
|
"app_id": self._active_tool_app_id or apps[0].app_id},
|
|
1357
1366
|
allow_app_selection=self._dialog_allows_app_selection(), parent=self)
|
|
1367
|
+
self.project_form_opened.emit(dlg)
|
|
1358
1368
|
if dlg.exec() != QDialog.Accepted:
|
|
1359
1369
|
return
|
|
1360
1370
|
fd = dlg.get_data()
|
|
@@ -38,6 +38,7 @@ def _project_color(index: int) -> str:
|
|
|
38
38
|
class ProjectsMenuWidget(QFrame):
|
|
39
39
|
project_requested = Signal(str)
|
|
40
40
|
project_created = Signal(str)
|
|
41
|
+
project_form_opened = Signal(object)
|
|
41
42
|
# The host app's open project was confirmed for deletion: the host closes it, then deletes it.
|
|
42
43
|
active_project_delete_requested = Signal(str)
|
|
43
44
|
|
|
@@ -173,6 +174,7 @@ class ProjectsMenuWidget(QFrame):
|
|
|
173
174
|
allow_app_selection=self.allow_app_selection,
|
|
174
175
|
parent=self,
|
|
175
176
|
)
|
|
177
|
+
self.project_form_opened.emit(dialog)
|
|
176
178
|
if dialog.exec() != ProjectFormDialog.Accepted:
|
|
177
179
|
return
|
|
178
180
|
data = dialog.get_data()
|
|
@@ -224,6 +226,7 @@ class ProjectsMenuWidget(QFrame):
|
|
|
224
226
|
allow_app_selection=False,
|
|
225
227
|
parent=self,
|
|
226
228
|
)
|
|
229
|
+
self.project_form_opened.emit(dialog)
|
|
227
230
|
if dialog.exec() != ProjectFormDialog.Accepted:
|
|
228
231
|
return
|
|
229
232
|
data = dialog.get_data()
|
|
@@ -40,9 +40,11 @@ from .table_config import (
|
|
|
40
40
|
DEFAULT_PAGE_SIZES,
|
|
41
41
|
)
|
|
42
42
|
from .table_model import SmartTableModel
|
|
43
|
+
from .table_interaction import TableInteraction
|
|
43
44
|
|
|
44
45
|
__all__ = [
|
|
45
46
|
"SmartTableView",
|
|
47
|
+
"TableInteraction",
|
|
46
48
|
"SessionProvider",
|
|
47
49
|
"TableConfig",
|
|
48
50
|
"ToolbarAction",
|
|
@@ -65,7 +65,8 @@ from .pagination_control import PaginationControl
|
|
|
65
65
|
from .query_builder import QueryBuilder
|
|
66
66
|
from .session_provider import SessionProvider
|
|
67
67
|
from .table_config import ColumnDef, ColumnKind, FilterOperator, FilterSpec, SortSpec, TableConfig, TableLoadMode, ToolbarAction
|
|
68
|
-
from .table_model import RAW_OBJECT_ROLE, SmartTableModel
|
|
68
|
+
from .table_model import COLUMN_DEF_ROLE, RAW_OBJECT_ROLE, SmartTableModel
|
|
69
|
+
from .table_interaction import TableInteraction
|
|
69
70
|
|
|
70
71
|
_TOOLBAR_ICONS = Path(__file__).parent / "icons"
|
|
71
72
|
|
|
@@ -93,7 +94,10 @@ class SmartTableView(QWidget):
|
|
|
93
94
|
Smart table with pagination, filtering, sorting and column configuration.
|
|
94
95
|
|
|
95
96
|
Signals:
|
|
97
|
+
cell_pressed(object, str, Qt.KeyboardModifiers):
|
|
98
|
+
(raw_obj, field, modifiers) before a left press changes the selection.
|
|
96
99
|
row_clicked(object): SQLModel object for the clicked row.
|
|
100
|
+
row_reselected(object): An already selected row was clicked again.
|
|
97
101
|
row_double_clicked(object): SQLModel object for the double-clicked row.
|
|
98
102
|
selection_changed(list): List of selected objects.
|
|
99
103
|
edit_committed(object, str, object):
|
|
@@ -101,7 +105,9 @@ class SmartTableView(QWidget):
|
|
|
101
105
|
data_refreshed(int): Total item count after each load.
|
|
102
106
|
"""
|
|
103
107
|
|
|
108
|
+
cell_pressed = Signal(object, str, Qt.KeyboardModifiers)
|
|
104
109
|
row_clicked = Signal(object)
|
|
110
|
+
row_reselected = Signal(object)
|
|
105
111
|
row_double_clicked = Signal(object)
|
|
106
112
|
selection_changed = Signal(list)
|
|
107
113
|
edit_committed = Signal(object, str, object)
|
|
@@ -321,6 +327,9 @@ class SmartTableView(QWidget):
|
|
|
321
327
|
# ── Table View ───────────────────────
|
|
322
328
|
self._table = QTableView()
|
|
323
329
|
self._table.setModel(self._model)
|
|
330
|
+
self._input = TableInteraction(self._table)
|
|
331
|
+
self._input.pressed.connect(self._on_cell_pressed)
|
|
332
|
+
self._input.reselected.connect(lambda _view, index, _mods: self.row_reselected.emit(index.data(RAW_OBJECT_ROLE)))
|
|
324
333
|
self._table.setObjectName("SmartTable")
|
|
325
334
|
|
|
326
335
|
sel_mode = (QAbstractItemView.ExtendedSelection
|
|
@@ -746,6 +755,11 @@ class SmartTableView(QWidget):
|
|
|
746
755
|
self._refresh_persistent_widgets()
|
|
747
756
|
self._schedule_infinite_autofill()
|
|
748
757
|
|
|
758
|
+
def _on_cell_pressed(self, _view, index, modifiers):
|
|
759
|
+
raw, column = index.data(RAW_OBJECT_ROLE), index.data(COLUMN_DEF_ROLE)
|
|
760
|
+
if raw is not None and column is not None:
|
|
761
|
+
self.cell_pressed.emit(raw, column.field, modifiers)
|
|
762
|
+
|
|
749
763
|
def _on_row_clicked(self, index):
|
|
750
764
|
raw = self._model.data(index, RAW_OBJECT_ROLE)
|
|
751
765
|
if raw is not None:
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Table input signals, independent of any viewer or data model."""
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import QEvent, QObject, Qt, Signal
|
|
4
|
+
from PySide6.QtWidgets import QAbstractItemView, QApplication, QWidget
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TableInteraction(QObject):
|
|
8
|
+
"""Observe one Qt table or the tables under a window, including tables added later.
|
|
9
|
+
|
|
10
|
+
``pressed(view, index, modifiers)`` precedes native selection changes;
|
|
11
|
+
``reselected`` reports a click on an already selected cell.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
pressed = Signal(object, object, Qt.KeyboardModifiers)
|
|
15
|
+
reselected = Signal(object, object, Qt.KeyboardModifiers)
|
|
16
|
+
|
|
17
|
+
def __init__(self, parent: QWidget):
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self._pressed = None
|
|
20
|
+
if isinstance(parent, QAbstractItemView):
|
|
21
|
+
parent.viewport().installEventFilter(self)
|
|
22
|
+
else:
|
|
23
|
+
QApplication.instance().installEventFilter(self)
|
|
24
|
+
|
|
25
|
+
def eventFilter(self, watched, event):
|
|
26
|
+
if (event.type() == QEvent.Type.MouseButtonPress
|
|
27
|
+
and event.button() == Qt.MouseButton.LeftButton
|
|
28
|
+
and isinstance(watched, QWidget)):
|
|
29
|
+
view = watched.parentWidget()
|
|
30
|
+
if (isinstance(view, QAbstractItemView) and watched is view.viewport()
|
|
31
|
+
and (view is self.parent() or self.parent().isAncestorOf(view))):
|
|
32
|
+
index = view.indexAt(event.position().toPoint())
|
|
33
|
+
if index.isValid():
|
|
34
|
+
self._pressed = (view, index, event.modifiers(), view.selectionModel().isSelected(index))
|
|
35
|
+
self.pressed.emit(view, index, event.modifiers())
|
|
36
|
+
elif event.type() == QEvent.Type.MouseButtonRelease and self._pressed is not None:
|
|
37
|
+
view, index, modifiers, was_selected = self._pressed
|
|
38
|
+
self._pressed = None
|
|
39
|
+
if (event.button() == Qt.MouseButton.LeftButton and watched is view.viewport()
|
|
40
|
+
and was_selected and view.indexAt(event.position().toPoint()) == index):
|
|
41
|
+
self.reselected.emit(view, index, modifiers)
|
|
42
|
+
return super().eventFilter(watched, event)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Attach a tour to existing widgets/signals without changing their slots."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from collections.abc import Mapping
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
5
|
import weakref
|
|
6
6
|
|
|
7
7
|
from PySide6.QtCore import QEvent, QObject, Qt, QTimer, Signal, SignalInstance, Slot
|
|
8
|
-
from PySide6.QtWidgets import QApplication, QWidget
|
|
8
|
+
from PySide6.QtWidgets import QApplication, QDialog, QWidget
|
|
9
9
|
from shiboken6 import isValid
|
|
10
10
|
|
|
11
11
|
from .bindings import TutorialEvent, TutorialTarget
|
|
@@ -54,10 +54,14 @@ class TutorialController(QObject):
|
|
|
54
54
|
|
|
55
55
|
def __init__(self, *, targets: Mapping[str, TutorialTarget],
|
|
56
56
|
events: Mapping[str, TutorialEvent] | None = None,
|
|
57
|
+
instruction_provider: Callable[[TutorialStep], str] | None = None,
|
|
58
|
+
dim_background: bool = True,
|
|
57
59
|
parent: QObject | None = None):
|
|
58
60
|
super().__init__(parent)
|
|
59
61
|
self.targets = dict(targets)
|
|
60
62
|
self.events = dict(events or {})
|
|
63
|
+
self.instruction_provider = instruction_provider
|
|
64
|
+
self.dim_background = dim_background
|
|
61
65
|
self._tutorial: Tutorial | None = None
|
|
62
66
|
self._index = -1
|
|
63
67
|
self._completed: set[str] = set()
|
|
@@ -67,7 +71,7 @@ class TutorialController(QObject):
|
|
|
67
71
|
self._previous_focus = None
|
|
68
72
|
self._tracker = QTimer(self)
|
|
69
73
|
self._tracker.setInterval(80)
|
|
70
|
-
self._tracker.timeout.connect(self.
|
|
74
|
+
self._tracker.timeout.connect(self.refresh)
|
|
71
75
|
|
|
72
76
|
@property
|
|
73
77
|
def is_active(self) -> bool:
|
|
@@ -170,7 +174,7 @@ class TutorialController(QObject):
|
|
|
170
174
|
# overlay (and comes back). Otherwise the tour would stay active, unseen, eating Escape.
|
|
171
175
|
if isinstance(target, QWidget) and isValid(target) and not target.isVisible():
|
|
172
176
|
raise ValueError(f"Target '{step.target}' is hidden; provide a prepare callback")
|
|
173
|
-
self.
|
|
177
|
+
self.refresh()
|
|
174
178
|
if not self.is_active:
|
|
175
179
|
return
|
|
176
180
|
if step.advance_on is not None:
|
|
@@ -181,12 +185,18 @@ class TutorialController(QObject):
|
|
|
181
185
|
self._target.setFocus()
|
|
182
186
|
else:
|
|
183
187
|
self.overlay.focus_guide()
|
|
188
|
+
if step.advance_on is not None and step.id not in self._completed:
|
|
189
|
+
check = self.events[step.advance_on].is_complete
|
|
190
|
+
if check is not None and check():
|
|
191
|
+
self._completed.add(step.id)
|
|
192
|
+
self.next()
|
|
184
193
|
except Exception as exc:
|
|
185
194
|
if self.is_active:
|
|
186
195
|
self._fail(f"Cannot show tutorial step '{step.id}': {exc}")
|
|
187
196
|
|
|
188
197
|
@Slot()
|
|
189
|
-
def
|
|
198
|
+
def refresh(self):
|
|
199
|
+
"""Resolve the current target and instructions after an application UI change."""
|
|
190
200
|
step = self.current_step
|
|
191
201
|
if step is None:
|
|
192
202
|
return
|
|
@@ -207,13 +217,15 @@ class TutorialController(QObject):
|
|
|
207
217
|
if self.overlay is not None:
|
|
208
218
|
self.overlay.hide()
|
|
209
219
|
self.overlay.deleteLater()
|
|
210
|
-
self._overlay = TutorialOverlay(host)
|
|
220
|
+
self._overlay = TutorialOverlay(host, dim_background=self.dim_background)
|
|
211
221
|
self._overlay.next_requested.connect(self.next)
|
|
212
222
|
self._overlay.back_requested.connect(self.back)
|
|
213
223
|
self._overlay.exit_requested.connect(self.stop)
|
|
214
224
|
self.destroyed.connect(self._overlay.deleteLater)
|
|
215
225
|
if self.overlay.step is not step:
|
|
216
226
|
self.overlay.set_step(self._tutorial.title, step, self._index, len(self._tutorial.steps))
|
|
227
|
+
if self.instruction_provider is not None:
|
|
228
|
+
self.overlay.set_text(self.instruction_provider(step))
|
|
217
229
|
self.overlay.next_button.setEnabled(step.advance_on is None or step.id in self._completed)
|
|
218
230
|
region = self.targets[step.target].rect
|
|
219
231
|
self.overlay.reposition(target, interactive=step.advance_on is not None,
|
|
@@ -233,6 +245,14 @@ class TutorialController(QObject):
|
|
|
233
245
|
self._completed.add(self.current_step.id)
|
|
234
246
|
self.next()
|
|
235
247
|
|
|
248
|
+
@Slot(int)
|
|
249
|
+
def _on_dialog_finished(self, result: int):
|
|
250
|
+
if self.is_active:
|
|
251
|
+
if result == QDialog.DialogCode.Accepted:
|
|
252
|
+
self.refresh()
|
|
253
|
+
else:
|
|
254
|
+
self.stop()
|
|
255
|
+
|
|
236
256
|
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
237
257
|
if not self.is_active or not isinstance(watched, QWidget) or self.overlay is None:
|
|
238
258
|
return False
|
|
@@ -242,6 +262,10 @@ class TutorialController(QObject):
|
|
|
242
262
|
self.stop()
|
|
243
263
|
return True
|
|
244
264
|
if event.type() == QEvent.Type.Close and watched is self.overlay.parentWidget():
|
|
265
|
+
if isinstance(watched, QDialog):
|
|
266
|
+
self.overlay.hide()
|
|
267
|
+
watched.finished.connect(self._on_dialog_finished, Qt.ConnectionType.SingleShotConnection)
|
|
268
|
+
return False
|
|
245
269
|
self.stop()
|
|
246
270
|
return False
|
|
247
271
|
if not self.overlay.isVisible():
|
|
@@ -19,8 +19,9 @@ class TutorialOverlay(QWidget):
|
|
|
19
19
|
back_requested = Signal()
|
|
20
20
|
exit_requested = Signal()
|
|
21
21
|
|
|
22
|
-
def __init__(self, host: QWidget):
|
|
22
|
+
def __init__(self, host: QWidget, *, dim_background: bool = True):
|
|
23
23
|
super().__init__(host)
|
|
24
|
+
self.dim_background = dim_background
|
|
24
25
|
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
|
25
26
|
self.hole = QRect()
|
|
26
27
|
self.step: TutorialStep | None = None
|
|
@@ -74,6 +75,14 @@ class TutorialOverlay(QWidget):
|
|
|
74
75
|
self.setAccessibleName(tutorial_title)
|
|
75
76
|
self.setAccessibleDescription(f"{step.title}. {step.text}")
|
|
76
77
|
|
|
78
|
+
def set_text(self, text: str):
|
|
79
|
+
"""Update instructions without resetting their layout or scroll position each tick."""
|
|
80
|
+
if self.body.toPlainText() != text:
|
|
81
|
+
self.body.setPlainText(text)
|
|
82
|
+
self._size_key = None
|
|
83
|
+
self._place_key = None
|
|
84
|
+
self.setAccessibleDescription(f"{self.step.title}. {text}")
|
|
85
|
+
|
|
77
86
|
def reposition(self, target: QWidget, *, interactive: bool, rect: QRect | None = None):
|
|
78
87
|
host = self.parentWidget()
|
|
79
88
|
self.setGeometry(host.rect())
|
|
@@ -143,12 +152,13 @@ class TutorialOverlay(QWidget):
|
|
|
143
152
|
def paintEvent(self, event):
|
|
144
153
|
painter = QPainter(self)
|
|
145
154
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
if self.dim_background:
|
|
156
|
+
shade = self.palette().color(QPalette.ColorRole.Shadow)
|
|
157
|
+
shade.setAlpha(170)
|
|
158
|
+
path = QPainterPath()
|
|
159
|
+
path.addRect(QRectF(self.rect()))
|
|
160
|
+
opening = QPainterPath()
|
|
161
|
+
opening.addRect(QRectF(self.hole))
|
|
162
|
+
painter.fillPath(path.subtracted(opening), shade)
|
|
153
163
|
painter.setPen(QPen(self.palette().color(QPalette.ColorRole.Highlight), 3))
|
|
154
164
|
painter.drawRoundedRect(QRectF(self.hole.adjusted(-3, -3, 3, 3)), 6, 6)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ms_components
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: PySide6 UI components for MolSuite: smart table, project browser, job monitor, PyMOL dock, steppers.
|
|
5
5
|
Author: Mario S. Valdés-Tresanco, Mario E. Valdés-Tresanco
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,6 +9,7 @@ docs/api_reference.md
|
|
|
9
9
|
docs/examples.md
|
|
10
10
|
docs/index.md
|
|
11
11
|
docs/ms_tutorial.md
|
|
12
|
+
docs/project_form_events.md
|
|
12
13
|
docs/usage.md
|
|
13
14
|
examples/tutorial_demo.py
|
|
14
15
|
examples/tutorial_demo.toml
|
|
@@ -86,6 +87,7 @@ src/ms_components/ms_table/session_provider.py
|
|
|
86
87
|
src/ms_components/ms_table/smart_table_view.py
|
|
87
88
|
src/ms_components/ms_table/sort_panel.py
|
|
88
89
|
src/ms_components/ms_table/table_config.py
|
|
90
|
+
src/ms_components/ms_table/table_interaction.py
|
|
89
91
|
src/ms_components/ms_table/table_model.py
|
|
90
92
|
src/ms_components/ms_table/test_external_filters.py
|
|
91
93
|
src/ms_components/ms_table/test_toolbar.py
|
|
@@ -121,6 +123,7 @@ tests/test_query_builder_keyset.py
|
|
|
121
123
|
tests/test_shift_hscroll.py
|
|
122
124
|
tests/test_split_button_style.py
|
|
123
125
|
tests/test_step_dialog.py
|
|
126
|
+
tests/test_table_interaction.py
|
|
124
127
|
tests/test_table_toolbar.py
|
|
125
128
|
tests/test_table_view_state.py
|
|
126
129
|
tests/test_theme_contrast.py
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"docs/examples.md",
|
|
10
10
|
"docs/index.md",
|
|
11
11
|
"docs/ms_tutorial.md",
|
|
12
|
+
"docs/project_form_events.md",
|
|
12
13
|
"docs/usage.md",
|
|
13
14
|
"examples/tutorial_demo.py",
|
|
14
15
|
"examples/tutorial_demo.toml",
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"src/ms_components/ms_table/smart_table_view.py",
|
|
84
85
|
"src/ms_components/ms_table/sort_panel.py",
|
|
85
86
|
"src/ms_components/ms_table/table_config.py",
|
|
87
|
+
"src/ms_components/ms_table/table_interaction.py",
|
|
86
88
|
"src/ms_components/ms_table/table_model.py",
|
|
87
89
|
"src/ms_components/ms_table/test_external_filters.py",
|
|
88
90
|
"src/ms_components/ms_table/test_toolbar.py",
|
|
@@ -115,6 +117,7 @@
|
|
|
115
117
|
"tests/test_shift_hscroll.py",
|
|
116
118
|
"tests/test_split_button_style.py",
|
|
117
119
|
"tests/test_step_dialog.py",
|
|
120
|
+
"tests/test_table_interaction.py",
|
|
118
121
|
"tests/test_table_toolbar.py",
|
|
119
122
|
"tests/test_table_view_state.py",
|
|
120
123
|
"tests/test_theme_contrast.py",
|
|
@@ -9,6 +9,51 @@ from PySide6.QtWidgets import QDialog, QMessageBox
|
|
|
9
9
|
|
|
10
10
|
from ms_flow.core.apps import AppManifest
|
|
11
11
|
from ms_components.ms_projects import ProjectsMenuWidget
|
|
12
|
+
from ms_components.ms_projects import ProjectFormDialog
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_folder_selection_emits_only_when_confirmed_including_the_same_folder(monkeypatch, tmp_path):
|
|
16
|
+
QApplication.instance() or QApplication([])
|
|
17
|
+
backend = _FakeBackend()
|
|
18
|
+
form = ProjectFormDialog(apps=backend.list_apps(), mode="create",
|
|
19
|
+
initial_data={"path": str(tmp_path)})
|
|
20
|
+
selected, names = [], []
|
|
21
|
+
form.path_selected.connect(selected.append)
|
|
22
|
+
form.name_committed.connect(names.append)
|
|
23
|
+
try:
|
|
24
|
+
form.name_input.setText("Practice")
|
|
25
|
+
form.name_input.editingFinished.emit()
|
|
26
|
+
assert names == ["Practice"]
|
|
27
|
+
monkeypatch.setattr("ms_components.ms_projects.browser.QFileDialog.getExistingDirectory", lambda *a: "")
|
|
28
|
+
form.browse_button.click()
|
|
29
|
+
assert selected == [] and form.path_input.text() == str(tmp_path)
|
|
30
|
+
monkeypatch.setattr("ms_components.ms_projects.browser.QFileDialog.getExistingDirectory",
|
|
31
|
+
lambda *a: str(tmp_path))
|
|
32
|
+
form.browse_button.click()
|
|
33
|
+
assert selected == [str(tmp_path)]
|
|
34
|
+
assert form.path_row.isAncestorOf(form.browse_button)
|
|
35
|
+
assert form.save_button.text()
|
|
36
|
+
finally:
|
|
37
|
+
form.deleteLater()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_project_form_is_announced_before_its_modal_loop_and_cancel_does_not_create(monkeypatch):
|
|
41
|
+
QApplication.instance() or QApplication([])
|
|
42
|
+
widget = ProjectsMenuWidget(app_id="amdockvs", backend=_FakeBackend())
|
|
43
|
+
opened, created = [], []
|
|
44
|
+
widget.project_form_opened.connect(opened.append)
|
|
45
|
+
widget.project_created.connect(created.append)
|
|
46
|
+
|
|
47
|
+
def cancel(form):
|
|
48
|
+
assert opened == [form] and form.mode == "create"
|
|
49
|
+
return QDialog.Rejected
|
|
50
|
+
|
|
51
|
+
monkeypatch.setattr(ProjectFormDialog, "exec", cancel)
|
|
52
|
+
try:
|
|
53
|
+
widget.on_create_project()
|
|
54
|
+
assert len(opened) == 1 and created == []
|
|
55
|
+
finally:
|
|
56
|
+
widget.deleteLater()
|
|
12
57
|
|
|
13
58
|
|
|
14
59
|
class _FakeBackend:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from PySide6.QtCore import QPoint, Qt
|
|
3
|
+
from PySide6.QtTest import QTest
|
|
4
|
+
from PySide6.QtWidgets import QApplication
|
|
5
|
+
from sqlmodel import Field, SQLModel
|
|
6
|
+
|
|
7
|
+
from ms_components.ms_table import ColumnDef, SmartTableView, TableConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _InteractionRow(SQLModel, table=True):
|
|
11
|
+
id: int = Field(primary_key=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def table(monkeypatch):
|
|
16
|
+
app = QApplication.instance() or QApplication(["table-interaction-test"])
|
|
17
|
+
monkeypatch.setattr(SmartTableView, "refresh", lambda self: None)
|
|
18
|
+
widget = SmartTableView(
|
|
19
|
+
db=object(), config=TableConfig(
|
|
20
|
+
model_class=_InteractionRow, columns=[ColumnDef("id")], show_row_numbers=False))
|
|
21
|
+
widget._model.load_data([
|
|
22
|
+
{"id": number, "__raw__": _InteractionRow(id=number)} for number in (1, 2)
|
|
23
|
+
], 2, 1)
|
|
24
|
+
widget.resize(320, 260)
|
|
25
|
+
widget.show()
|
|
26
|
+
app.processEvents()
|
|
27
|
+
yield widget
|
|
28
|
+
widget.close()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize("modifiers", [Qt.NoModifier, Qt.ShiftModifier, Qt.ControlModifier])
|
|
32
|
+
def test_cell_pressed_reports_modifiers_before_selection_and_keeps_row_clicked(table, modifiers):
|
|
33
|
+
events = []
|
|
34
|
+
table.cell_pressed.connect(lambda obj, field, mods: events.append(
|
|
35
|
+
("pressed", obj.id, field, mods, table.get_selected_objects())))
|
|
36
|
+
table.row_clicked.connect(lambda obj: events.append(("clicked", obj.id)))
|
|
37
|
+
position = table.view.visualRect(table._model.index(1, 0)).center()
|
|
38
|
+
QTest.mouseClick(table.view.viewport(), Qt.LeftButton, modifiers, position)
|
|
39
|
+
assert events == [("pressed", 2, "id", modifiers, []), ("clicked", 2)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_empty_space_emits_no_cell_press(table):
|
|
43
|
+
pressed = []
|
|
44
|
+
table.cell_pressed.connect(lambda *args: pressed.append(args))
|
|
45
|
+
QTest.mouseClick(table.view.viewport(), Qt.LeftButton, Qt.NoModifier,
|
|
46
|
+
QPoint(10, table.view.viewport().height() - 2))
|
|
47
|
+
assert pressed == []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_reselecting_a_row_has_its_own_signal_without_faking_a_selection_change(table):
|
|
51
|
+
selected, reselected = [], []
|
|
52
|
+
table.selection_changed.connect(lambda rows: selected.append([row.id for row in rows]))
|
|
53
|
+
table.row_reselected.connect(lambda row: reselected.append(row.id))
|
|
54
|
+
position = table.view.visualRect(table._model.index(1, 0)).center()
|
|
55
|
+
QTest.mouseClick(table.view.viewport(), Qt.LeftButton, Qt.NoModifier, position)
|
|
56
|
+
QTest.mouseClick(table.view.viewport(), Qt.LeftButton, Qt.NoModifier, position)
|
|
57
|
+
assert selected == [[2]]
|
|
58
|
+
assert reselected == [2]
|
|
@@ -100,6 +100,82 @@ class Emitter(QObject):
|
|
|
100
100
|
done = Signal(str, str)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def test_completion_while_on_another_step_is_checked_on_return(ui):
|
|
104
|
+
app, window, button, other, controller = ui
|
|
105
|
+
emitter = Emitter()
|
|
106
|
+
completed = set()
|
|
107
|
+
controller.events["job.done"] = TutorialEvent(
|
|
108
|
+
emitter.done, accept=lambda job, status: job == "expected" and status == "completed",
|
|
109
|
+
is_complete=lambda: "expected" in completed,
|
|
110
|
+
)
|
|
111
|
+
controller.start(Tutorial("jobs", "Jobs", (
|
|
112
|
+
TutorialStep("first", "run", "Input", "Review input."),
|
|
113
|
+
TutorialStep("job", "run", "Run", "Wait.", "job.done"),
|
|
114
|
+
TutorialStep("last", "other", "Output", "Review output."),
|
|
115
|
+
)))
|
|
116
|
+
controller.next()
|
|
117
|
+
assert controller.index == 1 and not controller.overlay.next_button.isEnabled()
|
|
118
|
+
controller.back()
|
|
119
|
+
completed.add("expected")
|
|
120
|
+
emitter.done.emit("expected", "completed")
|
|
121
|
+
app.processEvents()
|
|
122
|
+
assert controller.index == 0
|
|
123
|
+
controller.next()
|
|
124
|
+
assert controller.index == 2
|
|
125
|
+
controller.back()
|
|
126
|
+
assert controller.index == 1 and controller.overlay.next_button.isEnabled()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_dynamic_instructions_refresh_without_reflowing_unchanged_text(ui):
|
|
130
|
+
app, window, button, other, controller = ui
|
|
131
|
+
instructions = ["Wait for the operation."]
|
|
132
|
+
controller.instruction_provider = lambda step: instructions[0]
|
|
133
|
+
controller.start(tour())
|
|
134
|
+
assert controller.overlay.body.toPlainText() == instructions[0]
|
|
135
|
+
instructions[0] = "The operation is ready. " * 100
|
|
136
|
+
controller.refresh()
|
|
137
|
+
assert controller.overlay.body.toPlainText() == instructions[0]
|
|
138
|
+
assert instructions[0] in controller.overlay.accessibleDescription()
|
|
139
|
+
controller.overlay.body.verticalScrollBar().setValue(20)
|
|
140
|
+
controller.refresh()
|
|
141
|
+
assert controller.overlay.body.verticalScrollBar().value() == 20
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_outline_only_presentation_preserves_the_background(ui):
|
|
145
|
+
app, window, button, other, controller = ui
|
|
146
|
+
before = window.grab().toImage()
|
|
147
|
+
controller.dim_background = False
|
|
148
|
+
controller.start(tour(interactive=True))
|
|
149
|
+
overlay = controller.overlay
|
|
150
|
+
sample = next(QPoint(x, y) for x in range(10, window.width(), 20)
|
|
151
|
+
for y in range(10, window.height(), 20)
|
|
152
|
+
if not overlay.panel.geometry().contains(x, y)
|
|
153
|
+
and not overlay.hole.adjusted(-5, -5, 5, 5).contains(x, y))
|
|
154
|
+
after = window.grab().toImage()
|
|
155
|
+
assert after.pixelColor(sample) == before.pixelColor(sample)
|
|
156
|
+
assert overlay.hole.size() == button.size()
|
|
157
|
+
assert QApplication.widgetAt(button.mapToGlobal(button.rect().center())) is button
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_accepting_a_dialog_keeps_the_tutorial_active_but_closing_cancels(ui):
|
|
161
|
+
app, window, button, other, controller = ui
|
|
162
|
+
dialog = QDialog(window)
|
|
163
|
+
action = QPushButton("Save", dialog)
|
|
164
|
+
dialog.show()
|
|
165
|
+
app.processEvents()
|
|
166
|
+
controller.targets["dialog"] = TutorialTarget(lambda: action if dialog.isVisible() else button)
|
|
167
|
+
controller.start(Tutorial("dialog", "Dialog", (
|
|
168
|
+
TutorialStep("save", "dialog", "Save", "Save the form."),
|
|
169
|
+
)))
|
|
170
|
+
dialog.accept()
|
|
171
|
+
app.processEvents()
|
|
172
|
+
assert controller.is_active and controller.overlay.parentWidget() is window
|
|
173
|
+
dialog.show()
|
|
174
|
+
controller.refresh()
|
|
175
|
+
dialog.close()
|
|
176
|
+
assert not controller.is_active
|
|
177
|
+
|
|
178
|
+
|
|
103
179
|
def test_job_predicate_runs_in_ui_thread_and_ignores_stale_duplicate_events(ui):
|
|
104
180
|
app, window, button, other, controller = ui
|
|
105
181
|
emitter = Emitter()
|
|
@@ -184,13 +260,13 @@ def test_resize_and_long_text_keep_callout_visible_and_scrollable(ui):
|
|
|
184
260
|
controller.start(definition)
|
|
185
261
|
window.resize(480, 400)
|
|
186
262
|
app.processEvents()
|
|
187
|
-
controller.
|
|
263
|
+
controller.refresh()
|
|
188
264
|
overlay = controller.overlay
|
|
189
265
|
assert overlay.rect().contains(overlay.panel.geometry())
|
|
190
266
|
assert not overlay.panel.geometry().intersects(overlay.hole)
|
|
191
267
|
assert overlay.body.verticalScrollBar().maximum() > 0
|
|
192
268
|
overlay.body.verticalScrollBar().setValue(20)
|
|
193
|
-
controller.
|
|
269
|
+
controller.refresh()
|
|
194
270
|
assert overlay.body.verticalScrollBar().value() == 20
|
|
195
271
|
assert overlay.hole.contains(overlay.mapFromGlobal(button.mapToGlobal(QPoint(2, 2))))
|
|
196
272
|
|
|
@@ -217,7 +293,7 @@ def test_menu_target_region_follows_the_action_geometry(ui):
|
|
|
217
293
|
previous = controller.overlay.hole
|
|
218
294
|
file_menu.menuAction().setText("Projects and input files")
|
|
219
295
|
app.processEvents()
|
|
220
|
-
controller.
|
|
296
|
+
controller.refresh()
|
|
221
297
|
assert controller.overlay.hole == expected_region()
|
|
222
298
|
assert controller.overlay.hole.left() > previous.left()
|
|
223
299
|
|
|
@@ -263,7 +339,7 @@ def test_deleted_target_and_prepare_errors_fail_with_cleanup(ui):
|
|
|
263
339
|
controller.start(tour())
|
|
264
340
|
button.deleteLater()
|
|
265
341
|
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
|
|
266
|
-
controller.
|
|
342
|
+
controller.refresh()
|
|
267
343
|
assert failures and "unavailable" in failures[-1]
|
|
268
344
|
assert not controller.is_active and controller.overlay is None
|
|
269
345
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_dockwidget/resources/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_pymol/icons/hydrogens-polar.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/pagination_control.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_components-0.3.4 → ms_components-0.3.5}/src/ms_components/ms_table/test_external_filters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|