MoleditPy-linux 4.3.1__tar.gz → 4.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/PKG-INFO +1 -1
  2. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/pyproject.toml +1 -1
  3. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
  4. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/custom_interactor_style.py +54 -6
  5. moleditpy_linux-4.3.2/src/moleditpy_linux/ui/custom_qt_interactor.py +75 -0
  6. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +2 -3
  7. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +6 -2
  8. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/ui_manager.py +43 -1
  9. moleditpy_linux-4.3.1/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -105
  10. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/LICENSE +0 -0
  11. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/README.md +0 -0
  12. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/setup.cfg +0 -0
  13. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  14. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  15. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  16. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  17. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  18. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/__init__.py +0 -0
  19. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/__main__.py +0 -0
  20. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  21. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/assets/icon.icns +0 -0
  22. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/assets/icon.ico +0 -0
  23. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/assets/icon.png +0 -0
  24. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/core/__init__.py +0 -0
  25. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  26. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/core/molecular_data.py +0 -0
  27. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/main.py +0 -0
  28. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/plugins/__init__.py +0 -0
  29. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
  30. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
  31. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
  32. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/__init__.py +0 -0
  33. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  34. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
  35. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  36. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  37. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
  38. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/app_state.py +0 -0
  39. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/atom_item.py +0 -0
  40. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/atom_picking.py +0 -0
  41. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  42. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/bond_item.py +0 -0
  43. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
  44. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
  45. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  46. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/compute_logic.py +0 -0
  47. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
  48. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
  49. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
  50. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  51. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
  52. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/export_logic.py +0 -0
  53. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  54. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/io_logic.py +0 -0
  55. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/main_window.py +0 -0
  56. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/main_window_init.py +0 -0
  57. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  58. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
  59. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
  60. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
  61. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  62. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
  63. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
  64. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  65. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  66. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
  67. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  68. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
  69. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  70. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/string_importers.py +0 -0
  71. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  72. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  73. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
  74. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
  75. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
  76. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  77. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/utils/__init__.py +0 -0
  78. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/utils/constants.py +0 -0
  79. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/utils/default_settings.py +0 -0
  80. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  81. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/utils/suppress_log.py +0 -0
  82. {moleditpy_linux-4.3.1 → moleditpy_linux-4.3.2}/src/moleditpy_linux/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.3.1
3
+ Version: 4.3.2
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "4.3.1"
8
+ version = "4.3.2"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.3.1
3
+ Version: 4.3.2
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -159,9 +159,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
159
159
  try:
160
160
  move_group_dialog.on_atom_picked(clicked_atom_idx)
161
161
  except (AttributeError, RuntimeError):
162
- # Safe defensive fallback catching AttributeError, RuntimeError
163
- logging.debug(
164
- "Suppressed non-critical error", exc_info=True
162
+ logging.warning(
163
+ "Move-dialog atom toggle failed", exc_info=True
165
164
  )
166
165
 
167
166
  QTimer.singleShot(0, _deferred_toggle)
@@ -273,9 +272,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
273
272
  closest_atom_idx
274
273
  )
275
274
  except (AttributeError, RuntimeError):
