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,395 @@
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, QPainter
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)
49
+ self.update_style()
50
+ self.setAcceptHoverEvents(True)
51
+ self.hovered = False
52
+ self.has_problem = False
53
+
54
+
55
+ def update_style(self):
56
+ # Allow updating font preference dynamically
57
+ font_size = 20
58
+ try:
59
+ if self.scene() and self.scene().views():
60
+ win = self.scene().views()[0].window()
61
+ if win and hasattr(win, 'settings'):
62
+ font_size = win.settings.get('atom_font_size_2d', 20)
63
+ except Exception:
64
+ pass
65
+ self.font = QFont(FONT_FAMILY, font_size, FONT_WEIGHT_BOLD)
66
+ self.prepareGeometryChange()
67
+
68
+ self.is_visible = not (self.symbol == 'C' and len(self.bonds) > 0 and self.charge == 0 and self.radical == 0)
69
+ self.update()
70
+
71
+ def boundingRect(self):
72
+ # --- paint()メソッドと完全に同じロジックでテキストの位置とサイズを計算 ---
73
+ # Get dynamic font size
74
+ font_size = 20
75
+ try:
76
+ if self.scene() and self.scene().views():
77
+ win = self.scene().views()[0].window()
78
+ if win and hasattr(win, 'settings'):
79
+ font_size = win.settings.get('atom_font_size_2d', 20)
80
+ except Exception:
81
+ pass
82
+ font = QFont(FONT_FAMILY, font_size, FONT_WEIGHT_BOLD)
83
+ fm = QFontMetricsF(font)
84
+
85
+ hydrogen_part = ""
86
+ if self.implicit_h_count > 0:
87
+ is_skeletal_carbon = (self.symbol == 'C' and
88
+ self.charge == 0 and
89
+ self.radical == 0 and
90
+ len(self.bonds) > 0)
91
+ if not is_skeletal_carbon:
92
+ hydrogen_part = "H"
93
+ if self.implicit_h_count > 1:
94
+ subscript_map = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
95
+ hydrogen_part += str(self.implicit_h_count).translate(subscript_map)
96
+
97
+ flip_text = False
98
+ if hydrogen_part and self.bonds:
99
+ my_pos_x = self.pos().x()
100
+ total_dx = 0.0
101
+ # Defensive: some bonds may have missing atom references (None) or C++ wrappers
102
+ # that have been deleted. Iterate and accumulate only valid partner positions.
103
+ for b in self.bonds:
104
+ # partner is the atom at the other end of the bond
105
+ partner = b.atom2 if b.atom1 is self else b.atom1
106
+ try:
107
+ if partner is None:
108
+ continue
109
+ # If SIP reports the wrapper as deleted, skip it
110
+ if sip_isdeleted_safe(partner):
111
+ continue
112
+ partner_pos = partner.pos()
113
+ if partner_pos is None:
114
+ continue
115
+ total_dx += partner_pos.x() - my_pos_x
116
+ except Exception:
117
+ # Skip any bond that raises while inspecting; keep UI tolerant
118
+ continue
119
+
120
+ if total_dx > 0:
121
+ flip_text = True
122
+
123
+ if flip_text:
124
+ display_text = hydrogen_part + self.symbol
125
+ else:
126
+ display_text = self.symbol + hydrogen_part
127
+
128
+ text_rect = fm.boundingRect(display_text)
129
+ text_rect.adjust(-2, -2, 2, 2)
130
+ if hydrogen_part:
131
+ symbol_rect = fm.boundingRect(self.symbol)
132
+ if flip_text:
133
+ offset_x = symbol_rect.width() // 2
134
+ text_rect.moveTo(offset_x - text_rect.width(), -text_rect.height() / 2)
135
+ else:
136
+ offset_x = -symbol_rect.width() // 2
137
+ text_rect.moveTo(offset_x, -text_rect.height() / 2)
138
+ else:
139
+ text_rect.moveCenter(QPointF(0, 0))
140
+
141
+ # 1. paint()で描画される背景の矩形(bg_rect)を計算する
142
+ bg_rect = text_rect.adjusted(-5, -8, 5, 8)
143
+
144
+ # 2. このbg_rectを基準として全体の描画領域を構築する
145
+ full_visual_rect = QRectF(bg_rect)
146
+
147
+ # 電荷記号の領域を計算に含める
148
+ if self.charge != 0:
149
+ # Chemical convention: single charge as "+"/"-", multiple as "2+"/"2-"
150
+ if self.charge == 1:
151
+ charge_str = "+"
152
+ elif self.charge == -1:
153
+ charge_str = "-"
154
+ else:
155
+ sign = '+' if self.charge > 0 else '-'
156
+ charge_str = f"{abs(self.charge)}{sign}"
157
+ charge_font = QFont("Arial", 12, QFont.Weight.Bold)
158
+ charge_fm = QFontMetricsF(charge_font)
159
+ charge_rect = charge_fm.boundingRect(charge_str)
160
+
161
+ if flip_text:
162
+ charge_pos = QPointF(text_rect.left() - charge_rect.width() - 2, text_rect.top())
163
+ else:
164
+ charge_pos = QPointF(text_rect.right() + 2, text_rect.top())
165
+ charge_rect.moveTopLeft(charge_pos)
166
+ full_visual_rect = full_visual_rect.united(charge_rect)
167
+
168
+ # ラジカル記号の領域を計算に含める
169
+ if self.radical > 0:
170
+ radical_area = QRectF(text_rect.center().x() - 8, text_rect.top() - 8, 16, 8)
171
+ full_visual_rect = full_visual_rect.united(radical_area)
172
+
173
+ # 3. 選択ハイライト等のための最終的なマージンを追加する
174
+ return full_visual_rect.adjusted(-3, -3, 3, 3)
175
+
176
+ def shape(self):
177
+ scene = self.scene()
178
+ if not scene or not scene.views():
179
+ path = QPainterPath()
180
+ hit_r = max(4.0, ATOM_RADIUS - 6.0) * 2
181
+ path.addEllipse(QRectF(-hit_r, -hit_r, hit_r * 2.0, hit_r * 2.0))
182
+ return path
183
+
184
+ view = scene.views()[0]
185
+ scale = view.transform().m11()
186
+
187
+ scene_radius = DESIRED_ATOM_PIXEL_RADIUS / scale
188
+
189
+ path = QPainterPath()
190
+ path.addEllipse(QPointF(0, 0), scene_radius, scene_radius)
191
+ return path
192
+
193
+ def paint(self, painter, option, widget):
194
+ # Color logic: check if we should use bond color (uniform) or CPK (element-specific)
195
+ color = CPK_COLORS.get(self.symbol, CPK_COLORS['DEFAULT'])
196
+ try:
197
+ if self.scene() and self.scene().views():
198
+ win = self.scene().views()[0].window()
199
+ if win and hasattr(win, 'settings'):
200
+ if win.settings.get('atom_use_bond_color_2d', False):
201
+ bond_col = win.settings.get('bond_color_2d', '#222222')
202
+ color = QColor(bond_col)
203
+ except Exception:
204
+ pass
205
+
206
+ if self.is_visible:
207
+ # 1. 描画の準備
208
+ # Ensure correct font is used (self.font should be updated by update_style)
209
+ painter.setFont(self.font)
210
+ fm = painter.fontMetrics()
211
+
212
+ # --- 水素部分のテキストを作成 ---
213
+ hydrogen_part = ""
214
+ if self.implicit_h_count > 0:
215
+ is_skeletal_carbon = (self.symbol == 'C' and
216
+ self.charge == 0 and
217
+ self.radical == 0 and
218
+ len(self.bonds) > 0)
219
+ if not is_skeletal_carbon:
220
+ hydrogen_part = "H"
221
+ if self.implicit_h_count > 1:
222
+ subscript_map = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
223
+ hydrogen_part += str(self.implicit_h_count).translate(subscript_map)
224
+
225
+ # --- テキストを反転させるか決定 ---
226
+ flip_text = False
227
+ # 水素ラベルがあり、結合が1本以上ある場合のみ反転を考慮
228
+ if hydrogen_part and self.bonds:
229
+
230
+ # 相対的なX座標で、結合が左右どちらに偏っているか判定
231
+ my_pos_x = self.pos().x()
232
+ total_dx = 0.0
233
+ # Defensive: some bonds may have missing atom references (None) or
234
+ # wrappers that were deleted by SIP. Only accumulate valid partner positions.
235
+ for bond in self.bonds:
236
+ try:
237
+ other_atom = bond.atom1 if bond.atom2 is self else bond.atom2
238
+ if other_atom is None:
239
+ continue
240
+ # If SIP reports the wrapper as deleted, skip it
241
+ try:
242
+ if sip_isdeleted_safe(other_atom):
243
+ continue
244
+ except Exception:
245
+ # If sip check fails, continue defensively
246
+ pass
247
+
248
+ other_pos = None
249
+ try:
250
+ other_pos = other_atom.pos()
251
+ except Exception:
252
+ # Accessing .pos() may raise if the C++ object was destroyed
253
+ other_pos = None
254
+
255
+ if other_pos is None:
256
+ continue
257
+
258
+ total_dx += (other_pos.x() - my_pos_x)
259
+ except Exception:
260
+ # Skip any problematic bond/partner rather than crashing the paint
261
+ continue
262
+
263
+ # 結合が主に右側にある場合はテキストを反転させる
264
+ if total_dx > 0:
265
+ flip_text = True
266
+
267
+ # --- 表示テキストとアライメントを最終決定 ---
268
+ if flip_text:
269
+ display_text = hydrogen_part + self.symbol
270
+ alignment_flag = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
271
+ else:
272
+ display_text = self.symbol + hydrogen_part
273
+ alignment_flag = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
274
+
275
+ text_rect = fm.boundingRect(display_text)
276
+ text_rect.adjust(-2, -2, 2, 2)
277
+ symbol_rect = fm.boundingRect(self.symbol) # 主元素のみの幅を計算
278
+
279
+ # --- テキストの描画位置を決定 ---
280
+ # 水素ラベルがない場合 (従来通り中央揃え)
281
+ if not hydrogen_part:
282
+ alignment_flag = Qt.AlignmentFlag.AlignCenter
283
+ text_rect.moveCenter(QPointF(0, 0).toPoint())
284
+ # 水素ラベルがあり、反転する場合 (右揃え)
285
+ elif flip_text:
286
+ # 主元素の中心が原子の中心に来るように、矩形の右端を調整
287
+ offset_x = symbol_rect.width() // 2
288
+ text_rect.moveTo(offset_x - text_rect.width(), -text_rect.height() // 2)
289
+ # 水素ラベルがあり、反転しない場合 (左揃え)
290
+ else:
291
+ # 主元素の中心が原子の中心に来るように、矩形の左端を調整
292
+ offset_x = -symbol_rect.width() // 2
293
+ text_rect.moveTo(offset_x, -text_rect.height() // 2)
294
+
295
+ # 2. 原子記号の背景を処理(白で塗りつぶす か 透明なら切り抜く)
296
+ if self.scene():
297
+ bg_brush = self.scene().backgroundBrush()
298
+ bg_rect = text_rect.adjusted(-5, -8, 5, 8)
299
+
300
+ if bg_brush.style() == Qt.BrushStyle.NoBrush:
301
+ # 背景が透明の場合は、CompositionMode_Clearを使って
302
+ # 重なっている結合の線を「消しゴム」のように消す
303
+ painter.save()
304
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
305
+ painter.setBrush(QColor(0, 0, 0, 255)) # 色は何でも良い(アルファが重要)
306
+ painter.setPen(Qt.PenStyle.NoPen)
307
+ painter.drawEllipse(bg_rect)
308
+ painter.restore()
309
+ else:
310
+ # 背景がある場合は、その背景色で塗りつぶす(従来通り)
311
+ painter.setBrush(bg_brush)
312
+ painter.setPen(Qt.PenStyle.NoPen)
313
+ painter.drawEllipse(bg_rect)
314
+
315
+ # 3. 原子記号自体を描画
316
+ # Color is already determined above
317
+ painter.setPen(QPen(color))
318
+ painter.drawText(text_rect, int(alignment_flag), display_text)
319
+
320
+ # --- 電荷とラジカルの描画 ---
321
+ if self.charge != 0:
322
+ # Chemical convention: single charge as "+"/"-", multiple as "2+"/"2-"
323
+ if self.charge == 1:
324
+ charge_str = "+"
325
+ elif self.charge == -1:
326
+ charge_str = "-"
327
+ else:
328
+ sign = '+' if self.charge > 0 else '-'
329
+ charge_str = f"{abs(self.charge)}{sign}"
330
+ charge_font = QFont("Arial", 12, QFont.Weight.Bold)
331
+ painter.setFont(charge_font)
332
+ charge_rect = painter.fontMetrics().boundingRect(charge_str)
333
+ # 電荷の位置も反転に対応
334
+ if flip_text:
335
+ charge_pos = QPointF(text_rect.left() - charge_rect.width() -2, text_rect.top() + charge_rect.height() - 2)
336
+ else:
337
+ charge_pos = QPointF(text_rect.right() + 2, text_rect.top() + charge_rect.height() - 2)
338
+ painter.setPen(Qt.GlobalColor.black)
339
+ painter.drawText(charge_pos, charge_str)
340
+
341
+ if self.radical > 0:
342
+ painter.setBrush(QBrush(Qt.GlobalColor.black))
343
+ painter.setPen(Qt.PenStyle.NoPen)
344
+ radical_pos_y = text_rect.top() - 5
345
+ if self.radical == 1:
346
+ painter.drawEllipse(QPointF(text_rect.center().x(), radical_pos_y), 3, 3)
347
+ elif self.radical == 2:
348
+ painter.drawEllipse(QPointF(text_rect.center().x() - 5, radical_pos_y), 3, 3)
349
+ painter.drawEllipse(QPointF(text_rect.center().x() + 5, radical_pos_y), 3, 3)
350
+
351
+
352
+ # --- 選択時のハイライトなど ---
353
+ if self.has_problem:
354
+ painter.setBrush(Qt.BrushStyle.NoBrush)
355
+ painter.setPen(QPen(QColor(255, 0, 0, 200), 4))
356
+ painter.drawRect(self.boundingRect())
357
+ elif self.isSelected():
358
+ painter.setBrush(Qt.BrushStyle.NoBrush)
359
+ painter.setPen(QPen(QColor(0, 100, 255), 3))
360
+ painter.drawRect(self.boundingRect())
361
+ if (not self.isSelected()) and getattr(self, 'hovered', False):
362
+ pen = QPen(QColor(144, 238, 144, 200), 3)
363
+ pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
364
+ painter.setBrush(Qt.BrushStyle.NoBrush)
365
+ painter.setPen(pen)
366
+ painter.drawRect(self.boundingRect())
367
+
368
+
369
+
370
+
371
+ # 約203行目 AtomItem クラス内
372
+
373
+ def itemChange(self, change, value):
374
+ res = super().itemChange(change, value)
375
+ if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
376
+ if self.flags() & QGraphicsItem.GraphicsItemFlag.ItemIsMovable:
377
+ # Prevent cascading updates during batch operations
378
+ if not getattr(self, '_updating_position', False):
379
+ for bond in self.bonds:
380
+ if bond.scene(): # Only update if bond is still in scene
381
+ bond.update_position()
382
+
383
+ return res
384
+
385
+ def hoverEnterEvent(self, event):
386
+ # シーンのモードにかかわらず、ホバー時にハイライトを有効にする
387
+ self.hovered = True
388
+ self.update()
389
+ super().hoverEnterEvent(event)
390
+
391
+ def hoverLeaveEvent(self, event):
392
+ if self.hovered:
393
+ self.hovered = False
394
+ self.update()
395
+ super().hoverLeaveEvent(event)