ms-components 0.3.0__tar.gz → 0.3.2__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.0 → ms_components-0.3.2}/PKG-INFO +1 -1
- ms_components-0.3.2/docs/api_reference.md +102 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/docs/usage.md +13 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/base.qss +21 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/widget.py +4 -1
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/__init__.py +2 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/widget.py +212 -4
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/cell_delegate.py +36 -24
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/query_builder.py +6 -4
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/smart_table_view.py +12 -2
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/table_config.py +8 -1
- ms_components-0.3.2/src/ms_components/opengl.py +139 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/theme.py +20 -1
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components.egg-info/PKG-INFO +1 -1
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components.egg-info/SOURCES.txt +3 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components.egg-info/scm_file_list.json +3 -0
- ms_components-0.3.2/src/ms_components.egg-info/scm_version.json +8 -0
- ms_components-0.3.2/tests/test_opengl.py +166 -0
- ms_components-0.3.2/tests/test_progress_column.py +48 -0
- ms_components-0.3.2/tests/test_pymol_grid.py +278 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_query_builder_keyset.py +54 -0
- ms_components-0.3.0/docs/api_reference.md +0 -48
- ms_components-0.3.0/src/ms_components.egg-info/scm_version.json +0 -8
- ms_components-0.3.0/tests/test_progress_column.py +0 -36
- {ms_components-0.3.0 → ms_components-0.3.2}/.github/workflows/docs.yml +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/.github/workflows/publish.yml +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/.gitignore +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/LICENSE +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/README.md +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/docs/examples.md +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/docs/index.md +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/docs/ms_tutorial.md +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/examples/tutorial_demo.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/examples/tutorial_demo.toml +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/pyproject.toml +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/setup.cfg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/chips.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/listview.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_executors.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/bridge.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/cards.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/config.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/detail_panel.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/formatting.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/job_cards.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/models.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/palette.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/polling.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_monitor/widget.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_projects/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_projects/app.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_projects/browser.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_projects/scratch.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_projects/widget.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/box.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/center.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/display.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_pymol/plugins.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_settings/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_settings/panel.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_stepper.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/action_header.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/column_panel.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/example_usage.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/filter_panel.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/columns.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/export.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/filter.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/menu.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/reload.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/search.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/settings.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/icons/sort.svg +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/pagination_control.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/session_provider.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/sort_panel.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/table_model.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/test_external_filters.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_table/test_toolbar.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_tutorial/__init__.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_tutorial/bindings.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_tutorial/controller.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_tutorial/models.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/ms_tutorial/overlay.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/pagination.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/palettes.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/step_dialog.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/tool_panel.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components/wheel.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components.egg-info/dependency_links.txt +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components.egg-info/requires.txt +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/src/ms_components.egg-info/top_level.txt +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_app_settings.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_chips.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_dock_window.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_executors_ui.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_molsuite_monitor_bridge.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_molsuite_monitor_widgets.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_monitor_config.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_ms_projects_widget.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_project_browser.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_pymol_toolbar.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_shift_hscroll.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_split_button_style.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_step_dialog.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_table_toolbar.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_table_view_state.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_theme_contrast.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/tests/test_tutorial.py +0 -0
- {ms_components-0.3.0 → ms_components-0.3.2}/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.2
|
|
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
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
The reference below is generated directly from the current Python sources.
|
|
4
|
+
Names beginning with an underscore are implementation details.
|
|
5
|
+
|
|
6
|
+
## Smart table
|
|
7
|
+
|
|
8
|
+
::: ms_components.ms_table
|
|
9
|
+
|
|
10
|
+
## Monitor
|
|
11
|
+
|
|
12
|
+
::: ms_components.ms_monitor
|
|
13
|
+
|
|
14
|
+
## Hosts and executors
|
|
15
|
+
|
|
16
|
+
::: ms_components.ms_executors
|
|
17
|
+
|
|
18
|
+
## Projects
|
|
19
|
+
|
|
20
|
+
::: ms_components.ms_projects
|
|
21
|
+
|
|
22
|
+
## Settings
|
|
23
|
+
|
|
24
|
+
::: ms_components.ms_settings
|
|
25
|
+
|
|
26
|
+
## PyMOL
|
|
27
|
+
|
|
28
|
+
::: ms_components.ms_pymol
|
|
29
|
+
|
|
30
|
+
Comparison scenes provide grid and overlay layouts without reloading structures
|
|
31
|
+
or changing the camera. Assign each structure/pose pair the same positive PyMOL
|
|
32
|
+
`grid_slot`, then pass its display name:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
dock.set_scene_context(
|
|
36
|
+
"comparison",
|
|
37
|
+
grid_labels={1: "Complex A", 3: "Complex B"},
|
|
38
|
+
)
|
|
39
|
+
dock.set_comparison_layout("overlay")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`grid_labels` is also accepted by `PymolControlBar` and `PymolSceneContext`.
|
|
43
|
+
Without explicit labels, molecular object titles (or object names when no title
|
|
44
|
+
is available) are joined per positive `grid_slot` within the context's `target`.
|
|
45
|
+
Applications must pass display names if those names are not present in PyMOL.
|
|
46
|
+
|
|
47
|
+
The **Display > Comparison > Grid / Overlay** menu appears only in comparison
|
|
48
|
+
scenes. The chosen layout persists when changing poses or applying presets.
|
|
49
|
+
Overlay displays the existing coordinates together; alignment remains the
|
|
50
|
+
caller's responsibility.
|
|
51
|
+
|
|
52
|
+
Each label is a pseudoatom named `panel_text<slot>`, initially sized 14.
|
|
53
|
+
Grid labels appear at the top-left of their panel; overlay labels form a stacked
|
|
54
|
+
legend that follows viewport resizing. Receptor cartoons, carbon atoms and label
|
|
55
|
+
text share the slot's `COMPARISON_COLORS` color. Heteroatom colors are preserved.
|
|
56
|
+
The label outline uses PyMOL's background-adaptive `front` color for readability
|
|
57
|
+
on light and dark backgrounds. The palette has ten colors and repeats beyond ten
|
|
58
|
+
slots.
|
|
59
|
+
Existing labels are updated in place, so individual settings remain editable,
|
|
60
|
+
for example `cmd.set("label_size", 18, "panel_text3")`. Presets restore label
|
|
61
|
+
visibility after changing molecular representations.
|
|
62
|
+
|
|
63
|
+
To replace one pose without rebuilding the scene, pass its bytes directly:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
dock.replace_comparison_object(
|
|
67
|
+
"pose_a", pdb_bytes, "pdb",
|
|
68
|
+
reference="receptor_a", label="Complex A [P2]",
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The new object inherits the reference's transformation and slot, is shown as
|
|
73
|
+
sticks, and receives the same comparison color. Camera and auto-zoom settings
|
|
74
|
+
are preserved. The previous object remains available until the replacement loads
|
|
75
|
+
successfully. This API neither fetches data nor calls the hosting application.
|
|
76
|
+
Hosts remain responsible for reading their data and discarding stale requests.
|
|
77
|
+
|
|
78
|
+
Setting any non-comparison scene context after a comparison context removes
|
|
79
|
+
the managed labels and sets `grid_mode` to zero. Applications should update
|
|
80
|
+
the scene context when leaving their off-target view, even without loading a
|
|
81
|
+
new structure. Unrelated grid scenes and user-created annotations are not
|
|
82
|
+
removed. `"offtarget"` remains accepted as an alias for `"comparison"`.
|
|
83
|
+
|
|
84
|
+
## Stepper
|
|
85
|
+
|
|
86
|
+
::: ms_components.ms_stepper
|
|
87
|
+
|
|
88
|
+
## Guided tutorials
|
|
89
|
+
|
|
90
|
+
::: ms_components.ms_tutorial
|
|
91
|
+
|
|
92
|
+
## Tool panel
|
|
93
|
+
|
|
94
|
+
::: ms_components.tool_panel
|
|
95
|
+
|
|
96
|
+
## Step dialog
|
|
97
|
+
|
|
98
|
+
::: ms_components.step_dialog
|
|
99
|
+
|
|
100
|
+
## Theme
|
|
101
|
+
|
|
102
|
+
::: ms_components.theme
|
|
@@ -178,6 +178,19 @@ panel.show()
|
|
|
178
178
|
|
|
179
179
|
## Projects, settings, and PyMOL
|
|
180
180
|
|
|
181
|
+
Applications embedding PyMOL should call
|
|
182
|
+
`ms_components.opengl.prepare_pymol_opengl()` **before creating `QApplication`**.
|
|
183
|
+
It requests desktop OpenGL and shared contexts, then checks the actual context
|
|
184
|
+
and compiles legacy GLSL shaders in an isolated process. A failed native check
|
|
185
|
+
can select `xcb` on Linux only when an X11 display passes the same check and
|
|
186
|
+
`QT_QPA_PLATFORM` was not explicitly set. Windows and macOS retain their native
|
|
187
|
+
platforms. If no tested backend works, it raises `RuntimeError` before the
|
|
188
|
+
application starts. Skip this call when PyMOL is disabled or running headlessly.
|
|
189
|
+
|
|
190
|
+
`PymolDockWidget` also requests desktop OpenGL on its own widget and validates
|
|
191
|
+
the actual context before drawing. An incompatible context displays an error
|
|
192
|
+
in the viewer instead of passing it to PyMOL.
|
|
193
|
+
|
|
181
194
|
- `ProjectsMenuWidget(app_id=...)` and `ProjectBrowserWindow(app_id=...)` list
|
|
182
195
|
and create projects through the public `ms_flow` project catalog. Connect
|
|
183
196
|
`project_requested` to open the chosen project; do not read the MolSuite
|
|
@@ -64,6 +64,19 @@ QProgressBar#ToolProgress::chunk {
|
|
|
64
64
|
background: palette(highlight);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* ms_table: a PROGRESS cell, a filled bar with its text on top. The fill color is
|
|
68
|
+
the `tone` property, from the theme accents (theme.tone_qss). The track is the
|
|
69
|
+
header tone: it keeps text contrast and stands out from the rows. */
|
|
70
|
+
QProgressBar#CellProgress {
|
|
71
|
+
border: none;
|
|
72
|
+
border-radius: 3px;
|
|
73
|
+
background: palette(dark);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
QProgressBar#CellProgress::chunk {
|
|
77
|
+
border-radius: 3px;
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
|
|
68
81
|
/* Left gap for the first tab: `tab-bar { left }` shifts the whole bar.
|
|
69
82
|
Do NOT use margins on ::tab -- Qt places the close button flush to the full
|
|
@@ -154,6 +167,14 @@ QToolButton[popupMode="MenuButtonPopup"]::menu-button {
|
|
|
154
167
|
width: 18px;
|
|
155
168
|
}
|
|
156
169
|
|
|
170
|
+
/* Qt's own tooltip colours are a pale grey that dark themes leave unreadable. */
|
|
171
|
+
QToolTip {
|
|
172
|
+
background: palette(window);
|
|
173
|
+
color: palette(text);
|
|
174
|
+
border: 1px solid palette(highlight);
|
|
175
|
+
padding: 4px;
|
|
176
|
+
}
|
|
177
|
+
|
|
157
178
|
/* Card = content surface. Nav trees/lists stay flat unless opted in. */
|
|
158
179
|
QTableView,
|
|
159
180
|
QTreeView[card="true"],
|
|
@@ -734,7 +734,10 @@ class VerticalToolbar(QWidget):
|
|
|
734
734
|
yield w
|
|
735
735
|
|
|
736
736
|
def _layout_has_buttons(self, layout: QVBoxLayout) -> bool:
|
|
737
|
-
|
|
737
|
+
# Action buttons (no dock_id) count too: a section holding only those is not empty.
|
|
738
|
+
return any(
|
|
739
|
+
isinstance(layout.itemAt(i).widget(), QToolButton) for i in range(layout.count())
|
|
740
|
+
)
|
|
738
741
|
|
|
739
742
|
def _update_separator_visibility(self):
|
|
740
743
|
self._separator.setVisible(
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
from collections.abc import Callable, Mapping
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from ms_components.ms_dockwidget.widget import MSDockWidget
|
|
11
|
+
from ms_components.opengl import pymol_context_error, pymol_surface_format
|
|
10
12
|
|
|
11
13
|
os.environ.setdefault("QT_API", "pyside6")
|
|
12
14
|
|
|
13
15
|
from PySide6.QtCore import QByteArray, QEvent, QSize, Qt, QTimer, Signal
|
|
14
|
-
from PySide6.QtGui import QAction, QActionGroup, QIcon, QPainter, QPixmap
|
|
16
|
+
from PySide6.QtGui import QAction, QActionGroup, QGuiApplication, QIcon, QPainter, QPixmap
|
|
15
17
|
from PySide6.QtSvg import QSvgRenderer
|
|
16
18
|
from PySide6.QtWidgets import (
|
|
17
19
|
QFrame,
|
|
@@ -36,6 +38,44 @@ except ImportError:
|
|
|
36
38
|
from . import plugins as _pymol_plugins # noqa: F401
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
class _PymolGLWidget(PyMOLGLWidget):
|
|
42
|
+
def __init__(self, parent):
|
|
43
|
+
super().__init__(parent)
|
|
44
|
+
self._opengl_error: str | None = None
|
|
45
|
+
self._opengl_error_label: QLabel | None = None
|
|
46
|
+
if hasattr(self, "setFormat"):
|
|
47
|
+
self.setFormat(pymol_surface_format(self.format()))
|
|
48
|
+
|
|
49
|
+
def initializeGL(self):
|
|
50
|
+
context = self.context()
|
|
51
|
+
self._opengl_error = pymol_context_error(context)
|
|
52
|
+
if self._opengl_error_label is not None:
|
|
53
|
+
self._opengl_error_label.hide()
|
|
54
|
+
if self._opengl_error:
|
|
55
|
+
logging.getLogger(__name__).error(self._opengl_error)
|
|
56
|
+
if self._opengl_error_label is None:
|
|
57
|
+
self._opengl_error_label = QLabel(self)
|
|
58
|
+
self._opengl_error_label.setWordWrap(True)
|
|
59
|
+
QVBoxLayout(self).addWidget(self._opengl_error_label)
|
|
60
|
+
self._opengl_error_label.setText(f"PyMOL viewer unavailable.\n{self._opengl_error}")
|
|
61
|
+
self._opengl_error_label.show()
|
|
62
|
+
return
|
|
63
|
+
fmt = context.format()
|
|
64
|
+
logging.getLogger(__name__).info(
|
|
65
|
+
"PyMOL context: %s, OpenGL %s.%s %s",
|
|
66
|
+
QGuiApplication.platformName(), *fmt.version(), fmt.profile().name,
|
|
67
|
+
)
|
|
68
|
+
super().initializeGL()
|
|
69
|
+
|
|
70
|
+
def paintGL(self):
|
|
71
|
+
if self._opengl_error is None:
|
|
72
|
+
super().paintGL()
|
|
73
|
+
|
|
74
|
+
def resizeGL(self, width, height):
|
|
75
|
+
if self._opengl_error is None:
|
|
76
|
+
super().resizeGL(width, height)
|
|
77
|
+
|
|
78
|
+
|
|
39
79
|
SELECTION_MODES = (
|
|
40
80
|
("Atoms", 0),
|
|
41
81
|
("Residues", 1),
|
|
@@ -53,6 +93,10 @@ REPRESENTATIONS = (
|
|
|
53
93
|
)
|
|
54
94
|
|
|
55
95
|
_TOOLBAR_ICONS = Path(__file__).parent / "icons"
|
|
96
|
+
COMPARISON_COLORS = (
|
|
97
|
+
"orange", "tv_green", "cyan", "magenta", "yellow",
|
|
98
|
+
"salmon", "violet", "lime", "deepteal", "hotpink",
|
|
99
|
+
)
|
|
56
100
|
|
|
57
101
|
|
|
58
102
|
@dataclass(frozen=True)
|
|
@@ -61,6 +105,11 @@ class PymolSceneContext:
|
|
|
61
105
|
target: str = "all"
|
|
62
106
|
selections: Mapping[str, str] = field(default_factory=dict)
|
|
63
107
|
default_preset: str = ""
|
|
108
|
+
grid_labels: Mapping[int, str] = field(default_factory=dict)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def is_comparison(self) -> bool:
|
|
112
|
+
return self.kind in {"comparison", "offtarget"}
|
|
64
113
|
|
|
65
114
|
def selection(self, role: str, fallback: str = "") -> str:
|
|
66
115
|
value = str(dict(self.selections or {}).get(str(role)) or "").strip()
|
|
@@ -100,6 +149,8 @@ class PymolControlBar(QFrame):
|
|
|
100
149
|
super().__init__(parent)
|
|
101
150
|
self._cmd = cmd
|
|
102
151
|
self._context = PymolSceneContext()
|
|
152
|
+
self._grid_label_objects: set[str] = set()
|
|
153
|
+
self._comparison_layout = "grid"
|
|
103
154
|
self._presets: dict[str, PymolPresetSpec] = {}
|
|
104
155
|
self.setObjectName("PymolControlBar")
|
|
105
156
|
|
|
@@ -168,6 +219,18 @@ class PymolControlBar(QFrame):
|
|
|
168
219
|
"Apply a representation to the current selection or scene target.",
|
|
169
220
|
menu=representation_menu,
|
|
170
221
|
)
|
|
222
|
+
self.comparison_menu = representation_menu.addMenu("Comparison")
|
|
223
|
+
self.comparison_menu.menuAction().setVisible(False)
|
|
224
|
+
comparison_group = QActionGroup(self.comparison_menu)
|
|
225
|
+
comparison_group.setExclusive(True)
|
|
226
|
+
self._comparison_layout_actions: dict[str, QAction] = {}
|
|
227
|
+
for label, value in (("Grid", "grid"), ("Overlay", "overlay")):
|
|
228
|
+
action = self.comparison_menu.addAction(label)
|
|
229
|
+
action.setCheckable(True)
|
|
230
|
+
action.setChecked(value == self._comparison_layout)
|
|
231
|
+
comparison_group.addAction(action)
|
|
232
|
+
action.triggered.connect(lambda checked=False, layout=value: self.set_comparison_layout(layout))
|
|
233
|
+
self._comparison_layout_actions[value] = action
|
|
171
234
|
|
|
172
235
|
hide_menu = QMenu("Hide", bar)
|
|
173
236
|
for label, value in (("Everything", "everything"), *REPRESENTATIONS):
|
|
@@ -359,6 +422,7 @@ class PymolControlBar(QFrame):
|
|
|
359
422
|
target: str = "all",
|
|
360
423
|
selections: Mapping[str, str] | None = None,
|
|
361
424
|
default_preset: str = "",
|
|
425
|
+
grid_labels: Mapping[int, str] | None = None,
|
|
362
426
|
) -> None:
|
|
363
427
|
if isinstance(context, PymolSceneContext):
|
|
364
428
|
normalized = context
|
|
@@ -372,11 +436,136 @@ class PymolControlBar(QFrame):
|
|
|
372
436
|
if str(key).strip() and str(value).strip()
|
|
373
437
|
},
|
|
374
438
|
default_preset=str(default_preset or "").strip(),
|
|
439
|
+
grid_labels=dict(grid_labels or {}),
|
|
375
440
|
)
|
|
441
|
+
was_comparison = self._context.is_comparison
|
|
376
442
|
self._context = normalized
|
|
443
|
+
self.comparison_menu.menuAction().setVisible(normalized.is_comparison)
|
|
444
|
+
if was_comparison or normalized.is_comparison:
|
|
445
|
+
self._run_command("Grid labels", self._sync_grid_labels)
|
|
377
446
|
self.refresh_context_label()
|
|
378
447
|
self._refresh_preset_menu()
|
|
379
448
|
|
|
449
|
+
@property
|
|
450
|
+
def comparison_layout(self) -> str:
|
|
451
|
+
return self._comparison_layout
|
|
452
|
+
|
|
453
|
+
def set_comparison_layout(self, layout: str) -> None:
|
|
454
|
+
if layout not in self._comparison_layout_actions:
|
|
455
|
+
raise ValueError(f"Unsupported comparison layout: {layout}")
|
|
456
|
+
if not self._context.is_comparison:
|
|
457
|
+
raise ValueError("A comparison scene is required to change its layout.")
|
|
458
|
+
previous = self._comparison_layout
|
|
459
|
+
self._comparison_layout = layout
|
|
460
|
+
|
|
461
|
+
def apply() -> None:
|
|
462
|
+
self._sync_grid_labels()
|
|
463
|
+
self._cmd.refresh()
|
|
464
|
+
|
|
465
|
+
if not self._run_command("Comparison layout", apply):
|
|
466
|
+
self._comparison_layout = previous
|
|
467
|
+
for value, action in self._comparison_layout_actions.items():
|
|
468
|
+
action.setChecked(value == self._comparison_layout)
|
|
469
|
+
|
|
470
|
+
def _sync_grid_labels(self, *, color_objects: bool = True) -> None:
|
|
471
|
+
labels = dict(self._context.grid_labels)
|
|
472
|
+
if self._context.is_comparison:
|
|
473
|
+
self._cmd.set("grid_mode", int(self._comparison_layout == "grid"))
|
|
474
|
+
slot_names: dict[int, list[str]] = {}
|
|
475
|
+
for obj in self._cmd.get_object_list(self._context.target):
|
|
476
|
+
if obj in self._grid_label_objects:
|
|
477
|
+
continue
|
|
478
|
+
slot = self._cmd.get_setting_int("grid_slot", obj)
|
|
479
|
+
if slot > 0:
|
|
480
|
+
color = COMPARISON_COLORS[(slot - 1) % len(COMPARISON_COLORS)]
|
|
481
|
+
if color_objects:
|
|
482
|
+
self._cmd.set("cartoon_color", color, selection=obj)
|
|
483
|
+
self._cmd.color(color, f"({obj}) and elem C")
|
|
484
|
+
slot_names.setdefault(slot, []).append(self._cmd.get_title(obj, 1) or obj)
|
|
485
|
+
if not labels:
|
|
486
|
+
labels = {slot: " + ".join(names) for slot, names in slot_names.items()}
|
|
487
|
+
else:
|
|
488
|
+
labels = {}
|
|
489
|
+
self._cmd.set("grid_mode", 0)
|
|
490
|
+
|
|
491
|
+
wanted = {f"panel_text{slot}" for slot in labels}
|
|
492
|
+
for obj in self._grid_label_objects - wanted:
|
|
493
|
+
self._cmd.delete(obj)
|
|
494
|
+
self._grid_label_objects.intersection_update(wanted)
|
|
495
|
+
existing = set(self._cmd.get_names("objects"))
|
|
496
|
+
for slot, title in sorted(labels.items()):
|
|
497
|
+
if slot <= 0:
|
|
498
|
+
raise ValueError("Grid label slots must be positive integers.")
|
|
499
|
+
obj = f"panel_text{slot}"
|
|
500
|
+
if obj not in existing:
|
|
501
|
+
self._cmd.pseudoatom(obj, label=title)
|
|
502
|
+
self._grid_label_objects.add(obj)
|
|
503
|
+
self._cmd.hide("everything", obj)
|
|
504
|
+
self._cmd.set("label_relative_mode", 1, selection=obj)
|
|
505
|
+
self._cmd.set("label_size", 14, selection=obj)
|
|
506
|
+
self._cmd.set("label_position", [1, -1, 0], selection=obj)
|
|
507
|
+
self._cmd.set("grid_slot", slot, selection=obj)
|
|
508
|
+
elif obj in self._grid_label_objects:
|
|
509
|
+
self._cmd.label(obj, repr(title))
|
|
510
|
+
else:
|
|
511
|
+
raise ValueError(f"Grid label object '{obj}' already exists.")
|
|
512
|
+
color = COMPARISON_COLORS[(slot - 1) % len(COMPARISON_COLORS)]
|
|
513
|
+
self._cmd.set("label_color", color, selection=obj)
|
|
514
|
+
self._cmd.set("label_outline_color", "front", selection=obj)
|
|
515
|
+
self._cmd.show("labels", obj)
|
|
516
|
+
self._position_comparison_labels()
|
|
517
|
+
|
|
518
|
+
def _position_comparison_labels(self) -> None:
|
|
519
|
+
if not self._context.is_comparison:
|
|
520
|
+
return
|
|
521
|
+
existing = set(self._cmd.get_names("objects"))
|
|
522
|
+
height = max(1, self._cmd.get_viewport()[1])
|
|
523
|
+
offset = 0.0
|
|
524
|
+
for obj in sorted(self._grid_label_objects & existing,
|
|
525
|
+
key=lambda name: self._cmd.get_setting_int("grid_slot", name)):
|
|
526
|
+
self._cmd.set("label_screen_point", [-1, 1 - offset, 0], selection=obj)
|
|
527
|
+
if self._comparison_layout == "overlay":
|
|
528
|
+
offset += 2 * (abs(self._cmd.get_setting_float("label_size", obj)) + 6) / height
|
|
529
|
+
|
|
530
|
+
def replace_comparison_object(
|
|
531
|
+
self, name: str, data: bytes, fmt: str, *, reference: str, label: str,
|
|
532
|
+
) -> bool:
|
|
533
|
+
def apply() -> None:
|
|
534
|
+
if not self._context.is_comparison:
|
|
535
|
+
raise ValueError("A comparison scene is required to replace an object.")
|
|
536
|
+
objects = self._cmd.get_names("objects")
|
|
537
|
+
if name not in objects or reference not in objects:
|
|
538
|
+
raise ValueError("The comparison object and its reference must already exist.")
|
|
539
|
+
if name == reference:
|
|
540
|
+
raise ValueError("The comparison object must differ from its reference.")
|
|
541
|
+
slot = self._cmd.get_setting_int("grid_slot", reference)
|
|
542
|
+
if slot <= 0:
|
|
543
|
+
raise ValueError("The comparison reference must have a positive grid slot.")
|
|
544
|
+
auto_zoom = self._cmd.get_setting_int("auto_zoom")
|
|
545
|
+
temporary = self._cmd.get_unused_name("_comparison_pose")
|
|
546
|
+
try:
|
|
547
|
+
self._cmd.set("auto_zoom", 0)
|
|
548
|
+
self._cmd.load_raw(data, fmt, temporary)
|
|
549
|
+
self._cmd.matrix_copy(reference, temporary)
|
|
550
|
+
self._cmd.set("grid_slot", slot, temporary)
|
|
551
|
+
self._cmd.show("sticks", temporary)
|
|
552
|
+
self._cmd.color("atomic", temporary)
|
|
553
|
+
color = COMPARISON_COLORS[(slot - 1) % len(COMPARISON_COLORS)]
|
|
554
|
+
self._cmd.set("cartoon_color", color, temporary)
|
|
555
|
+
self._cmd.color(color, f"({temporary}) and elem C")
|
|
556
|
+
self._cmd.delete(name)
|
|
557
|
+
self._cmd.set_name(temporary, name)
|
|
558
|
+
self._context = replace(
|
|
559
|
+
self._context, grid_labels={**self._context.grid_labels, slot: label},
|
|
560
|
+
)
|
|
561
|
+
self._sync_grid_labels(color_objects=False)
|
|
562
|
+
finally:
|
|
563
|
+
if temporary in self._cmd.get_names("objects"):
|
|
564
|
+
self._cmd.delete(temporary)
|
|
565
|
+
self._cmd.set("auto_zoom", auto_zoom)
|
|
566
|
+
|
|
567
|
+
return self._run_command("Replace comparison object", apply)
|
|
568
|
+
|
|
380
569
|
def register_preset(self, spec: PymolPresetSpec) -> None:
|
|
381
570
|
key = str(spec.key or "").strip()
|
|
382
571
|
if not key:
|
|
@@ -507,6 +696,8 @@ class PymolControlBar(QFrame):
|
|
|
507
696
|
|
|
508
697
|
def apply() -> None:
|
|
509
698
|
spec.callback(self._cmd, self._context)
|
|
699
|
+
if self._context.is_comparison:
|
|
700
|
+
self._sync_grid_labels()
|
|
510
701
|
|
|
511
702
|
if self._run_command(spec.label, apply):
|
|
512
703
|
self.preset_applied.emit(normalized_key)
|
|
@@ -585,13 +776,13 @@ class PymolDockWidget(MSDockWidget):
|
|
|
585
776
|
|
|
586
777
|
def __init__(self, title: str, manager, parent=None):
|
|
587
778
|
super().__init__(title, manager, parent)
|
|
588
|
-
self.pymol_widget =
|
|
589
|
-
self.pymol_widget.installEventFilter(self)
|
|
779
|
+
self.pymol_widget = _PymolGLWidget(self)
|
|
590
780
|
if self.cmd is not None:
|
|
591
781
|
self.cmd.set("internal_gui", False)
|
|
592
782
|
|
|
593
783
|
self.control_bar = PymolControlBar(self.cmd, self)
|
|
594
784
|
self.toolbar = self.control_bar
|
|
785
|
+
self.pymol_widget.installEventFilter(self)
|
|
595
786
|
|
|
596
787
|
self._side_panel_widget: QWidget | None = None
|
|
597
788
|
self._splitter = QSplitter(Qt.Horizontal, self)
|
|
@@ -640,17 +831,31 @@ class PymolDockWidget(MSDockWidget):
|
|
|
640
831
|
target: str = "all",
|
|
641
832
|
selections: Mapping[str, str] | None = None,
|
|
642
833
|
default_preset: str = "",
|
|
834
|
+
grid_labels: Mapping[int, str] | None = None,
|
|
643
835
|
) -> None:
|
|
644
836
|
self.control_bar.set_scene_context(
|
|
645
837
|
context,
|
|
646
838
|
target=target,
|
|
647
839
|
selections=selections,
|
|
648
840
|
default_preset=default_preset,
|
|
841
|
+
grid_labels=grid_labels,
|
|
649
842
|
)
|
|
650
843
|
|
|
651
844
|
def register_preset(self, spec: PymolPresetSpec) -> None:
|
|
652
845
|
self.control_bar.register_preset(spec)
|
|
653
846
|
|
|
847
|
+
def set_comparison_layout(self, layout: str) -> None:
|
|
848
|
+
self.control_bar.set_comparison_layout(layout)
|
|
849
|
+
|
|
850
|
+
@property
|
|
851
|
+
def comparison_layout(self) -> str:
|
|
852
|
+
return self.control_bar.comparison_layout
|
|
853
|
+
|
|
854
|
+
def replace_comparison_object(
|
|
855
|
+
self, name: str, data: bytes, fmt: str, *, reference: str, label: str,
|
|
856
|
+
) -> bool:
|
|
857
|
+
return self.control_bar.replace_comparison_object(name, data, fmt, reference=reference, label=label)
|
|
858
|
+
|
|
654
859
|
def unregister_preset(self, key: str) -> None:
|
|
655
860
|
self.control_bar.unregister_preset(key)
|
|
656
861
|
|
|
@@ -687,10 +892,13 @@ class PymolDockWidget(MSDockWidget):
|
|
|
687
892
|
def eventFilter(self, watched, event):
|
|
688
893
|
if watched is self.pymol_widget and event.type() == QEvent.Type.MouseButtonRelease:
|
|
689
894
|
QTimer.singleShot(0, self.control_bar.refresh_context_label)
|
|
895
|
+
elif watched is self.pymol_widget and event.type() == QEvent.Type.Resize and self.scene_context.is_comparison:
|
|
896
|
+
QTimer.singleShot(0, self.control_bar._position_comparison_labels)
|
|
690
897
|
return super().eventFilter(watched, event)
|
|
691
898
|
|
|
692
899
|
|
|
693
900
|
__all__ = [
|
|
901
|
+
"COMPARISON_COLORS",
|
|
694
902
|
"PymolControlBar",
|
|
695
903
|
"PymolDockWidget",
|
|
696
904
|
"PymolPresetSpec",
|
|
@@ -58,10 +58,10 @@ from __future__ import annotations
|
|
|
58
58
|
|
|
59
59
|
from typing import Any, Callable, Optional
|
|
60
60
|
|
|
61
|
-
from PySide6.QtCore import QModelIndex, QRect, QRectF, QSize, Qt
|
|
62
|
-
from PySide6.QtGui import QImage, QPainter,
|
|
61
|
+
from PySide6.QtCore import QModelIndex, QPoint, QRect, QRectF, QSize, Qt
|
|
62
|
+
from PySide6.QtGui import QImage, QPainter, QPixmap, QRegion
|
|
63
63
|
from PySide6.QtWidgets import (
|
|
64
|
-
QApplication, QStyle, QStyledItemDelegate, QStyleOptionViewItem,
|
|
64
|
+
QApplication, QProgressBar, QStyle, QStyledItemDelegate, QStyleOptionViewItem,
|
|
65
65
|
QWidget,
|
|
66
66
|
)
|
|
67
67
|
|
|
@@ -159,6 +159,8 @@ class CellWidgetDelegate(QStyledItemDelegate):
|
|
|
159
159
|
# Persistent widget cache: row → QWidget
|
|
160
160
|
# Invalidated when the page changes (load_data)
|
|
161
161
|
self._persistent_widgets: dict[int, QWidget] = {}
|
|
162
|
+
# PROGRESS cells: one hidden bar per tone, so the tone property is set once.
|
|
163
|
+
self._progress_bars: dict[str, QProgressBar] = {}
|
|
162
164
|
|
|
163
165
|
# ── Invalidate the cache on page change ───────────────────────────────
|
|
164
166
|
|
|
@@ -185,30 +187,40 @@ class CellWidgetDelegate(QStyledItemDelegate):
|
|
|
185
187
|
|
|
186
188
|
row_data = self._get_row_data(index)
|
|
187
189
|
|
|
188
|
-
# ── progress mode: a fraction 0..1 as the style's own progress bar ──
|
|
190
|
+
# ── progress mode: a fraction 0..1 (or value/max) as the style's own progress bar ──
|
|
189
191
|
if col_def.kind == ColumnKind.PROGRESS:
|
|
190
192
|
try:
|
|
191
|
-
|
|
193
|
+
value = float(row_data.get(col_def.display_key) or 0.0)
|
|
194
|
+
maximum = float(row_data.get(col_def.progress_max) or 0.0) if col_def.progress_max else 1.0
|
|
195
|
+
fraction = min(max(value / maximum, 0.0), 1.0) if maximum > 0 else 0.0
|
|
192
196
|
except (TypeError, ValueError):
|
|
193
|
-
fraction = 0.0
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
197
|
+
value, maximum, fraction = 0.0, 0.0, 0.0
|
|
198
|
+
text = f"{int(fraction * 100)}%" # floored: 99.9% is not "100%"
|
|
199
|
+
if col_def.progress_max:
|
|
200
|
+
text = f"{int(value):,}/{int(maximum):,} ({text})"
|
|
201
|
+
tone = col_def.progress_tone(row_data) if col_def.progress_tone else "green"
|
|
202
|
+
# A hidden QProgressBar paints the cell, so the bar is the native one and its look is
|
|
203
|
+
# QSS (`QProgressBar#CellProgress`, `[tone=...]`). render(), not drawControl(): under a
|
|
204
|
+
# stylesheet drawControl lays the chunk out from the widget, not from the option. Into a
|
|
205
|
+
# pixmap: rendered on the view's painter it ignores the viewport offset and lands under
|
|
206
|
+
# the headers.
|
|
207
|
+
bar = self._progress_bars.get(tone)
|
|
208
|
+
if bar is None:
|
|
209
|
+
bar = self._progress_bars[tone] = QProgressBar()
|
|
210
|
+
bar.setObjectName("CellProgress")
|
|
211
|
+
bar.setProperty("tone", tone)
|
|
212
|
+
bar.setRange(0, 1000)
|
|
213
|
+
bar.setAlignment(Qt.AlignCenter)
|
|
214
|
+
bar.setValue(int(fraction * 1000))
|
|
215
|
+
bar.setFormat(text)
|
|
216
|
+
rect = option.rect.adjusted(4, 5, -4, -5)
|
|
217
|
+
bar.resize(rect.size())
|
|
218
|
+
ratio = painter.device().devicePixelRatioF()
|
|
219
|
+
pixmap = QPixmap(rect.size() * ratio)
|
|
220
|
+
pixmap.setDevicePixelRatio(ratio)
|
|
221
|
+
pixmap.fill(Qt.transparent)
|
|
222
|
+
bar.render(pixmap, QPoint(), QRegion(), QWidget.DrawChildren)
|
|
223
|
+
painter.drawPixmap(rect.topLeft(), pixmap)
|
|
212
224
|
return
|
|
213
225
|
|
|
214
226
|
# ── paint_factory mode (SVG / image) ───────────────────────────────
|
|
@@ -234,11 +234,13 @@ class QueryBuilder:
|
|
|
234
234
|
attr = self._resolve_attr(sort.field)
|
|
235
235
|
if attr is None:
|
|
236
236
|
return pk > pk_value
|
|
237
|
+
# SQLite puts NULLs first ascending and last descending.
|
|
238
|
+
rest_of_nulls = and_(attr.is_(None), pk > pk_value)
|
|
237
239
|
if sort_value is None:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return or_(
|
|
240
|
+
return or_(attr.is_not(None), rest_of_nulls) if not sort.descending else rest_of_nulls
|
|
241
|
+
if sort.descending:
|
|
242
|
+
return or_(attr < sort_value, and_(attr == sort_value, pk > pk_value), attr.is_(None))
|
|
243
|
+
return or_(attr > sort_value, and_(attr == sort_value, pk > pk_value))
|
|
242
244
|
|
|
243
245
|
def fetch(self) -> tuple[list[dict], int]:
|
|
244
246
|
"""Run the query and return (rows_as_dicts, total_count)."""
|
|
@@ -154,6 +154,15 @@ class SmartTableView(QWidget):
|
|
|
154
154
|
self._infinite_total_hint = total
|
|
155
155
|
self._infinite_total_exact = True
|
|
156
156
|
self._infinite_has_more = self._infinite_loaded_end < total
|
|
157
|
+
tail = self._infinite_pages.get(self._infinite_frontier_page)
|
|
158
|
+
if tail is not None and len(tail) < self._builder.page_size and self._infinite_has_more:
|
|
159
|
+
# The last page came back short and rows were added since: forget it, so the
|
|
160
|
+
# autofill set_total() triggers fetches it again once it is in view. Kept, its
|
|
161
|
+
# rows past the old end would stay blank forever.
|
|
162
|
+
del self._infinite_pages[self._infinite_frontier_page]
|
|
163
|
+
# The next page's cursor is the short page's last row: wrong now, so it is
|
|
164
|
+
# found by offset instead.
|
|
165
|
+
self._infinite_page_cursors.pop(self._infinite_frontier_page + 1, None)
|
|
157
166
|
self._model.set_total(total)
|
|
158
167
|
return True
|
|
159
168
|
|
|
@@ -749,8 +758,9 @@ class SmartTableView(QWidget):
|
|
|
749
758
|
self.load_next_page()
|
|
750
759
|
|
|
751
760
|
def _on_refresh_clicked(self):
|
|
752
|
-
# Reload this table
|
|
753
|
-
|
|
761
|
+
# Reload this table where the user is looking (scroll and selection kept), then let
|
|
762
|
+
# the host (e.g. DockingStudio) refresh surrounding state.
|
|
763
|
+
self.refresh_preserving_view()
|
|
754
764
|
self.refresh_clicked.emit()
|
|
755
765
|
|
|
756
766
|
# ──────────────────────────────────────────
|