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.
Files changed (130) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +67 -0
  4. qplot/_build_info.json +1 -0
  5. qplot/_metadata.py +75 -0
  6. qplot/_repair.py +14 -0
  7. qplot/_shutdown_supervisor.py +4049 -0
  8. qplot/_version.py +12 -0
  9. qplot/_windows_shutdown_job.py +1166 -0
  10. qplot/configuration/__init__.py +3 -0
  11. qplot/configuration/config.py +578 -0
  12. qplot/configuration/config_schema.json +357 -0
  13. qplot/configuration/fonts.py +3 -0
  14. qplot/configuration/scripts.py +255 -0
  15. qplot/configuration/themes/__init__.py +13 -0
  16. qplot/configuration/themes/_base.py +592 -0
  17. qplot/configuration/themes/blank.py +17 -0
  18. qplot/configuration/themes/dark.py +54 -0
  19. qplot/configuration/themes/light.py +54 -0
  20. qplot/datahandling/LoadFromDB.py +224 -0
  21. qplot/datahandling/__init__.py +47 -0
  22. qplot/datahandling/_trusted_live_helper.py +812 -0
  23. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  24. qplot/datahandling/database.py +2340 -0
  25. qplot/datahandling/dimensions.py +43 -0
  26. qplot/datahandling/file_identity.py +461 -0
  27. qplot/datahandling/parameter_data.py +149 -0
  28. qplot/datahandling/plot_progress.py +17 -0
  29. qplot/datahandling/qcodes_cache.py +277 -0
  30. qplot/datahandling/qcodes_compat.py +14 -0
  31. qplot/datahandling/readSQL.py +2442 -0
  32. qplot/datahandling/readonly.py +2566 -0
  33. qplot/datahandling/source_activity.py +21 -0
  34. qplot/datahandling/trusted_array_preview.py +148 -0
  35. qplot/datahandling/trusted_derived_cache.py +1605 -0
  36. qplot/datahandling/trusted_derived_rendering.py +876 -0
  37. qplot/datahandling/trusted_heatmap.py +246 -0
  38. qplot/datahandling/trusted_live.py +2808 -0
  39. qplot/datahandling/trusted_live_queries.py +5907 -0
  40. qplot/datahandling/trusted_live_service.py +1919 -0
  41. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  42. qplot/datahandling/trusted_plot.py +283 -0
  43. qplot/datahandling/trusted_presentation.py +1065 -0
  44. qplot/datahandling/trusted_snapshot.py +1132 -0
  45. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  46. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  47. qplot/diagnostics.py +241 -0
  48. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  49. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  50. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  51. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  52. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  53. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  54. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  55. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  56. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  57. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  58. qplot/testdata.py +1804 -0
  59. qplot/tools/__init__.py +13 -0
  60. qplot/tools/finite_means.py +123 -0
  61. qplot/tools/general.py +139 -0
  62. qplot/tools/heatmap_geometry.py +391 -0
  63. qplot/tools/operation_registry.py +179 -0
  64. qplot/tools/plot_tools.py +650 -0
  65. qplot/tools/sample_statistics.py +111 -0
  66. qplot/tools/worker.py +2838 -0
  67. qplot/windows/__init__.py +9 -0
  68. qplot/windows/_colorbar.py +422 -0
  69. qplot/windows/_commands.py +523 -0
  70. qplot/windows/_config_persistence.py +123 -0
  71. qplot/windows/_database_actions.py +4933 -0
  72. qplot/windows/_dataset_handle.py +221 -0
  73. qplot/windows/_display_precision.py +48 -0
  74. qplot/windows/_dragdrop.py +70 -0
  75. qplot/windows/_export_paths.py +1167 -0
  76. qplot/windows/_help.py +210 -0
  77. qplot/windows/_mixed_appearance.py +303 -0
  78. qplot/windows/_mixed_plots.py +461 -0
  79. qplot/windows/_native_averaging.py +323 -0
  80. qplot/windows/_native_transforms.py +551 -0
  81. qplot/windows/_plot1d_snap.py +612 -0
  82. qplot/windows/_plot1d_traces.py +2236 -0
  83. qplot/windows/_plot2d_colorbar.py +1127 -0
  84. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  85. qplot/windows/_plot2d_layers.py +2291 -0
  86. qplot/windows/_plot2d_sweeps.py +712 -0
  87. qplot/windows/_plotWin.py +2082 -0
  88. qplot/windows/_plot_actions.py +2703 -0
  89. qplot/windows/_plot_annotation.py +186 -0
  90. qplot/windows/_plot_appearance.py +283 -0
  91. qplot/windows/_plot_axis_scaling.py +1521 -0
  92. qplot/windows/_plot_data_viewbox.py +94 -0
  93. qplot/windows/_plot_drop_axes.py +224 -0
  94. qplot/windows/_plot_export.py +1445 -0
  95. qplot/windows/_plot_feedback.py +112 -0
  96. qplot/windows/_plot_marquee.py +659 -0
  97. qplot/windows/_plot_refresh.py +950 -0
  98. qplot/windows/_plot_source_promotion.py +178 -0
  99. qplot/windows/_plot_state.py +237 -0
  100. qplot/windows/_plot_status.py +115 -0
  101. qplot/windows/_plot_tiling.py +35 -0
  102. qplot/windows/_plot_transform_labels.py +60 -0
  103. qplot/windows/_preferences.py +724 -0
  104. qplot/windows/_refresh_interval.py +172 -0
  105. qplot/windows/_run_controls.py +789 -0
  106. qplot/windows/_shortcuts.py +47 -0
  107. qplot/windows/_subplots/__init__.py +8 -0
  108. qplot/windows/_subplots/subplot1d.py +532 -0
  109. qplot/windows/_subplots/subplot2d.py +919 -0
  110. qplot/windows/_svg_export.py +209 -0
  111. qplot/windows/_trusted_derived_qt.py +1759 -0
  112. qplot/windows/_widgets/__init__.py +23 -0
  113. qplot/windows/_widgets/_run_formatting.py +387 -0
  114. qplot/windows/_widgets/details_tables.py +1119 -0
  115. qplot/windows/_widgets/dropbox.py +290 -0
  116. qplot/windows/_widgets/operations.py +460 -0
  117. qplot/windows/_widgets/preview.py +2997 -0
  118. qplot/windows/_widgets/run_list_items.py +488 -0
  119. qplot/windows/_widgets/toolbar.py +111 -0
  120. qplot/windows/_widgets/treeWidgets.py +2572 -0
  121. qplot/windows/_window_controls.py +414 -0
  122. qplot/windows/main.py +1632 -0
  123. qplot/windows/plot1d.py +313 -0
  124. qplot/windows/plot2d.py +1935 -0
  125. qplotter-1.6.0.dist-info/METADATA +322 -0
  126. qplotter-1.6.0.dist-info/RECORD +130 -0
  127. qplotter-1.6.0.dist-info/WHEEL +5 -0
  128. qplotter-1.6.0.dist-info/entry_points.txt +4 -0
  129. qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
  130. 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)