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.
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/PKG-INFO +1 -1
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/pyproject.toml +1 -1
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/constants.py +1 -1
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_ui_manager.py +21 -2
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +1 -1
- moleditpy-2.2.0a2/src/moleditpy/plugins/Utility/atom_colorizer.py +262 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Utility/vdw_radii_overlay.py +150 -21
- moleditpy-2.2.0a1/src/moleditpy/plugins/Utility/atom_colorizer.py +0 -547
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/LICENSE +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/README.md +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/setup.cfg +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/__main__.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/main.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/__init__.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/about_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/align_plane_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/alignment_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/analysis_window.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/angle_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/assets/icon.icns +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/assets/icon.ico +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/assets/icon.png +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/atom_item.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/bond_item.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/bond_length_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/calculation_worker.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/color_settings_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/custom_interactor_style.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/dihedral_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_app_state.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_compute.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_export.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_main_init.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_project_io.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_string_importers.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_view_3d.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/mirror_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/molecular_data.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/molecule_scene.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/move_group_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/planarize_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/plugin_interface.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/plugin_manager.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/plugin_manager_window.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/settings_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/template_preview_item.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/template_preview_view.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/translation_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/user_template_dialog.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/modules/zoomable_view.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/animated_xyz_giffer.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/cube_viewer.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/mapped_cube_viewer.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/File/paste_xyz.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Optimization/conf_search.py +0 -0
- {moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Utility/console.py +0 -0
- {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.
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.2.
|
|
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
|
|
@@ -298,7 +298,7 @@ class MainWindowUiManager(object):
|
|
|
298
298
|
|
|
299
299
|
|
|
300
300
|
def dragEnterEvent(self, event):
|
|
301
|
-
"""
|
|
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
|
-
|
|
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()
|
{moleditpy-2.2.0a1 → moleditpy-2.2.0a2}/src/moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py
RENAMED
|
@@ -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__), "
|
|
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(
|
|
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 =
|
|
78
|
-
self.slider_occ.setValue(current_occ)
|
|
79
|
-
self.slider_occ.valueChanged.connect(self.
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
self.
|
|
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
|
|
134
|
-
custom_map = getattr(mw, '
|
|
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
|
-
|
|
153
|
-
#
|
|
154
|
-
if
|
|
155
|
-
|
|
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
|
-
|
|
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()
|