ms-components 0.5.2__tar.gz → 0.5.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. {ms_components-0.5.2 → ms_components-0.5.4}/PKG-INFO +2 -2
  2. {ms_components-0.5.2 → ms_components-0.5.4}/pyproject.toml +1 -1
  3. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_executors.py +14 -2
  4. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/widget.py +40 -0
  5. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_projects/browser.py +21 -11
  6. ms_components-0.5.4/src/ms_components/ms_pymol/icons/export.svg +5 -0
  7. ms_components-0.5.4/src/ms_components/ms_pymol/icons/save.svg +5 -0
  8. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/scene.py +1 -10
  9. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/widget.py +99 -5
  10. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/example_usage.py +2 -2
  11. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/metric_filter_bar.py +1 -1
  12. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/palettes.py +3 -0
  13. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/step_dialog.py +33 -2
  14. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/tool_panel.py +2 -2
  15. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components.egg-info/PKG-INFO +2 -2
  16. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components.egg-info/SOURCES.txt +2 -0
  17. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components.egg-info/requires.txt +1 -1
  18. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components.egg-info/scm_file_list.json +2 -0
  19. ms_components-0.5.4/src/ms_components.egg-info/scm_version.json +8 -0
  20. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_executors_ui.py +37 -0
  21. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_molsuite_monitor_widgets.py +116 -0
  22. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_pymol_scene.py +2 -0
  23. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_pymol_toolbar.py +86 -2
  24. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_step_dialog.py +22 -0
  25. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_theme_contrast.py +15 -0
  26. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_tool_panel.py +2 -2
  27. ms_components-0.5.2/src/ms_components.egg-info/scm_version.json +0 -8
  28. {ms_components-0.5.2 → ms_components-0.5.4}/.github/workflows/docs.yml +0 -0
  29. {ms_components-0.5.2 → ms_components-0.5.4}/.github/workflows/publish.yml +0 -0
  30. {ms_components-0.5.2 → ms_components-0.5.4}/.gitignore +0 -0
  31. {ms_components-0.5.2 → ms_components-0.5.4}/LICENSE +0 -0
  32. {ms_components-0.5.2 → ms_components-0.5.4}/MANIFEST.in +0 -0
  33. {ms_components-0.5.2 → ms_components-0.5.4}/README.md +0 -0
  34. {ms_components-0.5.2 → ms_components-0.5.4}/examples/tutorial_demo.py +0 -0
  35. {ms_components-0.5.2 → ms_components-0.5.4}/examples/tutorial_demo.toml +0 -0
  36. {ms_components-0.5.2 → ms_components-0.5.4}/licenses/qt-themes-LICENSE.txt +0 -0
  37. {ms_components-0.5.2 → ms_components-0.5.4}/setup.cfg +0 -0
  38. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/__init__.py +0 -0
  39. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/backdrop.py +0 -0
  40. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/base.qss +0 -0
  41. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/chips.py +0 -0
  42. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ketcher.py +0 -0
  43. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/listview.py +0 -0
  44. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_dockwidget/__init__.py +0 -0
  45. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
  46. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
  47. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
  48. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_dockwidget/widget.py +0 -0
  49. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/__init__.py +0 -0
  50. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/bridge.py +0 -0
  51. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/config.py +0 -0
  52. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/detail.py +0 -0
  53. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/formatting.py +0 -0
  54. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/jobs.py +0 -0
  55. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/models.py +0 -0
  56. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/palette.py +0 -0
  57. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/polling.py +0 -0
  58. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_monitor/resources.py +0 -0
  59. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_projects/__init__.py +0 -0
  60. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_projects/app.py +0 -0
  61. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_projects/backend.py +0 -0
  62. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_projects/scratch.py +0 -0
  63. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_projects/widget.py +0 -0
  64. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/__init__.py +0 -0
  65. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/command_line.py +0 -0
  66. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
  67. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/box.svg +0 -0
  68. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/center.svg +0 -0
  69. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/display.svg +0 -0
  70. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
  71. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/hydrogens-polar.svg +0 -0
  72. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/hydrogens.svg +0 -0
  73. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
  74. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
  75. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
  76. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
  77. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
  78. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
  79. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/style.svg +0 -0
  80. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
  81. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_pymol/plugins.py +0 -0
  82. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_settings/__init__.py +0 -0
  83. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_settings/panel.py +0 -0
  84. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_stepper.py +0 -0
  85. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/__init__.py +0 -0
  86. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/action_header.py +0 -0
  87. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/cell_delegate.py +0 -0
  88. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/column_panel.py +0 -0
  89. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/filter_panel.py +0 -0
  90. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/columns.svg +0 -0
  91. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/export.svg +0 -0
  92. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/filter.svg +0 -0
  93. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/menu.svg +0 -0
  94. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/reload.svg +0 -0
  95. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/search.svg +0 -0
  96. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/settings.svg +0 -0
  97. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
  98. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
  99. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/icons/sort.svg +0 -0
  100. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/pagination_control.py +0 -0
  101. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/query_builder.py +0 -0
  102. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/session_provider.py +0 -0
  103. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/smart_table_view.py +0 -0
  104. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/sort_panel.py +0 -0
  105. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/table_config.py +0 -0
  106. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/table_interaction.py +0 -0
  107. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/table_model.py +0 -0
  108. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/test_external_filters.py +0 -0
  109. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_table/test_toolbar.py +0 -0
  110. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_tutorial/__init__.py +0 -0
  111. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_tutorial/bindings.py +0 -0
  112. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_tutorial/controller.py +0 -0
  113. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_tutorial/models.py +0 -0
  114. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/ms_tutorial/overlay.py +0 -0
  115. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/opengl.py +0 -0
  116. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/pagination.py +0 -0
  117. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/theme.py +0 -0
  118. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components/wheel.py +0 -0
  119. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components.egg-info/dependency_links.txt +0 -0
  120. {ms_components-0.5.2 → ms_components-0.5.4}/src/ms_components.egg-info/top_level.txt +0 -0
  121. {ms_components-0.5.2 → ms_components-0.5.4}/tests/conftest.py +0 -0
  122. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_app_settings.py +0 -0
  123. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_chips.py +0 -0
  124. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_dock_window.py +0 -0
  125. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_ketcher.py +0 -0
  126. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_molsuite_monitor_bridge.py +0 -0
  127. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_monitor_config.py +0 -0
  128. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_ms_projects_widget.py +0 -0
  129. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_opengl.py +0 -0
  130. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_optional_integrations.py +0 -0
  131. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_progress_column.py +0 -0
  132. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_project_browser.py +0 -0
  133. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_pymol_command_line.py +0 -0
  134. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_pymol_grid.py +0 -0
  135. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_query_builder_keyset.py +0 -0
  136. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_shift_hscroll.py +0 -0
  137. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_split_button_style.py +0 -0
  138. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_table_interaction.py +0 -0
  139. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_table_toolbar.py +0 -0
  140. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_table_view_state.py +0 -0
  141. {ms_components-0.5.2 → ms_components-0.5.4}/tests/test_tutorial.py +0 -0
  142. {ms_components-0.5.2 → ms_components-0.5.4}/zensical.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ms_components
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: PySide6 UI components for MolSuite: smart table, project browser, job monitor, PyMOL dock, steppers.
5
5
  Author: Mario S. Valdés-Tresanco, Mario E. Valdés-Tresanco
