MoleditPy 2.2.0a2__py3-none-any.whl → 2.2.0a3__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.
Files changed (27) hide show
  1. moleditpy/modules/constants.py +1 -1
  2. moleditpy/modules/main_window_main_init.py +31 -13
  3. moleditpy/modules/plugin_interface.py +1 -10
  4. moleditpy/modules/plugin_manager.py +0 -3
  5. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/METADATA +1 -1
  6. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/RECORD +10 -27
  7. moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -919
  8. moleditpy/plugins/File/animated_xyz_giffer.py +0 -583
  9. moleditpy/plugins/File/cube_viewer.py +0 -689
  10. moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -1148
  11. moleditpy/plugins/File/mapped_cube_viewer.py +0 -552
  12. moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -1226
  13. moleditpy/plugins/File/paste_xyz.py +0 -336
  14. moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -930
  15. moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -1028
  16. moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +0 -286
  17. moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -65
  18. moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -268
  19. moleditpy/plugins/Optimization/conf_search.py +0 -224
  20. moleditpy/plugins/Utility/atom_colorizer.py +0 -262
  21. moleditpy/plugins/Utility/console.py +0 -163
  22. moleditpy/plugins/Utility/pubchem_ressolver.py +0 -244
  23. moleditpy/plugins/Utility/vdw_radii_overlay.py +0 -432
  24. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/WHEEL +0 -0
  25. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/entry_points.txt +0 -0
  26. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/licenses/LICENSE +0 -0
  27. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.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