MoleditPy-linux 2.2.4__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 (58) 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 +293 -0
  7. moleditpy_linux/modules/alignment_dialog.py +273 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/icon.icns +0 -0
  11. moleditpy_linux/modules/assets/icon.ico +0 -0
  12. moleditpy_linux/modules/assets/icon.png +0 -0
  13. moleditpy_linux/modules/atom_item.py +348 -0
  14. moleditpy_linux/modules/bond_item.py +406 -0
  15. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  16. moleditpy_linux/modules/calculation_worker.py +766 -0
  17. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  18. moleditpy_linux/modules/constants.py +88 -0
  19. moleditpy_linux/modules/constrained_optimization_dialog.py +679 -0
  20. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  21. moleditpy_linux/modules/custom_qt_interactor.py +59 -0
  22. moleditpy_linux/modules/dialog3_d_picking_mixin.py +108 -0
  23. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  24. moleditpy_linux/modules/main_window.py +842 -0
  25. moleditpy_linux/modules/main_window_app_state.py +780 -0
  26. moleditpy_linux/modules/main_window_compute.py +1242 -0
  27. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  28. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  29. moleditpy_linux/modules/main_window_edit_actions.py +1455 -0
  30. moleditpy_linux/modules/main_window_export.py +806 -0
  31. moleditpy_linux/modules/main_window_main_init.py +2006 -0
  32. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  33. moleditpy_linux/modules/main_window_project_io.py +434 -0
  34. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  35. moleditpy_linux/modules/main_window_ui_manager.py +606 -0
  36. moleditpy_linux/modules/main_window_view_3d.py +1531 -0
  37. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  38. moleditpy_linux/modules/mirror_dialog.py +122 -0
  39. moleditpy_linux/modules/molecular_data.py +302 -0
  40. moleditpy_linux/modules/molecule_scene.py +2000 -0
  41. moleditpy_linux/modules/move_group_dialog.py +598 -0
  42. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  43. moleditpy_linux/modules/planarize_dialog.py +221 -0
  44. moleditpy_linux/modules/plugin_interface.py +195 -0
  45. moleditpy_linux/modules/plugin_manager.py +309 -0
  46. moleditpy_linux/modules/plugin_manager_window.py +221 -0
  47. moleditpy_linux/modules/settings_dialog.py +1149 -0
  48. moleditpy_linux/modules/template_preview_item.py +157 -0
  49. moleditpy_linux/modules/template_preview_view.py +74 -0
  50. moleditpy_linux/modules/translation_dialog.py +365 -0
  51. moleditpy_linux/modules/user_template_dialog.py +692 -0
  52. moleditpy_linux/modules/zoomable_view.py +129 -0
  53. moleditpy_linux-2.2.4.dist-info/METADATA +936 -0
  54. moleditpy_linux-2.2.4.dist-info/RECORD +58 -0
  55. moleditpy_linux-2.2.4.dist-info/WHEEL +5 -0
  56. moleditpy_linux-2.2.4.dist-info/entry_points.txt +2 -0
  57. moleditpy_linux-2.2.4.dist-info/licenses/LICENSE +674 -0
  58. moleditpy_linux-2.2.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,348 @@
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 QGraphicsItem
14
+
15
+ from PyQt6.QtGui import (
16
+ QPen, QBrush, QColor, QFont, QPainterPath, QFontMetricsF
17
+ )
18
+
19
+ from PyQt6.QtCore import (
20
+ Qt, QPointF, QRectF
21
+ )
22
+
23
+ try:
24
+ from .constants import (
25
+ ATOM_RADIUS, DESIRED_ATOM_PIXEL_RADIUS,
26
+ FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
27
+ CPK_COLORS,
28
+ )
29
+ except Exception:
30
+ from modules.constants import (
31
+ ATOM_RADIUS, DESIRED_ATOM_PIXEL_RADIUS,
32
+ FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
33
+ CPK_COLORS,
34
+ )
35
+
36
+ try:
37
+ from . import sip_isdeleted_safe
38
+ except Exception:
39
+ from modules import sip_isdeleted_safe
40
+
41
+ class AtomItem(QGraphicsItem):
42
+ def __init__(self, atom_id, symbol, pos, charge=0, radical=0):
43
+ super().__init__()
44
+ self.atom_id, self.symbol, self.charge, self.radical, self.bonds, self.chiral_label = atom_id, symbol, charge, radical, [], None
45
+ self.setPos(pos)
46
+ self.implicit_h_count = 0
47
+ self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
48
+ self.setZValue(1); self.font = QFont(FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD); self.update_style()
49
+ self.setAcceptHoverEvents(True)
50
+ self.hovered = False
51
+ self.has_problem = False
52
+
53
+ def boundingRect(self):
54
+ # --- paint()メソッドと完全に同じロジックでテキストの位置とサイズを計算 ---
55
+ font = QFont(FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD)
56
+ fm = QFontMetricsF(font)
57
+
58
+ hydrogen_part = ""
59
+ if self.implicit_h_count > 0:
60
+ is_skeletal_carbon = (self.symbol == 'C' and
61
+ self.charge == 0 and
62
+ self.radical == 0 and
63
+ len(self.bonds) > 0)
64
+ if not is_skeletal_carbon:
65
+ hydrogen_part = "H"
66
+ if self.implicit_h_count > 1:
67
+ subscript_map = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
68
+ hydrogen_part += str(self.implicit_h_count).translate(subscript_map)
69
+
70
+ flip_text = False
71
+ if hydrogen_part and self.bonds:
72
+ my_pos_x = self.pos().x()
73
+ total_dx = 0.0
74
+ # Defensive: some bonds may have missing atom references (None) or C++ wrappers
75
+ # that have been deleted. Iterate and accumulate only valid partner positions.
76
+ for b in self.bonds:
77
+ # partner is the atom at the other end of the bond
78
+ partner = b.atom2 if b.atom1 is self else b.atom1
79
+ try:
80
+ if partner is None:
81
+ continue
82
+ # If SIP reports the wrapper as deleted, skip it
83
+ if sip_isdeleted_safe(partner):
84
+ continue
85
+ partner_pos = partner.pos()
86
+ if partner_pos is None:
87
+ continue
88
+ total_dx += partner_pos.x() - my_pos_x
89
+ except Exception:
90
+ # Skip any bond that raises while inspecting; keep UI tolerant
91
+ continue
92
+
93
+ if total_dx > 0:
94
+ flip_text = True
95
+
96
+ if flip_text:
97
+ display_text = hydrogen_part + self.symbol
98
+ else:
99
+ display_text = self.symbol + hydrogen_part
100
+
101
+ text_rect = fm.boundingRect(display_text)
102
+ text_rect.adjust(-2, -2, 2, 2)
103
+ if hydrogen_part:
104
+ symbol_rect = fm.boundingRect(self.symbol)
105
+ if flip_text:
106
+ offset_x = symbol_rect.width() // 2
107
+ text_rect.moveTo(offset_x - text_rect.width(), -text_rect.height() / 2)
108
+ else:
109
+ offset_x = -symbol_rect.width() // 2
110
+ text_rect.moveTo(offset_x, -text_rect.height() / 2)
111
+ else:
112
+ text_rect.moveCenter(QPointF(0, 0))
113
+
114
+ # 1. paint()で描画される背景の矩形(bg_rect)を計算する
115
+ bg_rect = text_rect.adjusted(-5, -8, 5, 8)
116
+
117
+ # 2. このbg_rectを基準として全体の描画領域を構築する
118
+ full_visual_rect = QRectF(bg_rect)
119
+
120
+ # 電荷記号の領域を計算に含める
121
+ if self.charge != 0:
122
+ # Chemical convention: single charge as "+"/"-", multiple as "2+"/"2-"
123
+ if self.charge == 1:
124
+ charge_str = "+"
125
+ elif self.charge == -1:
126
+ charge_str = "-"
127
+ else:
128
+ sign = '+' if self.charge > 0 else '-'
129
+ charge_str = f"{abs(self.charge)}{sign}"
130
+ charge_font = QFont("Arial", 12, QFont.Weight.Bold)
131
+ charge_fm = QFontMetricsF(charge_font)
132
+ charge_rect = charge_fm.boundingRect(charge_str)
133
+
134
+ if flip_text:
135
+ charge_pos = QPointF(text_rect.left() - charge_rect.width() - 2, text_rect.top())
136
+ else:
137
+ charge_pos = QPointF(text_rect.right() + 2, text_rect.top())
138
+ charge_rect.moveTopLeft(charge_pos)
139
+ full_visual_rect = full_visual_rect.united(charge_rect)
140
+
141
+ # ラジカル記号の領域を計算に含める
142
+ if self.radical > 0:
143
+ radical_area = QRectF(text_rect.center().x() - 8, text_rect.top() - 8, 16, 8)
144
+ full_visual_rect = full_visual_rect.united(radical_area)
145
+
146
+ # 3. 選択ハイライト等のための最終的なマージンを追加する
147
+ return full_visual_rect.adjusted(-3, -3, 3, 3)
148
+
149
+ def shape(self):
150
+ scene = self.scene()
151
+ if not scene or not scene.views():
152
+ path = QPainterPath()
153
+ hit_r = max(4.0, ATOM_RADIUS - 6.0) * 2
154
+ path.addEllipse(QRectF(-hit_r, -hit_r, hit_r * 2.0, hit_r * 2.0))
155
+ return path
156
+
157
+ view = scene.views()[0]
158
+ scale = view.transform().m11()
159
+
160
+ scene_radius = DESIRED_ATOM_PIXEL_RADIUS / scale
161
+
162
+ path = QPainterPath()
163
+ path.addEllipse(QPointF(0, 0), scene_radius, scene_radius)
164
+ return path
165
+
166
+ def paint(self, painter, option, widget):
167
+ color = CPK_COLORS.get(self.symbol, CPK_COLORS['DEFAULT'])
168
+ if self.is_visible:
169
+ # 1. 描画の準備
170
+ painter.setFont(self.font)
171
+ fm = painter.fontMetrics()
172
+
173
+ # --- 水素部分のテキストを作成 ---
174
+ hydrogen_part = ""
175
+ if self.implicit_h_count > 0:
176
+ is_skeletal_carbon = (self.symbol == 'C' and
177
+ self.charge == 0 and
178
+ self.radical == 0 and
179
+ len(self.bonds) > 0)
180
+ if not is_skeletal_carbon:
181
+ hydrogen_part = "H"
182
+ if self.implicit_h_count > 1:
183
+ subscript_map = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
184
+ hydrogen_part += str(self.implicit_h_count).translate(subscript_map)
185
+
186
+ # --- テキストを反転させるか決定 ---
187
+ flip_text = False
188
+ # 水素ラベルがあり、結合が1本以上ある場合のみ反転を考慮
189
+ if hydrogen_part and self.bonds:
190
+
191
+ # 相対的なX座標で、結合が左右どちらに偏っているか判定
192
+ my_pos_x = self.pos().x()
193
+ total_dx = 0.0
194
+ # Defensive: some bonds may have missing atom references (None) or
195
+ # wrappers that were deleted by SIP. Only accumulate valid partner positions.
196
+ for bond in self.bonds:
197
+ try:
198
+ other_atom = bond.atom1 if bond.atom2 is self else bond.atom2
199
+ if other_atom is None:
200
+ continue
201
+ # If SIP reports the wrapper as deleted, skip it
202
+ try:
203
+ if sip_isdeleted_safe(other_atom):
204
+ continue
205
+ except Exception:
206
+ # If sip check fails, continue defensively
207
+ pass
208
+
209
+ other_pos = None
210
+ try:
211
+ other_pos = other_atom.pos()
212
+ except Exception:
213
+ # Accessing .pos() may raise if the C++ object was destroyed
214
+ other_pos = None
215
+
216
+ if other_pos is None:
217
+ continue
218
+
219
+ total_dx += (other_pos.x() - my_pos_x)
220
+ except Exception:
221
+ # Skip any problematic bond/partner rather than crashing the paint
222
+ continue
223
+
224
+ # 結合が主に右側にある場合はテキストを反転させる
225
+ if total_dx > 0:
226
+ flip_text = True
227
+
228
+ # --- 表示テキストとアライメントを最終決定 ---
229
+ if flip_text:
230
+ display_text = hydrogen_part + self.symbol
231
+ alignment_flag = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
232
+ else:
233
+ display_text = self.symbol + hydrogen_part
234
+ alignment_flag = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
235
+
236
+ text_rect = fm.boundingRect(display_text)
237
+ text_rect.adjust(-2, -2, 2, 2)
238
+ symbol_rect = fm.boundingRect(self.symbol) # 主元素のみの幅を計算
239
+
240
+ # --- テキストの描画位置を決定 ---
241
+ # 水素ラベルがない場合 (従来通り中央揃え)
242
+ if not hydrogen_part:
243
+ alignment_flag = Qt.AlignmentFlag.AlignCenter
244
+ text_rect.moveCenter(QPointF(0, 0).toPoint())
245
+ # 水素ラベルがあり、反転する場合 (右揃え)
246
+ elif flip_text:
247
+ # 主元素の中心が原子の中心に来るように、矩形の右端を調整
248
+ offset_x = symbol_rect.width() // 2
249
+ text_rect.moveTo(offset_x - text_rect.width(), -text_rect.height() // 2)
250
+ # 水素ラベルがあり、反転しない場合 (左揃え)
251
+ else:
252
+ # 主元素の中心が原子の中心に来るように、矩形の左端を調整
253
+ offset_x = -symbol_rect.width() // 2
254
+ text_rect.moveTo(offset_x, -text_rect.height() // 2)
255
+
256
+ # 2. 原子記号の背景を白で塗りつぶす
257
+ if self.scene():
258
+ bg_brush = self.scene().backgroundBrush()
259
+ bg_rect = text_rect.adjusted(-5, -8, 5, 8)
260
+ painter.setBrush(bg_brush)
261
+ painter.setPen(Qt.PenStyle.NoPen)
262
+ painter.drawEllipse(bg_rect)
263
+
264
+ # 3. 原子記号自体を描画
265
+ if self.symbol == 'H':
266
+ painter.setPen(QPen(Qt.GlobalColor.black))
267
+ else:
268
+ painter.setPen(QPen(color))
269
+ painter.drawText(text_rect, int(alignment_flag), display_text)
270
+
271
+ # --- 電荷とラジカルの描画 ---
272
+ if self.charge != 0:
273
+ # Chemical convention: single charge as "+"/"-", multiple as "2+"/"2-"
274
+ if self.charge == 1:
275
+ charge_str = "+"
276
+ elif self.charge == -1:
277
+ charge_str = "-"
278
+ else:
279
+ sign = '+' if self.charge > 0 else '-'
280
+ charge_str = f"{abs(self.charge)}{sign}"
281
+ charge_font = QFont("Arial", 12, QFont.Weight.Bold)
282
+ painter.setFont(charge_font)
283
+ charge_rect = painter.fontMetrics().boundingRect(charge_str)
284
+ # 電荷の位置も反転に対応
285
+ if flip_text:
286
+ charge_pos = QPointF(text_rect.left() - charge_rect.width() -2, text_rect.top() + charge_rect.height() - 2)
287
+ else:
288
+ charge_pos = QPointF(text_rect.right() + 2, text_rect.top() + charge_rect.height() - 2)
289
+ painter.setPen(Qt.GlobalColor.black)
290
+ painter.drawText(charge_pos, charge_str)
291
+
292
+ if self.radical > 0:
293
+ painter.setBrush(QBrush(Qt.GlobalColor.black))
294
+ painter.setPen(Qt.PenStyle.NoPen)
295
+ radical_pos_y = text_rect.top() - 5
296
+ if self.radical == 1:
297
+ painter.drawEllipse(QPointF(text_rect.center().x(), radical_pos_y), 3, 3)
298
+ elif self.radical == 2:
299
+ painter.drawEllipse(QPointF(text_rect.center().x() - 5, radical_pos_y), 3, 3)
300
+ painter.drawEllipse(QPointF(text_rect.center().x() + 5, radical_pos_y), 3, 3)
301
+
302
+
303
+ # --- 選択時のハイライトなど ---
304
+ if self.has_problem:
305
+ painter.setBrush(Qt.BrushStyle.NoBrush)
306
+ painter.setPen(QPen(QColor(255, 0, 0, 200), 4))
307
+ painter.drawRect(self.boundingRect())
308
+ elif self.isSelected():
309
+ painter.setBrush(Qt.BrushStyle.NoBrush)
310
+ painter.setPen(QPen(QColor(0, 100, 255), 3))
311
+ painter.drawRect(self.boundingRect())
312
+ if (not self.isSelected()) and getattr(self, 'hovered', False):
313
+ pen = QPen(QColor(144, 238, 144, 200), 3)
314
+ pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
315
+ painter.setBrush(Qt.BrushStyle.NoBrush)
316
+ painter.setPen(pen)
317
+ painter.drawRect(self.boundingRect())
318
+
319
+ def update_style(self):
320
+ self.is_visible = not (self.symbol == 'C' and len(self.bonds) > 0 and self.charge == 0 and self.radical == 0)
321
+ self.update()
322
+
323
+
324
+ # 約203行目 AtomItem クラス内
325
+
326
+ def itemChange(self, change, value):
327
+ res = super().itemChange(change, value)
328
+ if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
329
+ if self.flags() & QGraphicsItem.GraphicsItemFlag.ItemIsMovable:
330
+ # Prevent cascading updates during batch operations
331
+ if not getattr(self, '_updating_position', False):
332
+ for bond in self.bonds:
333
+ if bond.scene(): # Only update if bond is still in scene
334
+ bond.update_position()
335
+
336
+ return res
337
+
338
+ def hoverEnterEvent(self, event):
339
+ # シーンのモードにかかわらず、ホバー時にハイライトを有効にする
340
+ self.hovered = True
341
+ self.update()
342
+ super().hoverEnterEvent(event)
343
+
344
+ def hoverLeaveEvent(self, event):
345
+ if self.hovered:
346
+ self.hovered = False
347
+ self.update()
348
+ super().hoverLeaveEvent(event)