6
6
  License-Expression: MIT
@@ -25,7 +25,7 @@ Requires-Dist: ms_flow>=0.7.1; extra == "projects"
25
25
  Provides-Extra: pymol
26
26
  Requires-Dist: pymol-open-source>=3.2.0a0; extra == "pymol"
27
27
  Provides-Extra: steps
28
- Requires-Dist: ms_env>=0.3.3; extra == "steps"
28
+ Requires-Dist: ms_env>=0.4.0; extra == "steps"
29
29
  Provides-Extra: table
30
30
  Requires-Dist: sqlmodel>=0.0.14; extra == "table"
31
31
  Provides-Extra: all
@@ -33,7 +33,7 @@ dependencies = [
33
33
  monitor = ["pydantic>=2.0"]
34
34
  projects = ["ms_flow>=0.7.1"]
35
35
  pymol = ["pymol-open-source>=3.2.0a0"]
36
- steps = ["ms_env>=0.3.3"]
36
+ steps = ["ms_env>=0.4.0"]
37
37
  table = ["sqlmodel>=0.0.14"]
38
38
  all = ["ms_components[monitor,projects,pymol,steps,table]"]
39
39
  # Test + development tooling.
@@ -371,8 +371,9 @@ class KeptPage(QWidget):
371
371
 
372
372
  threading.Thread(target=work, name="host-check", daemon=True).start()
373
373
 
374
- def _sync(self) -> None:
375
- """`Workspace.host_env` for the host as it is saved: what the app prepares there, made as it is here."""
374
+ def _sync(self, *_, unlock=None) -> None:
375
+ """`Workspace.host_env` for the host as it is saved: what the app prepares there, made as it is here.
376
+ `unlock` first removes the lock of an environment there that the user said was left behind."""
376
377
  name = self.names.currentItem().text()
377
378
  host = self._kept()[name]
378
379
  if host.env:
@@ -388,9 +389,12 @@ class KeptPage(QWidget):
388
389
 
389
390
  def work() -> None:
390
391
  try:
392
+ if unlock is not None:
393
+ unlock()
391
394
  self._workspace.host_env(host.address, None, lambda line: self._synced.emit(line, False))
392
395
  said = f"\n{name} is ready."
393
396
  except Exception as error: # noqa: BLE001 - said in the page, where it was asked for
397
+ self._sync_error = error
394
398
  said = f"\nCould not prepare {name}:\n{error}"
395
399
  self._synced.emit(said, True)
396
400
 
@@ -402,6 +406,14 @@ class KeptPage(QWidget):
402
406
  self.sync_button.setEnabled(last)
403
407
  if last:
404
408
  self.steps.setVisible(False)
409
+ error, self._sync_error = getattr(self, "_sync_error", None), None
410
+ if error is None:
411
+ return
412
+ from ms_components.step_dialog import offer_unlock # ms_env is the `steps` extra; a sync has it
413
+
414
+ forced = offer_unlock(self, error)
415
+ if forced:
416
+ self._sync(unlock=forced)
405
417
 
406
418
  def _show_report(self, text: str) -> None:
407
419
  self.report.setPlainText(text)
@@ -32,6 +32,7 @@ class MolSuiteMonitorWidget(QWidget):
32
32
  super().__init__(parent)
33
33
  self._bridge = None
34
34
  self._snapshot: ProjectMonitorState | None = None
35
+ self._select_active_on_snapshot = False
35
36
  # The failures of the selected job come from the journal: read again only when the job
36
37
  # changes or fails more, not each poll, so the list does not jump while it is read.
37
38
  self._failures_key = None
@@ -171,6 +172,39 @@ class MolSuiteMonitorWidget(QWidget):
171
172
  self._restyle()
172
173
  super().changeEvent(event)
173
174
 
175
+ def showEvent(self, event):
176
+ super().showEvent(event)
177
+ self.select_active_job()
178
+ # A hidden monitor can still hold the previous poll. Focus once more when
179
+ # fresh data arrives, unless the user changes selection in the meantime.
180
+ self._select_active_on_snapshot = True
181
+ if self._bridge is not None:
182
+ self._bridge.request_refresh()
183
+
184
+ def select_active_job(self) -> None:
185
+ """Focus running work when opened, keeping an already selected running job.
186
+
187
+ The poller orders jobs newest first within active and finished groups. Waiting work
188
+ is the fallback when nothing is running; with no active work, keep the selection.
189
+ """
190
+ jobs = self._jobs_view.jobs()
191
+ candidates = [job for job in jobs if job.status == "running"]
192
+ if not candidates:
193
+ candidates = [job for job in jobs if not job.is_terminal]
194
+ selected = self._selected_job()
195
+ if not candidates:
196
+ if selected is None and jobs:
197
+ self._jobs_view.select_job(jobs[0].job_id)
198
+ return
199
+ job_id = next((job.job_id for job in candidates if selected and job.job_id == selected.job_id),
200
+ candidates[0].job_id)
201
+ if job_id not in self._jobs_view.visible_job_ids():
202
+ self._search_input.clear()
203
+ self._status_filter.setCurrentIndex(0)
204
+ self._filter(None, None, host=False)
205
+ self._jobs_view.select_job(job_id)
206
+ self._jobs_view.scrollToItem(self._jobs_view.currentItem())
207
+
174
208
  def attach_bridge(self, bridge) -> None:
175
209
  if self._bridge is bridge:
176
210
  return
@@ -198,6 +232,7 @@ class MolSuiteMonitorWidget(QWidget):
198
232
 
199
233
  def clear_snapshot(self) -> None:
200
234
  self._snapshot = None
235
+ self._select_active_on_snapshot = False
201
236
  self._title_label.setText("No active project")
202
237
  self._summary_label.setText("Open a project to monitor jobs.")
203
238
  self._update_sync_state(None)
@@ -215,6 +250,8 @@ class MolSuiteMonitorWidget(QWidget):
215
250
  self._failures_key = None
216
251
 
217
252
  def update_snapshot(self, snapshot: ProjectMonitorState) -> None:
253
+ focus_active = self._select_active_on_snapshot
254
+ self._select_active_on_snapshot = False
218
255
  self._snapshot = snapshot
219
256
  if not snapshot.has_project:
220
257
  self.clear_snapshot()
@@ -232,6 +269,8 @@ class MolSuiteMonitorWidget(QWidget):
232
269
  self._jobs_view.set_jobs(snapshot.jobs)
233
270
  self._hosts.set_jobs(snapshot.jobs)
234
271
  self._rebuild_filters(snapshot)
272
+ if focus_active or self._selected_job() is None:
273
+ self.select_active_job()
235
274
  self._show_selected()
236
275
 
237
276
  def _rebuild_filters(self, snapshot: ProjectMonitorState) -> None:
@@ -298,6 +337,7 @@ class MolSuiteMonitorWidget(QWidget):
298
337
  self._bridge.resume_job(job_id, executor)
299
338
 
300
339
  def _on_current_job_changed(self, job_id) -> None:
340
+ self._select_active_on_snapshot = False
301
341
  if self._bridge is not None:
302
342
  self._bridge.select_job(job_id)
303
343
  self._show_selected()
@@ -550,7 +550,7 @@ class ProjectRowDelegate(QStyledItemDelegate):
550
550
  painter.setBrush(QBrush(palette.color(QPalette.ColorRole.Highlight)))
551
551
  painter.setPen(Qt.NoPen)
552
552
  painter.drawRect(QRect(rect.left(), rect.top()+1, 3, rect.height()-1))
553
- self._paint_name_col(painter, rect, project, palette)
553
+ self._paint_name_col(painter, rect, project, palette, option.font)
554
554
 
555
555
  elif col in (_COL_META, _COL_PATH, _COL_DATE):
556
556
  role = (
@@ -559,7 +559,7 @@ class ProjectRowDelegate(QStyledItemDelegate):
559
559
  else QPalette.ColorRole.PlaceholderText
560
560
  )
561
561
  painter.setPen(QPen(palette.color(role)))
562
- painter.setFont(QFont("", 9 if col == _COL_META else 8))
562
+ painter.setFont(self._font(option.font, 9 if col == _COL_META else 8))
563
563
  text = index.data(Qt.DisplayRole) or ""
564
564
  flags = Qt.AlignLeft | Qt.AlignVCenter
565
565
  if col == _COL_META:
@@ -568,10 +568,10 @@ class ProjectRowDelegate(QStyledItemDelegate):
568
568
  flags, text)
