MoleditPy-linux 3.0.4__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 (79) hide show
  1. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/PKG-INFO +1 -1
  2. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/pyproject.toml +1 -1
  3. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
  4. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/__main__.py +1 -1
  5. moleditpy_linux-3.0.6/src/moleditpy_linux/main.py +149 -0
  6. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/plugins/plugin_manager.py +63 -1
  7. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/plugins/plugin_manager_window.py +2 -38
  8. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/about_dialog.py +2 -1
  9. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -2
  10. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/angle_dialog.py +0 -2
  11. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -2
  12. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +26 -3
  13. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/dialog_logic.py +6 -6
  14. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -2
  15. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/main_window_init.py +9 -4
  16. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/planarize_dialog.py +0 -1
  17. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/translation_dialog.py +31 -8
  18. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/utils/constants.py +1 -1
  19. moleditpy_linux-3.0.4/src/moleditpy_linux/main.py +0 -78
  20. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/LICENSE +0 -0
  21. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/README.md +0 -0
  22. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/setup.cfg +0 -0
  23. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  24. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  25. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  26. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  27. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  28. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/__init__.py +0 -0
  29. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  30. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/assets/icon.icns +0 -0
  31. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/assets/icon.ico +0 -0
  32. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/assets/icon.png +0 -0
  33. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/core/__init__.py +0 -0
  34. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  35. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/core/molecular_data.py +0 -0
  36. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/plugins/__init__.py +0 -0
  37. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
  38. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/__init__.py +0 -0
  39. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  40. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  41. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/app_state.py +0 -0
  42. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/atom_item.py +0 -0
  43. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  44. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/bond_item.py +0 -0
  45. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
  46. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  47. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/compute_logic.py +0 -0
  48. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
  49. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
  50. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  51. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  52. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
  53. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/export_logic.py +0 -0
  54. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  55. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/io_logic.py +0 -0
  56. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/main_window.py +0 -0
  57. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  58. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
  59. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
  60. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
  61. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  62. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  63. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  64. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
  65. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  66. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
  67. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  68. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/sip_isdeleted_safe.py +0 -0
  69. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/string_importers.py +0 -0
  70. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  71. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  72. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/ui_manager.py +0 -0
  73. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
  74. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
  75. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  76. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/utils/__init__.py +0 -0
  77. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/utils/default_settings.py +0 -0
  78. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  79. {moleditpy_linux-3.0.4 → moleditpy_linux-3.0.6}/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.4
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-linux"
7
7
 
8
- version = "3.0.4"
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-linux
3
- Version: 3.0.4
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:
@@ -0,0 +1,149 @@
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 ctypes
14
+ import sys
15
+ import argparse
16
+ import logging
17
+ import os
18
+
19
+ try:
20
+ from .utils.constants import VERSION
21
+ except ImportError:
22
+ # Add the parent directory (src) to sys.path so 'moleditpy_linux.*' imports work
23
+ src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
24
+ if src_dir not in sys.path:
25
+ sys.path.insert(0, src_dir)
26
+ from moleditpy_linux.utils.constants import VERSION
27
+
28
+ # VERSION is resolved above (before Qt) so --version works without launching the app.
29
+
30
+ from PyQt6.QtWidgets import QApplication
31
+
32
+ try:
33
+ from .ui.main_window import MainWindow
34
+ except ImportError:
35
+ from moleditpy_linux.ui.main_window import MainWindow
36
+
37
+
38
+ def setup_logging():
39
+ logging.basicConfig(
40
+ level=logging.INFO,
41
+ format="%(asctime)s [%(levelname)s] %(name)s (%(pathname)s:%(lineno)d): %(message)s",
42
+ stream=sys.stdout,
43
+ force=True,
44
+ )
45
+
46
+ def handle_exception(exc_type, exc_value, exc_traceback):
47
+ """Log unhandled exceptions using the configured logging system."""
48
+ if issubclass(exc_type, KeyboardInterrupt):
49
+ # Allow keyboard interrupt to exit normally
50
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
51
+ return
52
+
53
+ logging.error(
54
+ "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
55
+ )
56
+
57
+ sys.excepthook = handle_exception
58
+
59
+
60
+ def main():
61
+ # Setup logging as early as possible
62
+ setup_logging()
63
+
64
+ # --- Additional handling for Windows taskbar icon ---
65
+ if sys.platform == "win32":
66
+ myappid = "hyoko.moleditpy_linux.1.0" # Application-specific ID (arbitrary)
67
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
68
+
69
+ parser = argparse.ArgumentParser(
70
+ prog="moleditpy", description="MoleditPy molecular editor"
71
+ )
72
+ parser.add_argument("file", nargs="?", default=None, help="File to open on startup")
73
+ _variant = " (Linux)" if "moleditpy_linux" in (__file__ or "") else ""
74
+ parser.add_argument(
75
+ "--version", action="version", version=f"MoleditPy{_variant} {VERSION}"
76
+ )
77
+ parser.add_argument(
78
+ "--safe",
79
+ action="store_true",
80
+ default=False,
81
+ help="Start in safe mode: skip loading all plugins",
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
+ )
88
+ # parse_known_args so Qt's own argv flags (e.g. -platform) are passed through
89
+ args, remaining = parser.parse_known_args()
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
+
146
+ app = QApplication([sys.argv[0]] + remaining)
147
+ window = MainWindow(initial_file=args.file, safe_mode=args.safe)
148
+ window.show()
149
+ sys.exit(app.exec())
@@ -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"
@@ -254,7 +295,28 @@ class PluginManager:
254
295
  )
