ms-components 0.3.2__tar.gz → 0.3.4__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.2/src/ms_components.egg-info → ms_components-0.3.4}/PKG-INFO +1 -1
- ms_components-0.3.4/src/ms_components/backdrop.py +34 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/base.qss +16 -4
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/__init__.py +2 -0
- ms_components-0.3.4/src/ms_components/ms_pymol/icons/hydrogens-polar.svg +5 -0
- ms_components-0.3.4/src/ms_components/ms_pymol/icons/hydrogens.svg +4 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/widget.py +100 -2
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_settings/panel.py +3 -2
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/query_builder.py +68 -36
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/smart_table_view.py +153 -64
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/table_config.py +2 -0
- ms_components-0.3.4/src/ms_components/palettes.py +39 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/theme.py +80 -135
- {ms_components-0.3.2 → ms_components-0.3.4/src/ms_components.egg-info}/PKG-INFO +1 -1
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components.egg-info/SOURCES.txt +3 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components.egg-info/scm_file_list.json +3 -0
- ms_components-0.3.4/src/ms_components.egg-info/scm_version.json +8 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_opengl.py +2 -1
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_pymol_grid.py +78 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_pymol_toolbar.py +57 -1
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_query_builder_keyset.py +79 -19
- ms_components-0.3.4/tests/test_theme_contrast.py +68 -0
- ms_components-0.3.2/src/ms_components/palettes.py +0 -29
- ms_components-0.3.2/src/ms_components.egg-info/scm_version.json +0 -8
- ms_components-0.3.2/tests/test_theme_contrast.py +0 -31
- {ms_components-0.3.2 → ms_components-0.3.4}/.github/workflows/docs.yml +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/.github/workflows/publish.yml +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/.gitignore +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/LICENSE +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/README.md +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/docs/api_reference.md +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/docs/examples.md +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/docs/index.md +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/docs/ms_tutorial.md +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/docs/usage.md +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/examples/tutorial_demo.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/examples/tutorial_demo.toml +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/pyproject.toml +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/setup.cfg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/chips.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/listview.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_dockwidget/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_dockwidget/widget.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_executors.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/bridge.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/cards.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/config.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/detail_panel.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/formatting.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/job_cards.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/models.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/palette.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/polling.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_monitor/widget.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_projects/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_projects/app.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_projects/browser.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_projects/scratch.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_projects/widget.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/box.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/center.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/display.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_pymol/plugins.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_settings/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_stepper.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/action_header.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/cell_delegate.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/column_panel.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/example_usage.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/filter_panel.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/columns.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/export.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/filter.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/menu.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/reload.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/search.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/settings.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/icons/sort.svg +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/pagination_control.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/session_provider.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/sort_panel.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/table_model.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/test_external_filters.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_table/test_toolbar.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_tutorial/__init__.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_tutorial/bindings.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_tutorial/controller.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_tutorial/models.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/ms_tutorial/overlay.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/opengl.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/pagination.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/step_dialog.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/tool_panel.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components/wheel.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components.egg-info/dependency_links.txt +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components.egg-info/requires.txt +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/src/ms_components.egg-info/top_level.txt +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_app_settings.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_chips.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_dock_window.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_executors_ui.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_molsuite_monitor_bridge.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_molsuite_monitor_widgets.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_monitor_config.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_ms_projects_widget.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_progress_column.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_project_browser.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_shift_hscroll.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_split_button_style.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_step_dialog.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_table_toolbar.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_table_view_state.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/tests/test_tutorial.py +0 -0
- {ms_components-0.3.2 → ms_components-0.3.4}/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.4
|
|
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,34 @@
|
|
|
1
|
+
"""A faint, centred icon painted under a widget's own children (welcome pages, empty states)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QRect, QSize
|
|
6
|
+
from PySide6.QtGui import QIcon, QPainter
|
|
7
|
+
from PySide6.QtWidgets import QWidget
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Backdrop(QWidget):
|
|
11
|
+
"""``icon`` drawn at ``opacity``, scaled to 80 % of the shorter side; ``None`` draws nothing."""
|
|
12
|
+
|
|
13
|
+
# One fixed-size render, scaled: asking the icon for each widget size would cache a
|
|
14
|
+
# multi-MB pixmap per size (~1 GB after a long window resize with a caching icon engine).
|
|
15
|
+
RENDER_PX = 512
|
|
16
|
+
|
|
17
|
+
def __init__(self, icon: QIcon | None = None, parent: QWidget | None = None, *, opacity: float = 0.06):
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self._icon = icon
|
|
20
|
+
self._opacity = opacity
|
|
21
|
+
|
|
22
|
+
def paintEvent(self, event) -> None:
|
|
23
|
+
if self._icon is not None:
|
|
24
|
+
side = int(min(self.width(), self.height()) * 0.8)
|
|
25
|
+
painter = QPainter(self)
|
|
26
|
+
painter.setOpacity(self._opacity)
|
|
27
|
+
painter.setRenderHint(QPainter.SmoothPixmapTransform)
|
|
28
|
+
painter.drawPixmap(QRect((self.width() - side) // 2, (self.height() - side) // 2, side, side),
|
|
29
|
+
self._icon.pixmap(QSize(self.RENDER_PX, self.RENDER_PX)))
|
|
30
|
+
painter.end()
|
|
31
|
+
super().paintEvent(event)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["Backdrop"]
|
|
@@ -111,7 +111,8 @@ QTextEdit,
|
|
|
111
111
|
QSpinBox,
|
|
112
112
|
QDoubleSpinBox,
|
|
113
113
|
QComboBox {
|
|
114
|
-
|
|
114
|
+
/* shadow = the control edge (3:1 on every surface); mid stays for hairlines */
|
|
115
|
+
border: 1px solid palette(shadow);
|
|
115
116
|
border-radius: 4px;
|
|
116
117
|
padding: 3px 6px;
|
|
117
118
|
}
|
|
@@ -136,7 +137,7 @@ QComboBox:focus {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
QPushButton {
|
|
139
|
-
border: 1px solid palette(
|
|
140
|
+
border: 1px solid palette(shadow);
|
|
140
141
|
border-radius: 4px;
|
|
141
142
|
padding: 5px 14px;
|
|
142
143
|
}
|
|
@@ -154,16 +155,27 @@ QPushButton:default {
|
|
|
154
155
|
border-color: palette(highlight);
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
QLineEdit:disabled,
|
|
159
|
+
QPlainTextEdit:disabled,
|
|
160
|
+
QTextEdit:disabled,
|
|
161
|
+
QSpinBox:disabled,
|
|
162
|
+
QDoubleSpinBox:disabled,
|
|
163
|
+
QComboBox:disabled,
|
|
164
|
+
QPushButton:disabled,
|
|
165
|
+
QToolButton[popupMode="MenuButtonPopup"]:disabled {
|
|
166
|
+
border-color: palette(mid);
|
|
167
|
+
}
|
|
168
|
+
|
|
157
169
|
/* A split button (QToolButton with a menu arrow) is an action button, not a toolbar icon:
|
|
158
170
|
framed like QPushButton, the arrow in its own segment. */
|
|
159
171
|
QToolButton[popupMode="MenuButtonPopup"] {
|
|
160
|
-
border: 1px solid palette(
|
|
172
|
+
border: 1px solid palette(shadow);
|
|
161
173
|
padding: 5px 14px;
|
|
162
174
|
padding-right: 24px;
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
QToolButton[popupMode="MenuButtonPopup"]::menu-button {
|
|
166
|
-
border-left: 1px solid palette(
|
|
178
|
+
border-left: 1px solid palette(shadow);
|
|
167
179
|
width: 18px;
|
|
168
180
|
}
|
|
169
181
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .widget import (
|
|
2
2
|
COMPARISON_COLORS,
|
|
3
|
+
associate_scene_objects,
|
|
3
4
|
PymolControlBar,
|
|
4
5
|
PymolDockWidget,
|
|
5
6
|
PymolPresetSpec,
|
|
@@ -8,6 +9,7 @@ from .widget import (
|
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"COMPARISON_COLORS",
|
|
12
|
+
"associate_scene_objects",
|
|
11
13
|
"PymolControlBar",
|
|
12
14
|
"PymolDockWidget",
|
|
13
15
|
"PymolPresetSpec",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<rect x="2" y="2" width="20" height="20" rx="5"/>
|
|
3
|
+
<path d="M7 7v10M13 7v10M7 12h6"/>
|
|
4
|
+
<path d="M16 12V7h1.5a1.5 1.5 0 0 1 0 3H16" stroke-width="1.2"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<rect x="2" y="2" width="20" height="20" rx="5"/>
|
|
3
|
+
<path d="M8 7v10M16 7v10M8 12h8"/>
|
|
4
|
+
</svg>
|
|
@@ -39,6 +39,8 @@ from . import plugins as _pymol_plugins # noqa: F401
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class _PymolGLWidget(PyMOLGLWidget):
|
|
42
|
+
before_render = Signal()
|
|
43
|
+
|
|
42
44
|
def __init__(self, parent):
|
|
43
45
|
super().__init__(parent)
|
|
44
46
|
self._opengl_error: str | None = None
|
|
@@ -69,6 +71,7 @@ class _PymolGLWidget(PyMOLGLWidget):
|
|
|
69
71
|
|
|
70
72
|
def paintGL(self):
|
|
71
73
|
if self._opengl_error is None:
|
|
74
|
+
self.before_render.emit()
|
|
72
75
|
super().paintGL()
|
|
73
76
|
|
|
74
77
|
def resizeGL(self, width, height):
|
|
@@ -123,6 +126,7 @@ class PymolPresetSpec:
|
|
|
123
126
|
callback: Callable[[Any, PymolSceneContext], None]
|
|
124
127
|
contexts: frozenset[str] = field(default_factory=frozenset)
|
|
125
128
|
tooltip: str = ""
|
|
129
|
+
per_grid_slot: bool = False
|
|
126
130
|
|
|
127
131
|
def supports(self, context_kind: str) -> bool:
|
|
128
132
|
return not self.contexts or str(context_kind) in self.contexts
|
|
@@ -138,6 +142,14 @@ def _builtin_preset(method_name: str):
|
|
|
138
142
|
return apply
|
|
139
143
|
|
|
140
144
|
|
|
145
|
+
def associate_scene_objects(cmd, objects, *, reference: str) -> None:
|
|
146
|
+
"""Keep generated interactions/geometry in their structure's comparison panel."""
|
|
147
|
+
slot = cmd.get_setting_int("grid_slot", reference)
|
|
148
|
+
for obj in objects:
|
|
149
|
+
if cmd.get_type(obj) != "object:group":
|
|
150
|
+
cmd.set("grid_slot", slot, obj)
|
|
151
|
+
|
|
152
|
+
|
|
141
153
|
class PymolControlBar(QFrame):
|
|
142
154
|
"""Compact, reusable controls for an embedded PyMOL scene."""
|
|
143
155
|
|
|
@@ -150,7 +162,9 @@ class PymolControlBar(QFrame):
|
|
|
150
162
|
self._cmd = cmd
|
|
151
163
|
self._context = PymolSceneContext()
|
|
152
164
|
self._grid_label_objects: set[str] = set()
|
|
165
|
+
self._preset_objects: set[str] = set()
|
|
153
166
|
self._comparison_layout = "grid"
|
|
167
|
+
self._hydrogen_mode = "none"
|
|
154
168
|
self._presets: dict[str, PymolPresetSpec] = {}
|
|
155
169
|
self.setObjectName("PymolControlBar")
|
|
156
170
|
|
|
@@ -246,6 +260,14 @@ class PymolControlBar(QFrame):
|
|
|
246
260
|
menu=hide_menu,
|
|
247
261
|
)
|
|
248
262
|
|
|
263
|
+
self.hydrogens_action = self._add_toolbar_action(
|
|
264
|
+
"Hydrogens", "hydrogens.svg",
|
|
265
|
+
"Hydrogens: hidden. Click to show all H in this PyMOL session.",
|
|
266
|
+
self.toggle_hydrogens,
|
|
267
|
+
checkable=True,
|
|
268
|
+
)
|
|
269
|
+
self.hydrogens_action.setData("none")
|
|
270
|
+
|
|
249
271
|
self.preset_menu = QMenu("Presets", bar)
|
|
250
272
|
self.preset_action = self._add_toolbar_action(
|
|
251
273
|
"Presets", "presets.svg",
|
|
@@ -264,6 +286,7 @@ class PymolControlBar(QFrame):
|
|
|
264
286
|
self._sync_selection_mode()
|
|
265
287
|
self.refresh_context_label()
|
|
266
288
|
self._refresh_preset_menu()
|
|
289
|
+
self.apply_hydrogen_visibility()
|
|
267
290
|
self._configure_overflow_button()
|
|
268
291
|
if self._cmd is None:
|
|
269
292
|
self.setEnabled(False)
|
|
@@ -443,6 +466,7 @@ class PymolControlBar(QFrame):
|
|
|
443
466
|
self.comparison_menu.menuAction().setVisible(normalized.is_comparison)
|
|
444
467
|
if was_comparison or normalized.is_comparison:
|
|
445
468
|
self._run_command("Grid labels", self._sync_grid_labels)
|
|
469
|
+
self.apply_hydrogen_visibility()
|
|
446
470
|
self.refresh_context_label()
|
|
447
471
|
self._refresh_preset_menu()
|
|
448
472
|
|
|
@@ -473,7 +497,7 @@ class PymolControlBar(QFrame):
|
|
|
473
497
|
self._cmd.set("grid_mode", int(self._comparison_layout == "grid"))
|
|
474
498
|
slot_names: dict[int, list[str]] = {}
|
|
475
499
|
for obj in self._cmd.get_object_list(self._context.target):
|
|
476
|
-
if obj in self._grid_label_objects:
|
|
500
|
+
if obj in self._grid_label_objects or obj in self._preset_objects:
|
|
477
501
|
continue
|
|
478
502
|
slot = self._cmd.get_setting_int("grid_slot", obj)
|
|
479
503
|
if slot > 0:
|
|
@@ -555,6 +579,10 @@ class PymolControlBar(QFrame):
|
|
|
555
579
|
self._cmd.color(color, f"({temporary}) and elem C")
|
|
556
580
|
self._cmd.delete(name)
|
|
557
581
|
self._cmd.set_name(temporary, name)
|
|
582
|
+
for obj in self._preset_objects & set(self._cmd.get_names("objects")):
|
|
583
|
+
if self._cmd.get_setting_int("grid_slot", obj) == slot:
|
|
584
|
+
self._cmd.delete(obj)
|
|
585
|
+
self._preset_objects.discard(obj)
|
|
558
586
|
self._context = replace(
|
|
559
587
|
self._context, grid_labels={**self._context.grid_labels, slot: label},
|
|
560
588
|
)
|
|
@@ -623,6 +651,7 @@ class PymolControlBar(QFrame):
|
|
|
623
651
|
def apply() -> None:
|
|
624
652
|
self._cmd.hide("everything", target)
|
|
625
653
|
self._cmd.show(normalized, target)
|
|
654
|
+
self.apply_hydrogen_visibility()
|
|
626
655
|
|
|
627
656
|
self._run_command(f"Display {normalized}", apply)
|
|
628
657
|
|
|
@@ -636,6 +665,41 @@ class PymolControlBar(QFrame):
|
|
|
636
665
|
lambda: self._cmd.hide(normalized, self.active_target()),
|
|
637
666
|
)
|
|
638
667
|
|
|
668
|
+
def toggle_hydrogens(self) -> None:
|
|
669
|
+
previous = self._hydrogen_mode
|
|
670
|
+
modes = ("none", "all", "polar")
|
|
671
|
+
self._hydrogen_mode = modes[(modes.index(previous) + 1) % len(modes)]
|
|
672
|
+
if not self.apply_hydrogen_visibility():
|
|
673
|
+
self._hydrogen_mode = previous
|
|
674
|
+
mode = self._hydrogen_mode
|
|
675
|
+
icon = "hydrogens-polar.svg" if mode == "polar" else "hydrogens.svg"
|
|
676
|
+
self._toolbar_action_icons[self.hydrogens_action] = icon
|
|
677
|
+
self.hydrogens_action.setIcon(self._toolbar_icon(icon))
|
|
678
|
+
self.hydrogens_action.setChecked(mode != "none")
|
|
679
|
+
self.hydrogens_action.setData(mode)
|
|
680
|
+
state, next_state = {
|
|
681
|
+
"none": ("hidden", "show all H"),
|
|
682
|
+
"all": ("all H", "show only polar H"),
|
|
683
|
+
"polar": ("polar H only (bonded to N, O or S)", "hide all H"),
|
|
684
|
+
}[mode]
|
|
685
|
+
self.hydrogens_action.setToolTip(
|
|
686
|
+
f"Hydrogens: {state}. Click to {next_state} in this PyMOL session."
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def apply_hydrogen_visibility(self) -> bool:
|
|
690
|
+
# Enforce the session policy before drawing, including externally loaded objects.
|
|
691
|
+
polar = "hydro and neighbor (elem N+O+S)"
|
|
692
|
+
shown = {"none": "", "all": "hydro", "polar": polar}[self._hydrogen_mode]
|
|
693
|
+
hidden = {"none": "hydro", "all": "", "polar": f"hydro and not ({polar})"}[self._hydrogen_mode]
|
|
694
|
+
|
|
695
|
+
def apply() -> None:
|
|
696
|
+
if hidden and self._cmd.count_atoms(f"({hidden}) and visible"):
|
|
697
|
+
self._cmd.hide("everything", hidden)
|
|
698
|
+
if shown and self._cmd.count_atoms(f"({shown}) and not rep sticks"):
|
|
699
|
+
self._cmd.show("sticks", shown)
|
|
700
|
+
|
|
701
|
+
return self._run_command("Hydrogens", apply)
|
|
702
|
+
|
|
639
703
|
def clear_selection(self) -> None:
|
|
640
704
|
if self._run_command("Clear selection", self._cmd.deselect):
|
|
641
705
|
self.refresh_context_label()
|
|
@@ -695,7 +759,38 @@ class PymolControlBar(QFrame):
|
|
|
695
759
|
)
|
|
696
760
|
|
|
697
761
|
def apply() -> None:
|
|
698
|
-
|
|
762
|
+
existing = set(self._cmd.get_names("objects"))
|
|
763
|
+
for obj in self._preset_objects & existing:
|
|
764
|
+
self._cmd.delete(obj)
|
|
765
|
+
self._preset_objects.clear()
|
|
766
|
+
if self._context.is_comparison and spec.per_grid_slot:
|
|
767
|
+
slots: dict[int, list[str]] = {}
|
|
768
|
+
source = " or ".join(f"({s})" for s in self._context.selections.values() if s) or self._context.target
|
|
769
|
+
for obj in self._cmd.get_object_list(source):
|
|
770
|
+
if obj not in self._grid_label_objects:
|
|
771
|
+
slot = self._cmd.get_setting_int("grid_slot", obj)
|
|
772
|
+
if slot > 0:
|
|
773
|
+
slots.setdefault(slot, []).append(obj)
|
|
774
|
+
if not slots:
|
|
775
|
+
spec.callback(self._cmd, self._context)
|
|
776
|
+
for slot, objects in sorted(slots.items()):
|
|
777
|
+
target = " or ".join(objects)
|
|
778
|
+
context = replace(
|
|
779
|
+
self._context, target=target,
|
|
780
|
+
selections={role: f"({selection}) and ({target})"
|
|
781
|
+
for role, selection in self._context.selections.items()},
|
|
782
|
+
)
|
|
783
|
+
before = set(self._cmd.get_names("objects"))
|
|
784
|
+
spec.callback(self._cmd, context)
|
|
785
|
+
for obj in set(self._cmd.get_names("objects")) - before:
|
|
786
|
+
# PyMOL reuses names such as polar_contacts on its next preset call.
|
|
787
|
+
name = self._cmd.get_unused_name(f"{obj}_slot{slot}")
|
|
788
|
+
self._cmd.set_name(obj, name)
|
|
789
|
+
self._preset_objects.add(name)
|
|
790
|
+
associate_scene_objects(self._cmd, [name], reference=objects[0])
|
|
791
|
+
else:
|
|
792
|
+
spec.callback(self._cmd, self._context)
|
|
793
|
+
self.apply_hydrogen_visibility()
|
|
699
794
|
if self._context.is_comparison:
|
|
700
795
|
self._sync_grid_labels()
|
|
701
796
|
|
|
@@ -714,6 +809,7 @@ class PymolControlBar(QFrame):
|
|
|
714
809
|
key=key,
|
|
715
810
|
label=label,
|
|
716
811
|
callback=_builtin_preset(method),
|
|
812
|
+
per_grid_slot=True,
|
|
717
813
|
)
|
|
718
814
|
|
|
719
815
|
def _sync_selection_mode(self) -> None:
|
|
@@ -781,6 +877,7 @@ class PymolDockWidget(MSDockWidget):
|
|
|
781
877
|
self.cmd.set("internal_gui", False)
|
|
782
878
|
|
|
783
879
|
self.control_bar = PymolControlBar(self.cmd, self)
|
|
880
|
+
self.pymol_widget.before_render.connect(self.control_bar.apply_hydrogen_visibility)
|
|
784
881
|
self.toolbar = self.control_bar
|
|
785
882
|
self.pymol_widget.installEventFilter(self)
|
|
786
883
|
|
|
@@ -899,6 +996,7 @@ class PymolDockWidget(MSDockWidget):
|
|
|
899
996
|
|
|
900
997
|
__all__ = [
|
|
901
998
|
"COMPARISON_COLORS",
|
|
999
|
+
"associate_scene_objects",
|
|
902
1000
|
"PymolControlBar",
|
|
903
1001
|
"PymolDockWidget",
|
|
904
1002
|
"PymolPresetSpec",
|
|
@@ -311,8 +311,8 @@ class AppSettingsPanel(QWidget):
|
|
|
311
311
|
font-weight: 700;
|
|
312
312
|
}
|
|
313
313
|
QLabel#sourceDefault { color: palette(text); background: rgba(128, 128, 128, 0.18); }
|
|
314
|
-
QLabel#sourceGlobal {
|
|
315
|
-
QLabel#sourceProject {
|
|
314
|
+
QLabel#sourceGlobal { background: rgba(76, 155, 232, 0.16); }
|
|
315
|
+
QLabel#sourceProject { background: rgba(52, 200, 138, 0.16); }
|
|
316
316
|
"""
|
|
317
317
|
)
|
|
318
318
|
|
|
@@ -743,6 +743,7 @@ class AppSettingsPanel(QWidget):
|
|
|
743
743
|
normalized = source if source in _SOURCE_LABELS else "default"
|
|
744
744
|
label.setText(_SOURCE_LABELS[normalized])
|
|
745
745
|
label.setObjectName(f"source{normalized.title()}")
|
|
746
|
+
label.setProperty("tone", {"global": "blue", "project": "green"}.get(normalized, "")) # theme text hue
|
|
746
747
|
label.style().unpolish(label)
|
|
747
748
|
label.style().polish(label)
|
|
748
749
|
|
|
@@ -24,6 +24,10 @@ from .table_config import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
# Past this many rows a DuckDB seek numbers the rows instead of an ORDER BY + OFFSET.
|
|
28
|
+
DUCKDB_RANK_FROM = 100_000
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
class QueryBuilder:
|
|
28
32
|
"""Builds and runs paginated queries from a TableConfig.
|
|
29
33
|
|
|
@@ -147,7 +151,8 @@ class QueryBuilder:
|
|
|
147
151
|
return []
|
|
148
152
|
stmt = self._build_base_statement().with_only_columns(pk)
|
|
149
153
|
with self._db.get_session() as session:
|
|
150
|
-
|
|
154
|
+
# scalars(): with expr columns the base select has several entities and exec() yields Rows.
|
|
155
|
+
return [int(value) for value in session.scalars(stmt).all() if value is not None]
|
|
151
156
|
|
|
152
157
|
def count(self) -> int:
|
|
153
158
|
"""Row total under the current filters/search, without fetching any row.
|
|
@@ -205,19 +210,51 @@ class QueryBuilder:
|
|
|
205
210
|
has_more = len(raw) > self._page_size
|
|
206
211
|
return [self._row_to_dict(row) for row in raw[:self._page_size]], has_more
|
|
207
212
|
|
|
208
|
-
def fetch_window_at(self, page_index: int
|
|
213
|
+
def fetch_window_at(self, page_index: int, anchor: tuple[Any, Any] | None = None,
|
|
214
|
+
anchor_page: int = 0) -> tuple[list[dict], bool]:
|
|
209
215
|
"""Direct load for a scrollbar jump to a page that has not been walked yet.
|
|
210
216
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
`anchor` is the keyset cursor that starts `anchor_page` (None = the top). The
|
|
218
|
+
walk from it to the target reads only (sort value, pk) - never whole rows -
|
|
219
|
+
and then the page itself is a keyset seek, so jumping to the end of a 4M-row
|
|
220
|
+
table no longer materializes 4M rows.
|
|
214
221
|
"""
|
|
215
|
-
offset = max(0, int(page_index)) * self._page_size
|
|
216
|
-
|
|
222
|
+
offset = max(0, int(page_index) - int(anchor_page)) * self._page_size
|
|
223
|
+
if self._get_pk_attr() is None: # no keyset possible: plain OFFSET
|
|
224
|
+
stmt = self._build_base_statement().offset(int(page_index) * self._page_size)
|
|
225
|
+
with self._db.get_session() as session:
|
|
226
|
+
raw = session.exec(stmt.limit(self._page_size + 1)).all()
|
|
227
|
+
return [self._row_to_dict(row) for row in raw[:self._page_size]], len(raw) > self._page_size
|
|
228
|
+
cursor = self._seek(anchor, offset) if offset else anchor
|
|
229
|
+
if offset and cursor is None:
|
|
230
|
+
return [], False # past the end (rows vanished under the scrollbar)
|
|
231
|
+
return self.fetch_window_after(cursor)
|
|
232
|
+
|
|
233
|
+
def _seek(self, anchor: tuple[Any, Any] | None, offset: int) -> tuple[Any, Any] | None:
|
|
234
|
+
"""Cursor of the `offset`-th row after `anchor`, reading only the key columns."""
|
|
235
|
+
pk = self._get_pk_attr()
|
|
236
|
+
attr = self._resolve_attr(self._sorts[0].field) if self._sorts else None
|
|
237
|
+
keys = [attr.label("_seek_sort") if attr is not None else None, pk.label("_seek_pk")]
|
|
238
|
+
stmt = self._build_base_statement()
|
|
239
|
+
clause = self._keyset_clause(anchor)
|
|
240
|
+
if clause is not None:
|
|
241
|
+
stmt = stmt.where(clause)
|
|
217
242
|
with self._db.get_session() as session:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
243
|
+
if offset > DUCKDB_RANK_FROM and session.get_bind().dialect.name == "duckdb":
|
|
244
|
+
# DuckDB turns ORDER BY + OFFSET into a top-N heap the size of the offset
|
|
245
|
+
# (2.3 s at 3.85M rows); numbering the rows sorts once (0.2-0.7 s), which
|
|
246
|
+
# loses to the heap on short walks. SQLite: OFFSET 0.3 s, row_number() 2 s.
|
|
247
|
+
rank = func.row_number().over(order_by=self._order_clauses()).label("_seek_rank")
|
|
248
|
+
inner = stmt.with_only_columns(*[k for k in keys if k is not None], rank) \
|
|
249
|
+
.order_by(None).subquery()
|
|
250
|
+
row = session.execute(select(*[c for c in inner.c if c.name != "_seek_rank"])
|
|
251
|
+
.where(inner.c._seek_rank == offset)).first()
|
|
252
|
+
else:
|
|
253
|
+
row = session.execute(stmt.with_only_columns(*[k for k in keys if k is not None])
|
|
254
|
+
.offset(offset - 1).limit(1)).first()
|
|
255
|
+
if row is None:
|
|
256
|
+
return None
|
|
257
|
+
return (row[0], row[1]) if attr is not None else (None, row[0])
|
|
221
258
|
|
|
222
259
|
def _keyset_clause(self, cursor: tuple[Any, Any] | None):
|
|
223
260
|
if cursor is None:
|
|
@@ -240,20 +277,19 @@ class QueryBuilder:
|
|
|
240
277
|
return or_(attr.is_not(None), rest_of_nulls) if not sort.descending else rest_of_nulls
|
|
241
278
|
if sort.descending:
|
|
242
279
|
return or_(attr < sort_value, and_(attr == sort_value, pk > pk_value), attr.is_(None))
|
|
243
|
-
|
|
280
|
+
# The redundant `attr >= value` is what lets SQLite seek the index when the values
|
|
281
|
+
# are bound parameters (without it: a SCAN, 0.2 s per page at 3.85M rows) and
|
|
282
|
+
# DuckDB prune row groups by zone map.
|
|
283
|
+
# ponytail: descending keeps its `IS NULL` arm and so its scan; same fix needs the
|
|
284
|
+
# null tail fetched as a second query.
|
|
285
|
+
return and_(attr >= sort_value, or_(attr > sort_value, and_(attr == sort_value, pk > pk_value)))
|
|
244
286
|
|
|
245
287
|
def fetch(self) -> tuple[list[dict], int]:
|
|
246
288
|
"""Run the query and return (rows_as_dicts, total_count)."""
|
|
247
|
-
stmt = self._build_base_statement()
|
|
248
|
-
count_stmt = self._build_count_statement()
|
|
249
289
|
with self._db.get_session() as session:
|
|
250
|
-
total: int = session.exec(
|
|
251
|
-
|
|
252
|
-
offset = (self._page - 1) * self._page_size
|
|
253
|
-
stmt = stmt.offset(offset).limit(self._page_size)
|
|
290
|
+
total: int = session.exec(self._build_count_statement()).one() # type: ignore[arg-type]
|
|
254
291
|
|
|
255
|
-
|
|
256
|
-
rows = [self._row_to_dict(r) for r in raw_rows]
|
|
292
|
+
rows, _has_more = self.fetch_window_at(self._page - 1)
|
|
257
293
|
return rows, total
|
|
258
294
|
|
|
259
295
|
# ──────────────────────────────────────────
|
|
@@ -302,24 +338,20 @@ class QueryBuilder:
|
|
|
302
338
|
if search_clause is not None:
|
|
303
339
|
stmt = stmt.where(search_clause)
|
|
304
340
|
|
|
305
|
-
|
|
306
|
-
if self._sorts:
|
|
307
|
-
for sort in self._sorts:
|
|
308
|
-
attr = self._resolve_attr(sort.field)
|
|
309
|
-
if attr is not None:
|
|
310
|
-
stmt = stmt.order_by(attr.desc() if sort.descending else attr.asc())
|
|
311
|
-
# PK tie-break: without it two rows with the same score have undefined
|
|
312
|
-
# order and the keyset cursor skips or repeats rows across pages.
|
|
313
|
-
pk = self._get_pk_attr()
|
|
314
|
-
if pk is not None:
|
|
315
|
-
stmt = stmt.order_by(pk)
|
|
316
|
-
else:
|
|
317
|
-
# Default order: primary key of the main table
|
|
318
|
-
pk = self._get_pk_attr()
|
|
319
|
-
if pk is not None:
|
|
320
|
-
stmt = stmt.order_by(pk)
|
|
341
|
+
return stmt.order_by(*self._order_clauses())
|
|
321
342
|
|
|
322
|
-
|
|
343
|
+
def _order_clauses(self) -> list:
|
|
344
|
+
clauses = []
|
|
345
|
+
for sort in self._sorts:
|
|
346
|
+
attr = self._resolve_attr(sort.field)
|
|
347
|
+
if attr is not None:
|
|
348
|
+
clauses.append(attr.desc() if sort.descending else attr.asc())
|
|
349
|
+
# PK tie-break (and default order): without it two rows with the same score have
|
|
350
|
+
# undefined order and the keyset cursor skips or repeats rows across pages.
|
|
351
|
+
pk = self._get_pk_attr()
|
|
352
|
+
if pk is not None:
|
|
353
|
+
clauses.append(pk)
|
|
354
|
+
return clauses
|
|
323
355
|
|
|
324
356
|
def _build_count_statement(self):
|
|
325
357
|
model = self._config.model_class
|