569
569
 
570
570
  elif col == _COL_STAR:
571
- self._paint_star(painter, rect, project, is_hover, palette)
571
+ self._paint_star(painter, rect, project, is_hover, palette, option.font)
572
572
 
573
573
  elif col == _COL_MENU:
574
- self._paint_menu_btn(painter, rect, is_hover, palette)
574
+ self._paint_menu_btn(painter, rect, is_hover, palette, option.font)
575
575
 
576
576
  painter.restore()
577
577
 
@@ -581,6 +581,7 @@ class ProjectRowDelegate(QStyledItemDelegate):
581
581
  rect: QRect,
582
582
  project: ProjectItem,
583
583
  palette: QPalette,
584
+ base_font: QFont,
584
585
  ):
585
586
  # Avatar
586
587
  av_x = rect.left() + 10
@@ -592,18 +593,18 @@ class ProjectRowDelegate(QStyledItemDelegate):
592
593
  painter.drawEllipse(av_r)
593
594
  tc = QColor("#202124") if color.lightness() > 150 else QColor("#ffffff")
594
595
  painter.setPen(tc)
595
- painter.setFont(QFont("", 12, QFont.Bold))
596
+ painter.setFont(self._font(base_font, 12, QFont.Bold))
596
597
  painter.drawText(av_r, Qt.AlignCenter, (project.name or "?")[:1].upper())
597
598
 
598
599
  # Name
599
600
  tx = av_x + self._AVATAR + 10
600
- painter.setFont(QFont("", 11, QFont.Bold))
601
+ painter.setFont(self._font(base_font, 11, QFont.Bold))
601
602
  painter.setPen(palette.color(QPalette.ColorRole.Text))
602
603
  painter.drawText(QRect(tx, rect.top()+8, rect.width()-tx-8, 20),
603
604
  Qt.AlignLeft | Qt.AlignVCenter, project.name or "")
604
605
 
605
606
  # Project id
