qplotter 1.6.0b3__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 (119) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +63 -0
  4. qplot/_metadata.py +12 -0
  5. qplot/_repair.py +14 -0
  6. qplot/_shutdown_supervisor.py +4049 -0
  7. qplot/_version.py +12 -0
  8. qplot/_windows_shutdown_job.py +1166 -0
  9. qplot/configuration/__init__.py +3 -0
  10. qplot/configuration/config.py +573 -0
  11. qplot/configuration/config_schema.json +319 -0
  12. qplot/configuration/scripts.py +255 -0
  13. qplot/configuration/themes/__init__.py +13 -0
  14. qplot/configuration/themes/_base.py +579 -0
  15. qplot/configuration/themes/blank.py +17 -0
  16. qplot/configuration/themes/dark.py +54 -0
  17. qplot/configuration/themes/light.py +54 -0
  18. qplot/datahandling/LoadFromDB.py +224 -0
  19. qplot/datahandling/__init__.py +47 -0
  20. qplot/datahandling/_trusted_live_helper.py +812 -0
  21. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  22. qplot/datahandling/database.py +2330 -0
  23. qplot/datahandling/dimensions.py +43 -0
  24. qplot/datahandling/file_identity.py +461 -0
  25. qplot/datahandling/parameter_data.py +149 -0
  26. qplot/datahandling/plot_progress.py +17 -0
  27. qplot/datahandling/qcodes_cache.py +277 -0
  28. qplot/datahandling/qcodes_compat.py +14 -0
  29. qplot/datahandling/readSQL.py +2442 -0
  30. qplot/datahandling/readonly.py +2561 -0
  31. qplot/datahandling/source_activity.py +21 -0
  32. qplot/datahandling/trusted_array_preview.py +127 -0
  33. qplot/datahandling/trusted_derived_cache.py +1605 -0
  34. qplot/datahandling/trusted_derived_rendering.py +868 -0
  35. qplot/datahandling/trusted_heatmap.py +241 -0
  36. qplot/datahandling/trusted_live.py +2808 -0
  37. qplot/datahandling/trusted_live_queries.py +5907 -0
  38. qplot/datahandling/trusted_live_service.py +1894 -0
  39. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  40. qplot/datahandling/trusted_plot.py +283 -0
  41. qplot/datahandling/trusted_presentation.py +1065 -0
  42. qplot/datahandling/trusted_snapshot.py +1132 -0
  43. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  44. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  45. qplot/diagnostics.py +241 -0
  46. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  47. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  48. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  49. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  50. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  51. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  52. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  53. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  54. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  55. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  56. qplot/testdata.py +1733 -0
  57. qplot/tools/__init__.py +13 -0
  58. qplot/tools/finite_means.py +114 -0
  59. qplot/tools/general.py +134 -0
  60. qplot/tools/heatmap_geometry.py +381 -0
  61. qplot/tools/operation_registry.py +179 -0
  62. qplot/tools/plot_tools.py +625 -0
  63. qplot/tools/sample_statistics.py +56 -0
  64. qplot/tools/worker.py +2787 -0
  65. qplot/windows/__init__.py +9 -0
  66. qplot/windows/_colorbar.py +422 -0
  67. qplot/windows/_commands.py +523 -0
  68. qplot/windows/_config_persistence.py +123 -0
  69. qplot/windows/_database_actions.py +4904 -0
  70. qplot/windows/_dataset_handle.py +221 -0
  71. qplot/windows/_dragdrop.py +71 -0
  72. qplot/windows/_export_paths.py +1167 -0
  73. qplot/windows/_help.py +169 -0
  74. qplot/windows/_native_averaging.py +173 -0
  75. qplot/windows/_native_transforms.py +389 -0
  76. qplot/windows/_plot1d_snap.py +608 -0
  77. qplot/windows/_plot1d_traces.py +2183 -0
  78. qplot/windows/_plot2d_colorbar.py +1126 -0
  79. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  80. qplot/windows/_plot2d_layers.py +2197 -0
  81. qplot/windows/_plot2d_sweeps.py +712 -0
  82. qplot/windows/_plotWin.py +1993 -0
  83. qplot/windows/_plot_actions.py +2695 -0
  84. qplot/windows/_plot_appearance.py +276 -0
  85. qplot/windows/_plot_axis_scaling.py +1485 -0
  86. qplot/windows/_plot_export.py +1445 -0
  87. qplot/windows/_plot_feedback.py +110 -0
  88. qplot/windows/_plot_marquee.py +656 -0
  89. qplot/windows/_plot_refresh.py +942 -0
  90. qplot/windows/_plot_state.py +239 -0
  91. qplot/windows/_plot_transform_labels.py +60 -0
  92. qplot/windows/_preferences.py +635 -0
  93. qplot/windows/_refresh_interval.py +70 -0
  94. qplot/windows/_run_controls.py +828 -0
  95. qplot/windows/_shortcuts.py +47 -0
  96. qplot/windows/_subplots/__init__.py +8 -0
  97. qplot/windows/_subplots/subplot1d.py +531 -0
  98. qplot/windows/_subplots/subplot2d.py +918 -0
  99. qplot/windows/_svg_export.py +209 -0
  100. qplot/windows/_trusted_derived_qt.py +1759 -0
  101. qplot/windows/_widgets/__init__.py +23 -0
  102. qplot/windows/_widgets/_run_formatting.py +387 -0
  103. qplot/windows/_widgets/details_tables.py +1119 -0
  104. qplot/windows/_widgets/dropbox.py +290 -0
  105. qplot/windows/_widgets/operations.py +460 -0
  106. qplot/windows/_widgets/preview.py +2997 -0
  107. qplot/windows/_widgets/run_list_items.py +488 -0
  108. qplot/windows/_widgets/toolbar.py +111 -0
  109. qplot/windows/_widgets/treeWidgets.py +2572 -0
  110. qplot/windows/_window_controls.py +413 -0
  111. qplot/windows/main.py +1635 -0
  112. qplot/windows/plot1d.py +311 -0
  113. qplot/windows/plot2d.py +1901 -0
  114. qplotter-1.6.0b3.dist-info/METADATA +318 -0
  115. qplotter-1.6.0b3.dist-info/RECORD +119 -0
  116. qplotter-1.6.0b3.dist-info/WHEEL +5 -0
  117. qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
  118. qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
  119. qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,523 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterable, Sequence
