MoleditPy 2.2.0a1__tar.gz → 2.2.0a2__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 (81) hide show
  1. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/PKG-INFO +1 -1
  2. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/pyproject.toml +1 -1
  3. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/constants.py +1 -1
  5. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_ui_manager.py +21 -2
  6. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +1 -1
  7. moleditpy-2.2.0a2/src/moleditpy/plugins/Utility/atom_colorizer.py +262 -0
  8. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Utility/vdw_radii_overlay.py +150 -21
  9. moleditpy-2.2.0a1/src/moleditpy/plugins/Utility/atom_colorizer.py +0 -547
  10. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/LICENSE +0 -0
  11. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/README.md +0 -0
  12. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/setup.cfg +0 -0
  13. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  14. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  15. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  16. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/requires.txt +0 -0
  17. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/top_level.txt +0 -0
  18. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/__init__.py +0 -0
  19. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/__main__.py +0 -0
  20. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/main.py +0 -0
  21. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/__init__.py +0 -0
  22. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/about_dialog.py +0 -0
  23. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/align_plane_dialog.py +0 -0
  24. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/alignment_dialog.py +0 -0
  25. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/analysis_window.py +0 -0
  26. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/angle_dialog.py +0 -0
  27. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/assets/icon.icns +0 -0
  28. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/assets/icon.ico +0 -0
  29. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/assets/icon.png +0 -0
  30. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/atom_item.py +0 -0
  31. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/bond_item.py +0 -0
  32. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/bond_length_dialog.py +0 -0
  33. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/calculation_worker.py +0 -0
  34. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/color_settings_dialog.py +0 -0
  35. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
  36. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/custom_interactor_style.py +0 -0
  37. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
  38. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
  39. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/dihedral_dialog.py +0 -0
  40. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window.py +0 -0
  41. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_app_state.py +0 -0
  42. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_compute.py +0 -0
  43. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
  44. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
  45. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
  46. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_export.py +0 -0
  47. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_main_init.py +0 -0
  48. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
  49. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_project_io.py +0 -0
  50. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_string_importers.py +0 -0
  51. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_view_3d.py +0 -0
  52. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
  53. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/mirror_dialog.py +0 -0
  54. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/molecular_data.py +0 -0
  55. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/molecule_scene.py +0 -0
  56. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/move_group_dialog.py +0 -0
  57. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
  58. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/planarize_dialog.py +0 -0
  59. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/plugin_interface.py +0 -0
  60. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/plugin_manager.py +0 -0
  61. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/plugin_manager_window.py +0 -0
  62. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/settings_dialog.py +0 -0
  63. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/template_preview_item.py +0 -0
  64. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/template_preview_view.py +0 -0
  65. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/translation_dialog.py +0 -0
  66. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/user_template_dialog.py +0 -0
  67. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/zoomable_view.py +0 -0
  68. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -0
  69. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/animated_xyz_giffer.py +0 -0
  70. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/cube_viewer.py +0 -0
  71. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -0
  72. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/mapped_cube_viewer.py +0 -0
  73. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -0
  74. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/paste_xyz.py +0 -0
  75. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -0
  76. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -0
  77. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -0
  78. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -0
  79. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Optimization/conf_search.py +0 -0
  80. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Utility/console.py +0 -0
  81. {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Utility/pubchem_ressolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.2.0a1
3
+ Version: 2.2.0a2
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 = "2.2.0a1"
8
+ version = "2.2.0a2"
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: 2.2.0a1
3
+ Version: 2.2.0a2
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
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QFont, QColor
16
16
  from rdkit import Chem
17
17
 
18
18
  #Version
19
- VERSION = '2.2.0a1'
19
+ VERSION = '2.2.0a2'
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -298,7 +298,7 @@ class MainWindowUiManager(object):
298
298
 
299
299
 
300
300
  def dragEnterEvent(self, event):
301
- """ウィンドウ全体で .pmeraw、.pmeprj、.mol、.sdf、.xyz ファイルのドラッグを受け入れる"""
301
+ """ウィンドウ全体でサポートされているファイルのドラッグを受け入れる"""
302
302
  # Accept if any dragged local file has a supported extension
303
303
  if event.mimeData().hasUrls():
304
304
  urls = event.mimeData().urls()
@@ -306,9 +306,28 @@ class MainWindowUiManager(object):
306
306
  try:
307
307
  if url.isLocalFile():
308
308
  file_path = url.toLocalFile()
309
- if file_path.lower().endswith(('.pmeraw', '.pmeprj', '.mol', '.sdf', '.xyz')):
309
+ file_lower = file_path.lower()
310
+
311
+ # Built-in extensions
312
+ if file_lower.endswith(('.pmeraw', '.pmeprj', '.mol', '.sdf', '.xyz')):
310
313
  event.acceptProposedAction()
311
314
  return
315
+
316
+ # Plugin-registered file openers
317
+ if self.plugin_manager and hasattr(self.plugin_manager, 'file_openers'):
318
+ for ext in self.plugin_manager.file_openers.keys():
319
+ if file_lower.endswith(ext):
320
+ event.acceptProposedAction()
321
+ return
322
+
323
+ # Plugin drop handlers (accept more liberally for custom logic)
324
+ # A plugin drop handler might handle it, so accept
325
+ if self.plugin_manager and hasattr(self.plugin_manager, 'drop_handlers'):
326
+ if len(self.plugin_manager.drop_handlers) > 0:
327
+ # Accept any file if drop handlers are registered
328
+ # They will check the file type in dropEvent
329
+ event.acceptProposedAction()
330
+ return
312
331
  except Exception:
313
332
  continue
314
333
  event.ignore()
@@ -9,7 +9,7 @@ import json
9
9
  __version__="2025.12.25"
10
10
  __author__="HiroYokoyama"
11
11
  PLUGIN_NAME = "ORCA xyz2inp GUI"
12
- SETTINGS_JSON = os.path.join(os.path.dirname(__file__), "orca_xyz2inp_settings.json")
12
+ SETTINGS_JSON = os.path.join(os.path.dirname(__file__), "orca_xyz2inp_gui.json")
13
13
 
14
14
  class OrcaInputDialog(QDialog):
15
15
  def __init__(self, main_window):
@@ -0,0 +1,262 @@
1
+ import numpy as np
2
+ import pyvista as pv
3
+ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
4
+ QLabel, QColorDialog, QDockWidget, QMessageBox,
5
+ QLineEdit, QListWidget, QAbstractItemView, QGroupBox, QDialog)
6
+ from PyQt6.QtGui import QColor, QCloseEvent
7
+ from PyQt6.QtCore import Qt
8
+ import traceback
9
+ import sys
10
+ import os
11
+ import json
12
+
13
+ # Try importing from the installed package first (pip package structure)
14
+ try:
15
+ from moleditpy.modules.constants import CPK_COLORS_PV
16
+ except ImportError:
17
+ # Fallback to local 'modules' if running from source or sys.path is set that way
18
+ try:
19
+ from modules.constants import CPK_COLORS_PV
20
+ except ImportError:
21
+ # Final fallback map
22
+ CPK_COLORS_PV = {}
23
+
24
+ __version__="2025.12.25"
25
+ __author__="HiroYokoyama"
26
+
27
+ PLUGIN_NAME = "Atom Colorizer"
28
+
29
+ class AtomColorizerWindow(QDialog):
30
+ def __init__(self, main_window):
31
+ super().__init__(parent=main_window)
32
+ self.mw = main_window
33
+ self.plotter = self.mw.plotter
34
+
35
+ # Set window properties for modeless behavior
36
+ self.setModal(False)
37
+ self.setWindowTitle(PLUGIN_NAME)
38
+ self.setWindowFlags(Qt.WindowType.Window) # Ensures it has min/max/close buttons
39
+
40
+ # Initialize current_color as QColor object
41
+ self.current_color = QColor(255, 0, 0) # Default red
42
+
43
+ self.init_ui()
44
+
45
+ # Auto-enable 3D Selection (Measurement Mode) if not already active
46
+ try:
47
+ if hasattr(self.mw, 'measurement_mode') and not self.mw.measurement_mode:
48
+ if hasattr(self.mw, 'toggle_measurement_mode'):
49
+ self.mw.toggle_measurement_mode(True)
50
+ # Sync UI button state if possible
51
+ if hasattr(self.mw, 'measurement_action'):
52
+ self.mw.measurement_action.setChecked(True)
53
+ except Exception as e:
54
+ print(f"Failed to auto-enable 3D selection: {e}")
55
+
56
+ def init_ui(self):
57
+ layout = QVBoxLayout()
58
+
59
+ # Information Label
60
+ info_label = QLabel("Select atoms in the 3D viewer and apply color.")
61
+ info_label.setWordWrap(True)
62
+ layout.addWidget(info_label)
63
+
64
+ # Selection Group
65
+ sel_group = QGroupBox("Selection")
66
+ sel_layout = QVBoxLayout()
67
+
68
+ self.le_indices = QLineEdit()
69
+ self.le_indices.setPlaceholderText("e.g. 0, 1, 5")
70
+ sel_layout.addWidget(self.le_indices)
71
+
72
+ # Auto-update timer
73
+ from PyQt6.QtCore import QTimer
74
+ self.sel_timer = QTimer(self)
75
+ self.sel_timer.timeout.connect(self._auto_update_selection)
76
+ self.sel_timer.start(200) # Check every 200ms
77
+
78
+ sel_group.setLayout(sel_layout)
79
+ layout.addWidget(sel_group)
80
+
81
+ # Color Group
82
+ col_group = QGroupBox("Color")
83
+ col_layout = QVBoxLayout()
84
+
85
+ self.btn_color = QPushButton("Choose Color")
86
+ # Initial style based on self.current_color (QColor object)
87
+ self.btn_color.setStyleSheet(f"background-color: {self.current_color.name()}; color: {'black' if self.current_color.lightness() > 128 else 'white'};")
88
+ self.btn_color.clicked.connect(self.choose_color)
89
+ col_layout.addWidget(self.btn_color)
90
+
91
+ btn_apply = QPushButton("Apply Color")
92
+ btn_apply.clicked.connect(self.apply_color)
93
+ col_layout.addWidget(btn_apply)
94
+
95
+ col_group.setLayout(col_layout)
96
+ layout.addWidget(col_group)
97
+
98
+ # Reset Group
99
+ reset_group = QGroupBox("Reset")
100
+ reset_layout = QVBoxLayout()
101
+
102
+ btn_reset = QPushButton("Reset to Element Colors")
103
+ btn_reset.clicked.connect(self.reset_colors)
104
+ reset_layout.addWidget(btn_reset)
105
+
106
+ reset_group.setLayout(reset_layout)
107
+ layout.addWidget(reset_group)
108
+
109
+ layout.addStretch()
110
+
111
+ # Close Button
112
+ close_btn = QPushButton("Close")
113
+ close_btn.clicked.connect(self.close)
114
+ layout.addWidget(close_btn)
115
+
116
+ self.setLayout(layout)
117
+
118
+ # Resize window to a reasonable default
119
+ self.resize(300, 400)
120
+
121
+ def get_selection_from_viewer(self):
122
+ """
123
+ Get selected atom indices from the main window.
124
+ Only checks 3D selection and Measurement selection. 2D selection is ignored per request.
125
+ """
126
+ indices = set()
127
+
128
+ # 1. Check direct 3D selection (e.g. from 3D Drag or specific 3D select tools)
129
+ if hasattr(self.mw, 'selected_atoms_3d') and self.mw.selected_atoms_3d:
130
+ indices.update(self.mw.selected_atoms_3d)
131
+
132
+ # 2. Check measurement selection (commonly used for picking atoms in 3D)
133
+ if hasattr(self.mw, 'selected_atoms_for_measurement') and self.mw.selected_atoms_for_measurement:
134
+ # selected_atoms_for_measurement might be list of int or objects, typically ints in this internal API
135
+ for item in self.mw.selected_atoms_for_measurement:
136
+ if isinstance(item, int):
137
+ indices.add(item)
138
+
139
+ # Update the line edit
140
+ sorted_indices = sorted(list(indices))
141
+ new_text = ",".join(map(str, sorted_indices))
142
+ if self.le_indices.text() != new_text:
143
+ self.le_indices.setText(new_text)
144
+
145
+ def _auto_update_selection(self):
146
+ """Timer slot to auto-update selection."""
147
+ if self.le_indices.hasFocus():
148
+ return
149
+ self.get_selection_from_viewer()
150
+
151
+ def choose_color(self):
152
+ c = QColorDialog.getColor(initial=self.current_color, title="Select Color")
153
+ if c.isValid():
154
+ self.current_color = c
155
+ # Update button style
156
+ self.btn_color.setStyleSheet(f"background-color: {c.name()}; color: {'black' if c.lightness() > 128 else 'white'};")
157
+
158
+ def apply_color(self):
159
+ txt = self.le_indices.text().strip()
160
+ if not txt:
161
+ QMessageBox.warning(self, "Warning", "No atoms selected. Please select atoms in the 3D viewer first.")
162
+ return
163
+
164
+ try:
165
+ str_indices = [x.strip() for x in txt.split(',') if x.strip()]
166
+ target_indices = [int(x) for x in str_indices]
167
+ except ValueError:
168
+ QMessageBox.warning(self, "Error", "Invalid indices format.")
169
+ return
170
+
171
+ if not self.mw.current_mol:
172
+ QMessageBox.warning(self, "Error", "No molecule loaded.")
173
+ return
174
+
175
+ try:
176
+ # Use the API to set atom colors
177
+ hex_color = self.current_color.name()
178
+
179
+ for idx in target_indices:
180
+ if 0 <= idx < self.mw.current_mol.GetNumAtoms():
181
+ # Access via main_window_view_3d proxy
182
+ if hasattr(self.mw, 'main_window_view_3d'):
183
+ self.mw.main_window_view_3d.update_atom_color_override(idx, hex_color)
184
+ else:
185
+ # Fallback if unproxied (unlikely in this architecture)
186
+ pass
187
+
188
+ except Exception as e:
189
+ QMessageBox.critical(self, "Error", f"Failed to apply color: {e}")
190
+ traceback.print_exc()
191
+
192
+ def reset_colors(self):
193
+ if not self.mw.current_mol:
194
+ return
195
+
196
+ try:
197
+ # Clear all color overrides using the API
198
+ for i in range(self.mw.current_mol.GetNumAtoms()):
199
+ if hasattr(self.mw, 'main_window_view_3d'):
200
+ self.mw.main_window_view_3d.update_atom_color_override(i, None)
201
+
202
+ except Exception as e:
203
+ QMessageBox.critical(self, "Error", f"Failed to reset colors: {e}")
204
+
205
+
206
+ # Global reference to keep window alive
207
+ _atom_colorizer_window = None
208
+
209
+ def run(mw):
210
+ global _atom_colorizer_window
211
+
212
+ # Check if window already exists
213
+ if _atom_colorizer_window is None:
214
+ _atom_colorizer_window = AtomColorizerWindow(mw)
215
+ # Handle cleanup when window is closed
216
+ _atom_colorizer_window.finished.connect(lambda: _cleanup_window())
217
+
218
+ _atom_colorizer_window.show()
219
+ _atom_colorizer_window.raise_()
220
+ _atom_colorizer_window.activateWindow()
221
+
222
+ def _cleanup_window():
223
+ global _atom_colorizer_window
224
+ _atom_colorizer_window = None
225
+
226
+
227
+ def initialize(context):
228
+ """
229
+ Register plugin save/load handlers for persistence.
230
+ """
231
+ mw = context.get_main_window()
232
+
233
+ def save_handler():
234
+ """Save color overrides to project file."""
235
+ # _plugin_color_overrides is stored on the MainWindow instance by the API
236
+ if not hasattr(mw, '_plugin_color_overrides'):
237
+ return {}
238
+
239
+ # Convert color overrides to JSON-serializable format
240
+ return {
241
+ "atom_colors": {str(k): v for k, v in mw._plugin_color_overrides.items()}
242
+ }
243
+
244
+ def load_handler(data):
245
+ """Load color overrides from project file."""
246
+ if not data:
247
+ return
248
+
249
+ atom_colors = data.get("atom_colors", {})
250
+
251
+ # Restore color overrides using the API
252
+ if hasattr(mw, 'main_window_view_3d'):
253
+ for atom_idx_str, hex_color in atom_colors.items():
254
+ try:
255
+ atom_idx = int(atom_idx_str)
256
+ mw.main_window_view_3d.update_atom_color_override(atom_idx, hex_color)
257
+ except Exception as e:
258
+ print(f"Failed to restore color for atom {atom_idx_str}: {e}")
259
+
260
+ # Register handlers
261
+ context.register_save_handler(save_handler)
262
+ context.register_load_handler(load_handler)
@@ -7,8 +7,8 @@ import numpy as np
7
7
  import functools