606
- painter.setFont(QFont("", 8))
607
+ painter.setFont(self._font(base_font, 8))
607
608
  painter.setPen(palette.color(QPalette.ColorRole.PlaceholderText))
608
609
  painter.drawText(
609
610
  QRect(tx, rect.top() + 30, rect.width() - tx - 8, 16),
@@ -611,7 +612,16 @@ class ProjectRowDelegate(QStyledItemDelegate):
611
612
  f"ID: {project.id}",
612
613
  )
613
614
 
614
- def _paint_star(self, painter, rect, project, is_hover, palette: QPalette):
615
+ @staticmethod
616
+ def _font(base_font: QFont, point_size: int, weight: QFont.Weight | None = None) -> QFont:
617
+ """Keep the application's typeface while adjusting hierarchy in the project rows."""
618
+ font = QFont(base_font)
619
+ font.setPointSize(point_size)
620
+ if weight is not None:
621
+ font.setWeight(weight)
622
+ return font
623
+
624
+ def _paint_star(self, painter, rect, project, is_hover, palette: QPalette, base_font: QFont):
615
625
  if project.favorite:
616
626
  favorite = theme_color("yellow")
617
627
  favorite_bg = QColor(favorite)
@@ -628,10 +638,10 @@ class ProjectRowDelegate(QStyledItemDelegate):
628
638
  painter.setPen(QPen(palette.color(QPalette.ColorRole.Text)))
629
639
  else:
630
640
  painter.setPen(QPen(palette.color(QPalette.ColorRole.PlaceholderText)))
631
- painter.setFont(QFont("", 12))
641
+ painter.setFont(self._font(base_font, 12))
632
642
  painter.drawText(rect, Qt.AlignCenter, "★")
633
643
 
634
- def _paint_menu_btn(self, painter, rect, is_hover, palette: QPalette):
644
+ def _paint_menu_btn(self, painter, rect, is_hover, palette: QPalette, base_font: QFont):
635
645
  if is_hover:
636
646
  bh = self._BTN_SIZE
637
647
  r = rect.adjusted(3, (rect.height()-bh)//2, -3, -(rect.height()-bh)//2)
@@ -643,7 +653,7 @@ class ProjectRowDelegate(QStyledItemDelegate):
643
653
  if is_hover
644
654
  else QPalette.ColorRole.PlaceholderText
645
655
  )))
646
- painter.setFont(QFont("", 13, QFont.Bold))
656
+ painter.setFont(self._font(base_font, 13, QFont.Bold))
647
657
  painter.drawText(rect, Qt.AlignCenter, "⋯")
648
658
 
649
659
  def editorEvent(self, event, model, option, index):
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M12 3v12"/>
3
+ <path d="m7 10 5 5 5-5"/>
4
+ <path d="M5 17v3h14v-3"/>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
3
+ <path d="M17 21v-8H7v8"/>
4
+ <path d="M7 3v5h8"/>
5
+ </svg>
@@ -10,11 +10,10 @@ from __future__ import annotations
10
10
  import math
11
11
  from collections import OrderedDict
12
12
  from collections.abc import Callable
13
- from pathlib import Path
14
13
 
15
14
  from PySide6.QtCore import QEvent, QObject, QTimer
16
15
  from PySide6.QtGui import QAction
17
- from PySide6.QtWidgets import QFileDialog, QMenu, QMessageBox, QWidget
16
+ from PySide6.QtWidgets import QMenu, QMessageBox, QWidget
18
17
 
19
18
  from .widget import COMPARISON_COLORS
20
19
 
