MoleditPy 3.0.5__tar.gz → 3.0.6__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 (78) hide show
  1. {moleditpy-3.0.5 → moleditpy-3.0.6}/PKG-INFO +1 -1
  2. {moleditpy-3.0.5 → moleditpy-3.0.6}/pyproject.toml +1 -1
  3. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/__main__.py +1 -1
  5. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/main.py +60 -0
  6. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/plugins/plugin_manager.py +42 -0
  7. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/plugins/plugin_manager_window.py +2 -38
  8. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/align_plane_dialog.py +0 -2
  9. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/angle_dialog.py +0 -2
  10. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/bond_length_dialog.py +0 -2
  11. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/dialog_3d_picking_mixin.py +26 -3
  12. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/dialog_logic.py +6 -6
  13. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/dihedral_dialog.py +0 -2
  14. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/main_window_init.py +6 -2
  15. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/planarize_dialog.py +0 -1
  16. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/translation_dialog.py +31 -8
  17. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/utils/constants.py +1 -1
  18. {moleditpy-3.0.5 → moleditpy-3.0.6}/LICENSE +0 -0
  19. {moleditpy-3.0.5 → moleditpy-3.0.6}/README.md +0 -0
  20. {moleditpy-3.0.5 → moleditpy-3.0.6}/setup.cfg +0 -0
  21. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  22. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  23. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  24. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/MoleditPy.egg-info/requires.txt +0 -0
  25. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/MoleditPy.egg-info/top_level.txt +0 -0
  26. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/__init__.py +0 -0
  27. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/assets/file_icon.ico +0 -0
  28. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/assets/icon.icns +0 -0
  29. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/assets/icon.ico +0 -0
  30. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/assets/icon.png +0 -0
  31. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/core/__init__.py +0 -0
  32. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/core/mol_geometry.py +0 -0
  33. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/core/molecular_data.py +0 -0
  34. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/plugins/__init__.py +0 -0
  35. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/plugins/plugin_interface.py +0 -0
  36. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/__init__.py +0 -0
  37. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/about_dialog.py +0 -0
  38. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/alignment_dialog.py +0 -0
  39. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/analysis_window.py +0 -0
  40. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/app_state.py +0 -0
  41. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/atom_item.py +0 -0
  42. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/base_picking_dialog.py +0 -0
  43. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/bond_item.py +0 -0
  44. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/calculation_worker.py +0 -0
  45. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/color_settings_dialog.py +0 -0
  46. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/compute_logic.py +0 -0
  47. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
  48. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/custom_interactor_style.py +0 -0
  49. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
  50. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/edit_3d_logic.py +0 -0
  51. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/edit_actions_logic.py +0 -0
  52. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/export_logic.py +0 -0
  53. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/geometry_base_dialog.py +0 -0
  54. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/io_logic.py +0 -0
  55. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/main_window.py +0 -0
  56. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/mirror_dialog.py +0 -0
  57. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/molecular_scene_handler.py +0 -0
  58. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/molecule_scene.py +0 -0
  59. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/move_group_dialog.py +0 -0
  60. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  61. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/settings_dialog.py +0 -0
  62. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
  63. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
  64. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
  65. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
  66. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
  67. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/sip_isdeleted_safe.py +0 -0
  68. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/string_importers.py +0 -0
  69. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/template_preview_item.py +0 -0
  70. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/template_preview_view.py +0 -0
  71. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/ui_manager.py +0 -0
  72. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/user_template_dialog.py +0 -0
  73. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/view_3d_logic.py +0 -0
  74. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/ui/zoomable_view.py +0 -0
  75. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/utils/__init__.py +0 -0
  76. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/utils/default_settings.py +0 -0
  77. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
  78. {moleditpy-3.0.5 → moleditpy-3.0.6}/src/moleditpy/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.0.5
3
+ Version: 3.0.6
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"
7
7
 
8
- version = "3.0.5"
8
+ version = "3.0.6"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.0.5
3
+ Version: 3.0.6
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
@@ -11,7 +11,7 @@ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  print("-----------------------------------------------------")
14
- print("MoleditPy A Python-based molecular editing software")
14
+ print("MoleditPy - A Python-based molecular editing software")
15
15
  print("-----------------------------------------------------\n")