4
+ from dataclasses import dataclass, replace
5
+ from html import escape
6
+
7
+ from PyQt6 import QtCore, QtGui
8
+ from PyQt6 import QtWidgets as qtw
9
+ from PyQt6.QtGui import QKeySequence
10
+
11
+ from ._shortcuts import key_sequences, platform_key_sequences, standard_key_sequences
12
+
13
+ ShortcutSource = (
14
+ str
15
+ | QKeySequence
16
+ | Sequence[str | QKeySequence]
17
+ | Callable[[], Sequence[str | QKeySequence]]
18
+ )
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class CommandSpec:
23
+ """
24
+ Shared metadata for a user-facing command.
25
+
26
+ The registry keeps labels, shortcuts, status text, and shortcut-help text in
27
+ one place so menus, tooltips, and help dialogs do not drift apart.
28
+
29
+ """
30
+
31
+ command_id: str
32
+ text: str
33
+ status_tip: str = ""
34
+ shortcuts: ShortcutSource = ()
35
+ shortcut_context: QtCore.Qt.ShortcutContext = (
36
+ QtCore.Qt.ShortcutContext.WindowShortcut
37
+ )
38
+ object_name: str | None = None
39
+ help_section: str | None = None
40
+ help_shortcut: str | None = None
41
+ help_text: str | None = None
42
+ show_shortcut_in_context_menu: bool = True
43
+
44
+ def resolved_shortcuts(self) -> list[QKeySequence]:
45
+ """
46
+ Returns this command's active shortcuts for the current platform.
47
+
48
+ """
49
+ shortcuts = self.shortcuts() if callable(self.shortcuts) else self.shortcuts
50
+ if isinstance(shortcuts, (str, QKeySequence)):
51
+ return key_sequences([shortcuts])
52
+ return key_sequences(shortcuts)
53
+
54
+ def shortcut_display_text(self) -> str:
55
+ """
56
+ Returns a native display string for the active shortcuts.
57
+
58
+ """
59
+ return shortcut_display_text(self.resolved_shortcuts())
60
+
61
+ def help_row(self) -> tuple[str, str] | None:
62
+ """
63
+ Returns a `(shortcut, description)` row for shortcut help.
64
+
65
+ """
66
+ shortcut = self.help_shortcut or self.shortcut_display_text()
67
+ if not shortcut:
68
+ return None
69
+ return shortcut, self.help_text or self.status_tip
70
+
71
+
72
+ def _standard_shortcuts(
73
+ standard_key: QKeySequence.StandardKey,
74
+ fallback: Sequence[str | QKeySequence],
75
+ ) -> Callable[[], list[QKeySequence]]:
76
+ return lambda: standard_key_sequences(standard_key, fallback)
77
+
78
+
79
+ def _platform_shortcuts(
80
+ *,
81
+ mac: Sequence[str | QKeySequence] | None = None,
82
+ windows: Sequence[str | QKeySequence] | None = None,
83
+ other: Sequence[str | QKeySequence] | None = None,
84
+ ) -> Callable[[], list[QKeySequence]]:
85
+ return lambda: platform_key_sequences(mac=mac, windows=windows, other=other)
86
+
87
+
88
+ COMMANDS: dict[str, CommandSpec] = {
89
+ "help.quick_start": CommandSpec(
90
+ "help.quick_start",
91
+ "&Quick Start",
92
+ "Show the basic qPlot workflow",
93
+ "F1",
94
+ object_name="quickStartHelpAction",
95
+ help_section="General",
96
+ ),
97
+ "database.load": CommandSpec(
98
+ "database.load",
99
+ "&Load Database...",
100
+ "Load a QCoDeS database",
101
+ "Ctrl+L",
102
+ help_section="General",
103
+ ),
104
+ "database.close": CommandSpec(
105
+ "database.close",
106
+ "&Close Database",
107
+ "Close the current database and its plot windows",
108
+ object_name="closeDatabaseAction",
109
+ ),
110
+ "window.refresh": CommandSpec(
111
+ "window.refresh",
112
+ "&Refresh",
113
+ "Refresh the current window",
114
+ "R",
115
+ help_section="General",
116
+ ),
117
+ "window.close": CommandSpec(
118
+ "window.close",
119
+ "&Close Window",
120
+ "Close the current qPlot window",
121
+ _standard_shortcuts(QKeySequence.StandardKey.Close, ["Ctrl+W"]),
122
+ help_section="General",
123
+ help_shortcut="Ctrl+W / Cmd+W",
124
+ ),
125
+ "app.quit": CommandSpec(
126
+ "app.quit",
127
+ "&Quit qPlot",
128
+ "Close the database and quit qPlot",
129
+ _standard_shortcuts(QKeySequence.StandardKey.Quit, ["Ctrl+Q"]),
130
+ QtCore.Qt.ShortcutContext.ApplicationShortcut,
131
+ help_section="General",
132
+ help_shortcut="Ctrl+Q / Cmd+Q",
133
+ ),
134
+ "preferences.open": CommandSpec(
135
+ "preferences.open",
136
+ "&Preferences...",
137
+ "Open qPlot preferences",
138
+ "Ctrl+,",
139
+ help_section="General",
140
+ ),
141
+ "window.minimize": CommandSpec(
142
+ "window.minimize",
143
+ "&Minimize",
144
+ "Minimize this window",
145
+ _platform_shortcuts(mac=["Ctrl+M"], windows=["Alt+Space, N"]),
146
+ help_section="General",
147
+ help_shortcut="Ctrl+M / Alt+Space, N",
148
+ ),
149
+ "window.maximize_restore": CommandSpec(
150
+ "window.maximize_restore",
151
+ "Ma&ximize / Restore",
152
+ "Maximize or restore this window",
153
+ _platform_shortcuts(windows=["Alt+Space, X", "Alt+Space, R"]),
154
+ help_section="General",
155
+ help_shortcut="Alt+Space, X / Alt+Space, R",
156
+ help_text="Maximize or restore the current window on Windows",
157
+ ),
158
+ "window.full_screen": CommandSpec(
159
+ "window.full_screen",
160
+ "&Full Screen",
161
+ "Enter or leave full screen",
162
+ lambda: standard_key_sequences(
163
+ QKeySequence.StandardKey.FullScreen,
164
+ platform_key_sequences(
165
+ mac=["Ctrl+Meta+F"],
166
+ windows=["F11", "Alt+Enter"],
167
+ ),
168
+ ),
169
+ help_section="General",
170
+ help_shortcut="Ctrl+Cmd+F / F11",
171
+ ),
172
+ "copy.selection": CommandSpec(
173
+ "copy.selection",
174
+ "Copy Selection",
175
+ "Copy selected cells or rows in the run details pane",
176
+ _standard_shortcuts(QKeySequence.StandardKey.Copy, ["Ctrl+C"]),
177
+ QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut,
178
+ help_section="General",
179
+ help_shortcut="Ctrl+C / Cmd+C",
180
+ ),
181
+ "copy.cell": CommandSpec(
182
+ "copy.cell",
183
+ "Copy Cell",
184
+ "Copy the current cell or value in the run details pane",
185
+ "Ctrl+Shift+C",
186
+ QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut,
187
+ help_section="General",
188
+ help_shortcut="Ctrl+Shift+C / Cmd+Shift+C",
189
+ ),
190
+ "context.show": CommandSpec(
191
+ "context.show",
192
+ "Show Context Menu",
193
+ "Open the focused widget's context menu",
194
+ "Shift+F10",
195
+ help_section="General",
196
+ ),
197
+ "database.open_folder": CommandSpec(
198
+ "database.open_folder",
199
+ "Open Database &Folder",
200
+ "Open the current database folder",
201
+ "Ctrl+Shift+D",
202
+ help_section="General",
203
+ ),
204
+ "testdata.create_csv": CommandSpec(
205
+ "testdata.create_csv",
206
+ "Create Example &CSV...",
207
+ "Create an example CSV specification for generated test databases",
208
+ object_name="createTestDatabaseCsvAction",
209
+ ),
210
+ "testdata.export_collection": CommandSpec(
211
+ "testdata.export_collection",
212
+ "Export CSV &Collection...",
213
+ "Export the cumulative collection of test-database CSV specifications",
214
+ object_name="exportTestDatabaseCsvCollectionAction",
215
+ ),
216
+ "testdata.generate_database": CommandSpec(
217
+ "testdata.generate_database",
218
+ "Generate &Database from CSV...",
219
+ "Generate a QCoDeS test database from a CSV specification",
220
+ object_name="generateTestDatabaseAction",
221
+ ),
222
+ "window.main_front_back": CommandSpec(
223
+ "window.main_front_back",
224
+ "Main Window &Front/Back",
225
+ "Bring the main window to front, or behind the graph windows",
226
+ "Ctrl+Shift+M",
227
+ help_section="General",
228
+ ),
229
+ "run.plot_entered": CommandSpec(
230
+ "run.plot_entered",
231
+ "Plot Entered Run and Measurement",
232
+ "Plot the requested run and measurement",
233
+ "Ctrl+Return",
234
+ help_section="General",
235
+ ),
236
+ "run.plot_selected_all": CommandSpec(
237
+ "run.plot_selected_all",
238
+ "Plot All Measurements in Selected Run",
239
+ "Plot all measurements in the selected run",
240
+ "Ctrl+Shift+Return",
241
+ help_section="General",
242
+ ),
243
+ "run.plot_selected_measurements": CommandSpec(
244
+ "run.plot_selected_measurements",
245
+ "Plot Measurements 1 to 9 in Selected Run",
246
+ "Plot measurements 1 to 9 in the selected run",
247
+ help_section="General",
248
+ help_shortcut="Ctrl+1 to Ctrl+9",
249
+ ),
250
+ "plots.close_all": CommandSpec(
251
+ "plots.close_all",
252
+ "Close All &Plot Windows",
253
+ "Close all plot windows",
254
+ "Ctrl+Shift+W",
255
+ help_section="General",
256
+ ),
257
+ "plot.autoscale": CommandSpec(
258
+ "plot.autoscale",
259
+ "Autoscale",
260
+ "Return all plot axes to autoscale mode",
261
+ "Ctrl+0",
262
+ help_section="Plot Windows",
263
+ ),
264
+ "plot.copy_image": CommandSpec(
265
+ "plot.copy_image",
266
+ "&Copy Plot Image",
267
+ "Copy the plot image using the selected copy format or resolution",
268
+ _standard_shortcuts(QKeySequence.StandardKey.Copy, ["Ctrl+C"]),
269
+ object_name="copyPlotImageAction",
270
+ help_section="Plot Windows",
271
+ help_shortcut="Ctrl+C / Cmd+C",
272
+ ),
273
+ "plot.print": CommandSpec(
274
+ "plot.print",
275
+ "&Print Plot...",
276
+ "Print the visible plot area",
277
+ _standard_shortcuts(QKeySequence.StandardKey.Print, ["Ctrl+P"]),
278
+ object_name="printPlotAction",
279
+ help_section="Plot Windows",
280
+ help_shortcut="Ctrl+P / Cmd+P",
281
+ ),
282
+ "plot.export": CommandSpec(
283
+ "plot.export",
284
+ "&Export Plot...",
285
+ "Open the plot export dialog",
286
+ "Ctrl+E",
287
+ object_name="exportPlotAction",
288
+ help_section="Plot Windows",
289
+ help_text="Export the plot",
290
+ ),
291
+ "plot.toggle_operations": CommandSpec(
292
+ "plot.toggle_operations",
293
+ "View Operations",
294
+ "Show or hide the operations panel",
295
+ ),
296
+ "toolbar.refresh": CommandSpec(
297
+ "toolbar.refresh",
298
+ "Refresh Timer",
299
+ "Show or hide the refresh toolbar",
300
+ "Ctrl+Alt+R",
301
+ help_section="Plot Windows",
302
+ ),
303
+ "toolbar.coordinates": CommandSpec(
304
+ "toolbar.coordinates",
305
+ "Co-ordinates",
306
+ "Show or hide the coordinate toolbar",
307
+ "Ctrl+Alt+C",
308
+ help_section="Plot Windows",
309
+ ),
310
+ "toolbar.axis_control": CommandSpec(
311
+ "toolbar.axis_control",
312
+ "Data axes",
313
+ "Show or hide the data-axis panel",
314
+ "Ctrl+Alt+A",
315
+ help_section="Plot Windows",
316
+ ),
317
+ "toolbar.operations": CommandSpec(
318
+ "toolbar.operations",
319
+ "Operations",
320
+ "Show or hide the operations dock",
321
+ "Ctrl+Alt+O",
322
+ help_section="Plot Windows",
323
+ ),
324
+ "plot.snap_to_trace": CommandSpec(
325
+ "plot.snap_to_trace",
326
+ "Snap to &Trace",
327
+ "Snap the 1D coordinate readout to the nearest trace point",
328
+ "S",
329
+ help_section="Plot Windows",
330
+ ),
331
+ "heatmap.autoscale_color": CommandSpec(
332
+ "heatmap.autoscale_color",
333
+ "Autoscale Color",
334
+ "Autoscale the colour range",
335
+ "C",
336
+ help_section="Heatmaps",
337
+ ),
338
+ "heatmap.horizontal_cut": CommandSpec(
339
+ "heatmap.horizontal_cut",
340
+ "Horizontal Cut",
341
+ "Open a horizontal cut",
342
+ "H",
343
+ help_section="Heatmaps",
344
+ ),
345
+ "heatmap.vertical_cut": CommandSpec(
346
+ "heatmap.vertical_cut",
347
+ "Vertical Cut",
348
+ "Open a vertical cut",
349
+ "V",
350
+ help_section="Heatmaps",
351
+ ),
352
+ "heatmap.move_cut": CommandSpec(
353
+ "heatmap.move_cut",
354
+ "Move Selected Cut",
355
+ "Move the selected cut cursor by one pixel",
356
+ help_section="Heatmaps",
357
+ help_shortcut="Arrow keys",
358
+ ),
359
+ }
360
+
361
+ SHORTCUT_HELP_SECTIONS = (
362
+ "General",
363
+ "Plot Windows",
364
+ "Heatmaps",
365
+ )
366
+
367
+
368
+ def command_spec(command_id: str) -> CommandSpec:
369
+ """
370
+ Returns a registered command specification.
371
+
372
+ """
373
+ return COMMANDS[command_id]
374
+
375
+
376
+ def plot_measurement_command_spec(index: int) -> CommandSpec:
377
+ """
378
+ Returns the indexed run-measurement shortcut command.
379
+
380
+ """
381
+ number = index + 1
382
+ return CommandSpec(
383
+ f"run.plot_selected_measurement_{number}",
384
+ f"Plot Measurement {number} in Selected Run",
385
+ f"Plot measurement {number} in the selected run",
386
+ f"Ctrl+{number}",
387
+ )
388
+
389
+
390
+ def toolbar_toggle_command_spec(title: str) -> CommandSpec | None:
391
+ """
392
+ Returns the command used for a plot toolbar or dock toggle action.
393
+
394
+ """
395
+ by_title = {
396
+ "Refresh Timer": "toolbar.refresh",
397
+ "Co-ordinates": "toolbar.coordinates",
398
+ "Data axes": "toolbar.axis_control",
399
+ "Operations": "toolbar.operations",
400
+ }
401
+ command_id = by_title.get(title)
402
+ if command_id is None:
403
+ return None
404
+ return command_spec(command_id)
405
+
406
+
407
+ def create_action(
408
+ command: str | CommandSpec,
409
+ parent: qtw.QWidget,
410
+ *,
411
+ text: str | None = None,
412
+ object_name: str | None = None,
413
+ status_tip: str | None = None,
414
+ checkable: bool = False,
415
+ ) -> QtGui.QAction:
416
+ """
417
+ Creates a QAction and applies the shared command metadata.
418
+
419
+ """
420
+ spec = command_spec(command) if isinstance(command, str) else command
421
+ if status_tip is not None:
422
+ spec = replace(spec, status_tip=status_tip)
423
+ action = QtGui.QAction(text or spec.text, parent)
424
+ action.setCheckable(checkable)
425
+ configure_action(action, spec, object_name=object_name)
426
+ return action
427
+
428
+
429
+ def configure_action(
430
+ action: QtGui.QAction,
431
+ command: str | CommandSpec,
432
+ *,
433
+ object_name: str | None = None,
434
+ add_to: qtw.QWidget | None = None,
435
+ set_text: bool = False,
436
+ set_tooltip: bool = True,
437
+ ) -> QtGui.QAction:
438
+ """
439
+ Applies command shortcuts, status text, tooltip, and object name to an action.
440
+
441
+ """
442
+ spec = command_spec(command) if isinstance(command, str) else command
443
+
444
+ if set_text:
445
+ action.setText(spec.text)
446
+
447
+ shortcuts = spec.resolved_shortcuts()
448
+ if shortcuts:
449
+ action.setShortcuts(shortcuts)
450
+
451
+ action.setShortcutContext(spec.shortcut_context)
452
+ if hasattr(action, "setShortcutVisibleInContextMenu"):
453
+ action.setShortcutVisibleInContextMenu(spec.show_shortcut_in_context_menu)
454
+
455
+ if spec.status_tip:
456
+ action.setStatusTip(spec.status_tip)
457
+ if set_tooltip:
458
+ tooltip = spec.status_tip
459
+ shortcut_text = shortcut_display_text(shortcuts)
460
+ if shortcut_text:
461
+ tooltip = f"{tooltip} ({shortcut_text})"
462
+ action.setToolTip(tooltip)
463
+
464
+ resolved_object_name = object_name or spec.object_name
465
+ if resolved_object_name:
466
+ action.setObjectName(resolved_object_name)
467
+
468
+ if add_to is not None and action not in add_to.actions():
469
+ add_to.addAction(action)
470
+
471
+ return action
472
+
473
+
474
+ def shortcut_display_text(shortcuts: Iterable[QKeySequence]) -> str:
475
+ """
476
+ Formats shortcuts for status tips and generated help.
477
+
478
+ """
479
+ return " / ".join(
480
+ shortcut.toString(QKeySequence.SequenceFormat.NativeText)
481
+ for shortcut in shortcuts
482
+ if not shortcut.isEmpty()
483
+ )
484
+
485
+
486
+ def shortcut_help_html() -> str:
487
+ """
488
+ Builds the keyboard shortcut help HTML from registered command metadata.
489
+
490
+ """
491
+ parts = ["<h2>Keyboard Shortcuts</h2>"]
492
+ for section in SHORTCUT_HELP_SECTIONS:
493
+ rows: list[tuple[str, str]] = []
494
+ for spec in COMMANDS.values():
495
+ if spec.help_section != section:
496
+ continue
497
+ row = spec.help_row()
498
+ if row is not None:
499
+ rows.append(row)
500
+ if not rows:
501
+ continue
502
+
503
+ parts.append(f"<h3>{escape(section)}</h3>")
504
+ parts.append('<table cellspacing="4" cellpadding="3">')
505
+ for shortcut, description in rows:
506
+ parts.append(
507
+ " <tr>"
508
+ f"<td><b>{escape(shortcut)}</b></td>"
509
+ f"<td>{escape(description)}</td>"
510
+ "</tr>"
511
+ )
512
+ parts.append("</table>")
513
+
514
+ return "\n".join(parts)
515
+
516
+
517
+ def command_with_status(command_id: str, status_tip: str) -> CommandSpec:
518
+ """
519
+ Returns a registered command with a context-specific status tip.
520
+
521
+ """
522
+ spec = command_spec(command_id)
523
+ return replace(spec, status_tip=status_tip)
@@ -0,0 +1,123 @@
1
+ from collections.abc import Callable, Mapping
2
+ from typing import Any
3
+
4
+ from PyQt6 import QtWidgets as qtw
5
+
6
+ from qplot.diagnostics import log_exception
7
+
8
+
9
+ def set_widget_value_without_signals(
10
+ widget: Any,
11
+ setter: Callable[[Any], None],
12
+ value: Any,
13
+ ) -> None:
14
+ """Set a Qt control value while preserving its signal-blocking state."""
15
+
16
+ signals_were_blocked = widget.blockSignals(True)
17
+ try:
18
+ setter(value)
19
+ finally:
20
+ widget.blockSignals(signals_were_blocked)
21
+
22
+
23
+ def persist_config_action(
24
+ owner: Any,
25
+ operation: Callable[[], None],
26
+ description: str,
27
+ rollback: Callable[[], None] | None = None,
28
+ ) -> bool:
29
+ """Run one config write, rolling back GUI state and reporting one error."""
30
+
31
+ try:
32
+ operation()
33
+ except Exception as error:
34
+ log_exception(
35
+ f"Persisting {description} failed",
36
+ error,
37
+ __name__,
38
+ )
39
+ if rollback is not None:
40
+ try:
41
+ rollback()
42
+ except Exception as rollback_error:
43
+ log_exception(
44
+ f"Rolling back {description} controls failed",
45
+ rollback_error,
46
+ __name__,
47
+ )
48
+ _show_config_error(owner, description, error)
49
+ return False
50
+
51
+ return True
52
+
53
+
54
+ def persist_config_value(
55
+ owner: Any,
56
+ config: Any,
57
+ key: str,
58
+ value: Any,
59
+ description: str,
60
+ rollback: Callable[[], None] | None = None,
61
+ ) -> bool:
62
+ """Persist one config value through the shared GUI failure policy."""
63
+
64
+ return persist_config_action(
65
+ owner,
66
+ lambda: config.update(key, value),
67
+ description,
68
+ rollback,
69
+ )
70
+
71
+
72
+ def persist_config_values(
73
+ owner: Any,
74
+ config: Any,
75
+ values: Mapping[str, Any],
76
+ description: str,
77
+ rollback: Callable[[], None] | None = None,
78
+ ) -> bool:
79
+ """Persist related config values as one atomic GUI action."""
80
+
81
+ updates = dict(values)
82
+ return persist_config_action(
83
+ owner,
84
+ lambda: config.update_many(updates),
85
+ description,
86
+ rollback,
87
+ )
88
+
89
+
90
+ def _show_config_error(owner: Any, description: str, error: Exception) -> None:
91
+ title = "Settings Not Saved"
92
+ message = (
93
+ f"Could not save {description}. Your previous setting is still active. "
94
+ "Check that the qPlot configuration folder is writable, then try again."
95
+ )
96
+ show_error = getattr(owner, "show_error", None)
97
+ if callable(show_error):
98
+ try:
99
+ show_error(title, message, str(error))
100
+ except Exception as notification_error:
101
+ log_exception(
102
+ "Showing the configuration error failed",
103
+ notification_error,
104
+ __name__,
105
+ )
106
+ return
107
+
108
+ if not isinstance(owner, qtw.QWidget):
109
+ return
110
+
111
+ try:
112
+ box = qtw.QMessageBox(owner)
113
+ box.setIcon(qtw.QMessageBox.Icon.Warning)
114
+ box.setWindowTitle(title)
115
+ box.setText(message)
116
+ box.setDetailedText(str(error))
117
+ box.exec()
118
+ except Exception as notification_error:
119
+ log_exception(
120
+ "Showing the configuration error failed",
121
+ notification_error,
122
+ __name__,
123
+ )