@@ -383,13 +382,6 @@ def add_scene_actions(
383
382
  cmd.set("transparency", 0.55, name)
384
383
  cmd.zoom(f"({name} or {ligand})", 4)
385
384
 
386
- def save_png(cmd) -> None:
387
- filename, _filter = QFileDialog.getSaveFileName(
388
- parent, "Save PyMOL Image", str(Path.home() / "pymol.png"), "PNG Images (*.png)"
389
- )
390
- if filename:
391
- cmd.png(filename, ray=1)
392
-
393
385
  background = menu.addMenu("Background")
394
386
  for label, color in (("White", "white"), ("Black", "black"), ("Grey", "grey20")):
395
387
  menu_action(background, label, run("set the background", lambda cmd, c=color: cmd.bg_color(c)))
@@ -402,7 +394,6 @@ def add_scene_actions(
402
394
  menu_action(render, "Shadows On", run("set shadows", lambda cmd: cmd.set("ray_shadows", 1)))
403
395
  menu_action(render, "Shadows Off", run("set shadows", lambda cmd: cmd.set("ray_shadows", 0)))
404
396
  menu_action(render, "Ray", run("ray-trace", lambda cmd: cmd.ray()))
405
- menu_action(render, "Save PNG…", run("save the PNG", save_png))
406
397
 
407
398
  surface = menu.addMenu("Surface")
408
399
  menu_action(surface, "Vacuum Electrostatics…", lambda: _vacuum_electrostatics(dock, parent),
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import os
5
+ import re
5
6
  from collections.abc import Callable, Mapping
6
7
  from dataclasses import dataclass, field, replace
7
8
  from pathlib import Path
@@ -16,6 +17,7 @@ from PySide6.QtCore import QByteArray, QEvent, QSize, Qt, QTimer, Signal
16
17
  from PySide6.QtGui import QAction, QActionGroup, QGuiApplication, QIcon, QPainter, QPixmap
17
18
  from PySide6.QtSvg import QSvgRenderer
18
19
  from PySide6.QtWidgets import (
20
+ QFileDialog,
19
21
  QFrame,
20
22
  QHBoxLayout,
21
23
  QLabel,
@@ -215,7 +217,7 @@ class PymolControlBar(QFrame):
215
217
  bar.setContentsMargins(0, 0, 0, 0)
216
218
  bar.setIconSize(QSize(16, 16))
217
219
  bar.setToolButtonStyle(Qt.ToolButtonIconOnly)
218
- outer.addWidget(bar, 1)
220
+ outer.addWidget(bar)
219
221
 
220
222
  selection_menu = QMenu("Selection", bar)
221
223
  selection_group = QActionGroup(selection_menu)
@@ -306,12 +308,30 @@ class PymolControlBar(QFrame):
306
308
  menu=self.preset_menu,
307
309
  )
308
310
 
311
+ self.export_menu = QMenu("Export", bar)
312
+ self.export_pdb_action = self.export_menu.addAction("Save displayed complex as PDB…")
313
+ self.export_pdb_action.setToolTip("Export the displayed receptor and ligand as a PDB file.")
314
+ self.export_pdb_action.triggered.connect(
315
+ lambda checked=False: self._save_complex_pdb()
316
+ )
317
+ self.export_png_action = self.export_menu.addAction("Save ray-traced PNG…")
318
+ self.export_png_action.setToolTip("Save a ray-traced image at 300 dpi, sized for print.")
319
+ self.export_png_action.triggered.connect(
320
+ lambda checked=False: self._save_ray_traced_png()
321
+ )
322
+ self.export_action = self._add_toolbar_action(
323
+ "Export", "save.svg",
324
+ "Export the displayed structure as PDB or save a print-quality PNG.",
325
+ menu=self.export_menu,
326
+ )
327
+
309
328
  self.context_label = _ElidedLabel("Generic", self)
310
329
  self.context_label.setObjectName("PymolSceneContext")
311
330
  self.context_label.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
312
- self.context_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
331
+ # Ignored: the tools keep their width and the scene name takes only what is left.
332
+ self.context_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)
313
333
  self.context_label.setToolTip("Current molecular scene context.")
314
- outer.addWidget(self.context_label)
334
+ outer.addWidget(self.context_label, 1)
315
335
 
316
336
  self._register_builtin_presets()
317
337
  self._sync_selection_mode()
@@ -322,6 +342,69 @@ class PymolControlBar(QFrame):
322
342
  if self._cmd is None:
323
343
  self.setEnabled(False)
324
344
 
345
+ def _export_selection(self) -> str:
346
+ """Prefer the visible complex's named components over an ambiguous `all` target."""
347
+ selections = dict(self._context.selections or {})
348
+ components = [
349
+ selections[role]
350
+ for role in ("receptor", "ligand")
351
+ if selections.get(role)
352
+ ]
353
+ if components:
354
+ return " or ".join(f"({selection})" for selection in components)
355
+ return str(self._context.target or "all").strip() or "all"
356
+
357
+ def _export_stem(self) -> str:
358
+ context = self._context
359
+ selections = dict(context.selections or {})
360
+ components = [
361
+ selections[role]
362
+ for role in ("receptor", "ligand")
363
+ if selections.get(role)
364
+ ]
365
+ label = (
366
+ "complex"
367
+ if len(components) > 1 or context.kind == "complex"
368
+ else (components[0] if components else context.target)
369
+ )
370
+ stem = re.sub(r"[^A-Za-z0-9._-]+", "_", str(label or context.kind or "pymol")).strip("._-")
371
+ return stem or "pymol"
372
+
373
+ def _save_complex_pdb(self) -> None:
374
+ filename, _selected_filter = QFileDialog.getSaveFileName(
375
+ self,
376
+ "Save displayed complex as PDB",
377
+ str(Path.home() / f"{self._export_stem()}.pdb"),
378
+ "PDB files (*.pdb)",
379
+ )
380
+ if not filename:
381
+ return
382
+ path = Path(filename)
383
+ if path.suffix.lower() != ".pdb":
384
+ path = path.with_suffix(".pdb")
385
+ selection = self._export_selection()
386
+ self._run_command(
387
+ "Export PDB",
388
+ lambda: self._cmd.save(str(path), selection=selection, state=-1, format="pdb"),
389
+ )
390
+
391
+ def _save_ray_traced_png(self) -> None:
392
+ filename, _selected_filter = QFileDialog.getSaveFileName(
393
+ self,
394
+ "Save ray-traced PyMOL image",
395
+ str(Path.home() / f"{self._export_stem()}.png"),
396
+ "PNG images (*.png)",
397
+ )
398
+ if not filename:
399
+ return
400
+ path = Path(filename)
401
+ if path.suffix.lower() != ".png":
402
+ path = path.with_suffix(".png")
403
+ self._run_command(
404
+ "Save PNG",
405
+ lambda: self._cmd.png(str(path), width="8in", dpi=300, ray=1),
406
+ )
407
+
325
408
  def _add_toolbar_action(
326
409
  self,
327
410
  text: str,
@@ -342,12 +425,23 @@ class PymolControlBar(QFrame):
342
425
  self.toolbar.addAction(action)
343
426
  self._toolbar_action_icons[action] = icon_name
344
427
 
428
+ self._configure_action_button(action)
429
+ return action
430
+
431
+ def _configure_action_button(self, action: QAction) -> None:
345
432
  button = self.toolbar.widgetForAction(action)
346
433
  if isinstance(button, QToolButton):
347
434
  button.setObjectName("PymolToolbarButton")
348
- if menu is not None:
435
+ if action.menu() is not None:
349
436
  button.setPopupMode(QToolButton.InstantPopup)
350
- return action
437
+
438
+ def move_action_to_end(self, action: QAction) -> None:
439
+ """Move an existing control, preserving its menu-button behavior."""
440
+ self.toolbar.removeAction(action)
441
+ self.toolbar.addAction(action)
442
+ # Qt creates a new QToolButton on reinsertion, losing its popup mode.
443
+ self._configure_action_button(action)
444
+ self._configure_overflow_button()
351
445
 
352
446
  def add_menu_action(
353
447
  self,
@@ -360,12 +360,12 @@ def main():
360
360
 
361
361
  table = SmartTableView(db=db, config=config)
362
362
  table.row_double_clicked.connect(
363
- lambda obj: print(f"Double click: Order #{obj.id} — {obj.product}")
363
+ lambda obj: print(f"Double click: Order #{obj.id} - {obj.product}")
364
364
  )
365
365
  layout.addWidget(table)
366
366
 
367
367
  window.setCentralWidget(central)
368
- window.setWindowTitle("SmartTableView — Demo")
368
+ window.setWindowTitle("SmartTableView - Demo")
369
369
  window.resize(1100, 700)
370
370
  window.show()
371
371
 
@@ -127,7 +127,7 @@ class MetricFilterBar(QWidget):
127
127
  *,
128
128
  decimals: int = 3,
129
129
  default_value: float = 0.0,
130
- empty_text: str = "No filters — everything is shown.",
130
+ empty_text: str = "No filters - everything is shown.",
131
131
  ):
132
132
  super().__init__(parent)
133
133
  self.setStyleSheet(CHIP_STYLE)
@@ -29,8 +29,11 @@ THEMES: dict[str, dict[str, str]] = {
29
29
  "catppuccin_macchiato": {"window": "#24273a", "window_text": "#cad3f5", "base": "#1e2030", "alternate_base": "#24273a", "tooltip_base": "#1e2030", "tooltip_text": "#939ab7", "placeholder_text": "#888eaa", "text": "#cad3f5", "button": "#24273a", "button_text": "#cad3f5", "bright_text": "#08090a", "light": "#181926", "midlight": "#1e2030", "mid": "#5b6078", "dark": "#494d64", "shadow": "#6e738d", "highlight": "#c6a0f6", "highlighted_text": "#181926", "link": "#8aadf4", "link_visited": "#8aadf4", "accent": "#8aadf4", "disabled_text": "#61677f", "red": "#ed8796", "orange": "#f5a97f", "yellow": "#eed49f", "green": "#a6da95", "cyan": "#91d7e3", "blue": "#8aadf4", "magenta": "#c6a0f6"},
30
30
  "catppuccin_mocha": {"window": "#1e1e2e", "window_text": "#cdd6f4", "base": "#181825", "alternate_base": "#1e1e2e", "tooltip_base": "#181825", "tooltip_text": "#9399b2", "placeholder_text": "#82879f", "text": "#cdd6f4", "button": "#1e1e2e", "button_text": "#cdd6f4", "bright_text": "#090a0b", "light": "#11111b", "midlight": "#181825", "mid": "#585b70", "dark": "#45475a", "shadow": "#6c7086", "highlight": "#cba6f7", "highlighted_text": "#11111b", "link": "#89b4fa", "link_visited": "#89b4fa", "accent": "#89b4fa", "disabled_text": "#5d6075", "red": "#f38ba8", "orange": "#fab387", "yellow": "#f9e2af", "green": "#a6e3a1", "cyan": "#89dceb", "blue": "#89b4fa", "magenta": "#cba6f7"},
31
31
  "dracula": {"window": "#2d2f3c", "window_text": "#f8f8f2", "base": "#282a36", "alternate_base": "#2d2f3c", "tooltip_base": "#282a36", "tooltip_text": "#9ea0a6", "placeholder_text": "#97989b", "text": "#f8f8f2", "button": "#2d2f3c", "button_text": "#f8f8f2", "bright_text": "#070707", "light": "#1d1b22", "midlight": "#282a36", "mid": "#44475a", "dark": "#474959", "shadow": "#767985", "highlight": "#bd93f9", "highlighted_text": "#1d1b22", "link": "#ff79c6", "link_visited": "#ff79c6", "accent": "#ff79c6", "disabled_text": "#6e6f76", "red": "#ff6262", "orange": "#ffb86c", "yellow": "#f1fa8c", "green": "#50fa7b", "cyan": "#8be9fd", "blue": "#8a96bb", "magenta": "#bd93f9"},
32
+ "glen_light": {"window": "#f6f8fa", "window_text": "#1f2328", "base": "#ffffff", "alternate_base": "#fbfcfd", "tooltip_base": "#fbfcfd", "tooltip_text": "#444952", "placeholder_text": "#61666e", "text": "#1f2328", "button": "#f6f8fa", "button_text": "#1f2328", "bright_text": "#a7bcd7", "light": "#ffffff", "midlight": "#fbfcfd", "mid": "#9da1a6", "dark": "#bbbec2", "shadow": "#7f838a", "highlight": "#db68aa", "highlighted_text": "#1d1b22", "link": "#8164aa", "link_visited": "#8164aa", "accent": "#8164aa", "disabled_text": "#979a9e", "red": "#cf1d3d", "orange": "#b25600", "yellow": "#776d00", "green": "#1a7f37", "cyan": "#0a7a8a", "blue": "#2f5fd0", "magenta": "#b0337e"},
32
33
  "github_dark": {"window": "#262c36", "window_text": "#d1d7e0", "base": "#212830", "alternate_base": "#262c36", "tooltip_base": "#212830", "tooltip_text": "#989ea7", "placeholder_text": "#8d939c", "text": "#d1d7e0", "button": "#262c36", "button_text": "#d1d7e0", "bright_text": "#1d1e1f", "light": "#151b23", "midlight": "#212830", "mid": "#5f656f", "dark": "#4c525c", "shadow": "#727882", "highlight": "#79c0ff", "highlighted_text": "#151b23", "link": "#79c0ff", "link_visited": "#79c0ff", "accent": "#79c0ff", "disabled_text": "#656b75", "red": "#ffa198", "orange": "#ffa657", "yellow": "#e3b341", "green": "#56d364", "cyan": "#68cab2", "blue": "#79c0ff", "magenta": "#d2a8ff"},
33
34
  "github_light": {"window": "#f6f8fa", "window_text": "#1f2328", "base": "#ffffff", "alternate_base": "#fbfcfd", "tooltip_base": "#fbfcfd", "tooltip_text": "#444952", "placeholder_text": "#61666e", "text": "#1f2328", "button": "#f6f8fa", "button_text": "#1f2328", "bright_text": "#a7bcd7", "light": "#ffffff", "midlight": "#fbfcfd", "mid": "#9da1a6", "dark": "#bbbec2", "shadow": "#7f838a", "highlight": "#5a94c8", "highlighted_text": "#1f2328", "link": "#0071d5", "link_visited": "#0071d5", "accent": "#0071d5", "disabled_text": "#979a9e", "red": "#e21400", "orange": "#b85600", "yellow": "#8f6b15", "green": "#22822d", "cyan": "#2c7e6a", "blue": "#0071d5", "magenta": "#9838ff"},
35
+ "github_light_frappe": {"window": "#f6f8fa", "window_text": "#1f2328", "base": "#ffffff", "alternate_base": "#fbfcfd", "tooltip_base": "#fbfcfd", "tooltip_text": "#444952", "placeholder_text": "#61666e", "text": "#1f2328", "button": "#f6f8fa", "button_text": "#1f2328", "bright_text": "#a7bcd7", "light": "#ffffff", "midlight": "#fbfcfd", "mid": "#9da1a6", "dark": "#bbbec2", "shadow": "#7f838a", "highlight": "#a682bd", "highlighted_text": "#232634", "link": "#5d719f", "link_visited": "#5d719f", "accent": "#5d719f", "disabled_text": "#979a9e", "red": "#a65d5f", "orange": "#9a664c", "yellow": "#807051", "green": "#607950", "cyan": "#57777d", "blue": "#5d719f", "magenta": "#846797"},
36
+ "LF_light": {"window": "#f6f8fa", "window_text": "#1f2328", "base": "#ffffff", "alternate_base": "#fbfcfd", "tooltip_base": "#fbfcfd", "tooltip_text": "#444952", "placeholder_text": "#61666e", "text": "#1f2328", "button": "#f6f8fa", "button_text": "#1f2328", "bright_text": "#a7bcd7", "light": "#ffffff", "midlight": "#fbfcfd", "mid": "#9da1a6", "dark": "#bbbec2", "shadow": "#7f838a", "highlight": "#9f7aea", "highlighted_text": "#1f2328", "link": "#7c3aed", "link_visited": "#7c3aed", "accent": "#7c3aed", "disabled_text": "#979a9e", "red": "#e21400", "orange": "#b85600", "yellow": "#8f6b15", "green": "#22822d", "cyan": "#2c7e6a", "blue": "#0071d5", "magenta": "#9838ff"},
34
37
  "modern_dark": {"window": "#2a2a2a", "window_text": "#c8c8c8", "base": "#1f1f1f", "alternate_base": "#2a2a2a", "tooltip_base": "#1f1f1f", "tooltip_text": "#868686", "placeholder_text": "#919191", "text": "#c8c8c8", "button": "#2a2a2a", "button_text": "#c8c8c8", "bright_text": "#373737", "light": "#141414", "midlight": "#1f1f1f", "mid": "#424242", "dark": "#444444", "shadow": "#737373", "highlight": "#2aa5da", "highlighted_text": "#141414", "link": "#2ad7da", "link_visited": "#2ad7da", "accent": "#2ad7da", "disabled_text": "#696969", "red": "#e16f53", "orange": "#da8a2a", "yellow": "#d7da2a", "green": "#2ada6f", "cyan": "#2ad7da", "blue": "#2aa5da", "magenta": "#c06fe4"},
35
38
  "modern_light": {"window": "#e6ebf0", "window_text": "#000000", "base": "#ffffff", "alternate_base": "#eef2f6", "tooltip_base": "#eef2f6", "tooltip_text": "#484b4d", "placeholder_text": "#626568", "text": "#000000", "button": "#e6ebf0", "button_text": "#000000", "bright_text": "#ffffff", "light": "#ffffff", "midlight": "#eef2f6", "mid": "#979b9f", "dark": "#b1b6ba", "shadow": "#7d8084", "highlight": "#2590be", "highlighted_text": "#000000", "link": "#157677", "link_visited": "#157677", "accent": "#157677", "disabled_text": "#8f9295", "red": "#bb3f20", "orange": "#945c1a", "yellow": "#6b6d13", "green": "#157a3d", "cyan": "#157677", "blue": "#1a7196", "magenta": "#a22ad7"},
36
39
  "monokai": {"window": "#2d2a2e", "window_text": "#fcfcfa", "base": "#221f22", "alternate_base": "#2d2a2e", "tooltip_base": "#221f22", "tooltip_text": "#939293", "placeholder_text": "#939293", "text": "#fcfcfa", "button": "#2d2a2e", "button_text": "#fcfcfa", "bright_text": "#030303", "light": "#19181a", "midlight": "#221f22", "mid": "#5b595c", "dark": "#4e4c4f", "shadow": "#757375", "highlight": "#ffd866", "highlighted_text": "#19181a", "link": "#ff6188", "link_visited": "#ff6188", "accent": "#ff6188", "disabled_text": "#6b696b", "red": "#ff6188", "orange": "#fc9867", "yellow": "#ffd866", "green": "#a9dc76", "cyan": "#91dcaf", "blue": "#78dce8", "magenta": "#ab9df2"},
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import (
18
18
  QDialogButtonBox,
19
19
  QLabel,
20
20
  QListWidget,
21
+ QMessageBox,
21
22
  QPlainTextEdit,
22
23
  QProgressBar,
23
24
  QVBoxLayout,
@@ -142,9 +143,16 @@ class StepRunnerDialog(QDialog):
142
143
  self._on_success()
143
144
  self.finished_ok.emit(ok)
144
145
 
146
+ # The run is not left behind by Esc or the window's close either: a blocking caller would go
147
+ # on (and give up its locks) while the steps still change things.
148
+ def reject(self) -> None:
149
+ if not self._worker.isRunning():
150
+ super().reject()
151
+
145
152
  def closeEvent(self, event) -> None: # noqa: N802 - Qt naming
146
153
  if self._worker.isRunning():
147
- self._worker.wait(50)
154
+ event.ignore()
155
+ return
148
156
  super().closeEvent(event)
149
157
 
150
158
 
@@ -168,4 +176,27 @@ def run_steps_dialog(
168
176
  return dialog
169
177
 
170
178
 
171
- __all__ = ["StepRunnerDialog", "StepWorker", "run_steps_dialog"]
179
+ def offer_unlock(parent: QWidget, error: BaseException | None) -> Callable[[], dict] | None:
180
+ """When `error` is, or was raised from, an `ms_env.Locked`, ask whether to remove that lock.
181
+ Returns what removes it, to be run off the GUI thread (it may go over ssh) before trying
182
+ again; nothing when the error is another or the user said no."""
183
+ from ms_env import Locked, unlock
184
+
185
+ while error is not None and not isinstance(error, Locked):
186
+ error = error.__cause__ or error.__context__
187
+ if error is None:
188
+ return None
189
+ owner, where = error.owner, error.host.address or "this machine"
190
+ asked = QMessageBox.question(
191
+ parent, "Environment locked",
192
+ f"{error.prefix} on {where} is being changed by {owner.get('client', 'an unknown client')} "
193
+ f"(pid {owner.get('pid', '?')}) since {owner.get('since', '?')}.\n\n"
194
+ "Remove the lock only if that client has stopped: two clients changing one environment break it.\n\n"
195
+ "Remove the lock and try again?",
196
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
197
+ if asked != QMessageBox.StandardButton.Yes:
198
+ return None
199
+ return lambda: unlock(error.prefix, error.host)
200
+
201
+
202
+ __all__ = ["StepRunnerDialog", "StepWorker", "offer_unlock", "run_steps_dialog"]
@@ -338,13 +338,13 @@ class ActionBar(ContentBar):
338
338
  if total:
339
339
  self.progress.setRange(0, int(total))
340
340
  self.progress.setValue(min(done, int(total)))
341
- headline = f"{stage} — {format_count(done)} / {format_count(total)} {unit}"
341
+ headline = f"{stage} - {format_count(done)} / {format_count(total)} {unit}"
342
342
  if eta_seconds is None:
343
343
  eta_seconds = self._estimate_eta(done, int(total))
344
344
  parts.append("Estimating time…" if eta_seconds is None else format_eta(eta_seconds))
345
345
  else:
346
346
  self.progress.setRange(0, 0)
347
- headline = f"{stage} — {format_count(done)} {unit}"
347
+ headline = f"{stage} - {format_count(done)} {unit}"
348
348
  if detail:
349
349
  parts.append(detail)
350
350
  self._show(headline, " · ".join(parts), failures)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ms_components
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: PySide6 UI components for MolSuite: smart table, project browser, job monitor, PyMOL dock, steppers.
5
5
  Author: Mario S. Valdés-Tresanco, Mario E. Valdés-Tresanco
6
6
  License-Expression: MIT
@@ -25,7 +25,7 @@ Requires-Dist: ms_flow>=0.7.1; extra == "projects"
25
25
  Provides-Extra: pymol
26
26
  Requires-Dist: pymol-open-source>=3.2.0a0; extra == "pymol"
27
27
  Provides-Extra: steps
28
- Requires-Dist: ms_env>=0.3.3; extra == "steps"
28
+ Requires-Dist: ms_env>=0.4.0; extra == "steps"
29
29
  Provides-Extra: table
30
30
  Requires-Dist: sqlmodel>=0.0.14; extra == "table"
31
31
  Provides-Extra: all
@@ -62,6 +62,7 @@ src/ms_components/ms_pymol/icons/actions.svg
62
62
  src/ms_components/ms_pymol/icons/box.svg
63
63
  src/ms_components/ms_pymol/icons/center.svg
64
64
  src/ms_components/ms_pymol/icons/display.svg
65
+ src/ms_components/ms_pymol/icons/export.svg
65
66
  src/ms_components/ms_pymol/icons/hide.svg
66
67
  src/ms_components/ms_pymol/icons/hydrogens-polar.svg
67
68
  src/ms_components/ms_pymol/icons/hydrogens.svg
@@ -69,6 +70,7 @@ src/ms_components/ms_pymol/icons/menu.svg
69
70
  src/ms_components/ms_pymol/icons/orient.svg
70
71
  src/ms_components/ms_pymol/icons/presets.svg
71
72
  src/ms_components/ms_pymol/icons/rock.svg
73
+ src/ms_components/ms_pymol/icons/save.svg
72
74
  src/ms_components/ms_pymol/icons/selection.svg
73
75
  src/ms_components/ms_pymol/icons/settings.svg
74
76
  src/ms_components/ms_pymol/icons/style.svg
@@ -17,7 +17,7 @@ ms_flow>=0.7.1
17
17
  pymol-open-source>=3.2.0a0
18
18
 
19
19
  [steps]
20
- ms_env>=0.3.3
20
+ ms_env>=0.4.0
21
21
 
22
22
  [table]
23
23
  sqlmodel>=0.0.14
@@ -54,6 +54,7 @@
54
54
  "src/ms_components/ms_pymol/icons/box.svg",
55
55
  "src/ms_components/ms_pymol/icons/center.svg",
56
56
  "src/ms_components/ms_pymol/icons/display.svg",
57
+ "src/ms_components/ms_pymol/icons/export.svg",
57
58
  "src/ms_components/ms_pymol/icons/hide.svg",
58
59
  "src/ms_components/ms_pymol/icons/hydrogens-polar.svg",
59
60
  "src/ms_components/ms_pymol/icons/hydrogens.svg",
@@ -61,6 +62,7 @@
61
62
  "src/ms_components/ms_pymol/icons/orient.svg",
62
63
  "src/ms_components/ms_pymol/icons/presets.svg",
63
64
  "src/ms_components/ms_pymol/icons/rock.svg",
65
+ "src/ms_components/ms_pymol/icons/save.svg",
64
66
  "src/ms_components/ms_pymol/icons/selection.svg",
65
67
  "src/ms_components/ms_pymol/icons/settings.svg",
66
68
  "src/ms_components/ms_pymol/icons/style.svg",