MoleditPy-linux 4.3.1__py3-none-any.whl → 4.3.2__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.
@@ -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
  ):
@@ -10,9 +10,11 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- import time
13
+ import logging
14
14
  from typing import Any, Optional
15
15
 
16
+ from PyQt6.QtCore import QEvent
17
+ from PyQt6.QtGui import QMouseEvent
16
18
  from pyvistaqt import QtInteractor
17
19
 
18
20
 
@@ -27,9 +29,6 @@ class CustomQtInteractor(QtInteractor):
27
29
  ) -> None:
28
30
  super().__init__(parent, **kwargs)
29
31
  self.main_window = main_window
30
- self._last_click_time = 0.0
31
- self._click_count = 0
32
- self._ignore_next_release = False
33
32
 
34
33
  def wheelEvent(self, event: Any) -> None:
35
34
  """
@@ -45,61 +44,32 @@ class CustomQtInteractor(QtInteractor):
45
44
  def mouseReleaseEvent(self, event: Any) -> None:
46
45
  """
47
46
  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).
47
+ all 3D view operations.
50
48
  """
51
- if self._ignore_next_release:
52
- self._ignore_next_release = False
53
- event.accept()
54
- return
55
-
56
49
  super().mouseReleaseEvent(event) # Process parent class event first
57
50
  if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
58
51
  self.main_window.init_manager.view_2d.setFocus()
59
52
 
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
53
  def mouseDoubleClickEvent(self, event: Any) -> None:
83
- """Ignore mouse double-clicks on the 3D widget to avoid accidental actions.
54
+ """Re-dispatch double-clicks as plain presses so fast clicking works.
84
55
 
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.
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.
89
61
  """
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
62
  try:
101
- # Accept the event to mark it handled and prevent further processing.
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)
102
72
  event.accept()
103
73
  except (AttributeError, RuntimeError, TypeError):
104
- # If event doesn't support accept for some reason, just return.
105
- return
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,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
@@ -28,9 +28,9 @@ moleditpy_linux/ui/calculation_worker.py,sha256=bwrzCW5935Twe5EY7Mtd35YQaWJILvBP
28
28
  moleditpy_linux/ui/color_settings_dialog.py,sha256=RFp8h0R1yC8YwqOxc972Da9xO95Gu-0qywAAEeXMj4E,14224
29
29
  moleditpy_linux/ui/compute_logic.py,sha256=DIPmqzlQv-g-MK8EllAD_soG0tk8PMoW5oxEl-uvsJA,31216
30
30
  moleditpy_linux/ui/constrained_optimization_dialog.py,sha256=rc3YK6DOJdIbhriwsM2jOAW9j_-3PvFz1XtFHpGw4E4,36346
31
- moleditpy_linux/ui/custom_interactor_style.py,sha256=zbZCmAZAR1ywuENDIb_guIhu09IUn_kjeopr6FT2t2s,44529
32
- moleditpy_linux/ui/custom_qt_interactor.py,sha256=AZ5TtQjWUs4F5FFg-tBUZQlbk74UHboTjYpBQRUlhvQ,3827
33
- moleditpy_linux/ui/dialog_3d_picking_mixin.py,sha256=lH7NS7zF_TxhXEIr_mEPg-9LpI1AqpsZzMFAue33AIA,10705
31
+ moleditpy_linux/ui/custom_interactor_style.py,sha256=p48f1BD8OK36lyAimK69lXbITjuXRg7QO8aSwk4ogtI,46791
32
+ moleditpy_linux/ui/custom_qt_interactor.py,sha256=76L0Jpbg09VXzHOzJs9kG260Vs9qcwbt8Chn0iOopDg,2820
33
+ moleditpy_linux/ui/dialog_3d_picking_mixin.py,sha256=nsZQ27PYZrO9s_w5EXxxluVfOOmNKcv0vvm89IWN7mU,10604
34
34
  moleditpy_linux/ui/dialog_logic.py,sha256=KCz3ZPJap4K_tiNDjCzGqQHYxdEsvcUG6Lws2tDhvx4,19617
35
35
  moleditpy_linux/ui/dihedral_dialog.py,sha256=0-EeJZG6YCehCWwqCEOougzZjF-zyzKEHCUC_cls7GE,18833
36
36
  moleditpy_linux/ui/edit_3d_logic.py,sha256=DcdZZwPRkJ0RIReyTyvelkZILYG-vggop92dz2hkYSY,19192
@@ -44,7 +44,7 @@ moleditpy_linux/ui/mirror_dialog.py,sha256=Tsc2O9Ktjz0zzJWB_YkVgkqgDhytQ7ksN_Pwp
44
44
  moleditpy_linux/ui/molecular_scene_handler.py,sha256=Y53CceA2cbtPdY-n7mcnYsfRl2MgQT55R9xCMQ4m18o,70035