8
8
  import types
9
9
  from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QLabel,
10
- QSlider, QHBoxLayout, QPushButton)
11
- from PyQt6.QtGui import QAction
10
+ QSlider, QHBoxLayout, QPushButton, QDoubleSpinBox)
11
+ from PyQt6.QtGui import QAction, QColor
12
12
  from PyQt6.QtCore import Qt, QTimer
13
13
 
14
14
  # Try to import VDW radii from constants, fallback if needed
@@ -35,7 +35,8 @@ SETTINGS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vdw_ra
35
35
  # Global State
36
36
  _config_window = None
37
37
  _vdw_settings = {
38
- "occupancy": 0.3 # Opacity (0.0 - 1.0)
38
+ "occupancy": 0.3, # Opacity (0.0 - 1.0)
39
+ "resolution": 0.125 # Voxel spacing in Angstroms
39
40
  }
40
41
 
41
42
  def load_settings():
@@ -44,9 +45,10 @@ def load_settings():
44
45
  if os.path.exists(SETTINGS_FILE):
45
46
  with open(SETTINGS_FILE, 'r') as f:
46
47
  saved = json.load(f)
47
- # Filter to only keep occupancy
48
48
  if "occupancy" in saved:
49
49
  _vdw_settings["occupancy"] = float(saved["occupancy"])
