MoleditPy 2.2.0a2__py3-none-any.whl → 2.2.1__py3-none-any.whl
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/modules/constants.py +1 -1
- moleditpy/modules/main_window_main_init.py +32 -14
- moleditpy/modules/plugin_interface.py +1 -10
- moleditpy/modules/plugin_manager.py +0 -3
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/METADATA +1 -1
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/RECORD +10 -27
- moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -919
- moleditpy/plugins/File/animated_xyz_giffer.py +0 -583
- moleditpy/plugins/File/cube_viewer.py +0 -689
- moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -1148
- moleditpy/plugins/File/mapped_cube_viewer.py +0 -552
- moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -1226
- moleditpy/plugins/File/paste_xyz.py +0 -336
- moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -930
- moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -1028
- moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +0 -286
- moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -65
- moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -268
- moleditpy/plugins/Optimization/conf_search.py +0 -224
- moleditpy/plugins/Utility/atom_colorizer.py +0 -262
- moleditpy/plugins/Utility/console.py +0 -163
- moleditpy/plugins/Utility/pubchem_ressolver.py +0 -244
- moleditpy/plugins/Utility/vdw_radii_overlay.py +0 -432
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/WHEEL +0 -0
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/entry_points.txt +0 -0
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/top_level.txt +0 -0
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
from PyQt6.QtWidgets import (
|
|
2
|
-
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget,
|
|
3
|
-
QTableWidgetItem, QPushButton, QMessageBox, QLabel, QHeaderView, QAbstractItemView,
|
|
4
|
-
QApplication, QComboBox
|
|
5
|
-
)
|
|
6
|
-
from PyQt6.QtCore import Qt
|
|
7
|
-
from rdkit import Chem
|
|
8
|
-
from rdkit.Chem import AllChem
|
|
9
|
-
import copy
|
|
10
|
-
|
|
11
|
-
PLUGIN_NAME = "Conformational Search"
|
|
12
|
-
__version__="2025.12.25"
|
|
13
|
-
__author__="HiroYokoyama"
|
|
14
|
-
|
|
15
|
-
class ConformerSearchDialog(QDialog):
|
|
16
|
-
def __init__(self, main_window, parent=None):
|
|
17
|
-
super().__init__(parent)
|
|
18
|
-
self.main_window = main_window
|
|
19
|
-
self.setWindowTitle("Conformational Search & Preview")
|
|
20
|
-
self.resize(400, 500)
|
|
21
|
-
|
|
22
|
-
# メインウィンドウの分子への参照
|
|
23
|
-
self.target_mol = getattr(main_window, "current_mol", None)
|
|
24
|
-
|
|
25
|
-
# 計算用の一時的な分子(オリジナルを汚染しないため)
|
|
26
|
-
self.temp_mol = None
|
|
27
|
-
# 生成された配座データのリスト [(Energy, ConformerID), ...]
|
|
28
|
-
self.conformer_data = []
|
|
29
|
-
|
|
30
|
-
self.init_ui()
|
|
31
|
-
|
|
32
|
-
def init_ui(self):
|
|
33
|
-
layout = QVBoxLayout(self)
|
|
34
|
-
|
|
35
|
-
# 説明ラベル
|
|
36
|
-
self.lbl_info = QLabel("Click 'Run Search' to generate conformers.\nSelect a row to preview.")
|
|
37
|
-
layout.addWidget(self.lbl_info)
|
|
38
|
-
|
|
39
|
-
# Force Field Selection
|
|
40
|
-
hbox_ff = QHBoxLayout()
|
|
41
|
-
hbox_ff.addWidget(QLabel("Force Field:"))
|
|
42
|
-
self.combo_ff = QComboBox()
|
|
43
|
-
self.combo_ff.addItems(["MMFF94", "UFF"])
|
|
44
|
-
hbox_ff.addWidget(self.combo_ff)
|
|
45
|
-
hbox_ff.addStretch()
|
|
46
|
-
layout.addLayout(hbox_ff)
|
|
47
|
-
|
|
48
|
-
# Set default based on main window setting
|
|
49
|
-
default_method = getattr(self.main_window, "optimization_method", "MMFF_RDKIT")
|
|
50
|
-
if default_method:
|
|
51
|
-
default_method = default_method.upper()
|
|
52
|
-
if "UFF" in default_method:
|
|
53
|
-
self.combo_ff.setCurrentText("UFF")
|
|
54
|
-
else:
|
|
55
|
-
self.combo_ff.setCurrentText("MMFF94")
|
|
56
|
-
|
|
57
|
-
# 結果表示用テーブル
|
|
58
|
-
self.table = QTableWidget()
|
|
59
|
-
self.table.setColumnCount(2)
|
|
60
|
-
self.table.setHorizontalHeaderLabels(["Rank", "Energy (kcal/mol)"])
|
|
61
|
-
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
|
62
|
-
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
63
|
-
self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
64
|
-
self.table.itemClicked.connect(self.preview_conformer)
|
|
65
|
-
layout.addWidget(self.table)
|
|
66
|
-
|
|
67
|
-
# ボタンエリア
|
|
68
|
-
btn_layout = QHBoxLayout()
|
|
69
|
-
self.btn_run = QPushButton("Run Search")
|
|
70
|
-
self.btn_run.clicked.connect(self.run_search)
|
|
71
|
-
|
|
72
|
-
self.btn_close = QPushButton("Close")
|
|
73
|
-
self.btn_close.clicked.connect(self.accept) # 閉じる(現在のプレビュー状態で確定)
|
|
74
|
-
|
|
75
|
-
btn_layout.addWidget(self.btn_run)
|
|
76
|
-
btn_layout.addWidget(self.btn_close)
|
|
77
|
-
layout.addLayout(btn_layout)
|
|
78
|
-
|
|
79
|
-
def accept(self):
|
|
80
|
-
# Push undo state when closing the dialog (confirming the selection)
|
|
81
|
-
if hasattr(self.main_window, "push_undo_state"):
|
|
82
|
-
self.main_window.push_undo_state()
|
|
83
|
-
super().accept()
|
|
84
|
-
|
|
85
|
-
def run_search(self):
|
|
86
|
-
if not self.target_mol:
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
self.btn_run.setEnabled(False)
|
|
90
|
-
self.lbl_info.setText("Running conformational search... please wait.")
|
|
91
|
-
QApplication.processEvents()
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
# 計算用に分子を複製(水素が付加されていることを推奨)
|
|
95
|
-
mol_calc = copy.deepcopy(self.target_mol)
|
|
96
|
-
|
|
97
|
-
# 1. 配座生成 (ETKDGv3)
|
|
98
|
-
params = AllChem.ETKDGv3()
|
|
99
|
-
params.useSmallRingTorsions = True
|
|
100
|
-
cids = AllChem.EmbedMultipleConfs(mol_calc, numConfs=30, params=params)
|
|
101
|
-
|
|
102
|
-
if not cids:
|
|
103
|
-
QMessageBox.warning(self, PLUGIN_NAME, "Failed to generate conformers.")
|
|
104
|
-
self.lbl_info.setText("Failed.")
|
|
105
|
-
self.btn_run.setEnabled(True)
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
# 2. 構造最適化とエネルギー計算
|
|
109
|
-
results = []
|
|
110
|
-
selected_ff = self.combo_ff.currentText()
|
|
111
|
-
|
|
112
|
-
for i, cid in enumerate(cids):
|
|
113
|
-
energy = None
|
|
114
|
-
|
|
115
|
-
if selected_ff == "MMFF94":
|
|
116
|
-
# MMFF94 Optimize
|
|
117
|
-
if AllChem.MMFFOptimizeMolecule(mol_calc, confId=cid) != -1:
|
|
118
|
-
# Calculate Energy
|
|
119
|
-
prop = AllChem.MMFFGetMoleculeProperties(mol_calc)
|
|
120
|
-
if prop:
|
|
121
|
-
ff = AllChem.MMFFGetMoleculeForceField(mol_calc, prop, confId=cid)
|
|
122
|
-
if ff:
|
|
123
|
-
energy = ff.CalcEnergy()
|
|
124
|
-
|
|
125
|
-
elif selected_ff == "UFF":
|
|
126
|
-
# UFF Optimize
|
|
127
|
-
if AllChem.UFFOptimizeMolecule(mol_calc, confId=cid) != -1:
|
|
128
|
-
# Calculate Energy
|
|
129
|
-
ff = AllChem.UFFGetMoleculeForceField(mol_calc, confId=cid)
|
|
130
|
-
if ff:
|
|
131
|
-
energy = ff.CalcEnergy()
|
|
132
|
-
|
|
133
|
-
if energy is not None:
|
|
134
|
-
results.append((energy, cid))
|
|
135
|
-
|
|
136
|
-
# UIの応答性を維持
|
|
137
|
-
if i % 5 == 0:
|
|
138
|
-
QApplication.processEvents()
|
|
139
|
-
|
|
140
|
-
if not results:
|
|
141
|
-
QMessageBox.warning(self, PLUGIN_NAME, f"Optimization failed with {selected_ff}.")
|
|
142
|
-
self.btn_run.setEnabled(True)
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
# エネルギーが低い順にソート
|
|
146
|
-
results.sort(key=lambda x: x[0])
|
|
147
|
-
|
|
148
|
-
# データを保持
|
|
149
|
-
self.temp_mol = mol_calc
|
|
150
|
-
self.conformer_data = results
|
|
151
|
-
|
|
152
|
-
# テーブル更新
|
|
153
|
-
self.update_table()
|
|
154
|
-
self.lbl_info.setText(f"Found {len(results)} conformers ({selected_ff}).")
|
|
155
|
-
|
|
156
|
-
except Exception as e:
|
|
157
|
-
QMessageBox.critical(self, PLUGIN_NAME, f"Error during search: {str(e)}")
|
|
158
|
-
self.lbl_info.setText("Error occurred.")
|
|
159
|
-
finally:
|
|
160
|
-
self.btn_run.setEnabled(True)
|
|
161
|
-
|
|
162
|
-
def update_table(self):
|
|
163
|
-
self.table.setRowCount(0)
|
|
164
|
-
# base_energy = self.conformer_data[0][0] if self.conformer_data else 0
|
|
165
|
-
|
|
166
|
-
for rank, (energy, cid) in enumerate(self.conformer_data):
|
|
167
|
-
row_idx = self.table.rowCount()
|
|
168
|
-
self.table.insertRow(row_idx)
|
|
169
|
-
|
|
170
|
-
# Rank
|
|
171
|
-
self.table.setItem(row_idx, 0, QTableWidgetItem(str(rank + 1)))
|
|
172
|
-
|
|
173
|
-
# Energy
|
|
174
|
-
energy_str = f"{energy:.4f}"
|
|
175
|
-
self.table.setItem(row_idx, 1, QTableWidgetItem(energy_str))
|
|
176
|
-
|
|
177
|
-
# 隠しデータとしてConformer IDを持たせる
|
|
178
|
-
self.table.item(row_idx, 0).setData(Qt.ItemDataRole.UserRole, cid)
|
|
179
|
-
|
|
180
|
-
def preview_conformer(self, item):
|
|
181
|
-
"""リスト選択時にメインウィンドウの表示を更新"""
|
|
182
|
-
if not self.temp_mol or not self.target_mol:
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
row = item.row()
|
|
186
|
-
# Rankカラム(0)にCIDを埋め込んでいるので取得
|
|
187
|
-
cid = self.table.item(row, 0).data(Qt.ItemDataRole.UserRole)
|
|
188
|
-
|
|
189
|
-
# 選択された配座の座標を取得
|
|
190
|
-
source_conf = self.temp_mol.GetConformer(cid)
|
|
191
|
-
target_conf = self.target_mol.GetConformer() # 現在の表示用Conformer
|
|
192
|
-
|
|
193
|
-
# 座標のコピー
|
|
194
|
-
for i in range(self.target_mol.GetNumAtoms()):
|
|
195
|
-
pos = source_conf.GetAtomPosition(i)
|
|
196
|
-
target_conf.SetAtomPosition(i, pos)
|
|
197
|
-
|
|
198
|
-
# ビューの更新(ユーザー提供コードのロジックに従う)
|
|
199
|
-
if hasattr(self.main_window, "draw_molecule_3d"):
|
|
200
|
-
self.main_window.draw_molecule_3d(self.target_mol)
|
|
201
|
-
elif hasattr(self.main_window, "update_view"):
|
|
202
|
-
self.main_window.update_view()
|
|
203
|
-
elif hasattr(self.main_window, "gl_widget"):
|
|
204
|
-
# GLWidgetのリフレッシュ
|
|
205
|
-
getattr(self.main_window.gl_widget, "update", lambda: None)()
|
|
206
|
-
|
|
207
|
-
def run(mw):
|
|
208
|
-
mol = getattr(mw, "current_mol", None)
|
|
209
|
-
if not mol:
|
|
210
|
-
QMessageBox.warning(mw, PLUGIN_NAME, "No molecule loaded.")
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
# 既存のダイアログがあればアクティブにする
|
|
214
|
-
if hasattr(mw, "_conformer_search_dialog") and mw._conformer_search_dialog.isVisible():
|
|
215
|
-
mw._conformer_search_dialog.raise_()
|
|
216
|
-
mw._conformer_search_dialog.activateWindow()
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
dialog = ConformerSearchDialog(mw, parent=mw)
|
|
220
|
-
# 参照を保持してGCを防ぐ
|
|
221
|
-
mw._conformer_search_dialog = dialog
|
|
222
|
-
dialog.show() # モーダルではなくModeless(非ブロック)で表示
|
|
223
|
-
|
|
224
|
-
# initialize removed as it only registered the menu action
|
|
@@ -1,262 +0,0 @@
|
|
|
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)
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import io
|
|
3
|
-
import code
|
|
4
|
-
import traceback
|
|
5
|
-
from contextlib import redirect_stdout, redirect_stderr
|
|
6
|
-
from PyQt6.QtWidgets import (
|
|
7
|
-
QDialog, QVBoxLayout, QTextEdit, QLineEdit,
|
|
8
|
-
QPushButton, QLabel, QWidget
|
|
9
|
-
)
|
|
10
|
-
from PyQt6.QtGui import QFont, QColor
|
|
11
|
-
from PyQt6.QtCore import Qt
|
|
12
|
-
import rdkit.Chem as Chem
|
|
13
|
-
|
|
14
|
-
__version__="2025.12.25"
|
|
15
|
-
__author__="HiroYokoyama"
|
|
16
|
-
PLUGIN_NAME = "Python Console"
|
|
17
|
-
|
|
18
|
-
class HistoryLineEdit(QLineEdit):
|
|
19
|
-
def __init__(self, parent=None):
|
|
20
|
-
super().__init__(parent)
|
|
21
|
-
self.history = []
|
|
22
|
-
self.history_index = 0
|
|
23
|
-
|
|
24
|
-
def append_history(self, text):
|
|
25
|
-
if text and (not self.history or self.history[-1] != text):
|
|
26
|
-
self.history.append(text)
|
|
27
|
-
self.history_index = len(self.history)
|
|
28
|
-
|
|
29
|
-
def keyPressEvent(self, event):
|
|
30
|
-
if event.key() == Qt.Key.Key_Up:
|
|
31
|
-
if self.history_index > 0:
|
|
32
|
-
self.history_index -= 1
|
|
33
|
-
self.setText(self.history[self.history_index])
|
|
34
|
-
elif event.key() == Qt.Key.Key_Down:
|
|
35
|
-
if self.history_index < len(self.history) - 1:
|
|
36
|
-
self.history_index += 1
|
|
37
|
-
self.setText(self.history[self.history_index])
|
|
38
|
-
else:
|
|
39
|
-
self.history_index = len(self.history)
|
|
40
|
-
self.clear()
|
|
41
|
-
else:
|
|
42
|
-
super().keyPressEvent(event)
|
|
43
|
-
|
|
44
|
-
class PythonConsoleDialog(QDialog):
|
|
45
|
-
def __init__(self, main_window):
|
|
46
|
-
super().__init__(main_window)
|
|
47
|
-
self.main_window = main_window
|
|
48
|
-
self.setWindowTitle("MoleditPy Python Console")
|
|
49
|
-
self.resize(600, 400)
|
|
50
|
-
|
|
51
|
-
# UI Setup
|
|
52
|
-
layout = QVBoxLayout()
|
|
53
|
-
|
|
54
|
-
# Output Area (Log)
|
|
55
|
-
self.output_area = QTextEdit()
|
|
56
|
-
self.output_area.setReadOnly(True)
|
|
57
|
-
self.output_area.setStyleSheet("background-color: #1e1e1e; color: #dcdcdc;")
|
|
58
|
-
self.output_area.setFont(QFont("Consolas", 10))
|
|
59
|
-
layout.addWidget(self.output_area)
|
|
60
|
-
|
|
61
|
-
# Input Area with History
|
|
62
|
-
self.input_line = HistoryLineEdit()
|
|
63
|
-
self.input_line.setPlaceholderText("Enter Python code...")
|
|
64
|
-
self.input_line.setStyleSheet("background-color: #2d2d2d; color: #ffffff; border: 1px solid #3e3e3e;")
|
|
65
|
-
self.input_line.setFont(QFont("Consolas", 10))
|
|
66
|
-
self.input_line.returnPressed.connect(self.run_code)
|
|
67
|
-
layout.addWidget(self.input_line)
|
|
68
|
-
|
|
69
|
-
# Help Label
|
|
70
|
-
help_text = QLabel("Available vars: 'mw' (MainWindow), 'mol' (current_mol), 'Chem' (rdkit.Chem)")
|
|
71
|
-
help_text.setStyleSheet("color: gray; font-size: 10px;")
|
|
72
|
-
layout.addWidget(help_text)
|
|
73
|
-
|
|
74
|
-
self.setLayout(layout)
|
|
75
|
-
|
|
76
|
-
# Initialize execution environment (namespace)
|
|
77
|
-
self.local_scope = {
|
|
78
|
-
'mw': self.main_window,
|
|
79
|
-
'Chem': Chem,
|
|
80
|
-
'mol': self._get_best_mol(),
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
# Initialize Interpreter
|
|
84
|
-
self.interpreter = code.InteractiveInterpreter(self.local_scope)
|
|
85
|
-
|
|
86
|
-
self.append_output("MoleditPy Console Ready.")
|
|
87
|
-
self.append_output(">>> Type commands and press Enter.")
|
|
88
|
-
|
|
89
|
-
def _get_best_mol(self):
|
|
90
|
-
"""Helper to get the most relevant RDKit molecule object.
|
|
91
|
-
"""
|
|
92
|
-
mol = getattr(self.main_window, 'current_mol', None)
|
|
93
|
-
|
|
94
|
-
return mol
|
|
95
|
-
|
|
96
|
-
def append_output(self, text, color=None):
|
|
97
|
-
if color:
|
|
98
|
-
self.output_area.append(f"<span style='color: {color};'>{text}</span>")
|
|
99
|
-
else:
|
|
100
|
-
self.output_area.append(text)
|
|
101
|
-
|
|
102
|
-
def run_code(self):
|
|
103
|
-
command = self.input_line.text()
|
|
104
|
-
if not command:
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
# Handle History
|
|
108
|
-
self.input_line.append_history(command)
|
|
109
|
-
self.input_line.clear()
|
|
110
|
-
|
|
111
|
-
# Display Input
|
|
112
|
-
self.append_output(f">>> {command}", color="#4CAF50")
|
|
113
|
-
|
|
114
|
-
# Sync variables
|
|
115
|
-
self.local_scope['mol'] = self._get_best_mol()
|
|
116
|
-
|
|
117
|
-
if self.local_scope['mol'] is None:
|
|
118
|
-
# Optional: warn user if they try to use 'mol' and it's still None
|
|
119
|
-
# but only if 'mol' appears in command to avoid spam
|
|
120
|
-
if 'mol' in command:
|
|
121
|
-
print("Warning: 'mol' is None (no valid 2D or 3D structure found).")
|
|
122
|
-
|
|
123
|
-
# Capture Output
|
|
124
|
-
stdout_capture = io.StringIO()
|
|
125
|
-
stderr_capture = io.StringIO()
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
|
|
129
|
-
# runsource handles compilation and syntax errors.
|
|
130
|
-
# It returns True if more input is needed (incomplete code), which we can handle or just report.
|
|
131
|
-
more = self.interpreter.runsource(command, "<console>", "single")
|
|
132
|
-
|
|
133
|
-
if more:
|
|
134
|
-
print("(Incomplete input - multiline not fully supported yet)")
|
|
135
|
-
except Exception:
|
|
136
|
-
# This catches errors OUTSIDE runsource's internal handling if any
|
|
137
|
-
traceback.print_exc(file=stderr_capture)
|
|
138
|
-
|
|
139
|
-
# Process Captured Output
|
|
140
|
-
out_str = stdout_capture.getvalue()
|
|
141
|
-
err_str = stderr_capture.getvalue()
|
|
142
|
-
|
|
143
|
-
if out_str:
|
|
144
|
-
self.output_area.append(out_str.strip())
|
|
145
|
-
if err_str:
|
|
146
|
-
self.append_output(err_str.strip(), color="#FF5252")
|
|
147
|
-
|
|
148
|
-
# Refresh UI if needed
|
|
149
|
-
# If the user modified 'mol', we might want to push it back?
|
|
150
|
-
# For now, read-only assumption for simpler integration,
|
|
151
|
-
# but if they modify 'mw.data' directly, we might need a refresh.
|
|
152
|
-
pass
|
|
153
|
-
|
|
154
|
-
# Plugin Entry Point
|
|
155
|
-
def run(mw):
|
|
156
|
-
if not hasattr(mw, 'python_console_dialog'):
|
|
157
|
-
mw.python_console_dialog = PythonConsoleDialog(mw)
|
|
158
|
-
|
|
159
|
-
mw.python_console_dialog.show()
|
|
160
|
-
mw.python_console_dialog.raise_()
|
|
161
|
-
mw.python_console_dialog.activateWindow()
|
|
162
|
-
|
|
163
|
-
# initialize removed as it only registered the menu action
|