qplotter 1.6.0__py3-none-any.whl
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.
- qplot/__init__.py +46 -0
- qplot/__main__.py +232 -0
- qplot/_auxiliary_paths.py +67 -0
- qplot/_build_info.json +1 -0
- qplot/_metadata.py +75 -0
- qplot/_repair.py +14 -0
- qplot/_shutdown_supervisor.py +4049 -0
- qplot/_version.py +12 -0
- qplot/_windows_shutdown_job.py +1166 -0
- qplot/configuration/__init__.py +3 -0
- qplot/configuration/config.py +578 -0
- qplot/configuration/config_schema.json +357 -0
- qplot/configuration/fonts.py +3 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +592 -0
- qplot/configuration/themes/blank.py +17 -0
- qplot/configuration/themes/dark.py +54 -0
- qplot/configuration/themes/light.py +54 -0
- qplot/datahandling/LoadFromDB.py +224 -0
- qplot/datahandling/__init__.py +47 -0
- qplot/datahandling/_trusted_live_helper.py +812 -0
- qplot/datahandling/_trusted_live_protocol.py +1745 -0
- qplot/datahandling/database.py +2340 -0
- qplot/datahandling/dimensions.py +43 -0
- qplot/datahandling/file_identity.py +461 -0
- qplot/datahandling/parameter_data.py +149 -0
- qplot/datahandling/plot_progress.py +17 -0
- qplot/datahandling/qcodes_cache.py +277 -0
- qplot/datahandling/qcodes_compat.py +14 -0
- qplot/datahandling/readSQL.py +2442 -0
- qplot/datahandling/readonly.py +2566 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +148 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +876 -0
- qplot/datahandling/trusted_heatmap.py +246 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1919 -0
- qplot/datahandling/trusted_live_supervisor.py +2161 -0
- qplot/datahandling/trusted_plot.py +283 -0
- qplot/datahandling/trusted_presentation.py +1065 -0
- qplot/datahandling/trusted_snapshot.py +1132 -0
- qplot/datahandling/trusted_work_coordinator.py +2157 -0
- qplot/datahandling/trusted_work_scheduler.py +1204 -0
- qplot/diagnostics.py +241 -0
- qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
- qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
- qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
- qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
- qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
- qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
- qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
- qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
- qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
- qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
- qplot/testdata.py +1804 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +123 -0
- qplot/tools/general.py +139 -0
- qplot/tools/heatmap_geometry.py +391 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +650 -0
- qplot/tools/sample_statistics.py +111 -0
- qplot/tools/worker.py +2838 -0
- qplot/windows/__init__.py +9 -0
- qplot/windows/_colorbar.py +422 -0
- qplot/windows/_commands.py +523 -0
- qplot/windows/_config_persistence.py +123 -0
- qplot/windows/_database_actions.py +4933 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_display_precision.py +48 -0
- qplot/windows/_dragdrop.py +70 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +210 -0
- qplot/windows/_mixed_appearance.py +303 -0
- qplot/windows/_mixed_plots.py +461 -0
- qplot/windows/_native_averaging.py +323 -0
- qplot/windows/_native_transforms.py +551 -0
- qplot/windows/_plot1d_snap.py +612 -0
- qplot/windows/_plot1d_traces.py +2236 -0
- qplot/windows/_plot2d_colorbar.py +1127 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2291 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +2082 -0
- qplot/windows/_plot_actions.py +2703 -0
- qplot/windows/_plot_annotation.py +186 -0
- qplot/windows/_plot_appearance.py +283 -0
- qplot/windows/_plot_axis_scaling.py +1521 -0
- qplot/windows/_plot_data_viewbox.py +94 -0
- qplot/windows/_plot_drop_axes.py +224 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +112 -0
- qplot/windows/_plot_marquee.py +659 -0
- qplot/windows/_plot_refresh.py +950 -0
- qplot/windows/_plot_source_promotion.py +178 -0
- qplot/windows/_plot_state.py +237 -0
- qplot/windows/_plot_status.py +115 -0
- qplot/windows/_plot_tiling.py +35 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +724 -0
- qplot/windows/_refresh_interval.py +172 -0
- qplot/windows/_run_controls.py +789 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +532 -0
- qplot/windows/_subplots/subplot2d.py +919 -0
- qplot/windows/_svg_export.py +209 -0
- qplot/windows/_trusted_derived_qt.py +1759 -0
- qplot/windows/_widgets/__init__.py +23 -0
- qplot/windows/_widgets/_run_formatting.py +387 -0
- qplot/windows/_widgets/details_tables.py +1119 -0
- qplot/windows/_widgets/dropbox.py +290 -0
- qplot/windows/_widgets/operations.py +460 -0
- qplot/windows/_widgets/preview.py +2997 -0
- qplot/windows/_widgets/run_list_items.py +488 -0
- qplot/windows/_widgets/toolbar.py +111 -0
- qplot/windows/_widgets/treeWidgets.py +2572 -0
- qplot/windows/_window_controls.py +414 -0
- qplot/windows/main.py +1632 -0
- qplot/windows/plot1d.py +313 -0
- qplot/windows/plot2d.py +1935 -0
- qplotter-1.6.0.dist-info/METADATA +322 -0
- qplotter-1.6.0.dist-info/RECORD +130 -0
- qplotter-1.6.0.dist-info/WHEEL +5 -0
- qplotter-1.6.0.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Database/run provenance drawn in the plot scene, including exports."""
|
|
2
|
+
|
|
3
|
+
from html import escape
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pyqtgraph as pg
|
|
7
|
+
from PyQt6 import QtCore, QtGui, QtWidgets
|
|
8
|
+
|
|
9
|
+
from qplot.configuration.fonts import FONT_SIZE_KEY
|
|
10
|
+
|
|
11
|
+
SHOW_SOURCE_ANNOTATION_KEY = "user_preference.show_source_annotation"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run_ranges(run_ids):
|
|
15
|
+
"""Compress sequences of three or more; leave pairs as separate IDs."""
|
|
16
|
+
runs = sorted(set(run_ids))
|
|
17
|
+
groups = []
|
|
18
|
+
start = 0
|
|
19
|
+
while start < len(runs):
|
|
20
|
+
end = start
|
|
21
|
+
while end + 1 < len(runs) and runs[end + 1] == runs[end] + 1:
|
|
22
|
+
end += 1
|
|
23
|
+
if end - start >= 2:
|
|
24
|
+
groups.append(f"{runs[start]}-{runs[end]}")
|
|
25
|
+
else:
|
|
26
|
+
groups.extend(str(run) for run in runs[start:end + 1])
|
|
27
|
+
start = end + 1
|
|
28
|
+
return ", ".join(groups)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_plot_sources(sources, *, rich_text=False):
|
|
32
|
+
"""Group (database path, run ID, parameter label) in encounter order."""
|
|
33
|
+
databases = {}
|
|
34
|
+
for database, run_id, parameter in sources:
|
|
35
|
+
parameters = databases.setdefault(database, {})
|
|
36
|
+
parameters.setdefault(parameter, set()).add(run_id)
|
|
37
|
+
names = [Path(database).stem for database in databases]
|
|
38
|
+
lines = []
|
|
39
|
+
for database, parameters in databases.items():
|
|
40
|
+
name = Path(database).stem
|
|
41
|
+
if names.count(name) > 1:
|
|
42
|
+
name = str(database)
|
|
43
|
+
groups = "; ".join(
|
|
44
|
+
f"{_run_ranges(runs)} ({parameter})"
|
|
45
|
+
for parameter, runs in parameters.items()
|
|
46
|
+
)
|
|
47
|
+
lines.append(
|
|
48
|
+
f"<b>{escape(name)}:</b> {escape(groups)}"
|
|
49
|
+
if rich_text else f"{name}: {groups}"
|
|
50
|
+
)
|
|
51
|
+
return ("<br>" if rich_text else "\n").join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PlotSourceLabel(pg.GraphicsWidget):
|
|
55
|
+
"""A shaded source strip that wraps to the plot width."""
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.item = QtWidgets.QGraphicsTextItem(self)
|
|
60
|
+
self.item.document().setDocumentMargin(0)
|
|
61
|
+
self.text = ""
|
|
62
|
+
self.rich_text = None
|
|
63
|
+
self.background = QtGui.QColor("white")
|
|
64
|
+
self._height = 0.0
|
|
65
|
+
self.setSizePolicy(
|
|
66
|
+
QtWidgets.QSizePolicy.Policy.Expanding,
|
|
67
|
+
QtWidgets.QSizePolicy.Policy.Fixed,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def set_source_text(self, text, font, color, *, rich_text, background=None):
|
|
71
|
+
background = QtGui.QColor("white") if background is None else background
|
|
72
|
+
if (text == self.text and font == self.item.font()
|
|
73
|
+
and color == self.item.defaultTextColor()
|
|
74
|
+
and rich_text == self.rich_text and background == self.background):
|
|
75
|
+
return
|
|
76
|
+
self.text = text
|
|
77
|
+
self.rich_text = rich_text
|
|
78
|
+
self.background = background
|
|
79
|
+
self.item.setFont(font)
|
|
80
|
+
self.item.setDefaultTextColor(color)
|
|
81
|
+
self.item.setHtml(rich_text)
|
|
82
|
+
self._wrap()
|
|
83
|
+
self.update()
|
|
84
|
+
|
|
85
|
+
def paint(self, painter, option, widget=None):
|
|
86
|
+
if not self.text:
|
|
87
|
+
return
|
|
88
|
+
foreground = self.item.defaultTextColor()
|
|
89
|
+
|
|
90
|
+
def tint(amount):
|
|
91
|
+
return QtGui.QColor.fromRgbF(*(
|
|
92
|
+
base * (1 - amount) + ink * amount
|
|
93
|
+
for base, ink in zip(
|
|
94
|
+
self.background.getRgbF()[:3], foreground.getRgbF()[:3], strict=True,
|
|
95
|
+
)
|
|
96
|
+
))
|
|
97
|
+
|
|
98
|
+
painter.setBrush(tint(0.05))
|
|
99
|
+
painter.setPen(QtGui.QPen(tint(0.18), 1))
|
|
100
|
+
painter.drawRect(self.boundingRect().adjusted(0.5, 0.5, -0.5, -4.5))
|
|
101
|
+
|
|
102
|
+
def _wrap(self):
|
|
103
|
+
horizontal, vertical = 8, 5
|
|
104
|
+
self.item.setPos(horizontal, vertical)
|
|
105
|
+
self.item.setTextWidth(max(1.0, (self.size().width() or 600.0) - 2 * horizontal))
|
|
106
|
+
height = self.item.boundingRect().height() + 2 * vertical + 4 if self.text else 0.0
|
|
107
|
+
if height != self._height:
|
|
108
|
+
self._height = height
|
|
109
|
+
self.setMinimumHeight(height)
|
|
110
|
+
self.setMaximumHeight(height)
|
|
111
|
+
self.updateGeometry()
|
|
112
|
+
|
|
113
|
+
def resizeEvent(self, event):
|
|
114
|
+
super().resizeEvent(event)
|
|
115
|
+
self._wrap()
|
|
116
|
+
|
|
117
|
+
def sizeHint(self, which, constraint=None):
|
|
118
|
+
width = {
|
|
119
|
+
QtCore.Qt.SizeHint.MinimumSize: 0,
|
|
120
|
+
QtCore.Qt.SizeHint.PreferredSize: 300,
|
|
121
|
+
QtCore.Qt.SizeHint.MaximumSize: 16777215,
|
|
122
|
+
}.get(which, 0)
|
|
123
|
+
return QtCore.QSizeF(width, self._height)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class PlotAnnotationMixin:
|
|
127
|
+
def _init_plot_annotation(self):
|
|
128
|
+
self.source_annotation = PlotSourceLabel()
|
|
129
|
+
# PlotItem reserves row zero for its title, aligned with the data axes.
|
|
130
|
+
self.plot.layout.removeItem(self.plot.titleLabel)
|
|
131
|
+
self.plot.titleLabel.hide()
|
|
132
|
+
self.plot.layout.setRowMinimumHeight(0, 0)
|
|
133
|
+
self.plot.layout.setRowMaximumHeight(0, 16777215)
|
|
134
|
+
self.plot.layout.setRowPreferredHeight(0, -1)
|
|
135
|
+
self.plot.layout.addItem(self.source_annotation, 0, 1)
|
|
136
|
+
self._annotation_in_layout = True
|
|
137
|
+
self.trace_updated.connect(self._update_plot_annotation)
|
|
138
|
+
self._update_plot_annotation()
|
|
139
|
+
|
|
140
|
+
def _set_plot_annotation_visible(self, visible):
|
|
141
|
+
if visible != self._annotation_in_layout:
|
|
142
|
+
if visible:
|
|
143
|
+
self.plot.layout.setRowMinimumHeight(0, 0)
|
|
144
|
+
self.plot.layout.setRowMaximumHeight(0, 16777215)
|
|
145
|
+
self.plot.layout.setRowPreferredHeight(0, -1)
|
|
146
|
+
self.plot.layout.addItem(self.source_annotation, 0, 1)
|
|
147
|
+
else:
|
|
148
|
+
self.plot.layout.removeItem(self.source_annotation)
|
|
149
|
+
self.plot.layout.setRowFixedHeight(0, 0)
|
|
150
|
+
self._annotation_in_layout = visible
|
|
151
|
+
self.source_annotation.setVisible(visible)
|
|
152
|
+
|
|
153
|
+
def _update_plot_annotation(self):
|
|
154
|
+
annotation = self.__dict__.get("source_annotation")
|
|
155
|
+
if annotation is None:
|
|
156
|
+
return
|
|
157
|
+
visible = self.config.get(SHOW_SOURCE_ANNOTATION_KEY)
|
|
158
|
+
self._set_plot_annotation_visible(visible)
|
|
159
|
+
if not visible:
|
|
160
|
+
return
|
|
161
|
+
sources = []
|
|
162
|
+
if "lines" not in self.__dict__ and self.__dict__.get("line") is not None:
|
|
163
|
+
sources.append(self) # A standalone heatmap cut has no trace registry.
|
|
164
|
+
styles = self.__dict__.get("_trace_styles", {})
|
|
165
|
+
for key, line in self.__dict__.get("lines", {}).items():
|
|
166
|
+
if line is None:
|
|
167
|
+
continue
|
|
168
|
+
style = styles.get(key)
|
|
169
|
+
if style is not None and (not style.visible or style.opacity == 0):
|
|
170
|
+
continue
|
|
171
|
+
source = self if line is self.__dict__.get("line") else line.from_win
|
|
172
|
+
sources.append(source)
|
|
173
|
+
for layer in self.__dict__.get("heatmaps", {}).values():
|
|
174
|
+
if self._heatmap_is_visible(layer) and self._heatmap_opacity(layer) > 0:
|
|
175
|
+
sources.append(self if layer is self else layer.from_win)
|
|
176
|
+
# Keep the primary source first even when appearance rows are reordered.
|
|
177
|
+
sources.sort(key=lambda source: source is not self)
|
|
178
|
+
source_info = [source._plot_source_info for source in sources]
|
|
179
|
+
text = format_plot_sources(source_info)
|
|
180
|
+
font = QtGui.QFont(QtWidgets.QApplication.font())
|
|
181
|
+
font.setPointSize(self.config.get(FONT_SIZE_KEY))
|
|
182
|
+
annotation.set_source_text(
|
|
183
|
+
text, font, pg.mkColor(self.config.theme.plot_foreground),
|
|
184
|
+
rich_text=format_plot_sources(source_info, rich_text=True),
|
|
185
|
+
background=pg.mkColor(self.config.theme.plot_background),
|
|
186
|
+
)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Shared widgets used by plot-item appearance dialogs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from PyQt6 import QtCore, QtGui
|
|
8
|
+
from PyQt6 import QtWidgets as qtw
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CenteredPreviewDelegate(qtw.QStyledItemDelegate):
|
|
12
|
+
"""Paint an icon in the centre of its cell, optionally limiting its size."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent=None, *, icon_size: QtCore.QSize | None = None):
|
|
15
|
+
super().__init__(parent)
|
|
16
|
+
self.icon_size = icon_size
|
|
17
|
+
|
|
18
|
+
def paint(self, painter, option, index):
|
|
19
|
+
icon = index.data(QtCore.Qt.ItemDataRole.DecorationRole)
|
|
20
|
+
if not isinstance(icon, QtGui.QIcon) or icon.isNull():
|
|
21
|
+
super().paint(painter, option, index)
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
styled_option = qtw.QStyleOptionViewItem(option)
|
|
25
|
+
self.initStyleOption(styled_option, index)
|
|
26
|
+
styled_option.text = ""
|
|
27
|
+
styled_option.icon = QtGui.QIcon()
|
|
28
|
+
|
|
29
|
+
widget = styled_option.widget
|
|
30
|
+
style = widget.style() if widget else qtw.QApplication.style()
|
|
31
|
+
if style is None:
|
|
32
|
+
super().paint(painter, option, index)
|
|
33
|
+
return
|
|
34
|
+
style.drawControl(
|
|
35
|
+
qtw.QStyle.ControlElement.CE_ItemViewItem,
|
|
36
|
+
styled_option,
|
|
37
|
+
painter,
|
|
38
|
+
widget,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
available = styled_option.rect.size()
|
|
42
|
+
if self.icon_size is not None:
|
|
43
|
+
available = available.boundedTo(self.icon_size)
|
|
44
|
+
icon_size = icon.actualSize(available)
|
|
45
|
+
icon_rect = QtCore.QRect(QtCore.QPoint(), icon_size)
|
|
46
|
+
icon_rect.moveCenter(styled_option.rect.center())
|
|
47
|
+
icon.paint(painter, icon_rect, QtCore.Qt.AlignmentFlag.AlignCenter)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ReorderAppearanceTable(qtw.QTableWidget):
|
|
51
|
+
"""Three-column appearance table with internal multi-row reordering."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
dialog: Any,
|
|
56
|
+
*,
|
|
57
|
+
mime_type: str,
|
|
58
|
+
) -> None:
|
|
59
|
+
super().__init__(0, 3, dialog)
|
|
60
|
+
self.dialog = dialog
|
|
61
|
+
self._reorder_mime_type = mime_type
|
|
62
|
+
self._dragged_rows: list[int] = []
|
|
63
|
+
self._drag_origin = QtCore.QModelIndex()
|
|
64
|
+
self.setMouseTracking(True)
|
|
65
|
+
self.viewport().setMouseTracking(True)
|
|
66
|
+
self._drop_indicator = qtw.QWidget(self.viewport())
|
|
67
|
+
self._drop_indicator.setAttribute(
|
|
68
|
+
QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents
|
|
69
|
+
)
|
|
70
|
+
self._drop_indicator.setAttribute(
|
|
71
|
+
QtCore.Qt.WidgetAttribute.WA_StyledBackground
|
|
72
|
+
)
|
|
73
|
+
self._drop_indicator.setFixedHeight(2)
|
|
74
|
+
self._drop_indicator.setStyleSheet(
|
|
75
|
+
"background-color: palette(highlight);"
|
|
76
|
+
)
|
|
77
|
+
self._drop_indicator.hide()
|
|
78
|
+
|
|
79
|
+
def selectRow(self, row: int) -> None:
|
|
80
|
+
"""Select a whole row reliably before the table has been displayed."""
|
|
81
|
+
|
|
82
|
+
super().selectRow(row)
|
|
83
|
+
selection_model = self.selectionModel()
|
|
84
|
+
model = self.model()
|
|
85
|
+
if selection_model is None or model is None:
|
|
86
|
+
return
|
|
87
|
+
index = model.index(row, 0)
|
|
88
|
+
if not index.isValid():
|
|
89
|
+
return
|
|
90
|
+
selection_model.select(
|
|
91
|
+
index,
|
|
92
|
+
QtCore.QItemSelectionModel.SelectionFlag.Select
|
|
93
|
+
| QtCore.QItemSelectionModel.SelectionFlag.Rows,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
|
97
|
+
self._drag_origin = self.indexAt(event.position().toPoint())
|
|
98
|
+
super().mousePressEvent(event)
|
|
99
|
+
|
|
100
|
+
def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
|
|
101
|
+
try:
|
|
102
|
+
super().mouseReleaseEvent(event)
|
|
103
|
+
finally:
|
|
104
|
+
self._drag_origin = QtCore.QModelIndex()
|
|
105
|
+
|
|
106
|
+
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
|
|
107
|
+
self._update_reorder_cursor(event.position().toPoint())
|
|
108
|
+
super().mouseMoveEvent(event)
|
|
109
|
+
|
|
110
|
+
def leaveEvent(self, event: QtCore.QEvent) -> None:
|
|
111
|
+
self.viewport().unsetCursor()
|
|
112
|
+
super().leaveEvent(event)
|
|
113
|
+
|
|
114
|
+
def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None:
|
|
115
|
+
if self._is_internal_reorder_drag(event):
|
|
116
|
+
event.acceptProposedAction()
|
|
117
|
+
return
|
|
118
|
+
event.ignore()
|
|
119
|
+
|
|
120
|
+
def dragMoveEvent(self, event: QtGui.QDragMoveEvent) -> None:
|
|
121
|
+
if self._is_internal_reorder_drag(event):
|
|
122
|
+
self._show_drop_indicator(
|
|
123
|
+
self._drop_insertion_row(event.position().toPoint())
|
|
124
|
+
)
|
|
125
|
+
event.acceptProposedAction()
|
|
126
|
+
return
|
|
127
|
+
event.ignore()
|
|
128
|
+
|
|
129
|
+
def dragLeaveEvent(self, event: QtGui.QDragLeaveEvent) -> None:
|
|
130
|
+
self._hide_drop_indicator()
|
|
131
|
+
super().dragLeaveEvent(event)
|
|
132
|
+
|
|
133
|
+
def dropEvent(self, event: QtGui.QDropEvent) -> None:
|
|
134
|
+
self._hide_drop_indicator()
|
|
135
|
+
if not self._is_internal_reorder_drag(event):
|
|
136
|
+
event.ignore()
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
source_rows = self._dragged_rows
|
|
140
|
+
self._dragged_rows = []
|
|
141
|
+
if not source_rows:
|
|
142
|
+
event.ignore()
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
self.dialog._move_rows_to_position(
|
|
146
|
+
source_rows,
|
|
147
|
+
self._drop_insertion_row(event.position().toPoint()),
|
|
148
|
+
)
|
|
149
|
+
event.acceptProposedAction()
|
|
150
|
+
|
|
151
|
+
def startDrag(self, supported_actions):
|
|
152
|
+
del supported_actions
|
|
153
|
+
index = self._drag_origin
|
|
154
|
+
if not index.isValid():
|
|
155
|
+
index = self.currentIndex()
|
|
156
|
+
if not index.isValid() or index.column() == self.dialog._COL_PREVIEW:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
source_rows = self.dialog._selected_rows()
|
|
160
|
+
if not source_rows:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
mime_data = QtCore.QMimeData()
|
|
164
|
+
mime_data.setData(self._reorder_mime_type, QtCore.QByteArray())
|
|
165
|
+
drag = QtGui.QDrag(self)
|
|
166
|
+
drag.setMimeData(mime_data)
|
|
167
|
+
self._dragged_rows = source_rows
|
|
168
|
+
try:
|
|
169
|
+
drag.exec(QtCore.Qt.DropAction.MoveAction)
|
|
170
|
+
finally:
|
|
171
|
+
self._dragged_rows = []
|
|
172
|
+
self._hide_drop_indicator()
|
|
173
|
+
|
|
174
|
+
def _is_internal_reorder_drag(self, event: QtGui.QDropEvent) -> bool:
|
|
175
|
+
return (
|
|
176
|
+
event.source() is self
|
|
177
|
+
and event.mimeData().hasFormat(self._reorder_mime_type)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _drop_insertion_row(self, position: QtCore.QPoint) -> int:
|
|
181
|
+
index = self.indexAt(position)
|
|
182
|
+
if not index.isValid():
|
|
183
|
+
return self.rowCount()
|
|
184
|
+
if position.y() > self.visualRect(index).center().y():
|
|
185
|
+
return index.row() + 1
|
|
186
|
+
return index.row()
|
|
187
|
+
|
|
188
|
+
def _update_reorder_cursor(self, position: QtCore.QPoint) -> None:
|
|
189
|
+
index = self.indexAt(position)
|
|
190
|
+
if index.isValid() and index.column() != self.dialog._COL_PREVIEW:
|
|
191
|
+
self.viewport().setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
|
|
192
|
+
return
|
|
193
|
+
self.viewport().unsetCursor()
|
|
194
|
+
|
|
195
|
+
def _show_drop_indicator(self, destination_row: int) -> None:
|
|
196
|
+
if not self.rowCount():
|
|
197
|
+
self._hide_drop_indicator()
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
destination_row = max(0, min(destination_row, self.rowCount()))
|
|
201
|
+
if destination_row == self.rowCount():
|
|
202
|
+
index = self.model().index(self.rowCount() - 1, 0)
|
|
203
|
+
y_position = self.visualRect(index).bottom() + 1
|
|
204
|
+
else:
|
|
205
|
+
index = self.model().index(destination_row, 0)
|
|
206
|
+
y_position = self.visualRect(index).top()
|
|
207
|
+
self._drop_indicator.setGeometry(
|
|
208
|
+
0,
|
|
209
|
+
max(0, y_position - 1),
|
|
210
|
+
self.viewport().width(),
|
|
211
|
+
self._drop_indicator.height(),
|
|
212
|
+
)
|
|
213
|
+
self._drop_indicator.show()
|
|
214
|
+
self._drop_indicator.raise_()
|
|
215
|
+
|
|
216
|
+
def _hide_drop_indicator(self) -> None:
|
|
217
|
+
self._drop_indicator.hide()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def configure_appearance_table(
|
|
221
|
+
table: qtw.QTableWidget,
|
|
222
|
+
dialog: Any,
|
|
223
|
+
*,
|
|
224
|
+
object_name: str,
|
|
225
|
+
item_name: str,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Apply the common appearance-table layout and interaction policy."""
|
|
228
|
+
|
|
229
|
+
table.setObjectName(object_name)
|
|
230
|
+
table.setHorizontalHeaderLabels(["ID", "Preview", "Measurement"])
|
|
231
|
+
table.setEditTriggers(qtw.QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
232
|
+
table.setSelectionBehavior(qtw.QAbstractItemView.SelectionBehavior.SelectRows)
|
|
233
|
+
table.setSelectionMode(qtw.QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
234
|
+
table.setAlternatingRowColors(True)
|
|
235
|
+
table.setShowGrid(False)
|
|
236
|
+
table.setWordWrap(False)
|
|
237
|
+
table.setTextElideMode(QtCore.Qt.TextElideMode.ElideRight)
|
|
238
|
+
table.setIconSize(QtCore.QSize(64, 22))
|
|
239
|
+
table.setHorizontalScrollBarPolicy(
|
|
240
|
+
QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
|
241
|
+
)
|
|
242
|
+
table.setToolTip(f"Drag a {item_name} up or down to change its order.")
|
|
243
|
+
table.setDragEnabled(True)
|
|
244
|
+
table.setAcceptDrops(True)
|
|
245
|
+
table.viewport().setAcceptDrops(True)
|
|
246
|
+
table.setDragDropMode(qtw.QAbstractItemView.DragDropMode.DragDrop)
|
|
247
|
+
table.setDefaultDropAction(QtCore.Qt.DropAction.MoveAction)
|
|
248
|
+
table.setDropIndicatorShown(False)
|
|
249
|
+
table.setItemDelegateForColumn(
|
|
250
|
+
dialog._COL_PREVIEW,
|
|
251
|
+
CenteredPreviewDelegate(table),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
horizontal_header = table.horizontalHeader()
|
|
255
|
+
if horizontal_header is not None:
|
|
256
|
+
header_font = horizontal_header.font()
|
|
257
|
+
header_font.setBold(False)
|
|
258
|
+
horizontal_header.setFont(header_font)
|
|
259
|
+
for column in range(table.columnCount()):
|
|
260
|
+
header_item = table.horizontalHeaderItem(column)
|
|
261
|
+
if header_item is not None:
|
|
262
|
+
item_font = header_item.font()
|
|
263
|
+
item_font.setBold(False)
|
|
264
|
+
header_item.setFont(item_font)
|
|
265
|
+
horizontal_header.setFixedHeight(24)
|
|
266
|
+
horizontal_header.setSectionResizeMode(
|
|
267
|
+
dialog._COL_ID,
|
|
268
|
+
qtw.QHeaderView.ResizeMode.ResizeToContents,
|
|
269
|
+
)
|
|
270
|
+
horizontal_header.setSectionResizeMode(
|
|
271
|
+
dialog._COL_PREVIEW,
|
|
272
|
+
qtw.QHeaderView.ResizeMode.ResizeToContents,
|
|
273
|
+
)
|
|
274
|
+
horizontal_header.setSectionResizeMode(
|
|
275
|
+
dialog._COL_MEASUREMENT,
|
|
276
|
+
qtw.QHeaderView.ResizeMode.Stretch,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
vertical_header = table.verticalHeader()
|
|
280
|
+
if vertical_header is not None:
|
|
281
|
+
vertical_header.setVisible(False)
|
|
282
|
+
vertical_header.setDefaultSectionSize(28)
|
|
283
|
+
vertical_header.setMinimumSectionSize(24)
|