50
+ if "resolution" in saved:
51
+ _vdw_settings["resolution"] = float(saved["resolution"])
50
52
  except Exception as e:
51
53
  print(f"Error loading VDW settings: {e}")
52
54
 
@@ -63,7 +65,7 @@ class VDWConfigWindow(QDialog):
63
65
  self.mw = main_window
64
66
  self.setWindowTitle("VDW Overlay Settings")
65
67
  self.setModal(False)
66
- self.resize(300, 100)
68
+ self.resize(350, 150)
67
69
  self.init_ui()
68
70
 
69
71
  def init_ui(self):
@@ -74,14 +76,45 @@ class VDWConfigWindow(QDialog):
74
76
  occ_layout.addWidget(QLabel("Occupancy:"))
75
77
  self.slider_occ = QSlider(Qt.Orientation.Horizontal)
76
78
  self.slider_occ.setRange(0, 100)
77
- current_occ = int(_vdw_settings.get("occupancy", 0.3) * 100)
78
- self.slider_occ.setValue(current_occ)
79
- self.slider_occ.valueChanged.connect(self.on_occupancy_changed)
79
+ current_occ = _vdw_settings.get("occupancy", 0.3)
80
+ self.slider_occ.setValue(int(current_occ * 100))
81
+ self.slider_occ.valueChanged.connect(self.on_occupancy_slider_changed)
80
82
  occ_layout.addWidget(self.slider_occ)