16
16
 
17
17
  try:
@@ -80,9 +80,69 @@ def main():
80
80
  default=False,
81
81
  help="Start in safe mode: skip loading all plugins",
82
82
  )
83
+ parser.add_argument(
84
+ "--install-plugin",
85
+ metavar="PATH",
86
+ help="Install a plugin from a .py file, .zip, or folder (Headless)",
87
+ )
83
88
  # parse_known_args so Qt's own argv flags (e.g. -platform) are passed through
84
89
  args, remaining = parser.parse_known_args()
85
90
 
91
+ # --- Headless Plugin Installation ---
92
+ if args.install_plugin:
93
+ plugin_path = os.path.abspath(args.install_plugin)
94
+ if not os.path.exists(plugin_path):
95
+ print(f"Error: Plugin path not found: {plugin_path}")
96
+ sys.exit(1)
97
+
98
+ try:
99
+ from moleditpy.plugins.plugin_manager import PluginManager
100
+ except ImportError:
101
+ from .plugins.plugin_manager import PluginManager
102
+
103
+ pm = PluginManager()
104
+ sha256 = pm._compute_sha256(plugin_path)
105
+
106
+ # Extract metadata
107
+ metadata_file = plugin_path
108
+ if os.path.isdir(plugin_path):
109
+ init_py = os.path.join(plugin_path, "__init__.py")
110
+ if os.path.exists(init_py):
111
+ metadata_file = init_py
112
+
113
+ info = (
114
+ pm.get_plugin_info_safe(metadata_file)
115
+ if metadata_file.endswith(".py")
116
+ else {}
117
+ )
118
+
119
+ print("\n" + "=" * 40)
120
+ print(" PLUGIN INSTALLATION (HEADLESS)")
121
+ print("=" * 40)
122
+ print(f" Name: {info.get('name', os.path.basename(plugin_path))}")
123
+ print(f" Author: {info.get('author', 'Unknown')}")
124
+ print(f" Version: {info.get('version', 'Unknown')}")
125
+ print(f" Description: {info.get('description', 'No description')}")
126
+ print("-" * 40)
127
+ print(f" Path: {plugin_path}")
128
+ print(f" SHA-256: {sha256}")
129
+ print("=" * 40)
130
+
131
+ confirm = (
132
+ input("\nDo you want to proceed with installation? (y/N): ").strip().lower()
133
+ )
134
+ if confirm == "y":
135
+ success, msg = pm.install_plugin(plugin_path)
136
+ if success:
137
+ print(f"Success: {msg}")
138
+ sys.exit(0)
139
+ else:
140
+ print(f"Error: {msg}")
141
+ sys.exit(1)
142
+ else:
143
+ print("Installation aborted.")
144
+ sys.exit(0)
145
+
86
146
  app = QApplication([sys.argv[0]] + remaining)
87
147
  window = MainWindow(initial_file=args.file, safe_mode=args.safe)
88
148
  window.show()
@@ -12,6 +12,7 @@ DOI: 10.5281/zenodo.17268532
12
12
 
13
13
  from __future__ import annotations
14
14
  import ast
15
+ import hashlib
15
16
  import importlib.util
16
17
  import logging
17
18
  import os
@@ -32,6 +33,46 @@ except ImportError:
32
33
 
33
34
 
34
35
  class PluginManager:
36
+ def _compute_sha256(self, path: str) -> str:
37
+ """Computes SHA-256 for a file or a directory (concatenated hashes of all files)."""
38
+ if os.path.isfile(path):
39
+ return self._sha256_for_file(path)
40
+ if os.path.isdir(path):
41
+ return self._sha256_for_directory(path)
42
+ return "N/A"
43
+
44
+ def _sha256_for_file(self, path: str) -> str:
45
+ """Computes SHA-256 for a single file."""
46
+ hasher = hashlib.sha256()
47
+ try:
48
+ with open(path, "rb") as f:
49
+ for chunk in iter(lambda: f.read(8192), b""):
50
+ hasher.update(chunk)
51
+ return hasher.hexdigest()
52
+ except (AttributeError, OSError, RuntimeError, ValueError, TypeError):
53
+ return "N/A"
54
+
55
+ def _sha256_for_directory(self, dir_path: str) -> str:
56
+ """Computes SHA-256 for a directory by hashing all files in sorted order."""
57
+ hasher = hashlib.sha256()
58
+ try:
59
+ root = os.path.abspath(dir_path)
60
+ for current_root, _dirs, files in os.walk(root):
61
+ rel_root = os.path.relpath(current_root, root)
62
+ for filename in sorted(files):
63
+ file_path = os.path.join(current_root, filename)
64
+ rel_path = os.path.normpath(os.path.join(rel_root, filename))
65
+ # Hash the path to ensure directory structure is captured
66
+ hasher.update(rel_path.encode("utf-8", errors="replace"))
67
+ hasher.update(b"\0")
68
+ with open(file_path, "rb") as f:
69
+ for chunk in iter(lambda: f.read(8192), b""):
70
+ hasher.update(chunk)
71
+ hasher.update(b"\0")
72
+ return hasher.hexdigest()
73
+ except (AttributeError, OSError, RuntimeError, ValueError, TypeError):
74
+ return "N/A"
75
+
35
76
  def __init__(self, main_window: Any = None) -> None:
36
77
  self.plugin_dir: str = os.path.join(
37
78
  os.path.expanduser("~"), ".moleditpy", "plugins"
@@ -265,6 +306,7 @@ class PluginManager:
265
306
  parent_name = ".".join(parts[:i])
266
307
  if parent_name not in sys.modules:
267
308
  import types as _types
309
+
268
310
  stub = _types.ModuleType(parent_name)
269
311
  stub.__path__ = []
270
312
  stub.__package__ = parent_name
@@ -12,7 +12,7 @@ DOI: 10.5281/zenodo.17268532
12
12
 
13
13
  import os
14
14
  import shutil
15
- import hashlib
15
+
16
16
 
17
17
  from PyQt6.QtCore import Qt, QUrl
18
18
  from PyQt6.QtGui import QDesktopServices, QDragEnterEvent, QDropEvent
@@ -259,7 +259,7 @@ class PluginManagerWindow(QDialog):
259
259
  is_folder = True
260
260
 
261
261
  if is_valid:
262
- sha256_value = self._compute_sha256(file_path)
262
+ sha256_value = self.plugin_manager._compute_sha256(file_path)
263
263
  # Extract info and confirm
264
264
  info = {
265
265
  "name": os.path.basename(file_path),
@@ -314,39 +314,3 @@ class PluginManagerWindow(QDialog):
314
314
  summary += "Errors:\n" + "\n".join(errors)
315
315
 
316
316
  QMessageBox.information(self, "Plugin Installation", summary)
317
-
318
- def _compute_sha256(self, path):
319
- if os.path.isfile(path):
320
- return self._sha256_for_file(path)
321
- if os.path.isdir(path):
322
- return self._sha256_for_directory(path)
323
- return "N/A"
324
-
325
- def _sha256_for_file(self, path):
326
- hasher = hashlib.sha256()
327
- try:
328
- with open(path, "rb") as f:
329
- for chunk in iter(lambda: f.read(8192), b""):
330
- hasher.update(chunk)
331
- return hasher.hexdigest()
332
- except (AttributeError, OSError, RuntimeError, ValueError, TypeError):
333
- return "N/A"
334
-
335
- def _sha256_for_directory(self, dir_path):
336
- hasher = hashlib.sha256()
337
- try:
338
- root = os.path.abspath(dir_path)
339
- for current_root, _dirs, files in os.walk(root):
340
- rel_root = os.path.relpath(current_root, root)
341
- for filename in sorted(files):
342
- file_path = os.path.join(current_root, filename)
343
- rel_path = os.path.normpath(os.path.join(rel_root, filename))
344
- hasher.update(rel_path.encode("utf-8", errors="replace"))
345
- hasher.update(b"\0")
346
- with open(file_path, "rb") as f:
347
- for chunk in iter(lambda: f.read(8192), b""):
348
- hasher.update(chunk)
349
- hasher.update(b"\0")
350
- return hasher.hexdigest()
351
- except (AttributeError, OSError, RuntimeError, ValueError, TypeError):
352
- return "N/A"
@@ -97,8 +97,6 @@ class AlignPlaneDialog(BasePickingDialog):
97
97
  else:
98
98
  self.selected_atoms.add(atom_idx)
99
99
 
100
- # Display labels on the atoms
101
- self.show_atom_labels()
102
100
  self.update_display()
103
101
 
104
102
  def clear_selection(self):
@@ -169,8 +169,6 @@ class AngleDialog(GeometryBaseDialog):
169
169
  self.atom3_idx = None
170
170
  self._snapshot_positions = None
171
171
 
172
- # Display atom labels
173
- self.show_atom_labels()
174
172
  self.update_display()
175
173
 
176
174
  def clear_selection(self):
@@ -155,8 +155,6 @@ class BondLengthDialog(GeometryBaseDialog):
155
155
  self.atom1_idx = atom_idx
156
156
  self.atom2_idx = None
157
157
 
158
- # Display atom labels
159
- self.show_atom_labels()
160
158
  self.update_display()
161
159
 
162
160
  def clear_selection(self):
@@ -192,9 +192,14 @@ class Dialog3DPickingMixin:
192
192
  color : str, optional
193
193
  Label colour (default ``'yellow'``).
194
194
  """
195
- pos = self.main_window.view_3d_manager.atom_positions_3d[atom_idx]
195
+ plotter = self.main_window.view_3d_manager.plotter
196
+ try:
197
+ cam = plotter.camera_position
198
+ except (AttributeError, RuntimeError, TypeError):
199
+ cam = None
196
200
 
197
- label_actor = self.main_window.view_3d_manager.plotter.add_point_labels(
201
+ pos = self.main_window.view_3d_manager.atom_positions_3d[atom_idx]
202
+ label_actor = plotter.add_point_labels(
198
203
  [pos],
199
204
  [label_text],
200
205
  point_size=20,
@@ -204,6 +209,12 @@ class Dialog3DPickingMixin:
204
209
  )
205
210
  self.selection_labels.append(label_actor)
206
211
 
212
+ if cam is not None:
213
+ try:
214
+ plotter.camera_position = cam
215
+ except (AttributeError, RuntimeError, TypeError):
216
+ pass
217
+
207
218
  def show_atom_labels_for(self, atoms_and_labels, color="yellow"):
208
219
  """Clear existing labels and add new ones for each *(idx, text)* pair.
209
220
 
@@ -214,11 +225,17 @@ class Dialog3DPickingMixin:
214
225
  color : str, optional
215
226
  Label colour (default ``'yellow'``).
216
227
  """
228
+ plotter = self.main_window.view_3d_manager.plotter
229
+ try:
230
+ cam = plotter.camera_position
231
+ except (AttributeError, RuntimeError, TypeError):
232
+ cam = None
233
+
217
234
  self.clear_atom_labels()
218
235
 
219
236
  for atom_idx, label_text in atoms_and_labels:
220
237
  pos = self.main_window.view_3d_manager.atom_positions_3d[atom_idx]
221
- label_actor = self.main_window.view_3d_manager.plotter.add_point_labels(
238
+ label_actor = plotter.add_point_labels(
222
239
  [pos],
223
240
  [label_text],
224
241
  point_size=20,
@@ -227,3 +244,9 @@ class Dialog3DPickingMixin:
227
244
  always_visible=True,
228
245
  )
229
246
  self.selection_labels.append(label_actor)
247
+
248
+ if cam is not None:
249
+ try:
250
+ plotter.camera_position = cam
251
+ except (AttributeError, RuntimeError, TypeError):
252
+ pass
@@ -215,14 +215,14 @@ class DialogManager:
215
215
 
216
216
  def open_translation_dialog(self):
217
217
  """Open the translation dialog"""
218
+ # Get preselected atoms
219
+ preselected_atoms = self._get_preselected_atoms_3d()
220
+
218
221
  # Disable measurement mode
219
222
  if self.host.edit_3d_manager.measurement_mode:
220
223
  self.host.init_manager.measurement_action.setChecked(False)
221
224
  self.host.edit_3d_manager.toggle_measurement_mode(False)
222
225
 
223
- # Get preselected atoms
224
- preselected_atoms = self._get_preselected_atoms_3d()
225
-
226
226
  dialog = TranslationDialog(
227
227
  self.host.view_3d_manager.current_mol,
228
228
  self.host,
@@ -241,14 +241,14 @@ class DialogManager:
241
241
 
242
242
  def open_move_group_dialog(self):
243
243
  """Open Move Group dialog"""
244
+ # Get preselected atoms
245
+ preselected_atoms = self._get_preselected_atoms_3d()
246
+
244
247
  # Disable measurement mode
245
248
  if self.host.edit_3d_manager.measurement_mode:
246
249
  self.host.init_manager.measurement_action.setChecked(False)
247
250
  self.host.edit_3d_manager.toggle_measurement_mode(False)
248
251
 
249
- # Get preselected atoms
250
- preselected_atoms = self._get_preselected_atoms_3d()
251
-
252
252
  dialog = MoveGroupDialog(
253
253
  self.host.view_3d_manager.current_mol,
254
254
  self.host,
@@ -176,8 +176,6 @@ class DihedralDialog(GeometryBaseDialog):
176
176
  self.atom4_idx = None
177
177
  self._snapshot_positions = None
178
178
 
179
- # Display atom labels
180
- self.show_atom_labels()
181
179
  self.update_display()
182
180
 
183
181
  def clear_selection(self):
@@ -264,7 +264,10 @@ class MainInitManager:
264
264
  file_ext = ext_with_dot.lstrip(".")
265
265
 
266
266
  # 1. Custom Plugin Openers
267
- if self.host.plugin_manager and ext_with_dot in self.host.plugin_manager.file_openers:
267
+ if (
268
+ self.host.plugin_manager
269
+ and ext_with_dot in self.host.plugin_manager.file_openers
270
+ ):
268
271
  openers = self.host.plugin_manager.file_openers[ext_with_dot]
269
272
  # Iterate through openers (already sorted by priority)
270
273
  for opener_info in openers:
@@ -279,7 +282,8 @@ class MainInitManager:
279
282
  except Exception as e:
280
283
  logging.warning(
281
284
  "Plugin opener failed for '%s': %s",
282
- opener_info.get('plugin', 'Unknown'), e
285
+ opener_info.get("plugin", "Unknown"),
286
+ e,
283
287
  )
284
288
  # If this opener fails, try the next one or fall through to default
285
289
  continue
@@ -90,7 +90,6 @@ class PlanarizeDialog(BasePickingDialog):
90
90
  self.selected_atoms.remove(atom_idx)
91
91
  else:
92
92
  self.selected_atoms.add(atom_idx)
93
- self.show_atom_labels()
94
93
  self.update_display()
95
94
 
96
95
  def clear_selection(self):
@@ -40,11 +40,20 @@ class TranslationDialog(BasePickingDialog):
40
40
  if preselected_atoms:
41
41
  self.selected_atoms.update(preselected_atoms)
42
42
 
43
+ self._is_initializing = True
43
44
  self.init_ui()
45
+ self._is_initializing = False
44
46
 
45
47
  if self.selected_atoms:
46
- self.show_atom_labels()
48
+ self.tabs.blockSignals(True)
49
+ if len(self.selected_atoms) == 1:
50
+ self.tabs.setCurrentIndex(_TAB_ABSOLUTE)
51
+ self._populate_abs_inputs_from_atom(next(iter(self.selected_atoms)))
52
+ else:
53
+ self.tabs.setCurrentIndex(_TAB_DELTA)
54
+ self.tabs.blockSignals(False)
47
55
  self.update_display()
56
+ self.show_atom_labels()
48
57
 
49
58
  # ------------------------------------------------------------------
50
59
  # UI construction
@@ -169,6 +178,8 @@ class TranslationDialog(BasePickingDialog):
169
178
  # ------------------------------------------------------------------
170
179
 
171
180
  def _on_tab_changed(self, index):
181
+ if hasattr(self, "_is_initializing") and self._is_initializing:
182
+ return
172
183
  self.selected_atoms.clear()
173
184
  self.clear_atom_labels()
174
185
  self.update_display()
@@ -187,23 +198,27 @@ class TranslationDialog(BasePickingDialog):
187
198
  # Enforce single selection: replace previous atom
188
199
  self.selected_atoms = {atom_idx}
189
200
  self._populate_abs_inputs_from_atom(atom_idx)
190
- self.show_atom_labels()
191
201
  self.update_display()
202
+ self.show_atom_labels()
192
203
 
193
204
  def _delta_on_atom_picked(self, atom_idx):
194
205
  if atom_idx in self.selected_atoms:
195
206
  self.selected_atoms.remove(atom_idx)
196
207
  else:
197
208
  self.selected_atoms.add(atom_idx)
198
- self.show_atom_labels()
199
209
  self.update_display()
210
+ self.show_atom_labels()
200
211
 
201
212
  # ------------------------------------------------------------------
202
213
  # Absolute tab helpers
203
214
  # ------------------------------------------------------------------
204
215
 
205
216
  def _populate_abs_inputs_from_atom(self, atom_idx):
206
- pos = self.main_window.view_3d_manager.current_mol.GetConformer().GetPositions()[atom_idx]
217
+ pos = (
218
+ self.main_window.view_3d_manager.current_mol.GetConformer().GetPositions()[
219
+ atom_idx
220
+ ]
221
+ )
207
222
  self.abs_x_input.setText(f"{pos[0]:.4f}")
208
223
  self.abs_y_input.setText(f"{pos[1]:.4f}")
209
224
  self.abs_z_input.setText(f"{pos[2]:.4f}")
@@ -236,7 +251,9 @@ class TranslationDialog(BasePickingDialog):
236
251
  ty = float(self.abs_y_input.text())
237
252
  tz = float(self.abs_z_input.text())
238
253
  except ValueError:
239
- QMessageBox.warning(self, "Warning", "Please enter valid numbers for X, Y, Z.")
254
+ QMessageBox.warning(
255
+ self, "Warning", "Please enter valid numbers for X, Y, Z."
256
+ )
240
257
  return
241
258
 
242
259
  atom_idx = next(iter(self.selected_atoms))
@@ -291,7 +308,9 @@ class TranslationDialog(BasePickingDialog):
291
308
  dy = float(self.dy_input.text())
292
309
  dz = float(self.dz_input.text())
293
310
  except ValueError:
294
- QMessageBox.warning(self, "Warning", "Please enter valid numbers for dx, dy, dz.")
311
+ QMessageBox.warning(
312
+ self, "Warning", "Please enter valid numbers for dx, dy, dz."
313
+ )
295
314
  return
296
315
 
297
316
  if dx == 0 and dy == 0 and dz == 0:
@@ -325,10 +344,14 @@ class TranslationDialog(BasePickingDialog):
325
344
  self.abs_apply_btn.setEnabled(True)
326
345
  else:
327
346
  if count == 0:
328
- self.delta_selection_label.setText("Click atoms to select (minimum 1 required)")
347
+ self.delta_selection_label.setText(
348
+ "Click atoms to select (minimum 1 required)"
349
+ )
329
350
  self.apply_button.setEnabled(False)
330
351
  else:
331
- self.delta_selection_label.setText(f"Selected {count} atom{'s' if count != 1 else ''}")
352
+ self.delta_selection_label.setText(
353
+ f"Selected {count} atom{'s' if count != 1 else ''}"
354
+ )
332
355
  self.apply_button.setEnabled(True)
333
356
 
334
357
  def show_atom_labels(self):
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QColor, QFont
16
16
  from rdkit import Chem
17
17
 
18
18
  # Version
19
- VERSION = "3.0.5"
19
+ VERSION = "3.0.6"
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
File without changes
File without changes
File without changes