276
- # Safe defensive fallback catching AttributeError, RuntimeError
277
- logging.debug(
278
- "Suppressed non-critical error", exc_info=True
275
+ logging.warning(
276
+ "Measurement selection failed", exc_info=True
279
277
  )
280
278
 
281
279
  QTimer.singleShot(0, _deferred_measure)
@@ -383,6 +381,54 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
383
381
  # Standard right-click
384
382
  super().OnRightButtonDown()
385
383
 
384
+ def _heal_stuck_pointer_state(self, move_group_dialog: Any) -> None:
385
+ """Self-heal drag/rotate state whose release event was lost.
386
+
387
+ A release can be lost mid-gesture (e.g. pyvista temporarily swapping
388
+ the interactor style, a dialog grabbing events), leaving this style or
389
+ a Move Group dialog in a permanent drag/rotate state that blocks
390
+ interaction. Verify against the real button state on every move and
391
+ reset when the button is not actually held.
392
+ """
393
+ try:
394
+ buttons = QApplication.mouseButtons()
395
+ left_held = bool(buttons & Qt.MouseButton.LeftButton)
396
+ right_held = bool(buttons & Qt.MouseButton.RightButton)
397
+ any_held = buttons != Qt.MouseButton.NoButton
398
+ except (AttributeError, RuntimeError, TypeError):
399
+ return
400
+
401
+ if self._is_dragging_atom and not left_held:
402
+ self.reset_interactor_state()
403
+
404
+ if not any_held:
405
+ try:
406
+ if self.GetState() != 0: # stuck ROTATE/PAN/etc. without a button
407
+ self.reset_interactor_state()
408
+ except (AttributeError, RuntimeError, TypeError):
409
+ # Safe defensive fallback catching AttributeError, RuntimeError, TypeError
410
+ logging.debug("Suppressed non-critical error", exc_info=True)
411
+
412
+ if move_group_dialog is not None:
413
+ try:
414
+ if (
415
+ getattr(move_group_dialog, "is_dragging_group_vtk", False)
416
+ and not left_held
417
+ ):
418
+ move_group_dialog.is_dragging_group_vtk = False
419
+ move_group_dialog.drag_start_pos_vtk = None
420
+ move_group_dialog.mouse_moved_vtk = False
421
+ if (
422
+ getattr(move_group_dialog, "is_rotating_group_vtk", False)
423
+ and not right_held
424
+ ):
425
+ move_group_dialog.is_rotating_group_vtk = False
426
+ move_group_dialog.rotation_start_pos = None
427
+ move_group_dialog.rotation_mouse_moved = False
428
+ except (AttributeError, RuntimeError, TypeError):
429
+ # Safe defensive fallback catching AttributeError, RuntimeError, TypeError
430
+ logging.debug("Suppressed non-critical error", exc_info=True)
431
+
386
432
  def on_mouse_move(self, obj: Any, event: Any) -> None:
387
433
  """
388
434
  Handle mouse move (drag vs camera/hover).
@@ -402,6 +448,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
402
448
  break
403
449
  except (AttributeError, RuntimeError, TypeError):
404
450
  logging.warning("Caught exception in " + __file__, exc_info=True)
451
+
452
+ self._heal_stuck_pointer_state(move_group_dialog)
405
453
  if move_group_dialog and getattr(
406
454
  move_group_dialog, "is_dragging_group_vtk", False
407
455
  ):
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ import logging
14
+ from typing import Any, Optional
15
+
16
+ from PyQt6.QtCore import QEvent
17
+ from PyQt6.QtGui import QMouseEvent
18
+ from pyvistaqt import QtInteractor
19
+
20
+
21
+ class CustomQtInteractor(QtInteractor):
22
+ """PyVista QtInteractor subclass that exposes the main window for event handling."""
23
+
24
+ def __init__(
25
+ self,
26
+ parent: Optional[Any] = None,
27
+ main_window: Optional[Any] = None,
28
+ **kwargs: Any,
29
+ ) -> None:
30
+ super().__init__(parent, **kwargs)
31
+ self.main_window = main_window
32
+
33
+ def wheelEvent(self, event: Any) -> None:
34
+ """
35
+ Override the mouse wheel event.
36
+ """
37
+ # First call the parent class event to perform normal zoom processing
38
+ super().wheelEvent(event)
39
+
40
+ # Force focus back to the 2D view after zoom processing
41
+ if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
42
+ self.main_window.init_manager.view_2d.setFocus()
43
+
44
+ def mouseReleaseEvent(self, event: Any) -> None:
45
+ """
46
+ Override the Qt mouse release event to return focus to the 2D view after
47
+ all 3D view operations.
48
+ """
49
+ super().mouseReleaseEvent(event) # Process parent class event first
50
+ if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
51
+ self.main_window.init_manager.view_2d.setFocus()
52
+
53
+ def mouseDoubleClickEvent(self, event: Any) -> None:
54
+ """Re-dispatch double-clicks as plain presses so fast clicking works.
55
+
56
+ Qt turns every second fast click into a double-click event. Forwarding
57
+ it unchanged would reach VTK with a repeat count and be dropped by the
58
+ interactor style, so the click would be lost (e.g. when rapidly
59
+ selecting atoms in measurement mode). Synthesizing a normal press keeps
60
+ press/release pairing intact and makes each fast click act as a click.
61
+ """
62
+ try:
63
+ synthetic_press = QMouseEvent(
64
+ QEvent.Type.MouseButtonPress,
65
+ event.position(),
66
+ event.globalPosition(),
67
+ event.button(),
68
+ event.buttons(),
69
+ event.modifiers(),
70
+ )
71
+ super().mousePressEvent(synthetic_press)
72
+ event.accept()
73
+ except (AttributeError, RuntimeError, TypeError):
74
+ # Safe defensive fallback catching AttributeError, RuntimeError, TypeError
75
+ logging.debug("Suppressed non-critical error", exc_info=True)
@@ -82,9 +82,8 @@ class Dialog3DPickingMixin:
82
82
  try:
83
83
  self.on_atom_picked(int(closest_atom_idx))
84
84
  except (AttributeError, RuntimeError):
85
- # Safe defensive fallback catching AttributeError, RuntimeError
86
- logging.debug(
87
- "Suppressed non-critical error", exc_info=True
85
+ logging.warning(
86
+ "Dialog atom pick failed", exc_info=True
88
87
  )
89
88
 
90
89
  from PyQt6.QtCore import QTimer
@@ -696,9 +696,13 @@ class MoveSelectedAtomsDialog(BasePickingDialog):
696
696
  else:
697
697
  btn.setText("Box Selection: OFF")
698
698
  plotter.disable_picking()
699
- # Restore original style
699
+ # Restore original style via pyvista's bookkeeping so its
700
+ # update_style() re-asserts ours instead of RubberBandPick
700
701
  if self.original_style is not None:
701
- plotter.interactor.SetInteractorStyle(self.original_style)
702
+ try:
703
+ plotter.iren.style = self.original_style
704
+ except AttributeError:
705
+ plotter.interactor.SetInteractorStyle(self.original_style)
702
706
 
703
707
  def on_rectangle_picked(self, selection: Any) -> None:
704
708
  """Handle PyVista rectangle picking callback."""
@@ -322,9 +322,51 @@ class UIManager(QObject):
322
322
  style = CustomInteractorStyle(self.host)
323
323
 
324
324
  # Set interactor style
325
- self.host.view_3d_manager.plotter.interactor.SetInteractorStyle(style)
325
+ self._install_interactor_style(style)
326
326
  self.host.view_3d_manager.plotter.interactor.Initialize()
327
327
 
328
+ # Watchdog: if anything silently replaces the custom style (observed as
329
+ # "rotation works but pick/select is dead"), report it and reinstall.
330
+ self._expected_style = style
331
+ self._style_watchdog = QTimer(self)
332
+ self._style_watchdog.setInterval(2000)
333
+ self._style_watchdog.timeout.connect(self._check_interactor_style)
334
+ self._style_watchdog.start()
335
+
336
+ def _install_interactor_style(self, style: Any) -> None:
337
+ """Install *style* through pyvista's bookkeeping, not raw VTK.
338
+
339
+ pyvista's RenderWindowInteractor re-asserts its own ``_style_class``
340
+ via ``update_style()`` whenever its built-in double-click chart
341
+ handler fires (fast clicks!). Installing through the ``iren.style``
342
+ property keeps that bookkeeping pointing at our style so the
343
+ re-assert is a no-op instead of an eviction.
344
+ """
345
+ plotter = self.host.view_3d_manager.plotter
346
+ try:
347
+ plotter.iren.style = style
348
+ except AttributeError:
349
+ plotter.interactor.SetInteractorStyle(style)
350
+
351
+ def _check_interactor_style(self) -> None:
352
+ """Reinstall CustomInteractorStyle if it was silently replaced."""
353
+ try:
354
+ interactor = self.host.view_3d_manager.plotter.interactor
355
+ current = interactor.GetInteractorStyle()
356
+ if current is self._expected_style:
357
+ return
358
+ name = type(current).__name__
359
+ if "RubberBand" in name: # legitimate temporary box-selection style
360
+ return
361
+ logging.debug(
362
+ "3D interactor style was replaced by %s — reinstalling "
363
+ "CustomInteractorStyle",
364
+ name,
365
+ )
366
+ self._install_interactor_style(self._expected_style)
367
+ except (AttributeError, RuntimeError):
368
+ logging.debug("Style watchdog check failed", exc_info=True)
369
+
328
370
  def handle_drag_enter_event(self, event: QDragEnterEvent) -> None:
329
371
  """Internal handler for drag enter event (bypasses PyQt type checks in tests)."""
330
372
  mime_data = event.mimeData()
@@ -1,105 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- MoleditPy — A Python-based molecular editing software
6
-
7
- Author: Hiromichi Yokoyama
8
- License: GPL-3.0 license
9
- Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
- DOI: 10.5281/zenodo.17268532
11
- """
12
-
13
- import time
14
- from typing import Any, Optional
15
-
16
- from pyvistaqt import QtInteractor
17
-
18
-
19
- class CustomQtInteractor(QtInteractor):
20
- """PyVista QtInteractor subclass that exposes the main window for event handling."""
21
-
22
- def __init__(
23
- self,
24
- parent: Optional[Any] = None,
25
- main_window: Optional[Any] = None,
26
- **kwargs: Any,
27
- ) -> None:
28
- super().__init__(parent, **kwargs)
29
- self.main_window = main_window
30
- self._last_click_time = 0.0
31
- self._click_count = 0
32
- self._ignore_next_release = False
33
-
34
- def wheelEvent(self, event: Any) -> None:
35
- """
36
- Override the mouse wheel event.
37
- """
38
- # First call the parent class event to perform normal zoom processing
39
- super().wheelEvent(event)
40
-
41
- # Force focus back to the 2D view after zoom processing
42
- if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
43
- self.main_window.init_manager.view_2d.setFocus()
44
-
45
- def mouseReleaseEvent(self, event: Any) -> None:
46
- """
47
- Override the Qt mouse release event to return focus to the 2D view after
48
- all 3D view operations. Also filters out "Ghost Release" (release without
49
- a corresponding press).
50
- """
51
- if self._ignore_next_release:
52
- self._ignore_next_release = False
53
- event.accept()
54
- return
55
-
56
- super().mouseReleaseEvent(event) # Process parent class event first
57
- if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
58
- self.main_window.init_manager.view_2d.setFocus()
59
-
60
- def mousePressEvent(self, event: Any) -> None:
61
- """
62
- Custom mouse press handling to track accumulated clicks and filter out
63
- triple-clicks.
64
- """
65
- current_time = time.time()
66
- # Reset count if too much time has passed (0.5s is standard double-click time)
67
- if current_time - self._last_click_time > 0.5:
68
- self._click_count = 0
69
-
70
- self._click_count += 1
71
- self._last_click_time = current_time
72
-
73
- # If this is the 3rd click (or more), swallow it to prevent
74
- # the internal state desync that happens with rapid clicking sequences.
75
- if self._click_count >= 3:
76
- self._ignore_next_release = True
77
- event.accept()
78
- return
79
-
80
- super().mousePressEvent(event)
81
-
82
- def mouseDoubleClickEvent(self, event: Any) -> None:
83
- """Ignore mouse double-clicks on the 3D widget to avoid accidental actions.
84
-
85
- Swallow the double-click event so it doesn't trigger selection, editing,
86
- or camera jumps. We intentionally do not call the superclass handler.
87
- Crucially, we also flag the NEXT release event to be swallowed, preventing
88
- a "Ghost Release" (Release without Press) from reaching VTK.
89
- """
90
- current_time = time.time()
91
- self._last_click_time = current_time
92
- # Set to 2 to ensure the next click counts as 3rd
93
- if current_time - self._last_click_time < 0.5:
94
- self._click_count = 2
95
- else:
96
- self._click_count = 2 # Force sync
97
-
98
- self._ignore_next_release = True
99
-
100
- try:
101
- # Accept the event to mark it handled and prevent further processing.
102
- event.accept()
103
- except (AttributeError, RuntimeError, TypeError):
104
- # If event doesn't support accept for some reason, just return.
105
- return
File without changes