81
- self.lbl_occ_val = QLabel(f"{current_occ}%")
82
- occ_layout.addWidget(self.lbl_occ_val)
83
+
84
+ self.spin_occ = QDoubleSpinBox()
85
+ self.spin_occ.setRange(0.0, 1.0)
86
+ self.spin_occ.setSingleStep(0.05)
87
+ self.spin_occ.setValue(current_occ)
88
+ self.spin_occ.valueChanged.connect(self.on_occupancy_spin_changed)
89
+ occ_layout.addWidget(self.spin_occ)
90
+
83
91
  layout.addLayout(occ_layout)
92
+
93
+ # Resolution Slider
94
+ res_layout = QHBoxLayout()
95
+ res_layout.addWidget(QLabel("Resolution (Å):"))
96
+ self.slider_res = QSlider(Qt.Orientation.Horizontal)
97
+ self.slider_res.setRange(5, 50) # 0.05 to 0.50
98
+ current_res = _vdw_settings.get("resolution", 0.125)
99
+ self.slider_res.setValue(int(current_res * 100))
100
+ self.slider_res.valueChanged.connect(self.on_resolution_slider_changed)
101
+ res_layout.addWidget(self.slider_res)
102
+
103
+ self.spin_res = QDoubleSpinBox()
104
+ self.spin_res.setRange(0.05, 0.50)
105
+ self.spin_res.setSingleStep(0.005)
106
+ self.spin_res.setDecimals(3)
107
+ self.spin_res.setValue(current_res)
108
+ self.spin_res.valueChanged.connect(self.on_resolution_spin_changed)
109
+ res_layout.addWidget(self.spin_res)
84
110
 
