ms-components 0.3.1__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.1/src/ms_components.egg-info → ms_components-0.3.2}/PKG-INFO +1 -1
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/base.qss +21 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/widget.py +4 -1
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/cell_delegate.py +36 -24
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/query_builder.py +6 -4
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/smart_table_view.py +12 -2
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/table_config.py +8 -1
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/theme.py +20 -1
- {ms_components-0.3.1 → ms_components-0.3.2/src/ms_components.egg-info}/PKG-INFO +1 -1
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components.egg-info/scm_version.json +2 -2
- ms_components-0.3.2/tests/test_progress_column.py +48 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_query_builder_keyset.py +54 -0
- ms_components-0.3.1/tests/test_progress_column.py +0 -36
- {ms_components-0.3.1 → ms_components-0.3.2}/.github/workflows/docs.yml +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/.github/workflows/publish.yml +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/.gitignore +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/LICENSE +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/README.md +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/docs/api_reference.md +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/docs/examples.md +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/docs/index.md +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/docs/ms_tutorial.md +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/docs/usage.md +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/examples/tutorial_demo.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/examples/tutorial_demo.toml +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/pyproject.toml +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/setup.cfg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/chips.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/listview.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_executors.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/bridge.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/cards.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/config.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/detail_panel.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/formatting.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/job_cards.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/models.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/palette.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/polling.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_monitor/widget.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_projects/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_projects/app.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_projects/browser.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_projects/scratch.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_projects/widget.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/box.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/center.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/display.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/plugins.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_pymol/widget.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_settings/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_settings/panel.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_stepper.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/action_header.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/column_panel.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/example_usage.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/filter_panel.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/columns.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/export.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/filter.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/menu.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/reload.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/search.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/settings.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/icons/sort.svg +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/pagination_control.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/session_provider.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/sort_panel.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/table_model.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/test_external_filters.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_table/test_toolbar.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_tutorial/__init__.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_tutorial/bindings.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_tutorial/controller.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_tutorial/models.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/ms_tutorial/overlay.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/opengl.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/pagination.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/palettes.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/step_dialog.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/tool_panel.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components/wheel.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components.egg-info/SOURCES.txt +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components.egg-info/dependency_links.txt +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components.egg-info/requires.txt +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components.egg-info/scm_file_list.json +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/src/ms_components.egg-info/top_level.txt +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_app_settings.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_chips.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_dock_window.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_executors_ui.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_molsuite_monitor_bridge.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_molsuite_monitor_widgets.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_monitor_config.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_ms_projects_widget.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_opengl.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_project_browser.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_pymol_grid.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_pymol_toolbar.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_shift_hscroll.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_split_button_style.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_step_dialog.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_table_toolbar.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_table_view_state.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_theme_contrast.py +0 -0
- {ms_components-0.3.1 → ms_components-0.3.2}/tests/test_tutorial.py +0 -0
- {ms_components-0.3.1 → 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
|
|
@@ -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(
|
|
@@ -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
|
# ──────────────────────────────────────────
|
|
@@ -70,7 +70,7 @@ class ColumnKind(str, Enum):
|
|
|
70
70
|
NUMBER = "number"
|
|
71
71
|
INTEGER = "integer"
|
|
72
72
|
CHOICE = "choice"
|
|
73
|
-
# A fraction 0..1 drawn as a progress bar with its percentage
|
|
73
|
+
# A fraction 0..1 (or a count, with `progress_max`) drawn as a progress bar with its percentage.
|
|
74
74
|
PROGRESS = "progress"
|
|
75
75
|
|
|
76
76
|
|
|
@@ -109,6 +109,11 @@ class ColumnDef:
|
|
|
109
109
|
expr: SQLAlchemy expression producing the column value, for anything
|
|
110
110
|
that is not a model attribute (JSON_EXTRACT, CASE, func.*).
|
|
111
111
|
When set, `field` is only the key/label.
|
|
112
|
+
progress_max: PROGRESS columns only: the row key holding the maximum. Set, the
|
|
113
|
+
value is a count and the bar reads "value/max (pct%)", as
|
|
114
|
+
QProgressBar's "%v/%m (%p%)"; unset, the value is a 0..1 fraction.
|
|
115
|
+
progress_tone: PROGRESS columns only: callable(row_data) → a theme accent
|
|
116
|
+
("green", "red"), the bar's `tone` property. Unset, "green".
|
|
112
117
|
"""
|
|
113
118
|
field: str
|
|
114
119
|
label: Optional[str] = None
|
|
@@ -130,6 +135,8 @@ class ColumnDef:
|
|
|
130
135
|
tooltip: Any = None # str | Callable
|
|
131
136
|
db_column: Optional[str] = None
|
|
132
137
|
expr: Any = None # SQLAlchemy ColumnElement
|
|
138
|
+
progress_max: Optional[str] = None
|
|
139
|
+
progress_tone: Optional[Callable] = None
|
|
133
140
|
|
|
134
141
|
# ── Fields for rendering widgets inside a cell ──────────────────────
|
|
135
142
|
#
|
|
@@ -276,6 +276,25 @@ def resolve(name: str) -> str:
|
|
|
276
276
|
return DEFAULT_DARK if scheme == Qt.ColorScheme.Dark else DEFAULT_LIGHT
|
|
277
277
|
|
|
278
278
|
|
|
279
|
+
#: The accents a widget can wear through `setProperty("tone", name)`.
|
|
280
|
+
TONES = ("green", "red")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def tone_qss(theme: dict[str, str] | None = None) -> str:
|
|
284
|
+
"""`QProgressBar[tone="green"]` and siblings, from the live theme.
|
|
285
|
+
|
|
286
|
+
QSS has no palette role for the accents, so these rules are written per theme.
|
|
287
|
+
Translucent, so the bar's text stays readable on dark and light themes alike.
|
|
288
|
+
"""
|
|
289
|
+
theme = theme or _current
|
|
290
|
+
rules = []
|
|
291
|
+
for tone in TONES:
|
|
292
|
+
accent = QColor(theme[tone])
|
|
293
|
+
rgba = f"rgba({accent.red()}, {accent.green()}, {accent.blue()}, 150)"
|
|
294
|
+
rules.append(f'QProgressBar[tone="{tone}"]::chunk {{ background: {rgba}; }}')
|
|
295
|
+
return "\n".join(rules) + "\n"
|
|
296
|
+
|
|
297
|
+
|
|
279
298
|
def base_qss() -> str:
|
|
280
299
|
"""The shared geometry stylesheet (colors resolved from the live palette)."""
|
|
281
300
|
return files(__package__).joinpath("base.qss").read_text(encoding="utf-8")
|
|
@@ -297,7 +316,7 @@ def apply_theme(name: str, app: QApplication | None = None, *,
|
|
|
297
316
|
set_base_font(base_font_pt, app=app)
|
|
298
317
|
app.setStyle("Fusion") # Fusion honors QPalette ColorRoles most faithfully
|
|
299
318
|
app.setPalette(build_palette(_current))
|
|
300
|
-
app.setStyleSheet(base_qss())
|
|
319
|
+
app.setStyleSheet(base_qss() + tone_qss())
|
|
301
320
|
|
|
302
321
|
|
|
303
322
|
if __name__ == "__main__":
|
|
@@ -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,48 @@
|
|
|
1
|
+
"""A PROGRESS column fills the share of the cell its fraction says, clamped to 0..1,
|
|
2
|
+
in the tone its row asks for."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlmodel # noqa: F401 (pydantic before PySide6, or its lazy import breaks)
|
|
6
|
+
from PySide6.QtGui import QColor, QImage, QPainter, QStandardItemModel
|
|
7
|
+
from PySide6.QtWidgets import QApplication, QStyleOptionViewItem, QTableView
|
|
8
|
+
|
|
9
|
+
from ms_components import theme
|
|
10
|
+
from ms_components.ms_table.cell_delegate import CellWidgetDelegate
|
|
11
|
+
from ms_components.ms_table.table_config import ColumnDef, ColumnKind
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Model(QStandardItemModel):
|
|
15
|
+
rows = [{"done": 0.5}, {"done": None}, {"done": 3.0, "state": "failed"}]
|
|
16
|
+
|
|
17
|
+
def get_row_data(self, row):
|
|
18
|
+
return self.rows[row]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _tone(pixel: QColor) -> str:
|
|
22
|
+
if pixel.green() - pixel.red() > 40:
|
|
23
|
+
return "green"
|
|
24
|
+
return "red" if pixel.red() - pixel.green() > 40 else "track"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_a_progress_column_fills_its_fraction_of_the_cell():
|
|
28
|
+
app = QApplication.instance() or QApplication([])
|
|
29
|
+
app.setStyleSheet(theme.base_qss() + theme.tone_qss(theme.THEMES["dracula"]))
|
|
30
|
+
try:
|
|
31
|
+
view, model = QTableView(), _Model(3, 1)
|
|
32
|
+
view.setModel(model)
|
|
33
|
+
column = ColumnDef("done", kind=ColumnKind.PROGRESS,
|
|
34
|
+
progress_tone=lambda row: "red" if row.get("state") else "green")
|
|
35
|
+
delegate = CellWidgetDelegate(0, column, view)
|
|
36
|
+
filled = []
|
|
37
|
+
for row in range(3):
|
|
38
|
+
image = QImage(200, 30, QImage.Format_ARGB32)
|
|
39
|
+
image.fill(QColor("black"))
|
|
40
|
+
option = QStyleOptionViewItem()
|
|
41
|
+
option.widget, option.rect = view, image.rect()
|
|
42
|
+
painter = QPainter(image)
|
|
43
|
+
delegate.paint(painter, option, model.index(row, 0))
|
|
44
|
+
painter.end()
|
|
45
|
+
filled.append([_tone(image.pixelColor(x, 8)) for x in (20, 180)])
|
|
46
|
+
assert filled == [["green", "track"], ["track", "track"], ["red", "red"]]
|
|
47
|
+
finally:
|
|
48
|
+
app.setStyleSheet("")
|
|
@@ -169,3 +169,57 @@ def test_refresh_while_scrolled_loads_the_rows_on_screen(db):
|
|
|
169
169
|
# and the right row, not a neighbouring page's: the window is one contiguous run
|
|
170
170
|
assert view._model.get_raw_object(row).id == row + 1
|
|
171
171
|
view.close()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_rows_added_after_a_short_last_page_get_fetched_not_left_blank():
|
|
175
|
+
"""A running job: the first load brought 3 rows, refresh_counts() then says 30. The
|
|
176
|
+
short page must be fetched again, or rows 3..29 show as blank rows forever."""
|
|
177
|
+
app = QApplication.instance() or QApplication(["ms-table-tail-test"])
|
|
178
|
+
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=None)
|
|
179
|
+
SQLModel.metadata.create_all(engine, tables=[Hit.__table__])
|
|
180
|
+
add = lambda ids: (lambda s: (s.add_all([Hit(id=i, name=f"l{i}") for i in ids]), s.commit()))(Session(engine))
|
|
181
|
+
add(range(1, 4))
|
|
182
|
+
view = SmartTableView(_Db(engine), _config())
|
|
183
|
+
view.resize(600, 900)
|
|
184
|
+
view.show()
|
|
185
|
+
app.processEvents()
|
|
186
|
+
assert view._model.loaded_count == 3
|
|
187
|
+
|
|
188
|
+
for end in (11, 31): # the job keeps going: every tick, more rows
|
|
189
|
+
add(range(view._model.rowCount() + 1, end))
|
|
190
|
+
assert view.refresh_counts()
|
|
191
|
+
for _ in range(5):
|
|
192
|
+
app.processEvents()
|
|
193
|
+
assert view._model.rowCount() == end - 1
|
|
194
|
+
assert [view._model.get_raw_object(row).id for row in range(end - 1)] == list(range(1, end))
|
|
195
|
+
view.close()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Gap(SQLModel, table=True):
|
|
200
|
+
__tablename__ = "gap"
|
|
201
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
202
|
+
score: Optional[float] = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize("descending", [False, True])
|
|
206
|
+
def test_keyset_walks_a_null_block_longer_than_a_page(descending):
|
|
207
|
+
"""A ΔΔG column is NULL for every ligand without that off-target: the walk must keep
|
|
208
|
+
the whole null block (ascending: first; descending: last), not drop it after page 1."""
|
|
209
|
+
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=None)
|
|
210
|
+
SQLModel.metadata.create_all(engine, tables=[Gap.__table__])
|
|
211
|
+
with Session(engine) as session:
|
|
212
|
+
session.add_all([Gap(id=i, score=None if i % 3 else float(i)) for i in range(1, 351)])
|
|
213
|
+
session.commit()
|
|
214
|
+
config = TableConfig(model_class=Gap, columns=[ColumnDef("score")], page_size=PAGE,
|
|
215
|
+
load_mode=TableLoadMode.INFINITE,
|
|
216
|
+
default_sort=[SortSpec("score", descending=descending)])
|
|
217
|
+
builder = QueryBuilder(_Db(engine), config)
|
|
218
|
+
seen, cursor = [], None
|
|
219
|
+
while True:
|
|
220
|
+
rows, has_more = builder.fetch_window_after(cursor)
|
|
221
|
+
seen += [row["__raw__"].id for row in rows]
|
|
222
|
+
if not has_more:
|
|
223
|
+
break
|
|
224
|
+
cursor = builder.cursor_of(rows[-1])
|
|
225
|
+
assert sorted(seen) == list(range(1, 351))
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"""A PROGRESS column fills the share of the cell its fraction says, clamped to 0..1."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import sqlmodel # noqa: F401 (pydantic before PySide6, or its lazy import breaks)
|
|
5
|
-
from PySide6.QtGui import QColor, QImage, QPainter, QPalette, QStandardItemModel
|
|
6
|
-
from PySide6.QtWidgets import QApplication, QStyleOptionViewItem, QTableView
|
|
7
|
-
|
|
8
|
-
from ms_components.ms_table.cell_delegate import CellWidgetDelegate
|
|
9
|
-
from ms_components.ms_table.table_config import ColumnDef, ColumnKind
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class _Model(QStandardItemModel):
|
|
13
|
-
rows = [{"done": 0.5}, {"done": None}, {"done": 3.0}]
|
|
14
|
-
|
|
15
|
-
def get_row_data(self, row):
|
|
16
|
-
return self.rows[row]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_a_progress_column_fills_its_fraction_of_the_cell():
|
|
20
|
-
app = QApplication.instance() or QApplication([])
|
|
21
|
-
view, model = QTableView(), _Model(3, 1)
|
|
22
|
-
view.setModel(model)
|
|
23
|
-
delegate = CellWidgetDelegate(0, ColumnDef("done", kind=ColumnKind.PROGRESS), view)
|
|
24
|
-
filled = []
|
|
25
|
-
for row in range(3):
|
|
26
|
-
image = QImage(200, 30, QImage.Format_ARGB32)
|
|
27
|
-
image.fill(QColor("black"))
|
|
28
|
-
option = QStyleOptionViewItem()
|
|
29
|
-
option.widget, option.rect = view, image.rect()
|
|
30
|
-
option.palette.setColor(QPalette.Highlight, QColor("red"))
|
|
31
|
-
painter = QPainter(image)
|
|
32
|
-
delegate.paint(painter, option, model.index(row, 0))
|
|
33
|
-
painter.end()
|
|
34
|
-
filled.append([image.pixelColor(x, 8) == QColor("red") for x in (20, 180)])
|
|
35
|
-
assert filled == [[True, False], [False, False], [True, True]]
|
|
36
|
-
assert app
|
|
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.1 → ms_components-0.3.2}/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
|
|
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.1 → ms_components-0.3.2}/src/ms_components/ms_table/pagination_control.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_components-0.3.1 → ms_components-0.3.2}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|