MoleditPy-linux 3.0.5__tar.gz → 3.1.0__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 (79) hide show
  1. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/PKG-INFO +1 -1
  2. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/pyproject.toml +1 -1
  3. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
  4. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/__main__.py +1 -1
  5. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/main.py +60 -0
  6. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/plugins/plugin_manager.py +42 -0
  7. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/plugins/plugin_manager_window.py +2 -38
  8. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -2
  9. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/angle_dialog.py +0 -2
  10. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/app_state.py +2 -2
  11. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -2
  12. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +26 -3
  13. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/dialog_logic.py +6 -10
  14. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -2
  15. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/io_logic.py +21 -25
  16. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/main_window_init.py +6 -2
  17. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/planarize_dialog.py +0 -1
  18. moleditpy_linux-3.1.0/src/moleditpy_linux/ui/string_importers.py +208 -0
  19. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/translation_dialog.py +31 -8
  20. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/user_template_dialog.py +0 -4
  21. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/view_3d_logic.py +3 -2
  22. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/utils/constants.py +1 -1
  23. moleditpy_linux-3.0.5/src/moleditpy_linux/ui/string_importers.py +0 -257
  24. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/LICENSE +0 -0
  25. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/README.md +0 -0
  26. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/setup.cfg +0 -0
  27. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  28. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  29. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  30. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  31. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  32. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/__init__.py +0 -0
  33. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  34. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/assets/icon.icns +0 -0
  35. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/assets/icon.ico +0 -0
  36. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/assets/icon.png +0 -0
  37. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/core/__init__.py +0 -0
  38. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  39. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/core/molecular_data.py +0 -0
  40. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/plugins/__init__.py +0 -0
  41. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
  42. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/__init__.py +0 -0
  43. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  44. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  45. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  46. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/atom_item.py +0 -0
  47. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  48. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/bond_item.py +0 -0
  49. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
  50. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  51. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/compute_logic.py +0 -0
  52. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
  53. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
  54. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  55. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  56. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
  57. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/export_logic.py +0 -0
  58. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  59. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/main_window.py +0 -0
  60. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  61. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
  62. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
  63. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
  64. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  65. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  66. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  67. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
  68. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  69. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
  70. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  71. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/sip_isdeleted_safe.py +0 -0
  72. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  73. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  74. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/ui_manager.py +0 -0
  75. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  76. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/utils/__init__.py +0 -0
  77. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/utils/default_settings.py +0 -0
  78. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  79. {moleditpy_linux-3.0.5 → moleditpy_linux-3.1.0}/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: 3.0.5
3
+ Version: 3.1.0
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 = "3.0.5"
8
+ version = "3.1.0"
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: 3.0.5
3
+ Version: 3.1.0
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_linux.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):
@@ -364,9 +364,9 @@ class StateManager:
364
364
  # 'Save As' if not PMEPRJ
365
365
  file_path = self.host.init_manager.current_file_path
366
366
  if not file_path or not file_path.lower().endswith(".pmeprj"):
367
- self.save_project_as()
367
+ self.host.io_manager.save_project_as()
368
368
  else:
369
- self.save_project()
369
+ self.host.io_manager.save_project()
370
370
  return (
371
371
  not self.host.state_manager.has_unsaved_changes
372
372
  ) # Return True only if save was successful
@@ -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
@@ -200,10 +200,6 @@ class DialogManager:
200
200
  with open(filepath, "w", encoding="utf-8") as f:
201
201
  json.dump(template_data, f, indent=2, ensure_ascii=False)
202
202
 
203
- # Mark as saved (no unsaved changes for this operation)
204
- self.host.state_manager.has_unsaved_changes = False
205
- self.host.state_manager.update_window_title()
206
-
207
203
  QMessageBox.information(
208
204
  self.host, "Success", f"Template '{name}' saved successfully."
209
205
  )
@@ -215,14 +211,14 @@ class DialogManager:
215
211
 
216
212
  def open_translation_dialog(self):
217
213
  """Open the translation dialog"""
214
+ # Get preselected atoms
215
+ preselected_atoms = self._get_preselected_atoms_3d()
216
+
218
217
  # Disable measurement mode
219
218
  if self.host.edit_3d_manager.measurement_mode:
220
219
  self.host.init_manager.measurement_action.setChecked(False)
221
220
  self.host.edit_3d_manager.toggle_measurement_mode(False)
222
221
 