111
+ layout.addLayout(res_layout)
112
+
113
+ # Reset Button
114
+ btn_reset = QPushButton("Reset to Defaults")
115
+ btn_reset.clicked.connect(self.reset_defaults)
116
+ layout.addWidget(btn_reset)
117
+
85
118
  # Close Button
86
119
  btn_close = QPushButton("Close")
87
120
  btn_close.clicked.connect(self.close)
@@ -89,13 +122,93 @@ class VDWConfigWindow(QDialog):
89
122
 
90
123
  self.setLayout(layout)
91
124
 
92
- def on_occupancy_changed(self, value):
93
- opacity = value / 100.0
94
- _vdw_settings["occupancy"] = opacity
95
- self.lbl_occ_val.setText(f"{value}%")
125
+ def on_occupancy_slider_changed(self, value):
126
+ val_float = value / 100.0
127
+ self.spin_occ.blockSignals(True)
128
+ self.spin_occ.setValue(val_float)
129
+ self.spin_occ.blockSignals(False)
130
+ self._update_occupancy(val_float)
131
+
132
+ def on_occupancy_spin_changed(self, value):
133
+ val_int = int(value * 100)
134
+ self.slider_occ.blockSignals(True)
135
+ self.slider_occ.setValue(val_int)
136
+ self.slider_occ.blockSignals(False)
137
+ self._update_occupancy(value)
138
+
139
+ def _update_occupancy(self, value):
140
+ _vdw_settings["occupancy"] = value
141
+ save_settings()
142
+ self.update_view()
143
+
144
+ def on_resolution_slider_changed(self, value):
145
+ val_float = value / 100.0
146
+ self.spin_res.blockSignals(True)
147
+ self.spin_res.setValue(val_float)
148
+ self.spin_res.blockSignals(False)
149
+ self._update_resolution(val_float)
150
+
151
+ def on_resolution_spin_changed(self, value):
152
+ val_int = int(value * 100)
153
+ self.slider_res.blockSignals(True)
154
+ self.slider_res.setValue(val_int)
155
+ self.slider_res.blockSignals(False)
156
+ self._update_resolution(value)
157
+
158
+ def _update_resolution(self, value):
159
+ _vdw_settings["resolution"] = value
96
160
  save_settings()