255
296
  unique_module_name = unique_module_name.strip(".")
256
297
 
257
- spec = importlib.util.spec_from_file_location(unique_module_name, filepath)
298
+ is_package = os.path.basename(filepath) == "__init__.py"
299
+ pkg_dir = os.path.dirname(filepath) if is_package else None
300
+
301
+ # For package plugins, register stub parent packages so that
302
+ # relative imports (e.g. "from . import parser") resolve correctly.
303
+ if is_package and "." in unique_module_name:
304
+ parts = unique_module_name.split(".")
305
+ for i in range(1, len(parts)):
306
+ parent_name = ".".join(parts[:i])
307
+ if parent_name not in sys.modules:
308
+ import types as _types
309
+
310
+ stub = _types.ModuleType(parent_name)
311
+ stub.__path__ = []
312
+ stub.__package__ = parent_name
313
+ sys.modules[parent_name] = stub
314
+
315
+ spec = importlib.util.spec_from_file_location(
316
+ unique_module_name,
317
+ filepath,
318
+ submodule_search_locations=([pkg_dir] if is_package else None),
319
+ )
258
320
  if spec and spec.loader:
259
321
  module = importlib.util.module_from_spec(spec)
260
322
  sys.modules[spec.name] = module
@@ -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"
@@ -72,7 +72,8 @@ class AboutDialog(QDialog):
72
72
  layout.addWidget(self.image_label)
73
73
 
74
74
  # Add text information
75
- info_text = f"MoleditPy Ver. {VERSION}\nAuthor: Hiromichi Yokoyama\nLicense: GPL-3.0 license"
75
+ _variant = " (Linux)" if "moleditpy_linux" in (__file__ or "") else ""
76
+ info_text = f"MoleditPy{_variant} Ver. {VERSION}\nAuthor: Hiromichi Yokoyama\nLicense: GPL-3.0 license"
76
77
  info_label = QLabel(info_text)
77
78
  info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
78
79
  layout.addWidget(info_label)
@@ -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 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:
@@ -276,9 +279,11 @@ class MainInitManager:
276
279
  self.host.init_manager.current_file_path = file_path
277
280
  self.host.state_manager.update_window_title()
278
281
  return # Success
279
- except (AttributeError, RuntimeError, ValueError) as e:
280
- print(
281
- f"Plugin opener failed for '{opener_info.get('plugin', 'Unknown')}': {e}"
282
+ except Exception as e:
283
+ logging.warning(
284
+ "Plugin opener failed for '%s': %s",
285
+ opener_info.get("plugin", "Unknown"),
286
+ e,
282
287
  )
283
288
  # If this opener fails, try the next one or fall through to default
284
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.4"
19
+ VERSION = "3.0.6"
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -1,78 +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 ctypes
14
- import sys
15
- import argparse
16
- import logging
17
- import os
18
-
19
- from PyQt6.QtWidgets import QApplication
20
-
21
- try:
22
- from .ui.main_window import MainWindow
23
- except ImportError:
24
- # Add the parent directory (src) to sys.path so 'moleditpy_linux.*' imports work
25
- src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
26
- if src_dir not in sys.path:
27
- sys.path.insert(0, src_dir)
28
- from moleditpy_linux.ui.main_window import MainWindow
29
-
30
-
31
- def setup_logging():
32
- logging.basicConfig(
33
- level=logging.INFO,
34
- format="%(asctime)s [%(levelname)s] %(name)s (%(pathname)s:%(lineno)d): %(message)s",
35
- stream=sys.stdout,
36
- force=True,
37
- )
38
-
39
- def handle_exception(exc_type, exc_value, exc_traceback):
40
- """Log unhandled exceptions using the configured logging system."""
41
- if issubclass(exc_type, KeyboardInterrupt):
42
- # Allow keyboard interrupt to exit normally
43
- sys.__excepthook__(exc_type, exc_value, exc_traceback)
44
- return
45
-
46
- logging.error(
47
- "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
48
- )
49
-
50
- sys.excepthook = handle_exception
51
-
52
-
53
- def main():
54
- # Setup logging as early as possible
55
- setup_logging()
56
-
57
- # --- Additional handling for Windows taskbar icon ---
58
- if sys.platform == "win32":
59
- myappid = "hyoko.moleditpy_linux.1.0" # Application-specific ID (arbitrary)
60
- ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
61
-
62
- parser = argparse.ArgumentParser(
63
- prog="moleditpy", description="MoleditPy molecular editor"
64
- )
65
- parser.add_argument("file", nargs="?", default=None, help="File to open on startup")
66
- parser.add_argument(
67
- "--safe",
68
- action="store_true",
69
- default=False,
70
- help="Start in safe mode: skip loading all plugins",
71
- )
72
- # parse_known_args so Qt's own argv flags (e.g. -platform) are passed through
73
- args, remaining = parser.parse_known_args()
74
-
75
- app = QApplication([sys.argv[0]] + remaining)
76
- window = MainWindow(initial_file=args.file, safe_mode=args.safe)
77
- window.show()
78
- sys.exit(app.exec())
File without changes