MoleditPy-linux 2.4.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.
Files changed (59) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from PyQt6.QtWidgets import (
14
+ QDialog, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QMessageBox
15
+ )
16
+ from PyQt6.QtCore import Qt
17
+ import numpy as np
18
+
19
+ try:
20
+ from .dialog3_d_picking_mixin import Dialog3DPickingMixin
21
+ except Exception:
22
+ from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
23
+
24
+ class PlanarizeDialog(Dialog3DPickingMixin, QDialog):
25
+
26
+ """選択原子群を最適フィット平面へ投影して planarize するダイアログ
27
+ AlignPlane を参考にした選択UIを持ち、Apply ボタンで選択原子を平面へ直交射影する。
28
+ """
29
+ def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
30
+ QDialog.__init__(self, parent)
31
+ Dialog3DPickingMixin.__init__(self)
32
+ self.mol = mol
33
+ self.main_window = main_window
34
+ self.selected_atoms = set()
35
+
36
+ if preselected_atoms:
37
+ # 事前選択された原子を追加
38
+ self.selected_atoms.update(preselected_atoms)
39
+
40
+ self.init_ui()
41
+
42
+ if self.selected_atoms:
43
+ self.show_atom_labels()
44
+ self.update_display()
45
+
46
+ def init_ui(self):
47
+ self.setWindowTitle("Planarize")
48
+ self.setModal(False)
49
+ layout = QVBoxLayout(self)
50
+
51
+ instruction_label = QLabel("Click atoms in the 3D view to select them for planarization (minimum 3 required).")
52
+ instruction_label.setWordWrap(True)
53
+ layout.addWidget(instruction_label)
54
+
55
+ self.selection_label = QLabel("No atoms selected")
56
+ layout.addWidget(self.selection_label)
57
+
58
+
59
+ button_layout = QHBoxLayout()
60
+ self.clear_button = QPushButton("Clear Selection")
61
+ self.clear_button.clicked.connect(self.clear_selection)
62
+ button_layout.addWidget(self.clear_button)
63
+
64
+ # Select All Atoms ボタンを追加
65
+ self.select_all_button = QPushButton("Select All Atoms")
66
+ self.select_all_button.setToolTip("Select all atoms in the molecule for planarization")
67
+ self.select_all_button.clicked.connect(self.select_all_atoms)
68
+ button_layout.addWidget(self.select_all_button)
69
+
70
+ self.apply_button = QPushButton("Apply planarize")
71
+ self.apply_button.clicked.connect(self.apply_planarize)
72
+ self.apply_button.setEnabled(False)
73
+ button_layout.addWidget(self.apply_button)
74
+
75
+ close_button = QPushButton("Close")
76
+ close_button.clicked.connect(self.reject)
77
+ button_layout.addWidget(close_button)
78
+
79
+ button_layout.addStretch()
80
+
81
+ layout.addLayout(button_layout)
82
+
83
+ # enable picking
84
+ self.picker_connection = None
85
+ self.enable_picking()
86
+
87
+ def on_atom_picked(self, atom_idx):
88
+ if atom_idx in self.selected_atoms:
89
+ self.selected_atoms.remove(atom_idx)
90
+ else:
91
+ self.selected_atoms.add(atom_idx)
92
+ self.show_atom_labels()
93
+ self.update_display()
94
+
95
+ def clear_selection(self):
96
+ self.selected_atoms.clear()
97
+ self.clear_atom_labels()
98
+ self.update_display()
99
+
100
+ def update_display(self):
101
+ count = len(self.selected_atoms)
102
+ if count == 0:
103
+ self.selection_label.setText("Click atoms to select for planarize (minimum 3 required)")
104
+ self.apply_button.setEnabled(False)
105
+ else:
106
+ atom_list = sorted(self.selected_atoms)
107
+ atom_display = []
108
+ for i, atom_idx in enumerate(atom_list):
109
+ symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
110
+ atom_display.append(f"#{i+1}: {symbol}({atom_idx})")
111
+ self.selection_label.setText(f"Selected {count} atoms: {', '.join(atom_display)}")
112
+ self.apply_button.setEnabled(count >= 3)
113
+
114
+ def select_all_atoms(self):
115
+ """Select all atoms in the current molecule (or fallback) and update labels/UI."""
116
+ try:
117
+ # Prefer RDKit molecule if available
118
+ if hasattr(self, 'mol') and self.mol is not None:
119
+ try:
120
+ n = self.mol.GetNumAtoms()
121
+ # create a set of indices [0..n-1]
122
+ self.selected_atoms = set(range(n))
123
+ except Exception:
124
+ # fallback to main_window data map
125
+ self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
126
+ else:
127
+ # fallback to main_window data map
128
+ self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
129
+
130
+ # Update labels and display
131
+ self.show_atom_labels()
132
+ self.update_display()
133
+
134
+ except Exception as e:
135
+ QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
136
+
137
+ def show_atom_labels(self):
138
+ self.clear_atom_labels()
139
+ if not hasattr(self, 'selection_labels'):
140
+ self.selection_labels = []
141
+ if self.selected_atoms:
142
+ for i, atom_idx in enumerate(sorted(self.selected_atoms)):
143
+ pos = self.main_window.atom_positions_3d[atom_idx]
144
+ label_text = f"#{i+1}"
145
+ label_actor = self.main_window.plotter.add_point_labels(
146
+ [pos], [label_text],
147
+ point_size=20,
148
+ font_size=12,
149
+ text_color='cyan',
150
+ always_visible=True
151
+ )
152
+ self.selection_labels.append(label_actor)
153
+
154
+ def clear_atom_labels(self):
155
+ if hasattr(self, 'selection_labels'):
156
+ for label_actor in self.selection_labels:
157
+ try:
158
+ self.main_window.plotter.remove_actor(label_actor)
159
+ except Exception:
160
+ pass
161
+ self.selection_labels = []
162
+
163
+ def apply_planarize(self):
164
+ if not self.selected_atoms or len(self.selected_atoms) < 3:
165
+ QMessageBox.warning(self, "Warning", "Please select at least 3 atoms for planarize.")
166
+ return
167
+
168
+ try:
169
+ selected_indices = list(sorted(self.selected_atoms))
170
+ selected_positions = self.main_window.atom_positions_3d[selected_indices].copy()
171
+
172
+ centroid = np.mean(selected_positions, axis=0)
173
+ centered_positions = selected_positions - centroid
174
+
175
+ # SVDによる最小二乗平面の法線取得
176
+ u, s, vh = np.linalg.svd(centered_positions, full_matrices=False)
177
+ normal = vh[-1]
178
+ norm = np.linalg.norm(normal)
179
+ if norm == 0:
180
+ QMessageBox.warning(self, "Warning", "Cannot determine fit plane (degenerate positions).")
181
+ return
182
+ normal = normal / norm
183
+
184
+ # 各点を重心を通る平面へ直交射影
185
+ projections = centered_positions - np.outer(np.dot(centered_positions, normal), normal)
186
+ new_positions = projections + centroid
187
+
188
+ # 分子座標を更新
189
+ conf = self.mol.GetConformer()
190
+ for i, new_pos in zip(selected_indices, new_positions):
191
+ conf.SetAtomPosition(int(i), new_pos.tolist())
192
+ self.main_window.atom_positions_3d[int(i)] = new_pos
193
+
194
+ # 3Dビュー更新
195
+ self.main_window.draw_molecule_3d(self.mol)
196
+ self.main_window.update_chiral_labels()
197
+ self.main_window.push_undo_state()
198
+
199
+ QMessageBox.information(self, "Success", f"Planarized {len(selected_indices)} atoms to best-fit plane.")
200
+
201
+ except Exception as e:
202
+ QMessageBox.critical(self, "Error", f"Failed to planarize: {e}")
203
+
204
+ def closeEvent(self, event):
205
+ """ダイアログが閉じられる時の処理"""
206
+ self.clear_atom_labels()
207
+ self.disable_picking()
208
+ super().closeEvent(event)
209
+
210
+ def reject(self):
211
+ """キャンセル時の処理"""
212
+ self.clear_atom_labels()
213
+ self.disable_picking()
214
+ super().reject()
215
+
216
+ def accept(self):
217
+ """OK時の処理"""
218
+ self.clear_atom_labels()
219
+ self.disable_picking()
220
+ super().accept()
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from typing import Callable, Optional, Any
14
+
15
+ class PluginContext:
16
+ """
17
+ PluginContext provides a safe interface for plugins to interact with the application.
18
+ It is passed to the `initialize(context)` function of the plugin.
19
+ """
20
+ def __init__(self, manager, plugin_name: str):
21
+ self._manager = manager
22
+ self._plugin_name = plugin_name
23
+
24
+ def add_menu_action(self, path: str, callback: Callable, text: Optional[str] = None, icon: Optional[str] = None, shortcut: Optional[str] = None):
25
+ """
26
+ Register a menu action.
27
+
28
+ Args:
29
+ path: Menu path, e.g., "File/Import", "Edit", or "MyPlugin" (top level).
30
+ callback: Function to call when triggered.
31
+ text: Label for the action (defaults to last part of path if None).
32
+ icon: Path to icon or icon name (optional).
33
+ shortcut: Keyboard shortcut (optional).
34
+ """
35
+ self._manager.register_menu_action(self._plugin_name, path, callback, text, icon, shortcut)
36
+
37
+ def add_toolbar_action(self, callback: Callable, text: str, icon: Optional[str] = None, tooltip: Optional[str] = None):
38
+ """
39
+ Register a toolbar action.
40
+ """
41
+ self._manager.register_toolbar_action(self._plugin_name, callback, text, icon, tooltip)
42
+
43
+ def register_drop_handler(self, callback: Callable[[str], bool], priority: int = 0):
44
+ """
45
+ Register a handler for file drops.
46
+
47
+ Args:
48
+ callback: Function taking (file_path) -> bool. Returns True if handled.
49
+ priority: Higher priority handlers are tried first.
50
+ """
51
+ self._manager.register_drop_handler(self._plugin_name, callback, priority)
52
+
53
+
54
+
55
+ def get_3d_controller(self) -> 'Plugin3DController':
56
+ """
57
+ Returns a controller to manipulate the 3D scene (e.g. colors).
58
+ """
59
+ return Plugin3DController(self._manager.get_main_window())
60
+
61
+ def get_main_window(self) -> Any:
62
+ """
63
+ Returns the raw MainWindow instance.
64
+ Use with caution; prefer specific methods when available.
65
+ """
66
+ return self._manager.get_main_window()
67
+
68
+ @property
69
+ def current_molecule(self) -> Any:
70
+ """
71
+ Get or set the current molecule (RDKit Mol object).
72
+ """
73
+ mw = self._manager.get_main_window()
74
+ if mw:
75
+ return mw.current_mol
76
+ return None
77
+
78
+ @current_molecule.setter
79
+ def current_molecule(self, mol: Any):
80
+ mw = self._manager.get_main_window()
81
+ if mw:
82
+ mw.current_mol = mol
83
+ if hasattr(mw, 'draw_molecule_3d'):
84
+ mw.draw_molecule_3d(mol)
85
+
86
+ def add_export_action(self, label: str, callback: Callable):
87
+ """
88
+ Register a custom export action.
89
+
90
+ Args:
91
+ label: Text to display in the Export menu (e.g., "Export as MyFormat...").
92
+ callback: Function to call when triggered.
93
+ """
94
+ self._manager.register_export_action(self._plugin_name, label, callback)
95
+
96
+ def register_optimization_method(self, method_name: str, callback: Callable[[Any], bool]):
97
+ """
98
+ Register a custom 3D optimization method.
99
+
100
+ Args:
101
+ method_name: Name of the method to display in 3D Optimization menu.
102
+ callback: Function taking (rdkit_mol) -> bool (success).
103
+ Modifies the molecule in-place.
104
+ """
105
+ self._manager.register_optimization_method(self._plugin_name, method_name, callback)
106
+
107
+ def register_file_opener(self, extension: str, callback: Callable[[str], None], priority: int = 0):
108
+ """
109
+ Register a handler for opening a specific file extension.
110
+
111
+ Args:
112
+ extension: File extension including dot, e.g. ".xyz".
113
+ callback: Function taking (file_path) -> None.
114
+ Should load the file into the main window.
115
+ priority: Higher priority handlers are tried first (default 0).
116
+ """
117
+ self._manager.register_file_opener(self._plugin_name, extension, callback, priority)
118
+
119
+
120
+
121
+ def add_analysis_tool(self, label: str, callback: Callable):
122
+ """
123
+ Register a tool in the Analysis menu.
124
+
125
+ Args:
126
+ label: Text to display in the menu.
127
+ callback: Function to contact when triggered.
128
+ """
129
+ self._manager.register_analysis_tool(self._plugin_name, label, callback)
130
+
131
+ def register_save_handler(self, callback: Callable[[], dict]):
132
+ """
133
+ Register a callback to save state into the project file.
134
+
135
+ Args:
136
+ callback: Function returning a dict of serializable data.
137
+ """
138
+ self._manager.register_save_handler(self._plugin_name, callback)
139
+
140
+ def register_load_handler(self, callback: Callable[[dict], None]):
141
+ """
142
+ Register a callback to restore state from the project file.
143
+
144
+ Args:
145
+ callback: Function receiving the dict of saved data.
146
+ """
147
+ def register_load_handler(self, callback: Callable[[dict], None]):
148
+ """
149
+ Register a callback to restore state from the project file.
150
+
151
+ Args:
152
+ callback: Function receiving the dict of saved data.
153
+ """
154
+ self._manager.register_load_handler(self._plugin_name, callback)
155
+
156
+ def register_3d_context_menu(self, callback: Callable, label: str):
157
+ """Deprecated: This method does nothing. Kept for backward compatibility."""
158
+ print(f"Warning: Plugin '{self._plugin_name}' uses deprecated 'register_3d_context_menu'. This API has been removed.")
159
+
160
+ def register_3d_style(self, style_name: str, callback: Callable[[Any, Any], None]):
161
+ """
162
+ Register a custom 3D rendering style.
163
+
164
+ Args:
165
+ style_name: Name of the style (must be unique).
166
+ callback: Function taking (main_window, mol) -> None.
167
+ Should fully handle drawing the molecule in the 3D view.
168
+ """
169
+ self._manager.register_3d_style(self._plugin_name, style_name, callback)
170
+
171
+ def register_document_reset_handler(self, callback: Callable[[], None]):
172
+ """
173
+ Register a callback to be called when a new document is created (File→New).
174
+
175
+ Args:
176
+ callback: Function with no arguments that resets plugin state.
177
+ """
178
+ self._manager.register_document_reset_handler(self._plugin_name, callback)
179
+
180
+
181
+
182
+
183
+
184
+
185
+
186
+ class Plugin3DController:
187
+ """Helper to manipulate the 3D scene."""
188
+ def __init__(self, main_window):
189
+ self._mw = main_window
190
+
191
+ def set_atom_color(self, atom_index: int, color_hex: str):
192
+ """
193
+ Set the color of a specific atom in the 3D view.
194
+ Args:
195
+ atom_index: RDKit atom index.
196
+ color_hex: Hex string e.g., "#FF0000".
197
+ """
198
+ # This will need to hook into the actual 3D view logic
199
+ if hasattr(self._mw, 'main_window_view_3d'):
200
+ # Logic to update color map and trigger redraw
201
+ # For now we can assume we might need to expose a method in view_3d
202
+ self._mw.main_window_view_3d.update_atom_color_override(atom_index, color_hex)
203
+ self._mw.plotter.render()
204
+
205
+ def set_bond_color(self, bond_index: int, color_hex: str):
206
+ """
207
+ Set the color of a specific bond in the 3D view.
208
+
209
+ Args:
210
+ bond_index: RDKit bond index.
211
+ color_hex: Hex string e.g., "#00FF00".
212
+ """
213
+ if hasattr(self._mw, 'main_window_view_3d'):
214
+ self._mw.main_window_view_3d.update_bond_color_override(bond_index, color_hex)
215
+ self._mw.plotter.render()