223
- # Get preselected atoms
224
- preselected_atoms = self._get_preselected_atoms_3d()
225
-
226
222
  dialog = TranslationDialog(
227
223
  self.host.view_3d_manager.current_mol,
228
224
  self.host,
@@ -241,14 +237,14 @@ class DialogManager:
241
237
 
242
238
  def open_move_group_dialog(self):
243
239
  """Open Move Group dialog"""
240
+ # Get preselected atoms
241
+ preselected_atoms = self._get_preselected_atoms_3d()
242
+
244
243
  # Disable measurement mode
245
244
  if self.host.edit_3d_manager.measurement_mode:
246
245
  self.host.init_manager.measurement_action.setChecked(False)
247
246
  self.host.edit_3d_manager.toggle_measurement_mode(False)
248
247
 
249
- # Get preselected atoms
250
- preselected_atoms = self._get_preselected_atoms_3d()
251
-
252
248
  dialog = MoveGroupDialog(
253
249
  self.host.view_3d_manager.current_mol,
254
250
  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):
@@ -564,10 +564,7 @@ class IOManager:
564
564
  self.host.statusBar().showMessage(f"Export error: {e}")
565
565
 
566
566
  def load_mol_file(self, file_path: Optional[str] = None) -> None:
567
- """Regular 2D MOL file loading logic."""
568
- if not self.host.state_manager.check_unsaved_changes():
569
- return
570
-
567
+ """Import a MOL/SDF file and add its contents to the 2D editor."""
571
568
  if not file_path:
572
569
  default_dir = (
573
570
  os.path.dirname(self.host.init_manager.current_file_path)
@@ -601,11 +598,6 @@ class IOManager:
601
598
  raise ValueError("Failed to read molecule from file.")
602
599
 
603
600
  Chem.Kekulize(mol)
604
- self.host.ui_manager.restore_ui_for_editing()
605
- self.host.edit_actions_manager.clear_2d_editor(push_to_undo=False)
606
- self.host.view_3d_manager.current_mol = None
607
- self.host.view_3d_manager.plotter.clear()
608
- self.host.init_manager.analysis_action.setEnabled(False)
609
601
 
610
602
  if mol.GetNumConformers() == 0:
611
603
  AllChem.Compute2DCoords(mol)
@@ -615,9 +607,22 @@ class IOManager:
615
607
  AllChem.WedgeMolBonds(mol, conf)
616
608
 
617
609
  SCALE_FACTOR = 50.0
618
- view_center = self.host.init_manager.view_2d.mapToScene(
619
- self.host.init_manager.view_2d.viewport().rect().center()
620
- )
610
+ existing_atoms = self.host.state_manager.data.atoms
611
+ if existing_atoms:
612
+ max_x = max(
613
+ v["pos"].x() if hasattr(v["pos"], "x") else v["pos"][0]
614
+ for v in existing_atoms.values()
615
+ )
616
+ avg_y = sum(
617
+ v["pos"].y() if hasattr(v["pos"], "y") else v["pos"][1]
618
+ for v in existing_atoms.values()
619
+ ) / len(existing_atoms)
620
+ place_center = QPointF(max_x + 80.0, avg_y)
621
+ else:
622
+ place_center = self.host.init_manager.view_2d.mapToScene(
623
+ self.host.init_manager.view_2d.viewport().rect().center()
624
+ )
625
+
621
626
  positions = [conf.GetAtomPosition(i) for i in range(mol.GetNumAtoms())]
622
627
  mol_center_x = (
623
628
  sum(p.x for p in positions) / len(positions) if positions else 0.0
@@ -630,8 +635,8 @@ class IOManager:
630
635
  for i in range(mol.GetNumAtoms()):
631
636
  atom = mol.GetAtomWithIdx(i)
632
637
  pos = conf.GetAtomPosition(i)
633
- scene_x = ((pos.x - mol_center_x) * SCALE_FACTOR) + view_center.x()
634
- scene_y = (-(pos.y - mol_center_y) * SCALE_FACTOR) + view_center.y()
638
+ scene_x = ((pos.x - mol_center_x) * SCALE_FACTOR) + place_center.x()
639
+ scene_y = (-(pos.y - mol_center_y) * SCALE_FACTOR) + place_center.y()
635
640
  atom_id = self.host.init_manager.scene.create_atom(
636
641
  atom.GetSymbol(),
637
642
  QPointF(scene_x, scene_y),
@@ -662,18 +667,9 @@ class IOManager:
662
667
  bond_stereo=stereo,
663
668
  )
664
669
 
665
- self.host.statusBar().showMessage(f"Successfully loaded {file_path}")
666
- self.host.state_manager.reset_undo_stack()
667
- self.host.init_manager.current_file_path = file_path
668
- self.host.state_manager.has_unsaved_changes = False
669
- self.host.state_manager.update_window_title()
670
+ self.host.statusBar().showMessage(f"Successfully imported {file_path}")
670
671
  self.host.init_manager.scene.update_all_items()
671
-
672
- # Reset camera/zoom after drawing
673
- QTimer.singleShot(
674
- 50, lambda: self.host.view_3d_manager.plotter.view_isometric()
675
- )
676
- QTimer.singleShot(100, lambda: self.host.view_3d_manager.plotter.render())
672
+ self.host.edit_actions_manager.push_undo_state()
677
673
  QTimer.singleShot(100, self.host.view_3d_manager.fit_to_view)
678
674
  except Exception as e:
679
675
  self.host.statusBar().showMessage(f"Error loading file: {e}")
@@ -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):