97
161
  self.update_view()
98
162
 
163
+ def reset_defaults(self):
164
+ # Default values
165
+ def_occ = 0.3
166
+ def_res = 0.125
167
+
168
+ # Block signals to prevent redundant updates/saves during setting
169
+ self.slider_occ.blockSignals(True)
170
+ self.spin_occ.blockSignals(True)
171
+ self.slider_res.blockSignals(True)
172
+ self.spin_res.blockSignals(True)
173
+
174
+ # Set values
175
+ self.slider_occ.setValue(int(def_occ * 100))
176
+ self.spin_occ.setValue(def_occ)
177
+ self.slider_res.setValue(int(def_res * 100))
178
+ self.spin_res.setValue(def_res)
179
+
180
+ # Unblock
181
+ self.slider_occ.blockSignals(False)
182
+ self.spin_occ.blockSignals(False)
183
+ self.slider_res.blockSignals(False)
184
+ self.spin_res.blockSignals(False)
185
+
186
+ # Update settings and view once
187
+ _vdw_settings["occupancy"] = def_occ
188
+ _vdw_settings["resolution"] = def_res
189
+ save_settings()
190
+ self.update_view()
191
+
192
+ def refresh_ui_values(self):
193
+ """Update UI elements from global settings."""
194
+ occ = _vdw_settings.get("occupancy", 0.3)
195
+ res = _vdw_settings.get("resolution", 0.125)
196
+
197
+ self.slider_occ.blockSignals(True)
198
+ self.spin_occ.blockSignals(True)
199
+ self.slider_res.blockSignals(True)
200
+ self.spin_res.blockSignals(True)
201
+
202
+ self.slider_occ.setValue(int(occ * 100))
203
+ self.spin_occ.setValue(occ)
204
+ self.slider_res.setValue(int(res * 100))
205
+ self.spin_res.setValue(res)
206
+
207
+ self.slider_occ.blockSignals(False)
208
+ self.spin_occ.blockSignals(False)
209
+ self.slider_res.blockSignals(False)
210
+ self.spin_res.blockSignals(False)
211
+
99
212
  def update_view(self):
