MoleditPy 1.18.0__py3-none-any.whl → 2.0.0__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/align_plane_dialog.py +1 -1
- moleditpy/modules/alignment_dialog.py +1 -1
- moleditpy/modules/angle_dialog.py +2 -2
- moleditpy/modules/bond_length_dialog.py +2 -2
- moleditpy/modules/constants.py +1 -1
- moleditpy/modules/constrained_optimization_dialog.py +2 -2
- moleditpy/modules/dihedral_dialog.py +1 -1
- moleditpy/modules/main_window.py +4 -0
- moleditpy/modules/main_window_app_state.py +1 -1
- moleditpy/modules/main_window_edit_3d.py +7 -7
- moleditpy/modules/main_window_export.py +106 -49
- moleditpy/modules/main_window_main_init.py +68 -4
- moleditpy/modules/main_window_molecular_parsers.py +4 -3
- moleditpy/modules/main_window_project_io.py +2 -2
- moleditpy/modules/main_window_view_3d.py +13 -12
- moleditpy/modules/main_window_view_loaders.py +1 -1
- moleditpy/modules/molecule_scene.py +41 -13
- moleditpy/modules/planarize_dialog.py +1 -1
- moleditpy/modules/plugin_manager.py +85 -0
- moleditpy/modules/settings_dialog.py +5 -23
- moleditpy/modules/user_template_dialog.py +9 -8
- {moleditpy-1.18.0.dist-info → moleditpy-2.0.0.dist-info}/METADATA +3 -2
- {moleditpy-1.18.0.dist-info → moleditpy-2.0.0.dist-info}/RECORD +27 -26
- {moleditpy-1.18.0.dist-info → moleditpy-2.0.0.dist-info}/WHEEL +0 -0
- {moleditpy-1.18.0.dist-info → moleditpy-2.0.0.dist-info}/entry_points.txt +0 -0
- {moleditpy-1.18.0.dist-info → moleditpy-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-1.18.0.dist-info → moleditpy-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -19,6 +19,7 @@ MainWindow (main_window.py) から分離されたモジュール
|
|
|
19
19
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
import vtk
|
|
22
|
+
import logging
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
@@ -58,7 +59,7 @@ if OBABEL_AVAILABLE:
|
|
|
58
59
|
# If import fails here, disable OBABEL locally; avoid raising
|
|
59
60
|
pybel = None
|
|
60
61
|
OBABEL_AVAILABLE = False
|
|
61
|
-
|
|
62
|
+
logging.warning("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
|
|
62
63
|
else:
|
|
63
64
|
pybel = None
|
|
64
65
|
|
|
@@ -201,7 +202,7 @@ class MainWindowView3d(object):
|
|
|
201
202
|
resolution = self.settings.get('wireframe_resolution', 6)
|
|
202
203
|
rad = np.array([0.01 for s in sym]) # 極小値(使用されない)
|
|
203
204
|
elif self.current_3d_style == 'stick':
|
|
204
|
-
atom_radius = self.settings.get('
|
|
205
|
+
atom_radius = self.settings.get('stick_bond_radius', 0.15) # Use bond radius for atoms
|
|
205
206
|
resolution = self.settings.get('stick_resolution', 16)
|
|
206
207
|
rad = np.array([atom_radius for s in sym])
|
|
207
208
|
else: # ball_and_stick
|
|
@@ -278,7 +279,7 @@ class MainWindowView3d(object):
|
|
|
278
279
|
|
|
279
280
|
# 結合描画と同じ計算
|
|
280
281
|
sphere_radius = cyl_radius * radius_factor
|
|
281
|
-
except:
|
|
282
|
+
except Exception:
|
|
282
283
|
sphere_radius = 0.09 # デフォルト値
|
|
283
284
|
offset_distance = 0.15 # デフォルト値
|
|
284
285
|
|
|
@@ -437,7 +438,7 @@ class MainWindowView3d(object):
|
|
|
437
438
|
double_radius_factor = self.settings.get('ball_stick_double_bond_radius_factor', 0.8)
|
|
438
439
|
triple_radius_factor = self.settings.get('ball_stick_triple_bond_radius_factor', 0.75)
|
|
439
440
|
elif self.current_3d_style == 'wireframe':
|
|
440
|
-
double_radius_factor = self.settings.get('wireframe_double_bond_radius_factor',
|
|
441
|
+
double_radius_factor = self.settings.get('wireframe_double_bond_radius_factor', 0.8)
|
|
441
442
|
triple_radius_factor = self.settings.get('wireframe_triple_bond_radius_factor', 0.75)
|
|
442
443
|
elif self.current_3d_style == 'stick':
|
|
443
444
|
double_radius_factor = self.settings.get('stick_double_bond_radius_factor', 0.60)
|
|
@@ -660,7 +661,7 @@ class MainWindowView3d(object):
|
|
|
660
661
|
self.plotter.add_mesh(circle_line, color=torus_color, **mesh_props)
|
|
661
662
|
|
|
662
663
|
except Exception as e:
|
|
663
|
-
|
|
664
|
+
logging.error(f"Error rendering aromatic circles: {e}")
|
|
664
665
|
|
|
665
666
|
if getattr(self, 'show_chiral_labels', False):
|
|
666
667
|
try:
|
|
@@ -820,7 +821,7 @@ class MainWindowView3d(object):
|
|
|
820
821
|
try:
|
|
821
822
|
# 既存のE/Zラベルを削除
|
|
822
823
|
self.plotter.remove_actor('ez_labels')
|
|
823
|
-
except:
|
|
824
|
+
except Exception:
|
|
824
825
|
pass
|
|
825
826
|
|
|
826
827
|
pts, labels = [], []
|
|
@@ -837,7 +838,7 @@ class MainWindowView3d(object):
|
|
|
837
838
|
# 3D座標からステレオ化学を再計算 (molに対して行う)
|
|
838
839
|
# これにより、2Dでの描画状態に関わらず、現在の3D座標に基づいたE/Z判定が行われる
|
|
839
840
|
Chem.AssignStereochemistry(mol, cleanIt=True, force=True, flagPossibleStereoCenters=True)
|
|
840
|
-
except:
|
|
841
|
+
except Exception:
|
|
841
842
|
pass
|
|
842
843
|
|
|
843
844
|
for bond in mol.GetBonds():
|
|
@@ -1171,7 +1172,7 @@ class MainWindowView3d(object):
|
|
|
1171
1172
|
for nm in self.atom_label_legend_names:
|
|
1172
1173
|
try:
|
|
1173
1174
|
self.plotter.remove_actor(nm)
|
|
1174
|
-
except:
|
|
1175
|
+
except Exception:
|
|
1175
1176
|
pass
|
|
1176
1177
|
self.atom_label_legend_names = []
|
|
1177
1178
|
|
|
@@ -1237,12 +1238,12 @@ class MainWindowView3d(object):
|
|
|
1237
1238
|
for a in list(self.current_atom_info_labels):
|
|
1238
1239
|
try:
|
|
1239
1240
|
self.plotter.remove_actor(a)
|
|
1240
|
-
except:
|
|
1241
|
+
except Exception:
|
|
1241
1242
|
pass
|
|
1242
1243
|
else:
|
|
1243
1244
|
try:
|
|
1244
1245
|
self.plotter.remove_actor(self.current_atom_info_labels)
|
|
1245
|
-
except:
|
|
1246
|
+
except Exception:
|
|
1246
1247
|
pass
|
|
1247
1248
|
except Exception:
|
|
1248
1249
|
pass
|
|
@@ -1255,7 +1256,7 @@ class MainWindowView3d(object):
|
|
|
1255
1256
|
for nm in list(self.atom_label_legend_names):
|
|
1256
1257
|
try:
|
|
1257
1258
|
self.plotter.remove_actor(nm)
|
|
1258
|
-
except:
|
|
1259
|
+
except Exception:
|
|
1259
1260
|
pass
|
|
1260
1261
|
except Exception:
|
|
1261
1262
|
pass
|
|
@@ -1401,7 +1402,7 @@ class MainWindowView3d(object):
|
|
|
1401
1402
|
if renderer and hasattr(renderer, 'SetNumberOfLayers'):
|
|
1402
1403
|
try:
|
|
1403
1404
|
renderer.SetNumberOfLayers(2) # レイヤー0:3Dオブジェクト、レイヤー1:2Dオーバーレイ
|
|
1404
|
-
except:
|
|
1405
|
+
except Exception:
|
|
1405
1406
|
pass # PyVistaのバージョンによってはサポートされていない場合がある
|
|
1406
1407
|
|
|
1407
1408
|
# --- 3D軸ウィジェットの設定 ---
|
|
@@ -317,7 +317,7 @@ class MainWindowViewLoaders(object):
|
|
|
317
317
|
# Keep mol as-is (may lack conformer); downstream code checks for conformers
|
|
318
318
|
else:
|
|
319
319
|
raise
|
|
320
|
-
except:
|
|
320
|
+
except Exception:
|
|
321
321
|
self.statusBar().showMessage("Failed to generate 3D coordinates")
|
|
322
322
|
return
|
|
323
323
|
|
|
@@ -11,6 +11,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import traceback
|
|
14
|
+
import logging
|
|
14
15
|
|
|
15
16
|
from PyQt6.QtWidgets import (
|
|
16
17
|
QApplication, QGraphicsScene, QGraphicsItem,
|
|
@@ -114,6 +115,15 @@ class MoleculeScene(QGraphicsScene):
|
|
|
114
115
|
}
|
|
115
116
|
self.reinitialize_items()
|
|
116
117
|
|
|
118
|
+
|
|
119
|
+
def update_all_items(self):
|
|
120
|
+
"""全てのアイテムを強制的に再描画する"""
|
|
121
|
+
for item in self.items():
|
|
122
|
+
if isinstance(item, (AtomItem, BondItem)):
|
|
123
|
+
item.update()
|
|
124
|
+
if self.views():
|
|
125
|
+
self.views()[0].viewport().update()
|
|
126
|
+
|
|
117
127
|
def reinitialize_items(self):
|
|
118
128
|
self.template_preview = TemplatePreviewItem(); self.addItem(self.template_preview)
|
|
119
129
|
self.template_preview.hide(); self.template_preview_points = []; self.template_context = {}
|
|
@@ -184,6 +194,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
184
194
|
# Delete the entire rectangular selection
|
|
185
195
|
data_changed = self.delete_items(set(selected_items))
|
|
186
196
|
if data_changed:
|
|
197
|
+
self.update_all_items()
|
|
187
198
|
self.window.push_undo_state()
|
|
188
199
|
self.press_pos = None
|
|
189
200
|
event.accept()
|
|
@@ -203,11 +214,10 @@ class MoleculeScene(QGraphicsScene):
|
|
|
203
214
|
self.window.push_undo_state()
|
|
204
215
|
data_changed = False # ここでundo済みなので以降で積まない
|
|
205
216
|
except Exception as e:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
traceback.print_exc()
|
|
217
|
+
logging.error(f"Error clearing E/Z label: {e}", exc_info=True)
|
|
209
218
|
if hasattr(self.window, 'statusBar'):
|
|
210
219
|
self.window.statusBar().showMessage(f"Error clearing E/Z label: {e}", 5000)
|
|
220
|
+
self.update_all_items() # エラー時も整合性維持のため再描画
|
|
211
221
|
# AtomItemは何もしない
|
|
212
222
|
# --- 通常の処理 ---
|
|
213
223
|
elif isinstance(item, AtomItem):
|
|
@@ -234,6 +244,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
234
244
|
data_changed = self.delete_items({item})
|
|
235
245
|
|
|
236
246
|
if data_changed:
|
|
247
|
+
self.update_all_items()
|
|
237
248
|
self.window.push_undo_state()
|
|
238
249
|
self.press_pos = None
|
|
239
250
|
event.accept()
|
|
@@ -343,7 +354,9 @@ class MoleculeScene(QGraphicsScene):
|
|
|
343
354
|
self.data_changed_in_event = True
|
|
344
355
|
# イベント処理をここで完了させ、下のアイテムが選択されるのを防ぐ
|
|
345
356
|
self.start_atom=None; self.start_pos = None; self.press_pos = None
|
|
346
|
-
if self.data_changed_in_event:
|
|
357
|
+
if self.data_changed_in_event:
|
|
358
|
+
self.update_all_items()
|
|
359
|
+
self.window.push_undo_state()
|
|
347
360
|
return
|
|
348
361
|
|
|
349
362
|
released_item = self.itemAt(end_pos, self.views()[0].transform())
|
|
@@ -385,13 +398,13 @@ class MoleculeScene(QGraphicsScene):
|
|
|
385
398
|
else: # current_stereo == 4
|
|
386
399
|
new_stereo = 0 # E -> None
|
|
387
400
|
self.update_bond_stereo(b, new_stereo)
|
|
401
|
+
self.update_all_items() # 強制再描画
|
|
388
402
|
self.window.push_undo_state() # ここでUndo stackに積む
|
|
389
403
|
except Exception as e:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
traceback.print_exc()
|
|
404
|
+
logging.error(f"Error in E/Z stereo toggle: {e}", exc_info=True)
|
|
393
405
|
if hasattr(self.window, 'statusBar'):
|
|
394
406
|
self.window.statusBar().showMessage(f"Error changing E/Z stereochemistry: {e}", 5000)
|
|
407
|
+
self.update_all_items() # エラー時も整合性維持のため再描画
|
|
395
408
|
return # この後の処理は行わない
|
|
396
409
|
elif self.bond_stereo != 0 and b.order == self.bond_order and b.stereo == self.bond_stereo:
|
|
397
410
|
# 方向性を反転させる
|
|
@@ -491,6 +504,10 @@ class MoleculeScene(QGraphicsScene):
|
|
|
491
504
|
# 原子移動後に測定ラベルの位置を更新
|
|
492
505
|
self.window.update_2d_measurement_labels()
|
|
493
506
|
if self.views(): self.views()[0].viewport().update()
|
|
507
|
+
|
|
508
|
+
if self.data_changed_in_event:
|
|
509
|
+
self.update_all_items()
|
|
510
|
+
|
|
494
511
|
self.start_atom=None; self.start_pos = None; self.press_pos = None; self.temp_line = None
|
|
495
512
|
self.template_context = {}
|
|
496
513
|
# Clear user template data when switching modes
|
|
@@ -515,6 +532,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
515
532
|
self.data.atoms[item.atom_id]['charge'] = item.charge
|
|
516
533
|
item.update_style()
|
|
517
534
|
|
|
535
|
+
self.update_all_items()
|
|
518
536
|
self.window.push_undo_state()
|
|
519
537
|
|
|
520
538
|
event.accept()
|
|
@@ -610,7 +628,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
610
628
|
def create_bond(self, start_atom, end_atom, bond_order=None, bond_stereo=None):
|
|
611
629
|
try:
|
|
612
630
|
if start_atom is None or end_atom is None:
|
|
613
|
-
|
|
631
|
+
logging.error("Error: Cannot create bond with None atoms")
|
|
614
632
|
return
|
|
615
633
|
|
|
616
634
|
exist_b = self.find_bond_between(start_atom, end_atom)
|
|
@@ -637,9 +655,8 @@ class MoleculeScene(QGraphicsScene):
|
|
|
637
655
|
end_atom.update_style()
|
|
638
656
|
|
|
639
657
|
except Exception as e:
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
traceback.print_exc()
|
|
658
|
+
logging.error(f"Error creating bond: {e}", exc_info=True)
|
|
659
|
+
self.update_all_items() # エラーリカバリー
|
|
643
660
|
|
|
644
661
|
def add_molecule_fragment(self, points, bonds_info, existing_items=None, symbol='C'):
|
|
645
662
|
"""
|
|
@@ -1270,6 +1287,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1270
1287
|
print(f"Error during delete_items operation: {e}")
|
|
1271
1288
|
|
|
1272
1289
|
traceback.print_exc()
|
|
1290
|
+
self.update_all_items() # エラーリカバリー
|
|
1273
1291
|
return False
|
|
1274
1292
|
def purge_deleted_items(self):
|
|
1275
1293
|
"""Purge and release any held deleted-wrapper references.
|
|
@@ -1541,6 +1559,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1541
1559
|
|
|
1542
1560
|
# 計算した情報を使って、その場にフラグメントを追加
|
|
1543
1561
|
self.add_molecule_fragment(points, bonds_info, existing_items=existing_items)
|
|
1562
|
+
self.update_all_items()
|
|
1544
1563
|
self.window.push_undo_state()
|
|
1545
1564
|
|
|
1546
1565
|
# --- 動作2: カーソルが空白領域にある場合 (モード切替) ---
|
|
@@ -1566,6 +1585,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1566
1585
|
atom.radical = (atom.radical + 1) % 3
|
|
1567
1586
|
self.data.atoms[atom.atom_id]['radical'] = atom.radical
|
|
1568
1587
|
atom.update_style()
|
|
1588
|
+
self.update_all_items()
|
|
1569
1589
|
self.window.push_undo_state()
|
|
1570
1590
|
event.accept()
|
|
1571
1591
|
return
|
|
@@ -1586,6 +1606,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1586
1606
|
atom.charge += delta
|
|
1587
1607
|
self.data.atoms[atom.atom_id]['charge'] = atom.charge
|
|
1588
1608
|
atom.update_style()
|
|
1609
|
+
self.update_all_items()
|
|
1589
1610
|
self.window.push_undo_state()
|
|
1590
1611
|
event.accept()
|
|
1591
1612
|
return
|
|
@@ -1615,6 +1636,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1615
1636
|
for atom in atoms_to_update:
|
|
1616
1637
|
atom.update_style()
|
|
1617
1638
|
|
|
1639
|
+
self.update_all_items()
|
|
1618
1640
|
self.window.push_undo_state()
|
|
1619
1641
|
event.accept()
|
|
1620
1642
|
return
|
|
@@ -1668,9 +1690,9 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1668
1690
|
elif key == Qt.Key.Key_1 and (bond.order != 1 or bond.stereo != 0):
|
|
1669
1691
|
bond.order = 1; bond.stereo = 0
|
|
1670
1692
|
elif key == Qt.Key.Key_2 and (bond.order != 2 or bond.stereo != 0):
|
|
1671
|
-
bond.order = 2; bond.stereo = 0
|
|
1693
|
+
bond.order = 2; bond.stereo = 0
|
|
1672
1694
|
elif key == Qt.Key.Key_3 and bond.order != 3:
|
|
1673
|
-
bond.order = 3; bond.stereo = 0
|
|
1695
|
+
bond.order = 3; bond.stereo = 0
|
|
1674
1696
|
|
|
1675
1697
|
# 4. 実際に変更があった場合のみデータモデルを更新
|
|
1676
1698
|
if old_order != bond.order or old_stereo != bond.stereo:
|
|
@@ -1693,6 +1715,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1693
1715
|
bond.update()
|
|
1694
1716
|
|
|
1695
1717
|
if any_bond_changed:
|
|
1718
|
+
self.update_all_items()
|
|
1696
1719
|
self.window.push_undo_state()
|
|
1697
1720
|
|
|
1698
1721
|
if key in [Qt.Key.Key_1, Qt.Key.Key_2, Qt.Key.Key_3, Qt.Key.Key_W, Qt.Key.Key_D]:
|
|
@@ -1702,11 +1725,13 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1702
1725
|
if isinstance(self.hovered_item, BondItem) and self.hovered_item.order == 2:
|
|
1703
1726
|
if event.key() == Qt.Key.Key_Z:
|
|
1704
1727
|
self.update_bond_stereo(self.hovered_item, 3) # Z-isomer
|
|
1728
|
+
self.update_all_items()
|
|
1705
1729
|
self.window.push_undo_state()
|
|
1706
1730
|
event.accept()
|
|
1707
1731
|
return
|
|
1708
1732
|
elif event.key() == Qt.Key.Key_E:
|
|
1709
1733
|
self.update_bond_stereo(self.hovered_item, 4) # E-isomer
|
|
1734
|
+
self.update_all_items()
|
|
1710
1735
|
self.window.push_undo_state()
|
|
1711
1736
|
event.accept()
|
|
1712
1737
|
return
|
|
@@ -1814,6 +1839,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1814
1839
|
self.create_bond(start_atom, new_atom_item, bond_order=target_order, bond_stereo=0)
|
|
1815
1840
|
|
|
1816
1841
|
self.clearSelection()
|
|
1842
|
+
self.update_all_items()
|
|
1817
1843
|
self.window.push_undo_state()
|
|
1818
1844
|
event.accept()
|
|
1819
1845
|
return
|
|
@@ -1844,6 +1870,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1844
1870
|
items_to_process.add(item_at_cursor)
|
|
1845
1871
|
|
|
1846
1872
|
if self.delete_items(items_to_process):
|
|
1873
|
+
self.update_all_items()
|
|
1847
1874
|
self.window.push_undo_state()
|
|
1848
1875
|
self.window.statusBar().showMessage("Deleted selected items.")
|
|
1849
1876
|
|
|
@@ -1978,3 +2005,4 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1978
2005
|
traceback.print_exc()
|
|
1979
2006
|
if hasattr(self.window, 'statusBar'):
|
|
1980
2007
|
self.window.statusBar().showMessage(f"Error updating bond stereochemistry: {e}", 5000)
|
|
2008
|
+
self.update_all_items() # エラーリカバリー
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
plugin_manager.py
|
|
6
|
+
Manages discovery, loading, and execution of external plugins.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import importlib.util
|
|
12
|
+
import traceback
|
|
13
|
+
from PyQt6.QtGui import QDesktopServices
|
|
14
|
+
from PyQt6.QtCore import QUrl
|
|
15
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
16
|
+
|
|
17
|
+
class PluginManager:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.plugin_dir = os.path.join(os.path.expanduser('~'), '.moleditpy', 'plugins')
|
|
20
|
+
self.plugins = [] # List of {"name": str, "module": module_obj}
|
|
21
|
+
|
|
22
|
+
def ensure_plugin_dir(self):
|
|
23
|
+
"""Creates the plugin directory if it creates doesn't exist."""
|
|
24
|
+
if not os.path.exists(self.plugin_dir):
|
|
25
|
+
try:
|
|
26
|
+
os.makedirs(self.plugin_dir)
|
|
27
|
+
except OSError as e:
|
|
28
|
+
print(f"Error creating plugin directory: {e}")
|
|
29
|
+
|
|
30
|
+
def open_plugin_folder(self):
|
|
31
|
+
"""Opens the plugin directory in the OS file explorer."""
|
|
32
|
+
self.ensure_plugin_dir()
|
|
33
|
+
QDesktopServices.openUrl(QUrl.fromLocalFile(self.plugin_dir))
|
|
34
|
+
|
|
35
|
+
def discover_plugins(self, parent=None):
|
|
36
|
+
"""
|
|
37
|
+
Scans the plugin directory for .py files and attempts to import them.
|
|
38
|
+
Returns a list of valid loaded plugins.
|
|
39
|
+
"""
|
|
40
|
+
self.ensure_plugin_dir()
|
|
41
|
+
self.plugins = []
|
|
42
|
+
|
|
43
|
+
if not os.path.exists(self.plugin_dir):
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
for filename in os.listdir(self.plugin_dir):
|
|
47
|
+
if filename.endswith(".py") and not filename.startswith("__"):
|
|
48
|
+
filepath = os.path.join(self.plugin_dir, filename)
|
|
49
|
+
try:
|
|
50
|
+
# Dynamically import the module
|
|
51
|
+
spec = importlib.util.spec_from_file_location(filename[:-3], filepath)
|
|
52
|
+
if spec and spec.loader:
|
|
53
|
+
module = importlib.util.module_from_spec(spec)
|
|
54
|
+
sys.modules[spec.name] = module # helper for relative imports if needed
|
|
55
|
+
spec.loader.exec_module(module)
|
|
56
|
+
|
|
57
|
+
# Check for required attributes
|
|
58
|
+
plugin_name = getattr(module, 'PLUGIN_NAME', filename[:-3])
|
|
59
|
+
|
|
60
|
+
# Validate that it has a run function
|
|
61
|
+
if hasattr(module, 'run') and callable(module.run):
|
|
62
|
+
self.plugins.append({
|
|
63
|
+
'name': plugin_name,
|
|
64
|
+
'module': module
|
|
65
|
+
})
|
|
66
|
+
else:
|
|
67
|
+
print(f"Plugin {filename} skipped: Missing 'run(main_window)' function.")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
# Robust error handling with user notification
|
|
70
|
+
msg = f"Failed to load plugin {filename}:\n{e}"
|
|
71
|
+
print(msg)
|
|
72
|
+
traceback.print_exc()
|
|
73
|
+
if parent:
|
|
74
|
+
QMessageBox.warning(parent, "Plugin Load Error", msg)
|
|
75
|
+
|
|
76
|
+
return self.plugins
|
|
77
|
+
|
|
78
|
+
def run_plugin(self, module, main_window):
|
|
79
|
+
"""Executes the plugin's run method."""
|
|
80
|
+
try:
|
|
81
|
+
module.run(main_window)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
QMessageBox.critical(main_window, "Plugin Error", f"Error running plugin '{getattr(module, 'PLUGIN_NAME', 'Unknown')}':\n{e}")
|
|
84
|
+
traceback.print_exc()
|
|
85
|
+
|
|
@@ -53,7 +53,6 @@ class SettingsDialog(QDialog):
|
|
|
53
53
|
'wireframe_bond_radius': 0.01,
|
|
54
54
|
'wireframe_resolution': 6,
|
|
55
55
|
# Stick model parameters
|
|
56
|
-
'stick_atom_radius': 0.15,
|
|
57
56
|
'stick_bond_radius': 0.15,
|
|
58
57
|
'stick_resolution': 16,
|
|
59
58
|
# Multiple bond offset parameters (per-model)
|
|
@@ -278,8 +277,8 @@ class SettingsDialog(QDialog):
|
|
|
278
277
|
# Aromatic torus thickness factor
|
|
279
278
|
self.aromatic_torus_thickness_slider = QSlider(Qt.Orientation.Horizontal)
|
|
280
279
|
self.aromatic_torus_thickness_slider.setRange(10, 300) # 0.1x to 3.0x
|
|
281
|
-
self.aromatic_torus_thickness_slider.setValue(
|
|
282
|
-
self.aromatic_torus_thickness_label = QLabel("
|
|
280
|
+
self.aromatic_torus_thickness_slider.setValue(60) # Default 0.6x
|
|
281
|
+
self.aromatic_torus_thickness_label = QLabel("0.6")
|
|
283
282
|
self.aromatic_torus_thickness_slider.valueChanged.connect(
|
|
284
283
|
lambda v: self.aromatic_torus_thickness_label.setText(f"{v/100:.1f}")
|
|
285
284
|
)
|
|
@@ -564,17 +563,7 @@ class SettingsDialog(QDialog):
|
|
|
564
563
|
info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
|
|
565
564
|
form_layout.addRow(info_label)
|
|
566
565
|
|
|
567
|
-
#
|
|
568
|
-
self.stick_atom_radius_slider = QSlider(Qt.Orientation.Horizontal)
|
|
569
|
-
self.stick_atom_radius_slider.setRange(5, 50) # 0.05 ~ 0.5
|
|
570
|
-
self.stick_atom_radius_label = QLabel("0.15")
|
|
571
|
-
self.stick_atom_radius_slider.valueChanged.connect(lambda v: self.stick_atom_radius_label.setText(f"{v/100:.2f}"))
|
|
572
|
-
atom_radius_layout = QHBoxLayout()
|
|
573
|
-
atom_radius_layout.addWidget(self.stick_atom_radius_slider)
|
|
574
|
-
atom_radius_layout.addWidget(self.stick_atom_radius_label)
|
|
575
|
-
form_layout.addRow("Atom Radius:", atom_radius_layout)
|
|
576
|
-
|
|
577
|
-
# ボンド半径
|
|
566
|
+
# ボンド半径(原子半径も同じ値を使用)
|
|
578
567
|
self.stick_bond_radius_slider = QSlider(Qt.Orientation.Horizontal)
|
|
579
568
|
self.stick_bond_radius_slider.setRange(5, 50) # 0.05 ~ 0.5
|
|
580
569
|
self.stick_bond_radius_label = QLabel("0.15")
|
|
@@ -673,7 +662,7 @@ class SettingsDialog(QDialog):
|
|
|
673
662
|
'display_kekule_3d': self.default_settings.get('display_kekule_3d', False),
|
|
674
663
|
'always_ask_charge': self.default_settings.get('always_ask_charge', False),
|
|
675
664
|
'display_aromatic_circles_3d': self.default_settings.get('display_aromatic_circles_3d', False),
|
|
676
|
-
'aromatic_torus_thickness_factor': self.default_settings.get('aromatic_torus_thickness_factor',
|
|
665
|
+
'aromatic_torus_thickness_factor': self.default_settings.get('aromatic_torus_thickness_factor', 0.6),
|
|
677
666
|
},
|
|
678
667
|
"Ball & Stick": {
|
|
679
668
|
'ball_stick_atom_scale': self.default_settings['ball_stick_atom_scale'],
|
|
@@ -700,7 +689,6 @@ class SettingsDialog(QDialog):
|
|
|
700
689
|
'wireframe_triple_bond_radius_factor': self.default_settings.get('wireframe_triple_bond_radius_factor', 0.75)
|
|
701
690
|
},
|
|
702
691
|
"Stick": {
|
|
703
|
-
'stick_atom_radius': self.default_settings['stick_atom_radius'],
|
|
704
692
|
'stick_bond_radius': self.default_settings['stick_bond_radius'],
|
|
705
693
|
'stick_resolution': self.default_settings['stick_resolution'],
|
|
706
694
|
'stick_double_bond_offset_factor': self.default_settings.get('stick_double_bond_offset_factor', 1.5),
|
|
@@ -844,7 +832,6 @@ class SettingsDialog(QDialog):
|
|
|
844
832
|
'wireframe_bond_radius': self.wf_bond_radius_slider.value() / 100.0,
|
|
845
833
|
'wireframe_resolution': self.wf_resolution_slider.value(),
|
|
846
834
|
# Stick settings
|
|
847
|
-
'stick_atom_radius': self.stick_atom_radius_slider.value() / 100.0,
|
|
848
835
|
'stick_bond_radius': self.stick_bond_radius_slider.value() / 100.0,
|
|
849
836
|
'stick_resolution': self.stick_resolution_slider.value(),
|
|
850
837
|
# Multi-bond settings (per-model)
|
|
@@ -944,10 +931,6 @@ class SettingsDialog(QDialog):
|
|
|
944
931
|
self.wf_resolution_label.setText(str(settings_dict.get('wireframe_resolution', self.default_settings['wireframe_resolution'])))
|
|
945
932
|
|
|
946
933
|
# Stick設定
|
|
947
|
-
stick_atom_radius = int(settings_dict.get('stick_atom_radius', self.default_settings['stick_atom_radius']) * 100)
|
|
948
|
-
self.stick_atom_radius_slider.setValue(stick_atom_radius)
|
|
949
|
-
self.stick_atom_radius_label.setText(f"{stick_atom_radius/100:.2f}")
|
|
950
|
-
|
|
951
934
|
stick_bond_radius = int(settings_dict.get('stick_bond_radius', self.default_settings['stick_bond_radius']) * 100)
|
|
952
935
|
self.stick_bond_radius_slider.setValue(stick_bond_radius)
|
|
953
936
|
self.stick_bond_radius_label.setText(f"{stick_bond_radius/100:.2f}")
|
|
@@ -1019,7 +1002,7 @@ class SettingsDialog(QDialog):
|
|
|
1019
1002
|
self.always_ask_charge_checkbox.setChecked(settings_dict.get('always_ask_charge', self.default_settings.get('always_ask_charge', False)))
|
|
1020
1003
|
# Aromatic ring circle display and torus thickness factor
|
|
1021
1004
|
self.aromatic_circle_checkbox.setChecked(settings_dict.get('display_aromatic_circles_3d', self.default_settings.get('display_aromatic_circles_3d', False)))
|
|
1022
|
-
thickness_factor = float(settings_dict.get('aromatic_torus_thickness_factor', self.default_settings.get('aromatic_torus_thickness_factor',
|
|
1005
|
+
thickness_factor = float(settings_dict.get('aromatic_torus_thickness_factor', self.default_settings.get('aromatic_torus_thickness_factor', 0.6)))
|
|
1023
1006
|
try:
|
|
1024
1007
|
self.aromatic_torus_thickness_slider.setValue(int(thickness_factor * 100))
|
|
1025
1008
|
self.aromatic_torus_thickness_label.setText(f"{thickness_factor:.1f}")
|
|
@@ -1060,7 +1043,6 @@ class SettingsDialog(QDialog):
|
|
|
1060
1043
|
'wireframe_bond_radius': self.wf_bond_radius_slider.value() / 100.0,
|
|
1061
1044
|
'wireframe_resolution': self.wf_resolution_slider.value(),
|
|
1062
1045
|
# Stick settings
|
|
1063
|
-
'stick_atom_radius': self.stick_atom_radius_slider.value() / 100.0,
|
|
1064
1046
|
'stick_bond_radius': self.stick_bond_radius_slider.value() / 100.0,
|
|
1065
1047
|
'stick_resolution': self.stick_resolution_slider.value(),
|
|
1066
1048
|
# Multiple bond offset settings (per-model)
|
|
@@ -23,6 +23,7 @@ except Exception:
|
|
|
23
23
|
from modules.constants import VERSION, CPK_COLORS
|
|
24
24
|
import os
|
|
25
25
|
import json
|
|
26
|
+
import logging
|
|
26
27
|
|
|
27
28
|
class UserTemplateDialog(QDialog):
|
|
28
29
|
"""ユーザーテンプレート管理ダイアログ"""
|
|
@@ -106,7 +107,7 @@ class UserTemplateDialog(QDialog):
|
|
|
106
107
|
elif hasattr(child, 'refit_view'):
|
|
107
108
|
child.refit_view()
|
|
108
109
|
except Exception as e:
|
|
109
|
-
|
|
110
|
+
logging.warning(f"Warning: Failed to refit template previews: {e}")
|
|
110
111
|
|
|
111
112
|
def showEvent(self, event):
|
|
112
113
|
"""ダイアログ表示時にプレビューを適切にフィット"""
|
|
@@ -136,7 +137,7 @@ class UserTemplateDialog(QDialog):
|
|
|
136
137
|
template_data['filepath'] = filepath
|
|
137
138
|
self.user_templates.append(template_data)
|
|
138
139
|
except Exception as e:
|
|
139
|
-
|
|
140
|
+
logging.error(f"Error loading user templates: {e}")
|
|
140
141
|
|
|
141
142
|
self.update_template_grid()
|
|
142
143
|
|
|
@@ -146,7 +147,7 @@ class UserTemplateDialog(QDialog):
|
|
|
146
147
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
147
148
|
return json.load(f)
|
|
148
149
|
except Exception as e:
|
|
149
|
-
|
|
150
|
+
logging.error(f"Error loading template file {filepath}: {e}")
|
|
150
151
|
return None
|
|
151
152
|
|
|
152
153
|
def save_template_file(self, filepath, template_data):
|
|
@@ -156,7 +157,7 @@ class UserTemplateDialog(QDialog):
|
|
|
156
157
|
json.dump(template_data, f, indent=2, ensure_ascii=False)
|
|
157
158
|
return True
|
|
158
159
|
except Exception as e:
|
|
159
|
-
|
|
160
|
+
logging.error(f"Error saving template file {filepath}: {e}")
|
|
160
161
|
return False
|
|
161
162
|
|
|
162
163
|
def update_template_grid(self):
|
|
@@ -253,7 +254,7 @@ class UserTemplateDialog(QDialog):
|
|
|
253
254
|
if view and not rect.isEmpty():
|
|
254
255
|
view.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
|
|
255
256
|
except Exception as e:
|
|
256
|
-
|
|
257
|
+
logging.warning(f"Warning: Failed to fit preview view: {e}")
|
|
257
258
|
|
|
258
259
|
def draw_template_preview(self, scene, template_data, view_size=None):
|
|
259
260
|
"""テンプレートプレビューを描画 - fitInView縮小率に基づく動的スケーリング"""
|
|
@@ -303,7 +304,7 @@ class UserTemplateDialog(QDialog):
|
|
|
303
304
|
scale_factor = 4.0
|
|
304
305
|
|
|
305
306
|
# Debug info (can be removed in production)
|
|
306
|
-
#
|
|
307
|
+
# logging.debug(f"Mol size: {mol_size:.1f}, Fit scale: {fit_scale:.3f}, Scale factor: {scale_factor:.2f}")
|
|
307
308
|
else:
|
|
308
309
|
scale_factor = 1.0
|
|
309
310
|
|
|
@@ -481,7 +482,7 @@ class UserTemplateDialog(QDialog):
|
|
|
481
482
|
except Exception:
|
|
482
483
|
pass
|
|
483
484
|
except Exception as e:
|
|
484
|
-
|
|
485
|
+
logging.warning(f"Warning: Failed to switch main window to template mode: {e}")
|
|
485
486
|
|
|
486
487
|
def use_template(self, template_data):
|
|
487
488
|
"""テンプレートを使用(エディタに適用)"""
|
|
@@ -518,7 +519,7 @@ class UserTemplateDialog(QDialog):
|
|
|
518
519
|
# Mark selected and keep dialog open
|
|
519
520
|
self.selected_template = template_data
|
|
520
521
|
except Exception as e:
|
|
521
|
-
|
|
522
|
+
logging.warning(f"Warning: Failed to switch main window to template mode: {e}")
|
|
522
523
|
|
|
523
524
|
# Don't close dialog - keep it open for easy template switching
|
|
524
525
|
# self.accept()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
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
|
|
@@ -684,7 +684,6 @@ Classifier: Programming Language :: Python :: 3
|
|
|
684
684
|
Classifier: Operating System :: OS Independent
|
|
685
685
|
Classifier: Intended Audience :: Science/Research
|
|
686
686
|
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
687
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
688
687
|
Classifier: Programming Language :: Python :: 3.9
|
|
689
688
|
Classifier: Programming Language :: Python :: 3.10
|
|
690
689
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -760,6 +759,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
|
|
|
760
759
|
* Import structures from **MOL/SDF** files or **SMILES** strings.
|
|
761
760
|
* Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
|
|
762
761
|
* Export 2D and 3D views as high-resolution PNG images.
|
|
762
|
+
* **Plugin System:** Extend functionality with Python scripts. Place custom scripts in `~/.moleditpy/plugins` to add new features to the "Plugin" menu.
|
|
763
763
|
|
|
764
764
|
## Installation and Execution
|
|
765
765
|
|
|
@@ -874,6 +874,7 @@ This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**.
|
|
|
874
874
|
* **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
|
|
875
875
|
* 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
|
|
876
876
|
* 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
|
|
877
|
+
* **プラグインシステム:** Pythonスクリプトで機能を拡張できます。`~/.moleditpy/plugins` にスクリプトを配置することで、「Plugin」メニューに独自の機能を追加できます。
|
|
877
878
|
|
|
878
879
|
## インストールと実行
|
|
879
880
|
|