45
45
  moleditpy_linux/ui/molecule_scene.py,sha256=ZUZBaCyWaNEAITHI4JmJ__gQcI9kgttj8qBP6dgF6LU,44090
46
46
  moleditpy_linux/ui/move_group_dialog.py,sha256=3jfj8zmKrYs0N9537o3g0V3f0UOnr0pj8uNT0qvUlPM,27063
47
- moleditpy_linux/ui/move_selected_atoms_dialog.py,sha256=6Q43RXKL8Fmkf93emxUJYeel-QqMQ_lCyzeq8Bj6PV4,29656
47
+ moleditpy_linux/ui/move_selected_atoms_dialog.py,sha256=7_Krz69LDx71_pBr9_bP55UhT97SJZU-W6OEN59waDg,29889
48
48
  moleditpy_linux/ui/periodic_table_dialog.py,sha256=yadE1bbJaObAiIfpKwANqiDOBI8R3yOHvDTZYrw1J4Q,6134
49
49
  moleditpy_linux/ui/planarize_dialog.py,sha256=PvEXAx27iO3d9OWrr0kL4WTeNI2eMIx-FRZaFWW-cao,7936
50
50
  moleditpy_linux/ui/plugin_menu_manager.py,sha256=EXju-Bcy-Wxg_MpJLsV2nUgjlwVpyqZBVU5VohzUk5w,22101
@@ -53,7 +53,7 @@ moleditpy_linux/ui/string_importers.py,sha256=cgsMoIYRKKP-RZZzhcpCdlDqr8KJgmazZd
53
53
  moleditpy_linux/ui/template_preview_item.py,sha256=exJ4r3qmLOCQrEgl9ktNu4zTrgqixZcq82rUb9flWiM,7743
54
54
  moleditpy_linux/ui/template_preview_view.py,sha256=XMJ5tg4CdF2QA_6Yt0DMXAMWFugmiOadhIEvLpclsbo,3817
55
55
  moleditpy_linux/ui/translation_dialog.py,sha256=jAEHOCICixT1TcsQHnW-8h1o1HMxPC8Mw3_qYrUaoI0,16663
56
- moleditpy_linux/ui/ui_manager.py,sha256=7NbTQOdabHmmurReDdJTVPSZbwJzZb9FkHnYZWZApjw,26635
56
+ moleditpy_linux/ui/ui_manager.py,sha256=cljxqBlvlz98n5uiie1auKJdenGAeRIVF9e21R1hkDA,28568
57
57
  moleditpy_linux/ui/user_template_dialog.py,sha256=ZKGMyhqzS14CKor51YiEcHUXe8bNvughRDTG9-Pc-d0,29198
58
58
  moleditpy_linux/ui/view_3d_logic.py,sha256=fuXQKAQFH8e_Xm6e8mG2y4EN2EnBqFxJRqsloilBU9c,92757
59
59
  moleditpy_linux/ui/zoomable_view.py,sha256=Kpr9SaIYAKH-Fexm5-gi3cswsiT5V7g7yTk7acutZjA,6239
@@ -68,9 +68,9 @@ moleditpy_linux/utils/default_settings.py,sha256=6Q3Gw1vyG7uk-35yBtr1sKeJVnOjb4S
68
68
  moleditpy_linux/utils/sip_isdeleted_safe.py,sha256=aXoW_PDHAsu0SkqcvkUZAsWDhl9Ka8-VnzgNE5ub41Y,1215
69
69
  moleditpy_linux/utils/suppress_log.py,sha256=EFkN79q6QfYXAuii8g1fBuvFJrnVsio3aFTKMvAmXW4,928
70
70
  moleditpy_linux/utils/system_utils.py,sha256=j1RAiiKXg3N-rjpTh7yxKVAYED_kUJy4aVrY-9Hh0Mc,2674
71
- moleditpy_linux-4.3.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
72
- moleditpy_linux-4.3.1.dist-info/METADATA,sha256=gDXvrjy9PVvt0hF682Por7FWGI28jLri1gcvLLVtu2Q,65372
73
- moleditpy_linux-4.3.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
74
- moleditpy_linux-4.3.1.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
75
- moleditpy_linux-4.3.1.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
76
- moleditpy_linux-4.3.1.dist-info/RECORD,,
71
+ moleditpy_linux-4.3.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
72
+ moleditpy_linux-4.3.2.dist-info/METADATA,sha256=IE2cOdceTHiUxPeNUczyD4YL3fsLiSZyZwxYa1JwuXQ,65372
73
+ moleditpy_linux-4.3.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
74
+ moleditpy_linux-4.3.2.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
75
+ moleditpy_linux-4.3.2.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
76
+ moleditpy_linux-4.3.2.dist-info/RECORD,,