100
213
  # Trigger redraw if we are in the correct mode
101
214
  if hasattr(self.mw, 'current_3d_style') and self.mw.current_3d_style == "vdw_overlay":
@@ -130,8 +243,10 @@ def draw_vdw_overlay(mw, mol):
130
243
  radii = []
131
244
  atom_colors = []
132
245
 
133
- # Use custom colors if available, otherwise CPK
134
- custom_map = getattr(mw, 'custom_atom_colors', {})
246
+ # Use custom colors if available (API-based or legacy)
247
+ custom_map = getattr(mw, '_plugin_color_overrides', {})
248
+ if not custom_map:
249
+ custom_map = getattr(mw, 'custom_atom_colors', {})
135
250
 
136
251
  if mol.GetNumConformers() > 0:
137
252
  conf = mol.GetConformer()
@@ -149,10 +264,18 @@ def draw_vdw_overlay(mw, mol):
149
264
 
150
265
  # Color handling
151
266
  if i in custom_map:
152
- c = custom_map[i]
153
- # Normalize 0-255 to 0-1 if needed
154
- if any(x > 1.0 for x in c):
155
- c = [x/255.0 for x in c]
267
+ val = custom_map[i]
268
+ # Handling new API (Hex string) vs Legacy (List/Tuple)
269
+ if isinstance(val, str) and val.startswith('#'):
270
+ # Convert Hex to RGB [0-1]
271
+ qc = QColor(val)
272
+ c = [qc.redF(), qc.greenF(), qc.blueF()]
273
+ else:
274
+ # Assume legacy list/tuple
275
+ c = val
276
+ # Normalize 0-255 to 0-1 if needed
277
+ if any(x > 1.0 for x in c):
278
+ c = [x/255.0 for x in c]
156
279
  else:
157
280
  c = CPK_COLORS_PV.get(sym, [0.8, 0.8, 0.8]) # Default grey if missing
158
281
  atom_colors.append(c)
@@ -170,7 +293,10 @@ def draw_vdw_overlay(mw, mol):
170
293
  max_bounds = positions.max(axis=0) + padding
171
294
 
172
295
  # Resolution (voxel size in Angstroms)
173
- spacing = (0.125, 0.125, 0.125)
296
+ res_val = _vdw_settings.get("resolution", 0.125)
297
+ # Clamp to safe limits just in case
298
+ if res_val < 0.01: res_val = 0.01
299
+ spacing = (res_val, res_val, res_val)
174
300
 
175
301
  dims = np.ceil((max_bounds - min_bounds) / spacing).astype(int)
176
302
 
@@ -242,6 +368,9 @@ def run(mw):
242
368
  _config_window = VDWConfigWindow(mw)
243
369
  _config_window.finished.connect(lambda: _cleanup_config())
244
370
 
371
+ # Ensure UI reflects the loaded settings (important if window was already open or reused)
372
+ _config_window.refresh_ui_values()
373
+
245
374
  _config_window.show()
246
375
  _config_window.raise_()
247
376
  _config_window.activateWindow()