MoleditPy-linux 1.15.1__py3-none-any.whl → 1.16.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 (55) hide show
  1. moleditpy_linux/__init__.py +4 -0
  2. moleditpy_linux/__main__.py +29 -19742
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +29 -0
  5. moleditpy_linux/modules/about_dialog.py +92 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +281 -0
  7. moleditpy_linux/modules/alignment_dialog.py +261 -0
  8. moleditpy_linux/modules/analysis_window.py +197 -0
  9. moleditpy_linux/modules/angle_dialog.py +428 -0
  10. moleditpy_linux/modules/assets/icon.icns +0 -0
  11. moleditpy_linux/modules/atom_item.py +336 -0
  12. moleditpy_linux/modules/bond_item.py +303 -0
  13. moleditpy_linux/modules/bond_length_dialog.py +368 -0
  14. moleditpy_linux/modules/calculation_worker.py +754 -0
  15. moleditpy_linux/modules/color_settings_dialog.py +309 -0
  16. moleditpy_linux/modules/constants.py +76 -0
  17. moleditpy_linux/modules/constrained_optimization_dialog.py +667 -0
  18. moleditpy_linux/modules/custom_interactor_style.py +737 -0
  19. moleditpy_linux/modules/custom_qt_interactor.py +49 -0
  20. moleditpy_linux/modules/dialog3_d_picking_mixin.py +96 -0
  21. moleditpy_linux/modules/dihedral_dialog.py +431 -0
  22. moleditpy_linux/modules/main_window.py +830 -0
  23. moleditpy_linux/modules/main_window_app_state.py +747 -0
  24. moleditpy_linux/modules/main_window_compute.py +1203 -0
  25. moleditpy_linux/modules/main_window_dialog_manager.py +454 -0
  26. moleditpy_linux/modules/main_window_edit_3d.py +531 -0
  27. moleditpy_linux/modules/main_window_edit_actions.py +1449 -0
  28. moleditpy_linux/modules/main_window_export.py +744 -0
  29. moleditpy_linux/modules/main_window_main_init.py +1665 -0
  30. moleditpy_linux/modules/main_window_molecular_parsers.py +956 -0
  31. moleditpy_linux/modules/main_window_project_io.py +429 -0
  32. moleditpy_linux/modules/main_window_string_importers.py +270 -0
  33. moleditpy_linux/modules/main_window_ui_manager.py +567 -0
  34. moleditpy_linux/modules/main_window_view_3d.py +1163 -0
  35. moleditpy_linux/modules/main_window_view_loaders.py +350 -0
  36. moleditpy_linux/modules/mirror_dialog.py +110 -0
  37. moleditpy_linux/modules/molecular_data.py +290 -0
  38. moleditpy_linux/modules/molecule_scene.py +1895 -0
  39. moleditpy_linux/modules/move_group_dialog.py +586 -0
  40. moleditpy_linux/modules/periodic_table_dialog.py +72 -0
  41. moleditpy_linux/modules/planarize_dialog.py +209 -0
  42. moleditpy_linux/modules/settings_dialog.py +1034 -0
  43. moleditpy_linux/modules/template_preview_item.py +148 -0
  44. moleditpy_linux/modules/template_preview_view.py +62 -0
  45. moleditpy_linux/modules/translation_dialog.py +353 -0
  46. moleditpy_linux/modules/user_template_dialog.py +621 -0
  47. moleditpy_linux/modules/zoomable_view.py +98 -0
  48. {moleditpy_linux-1.15.1.dist-info → moleditpy_linux-1.16.1.dist-info}/METADATA +1 -2
  49. moleditpy_linux-1.16.1.dist-info/RECORD +54 -0
  50. moleditpy_linux-1.15.1.dist-info/RECORD +0 -9
  51. /moleditpy_linux/{assets → modules/assets}/icon.ico +0 -0
  52. /moleditpy_linux/{assets → modules/assets}/icon.png +0 -0
  53. {moleditpy_linux-1.15.1.dist-info → moleditpy_linux-1.16.1.dist-info}/WHEEL +0 -0
  54. {moleditpy_linux-1.15.1.dist-info → moleditpy_linux-1.16.1.dist-info}/entry_points.txt +0 -0
  55. {moleditpy_linux-1.15.1.dist-info → moleditpy_linux-1.16.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,621 @@
1
+ from PyQt6.QtWidgets import (
2
+ QDialog, QVBoxLayout, QLabel, QWidget, QGridLayout, QScrollArea,
3
+ QHBoxLayout, QPushButton, QGraphicsScene, QInputDialog, QMessageBox
4
+ )
5
+ from PyQt6.QtGui import QPainter, QFont, QColor, QPen, QBrush
6
+ from PyQt6.QtCore import Qt, QPointF, QRectF, QTimer, QDateTime, QLineF
7
+ from .template_preview_view import TemplatePreviewView
8
+ try:
9
+ from .constants import VERSION, CPK_COLORS
10
+ except Exception:
11
+ from modules.constants import VERSION, CPK_COLORS
12
+ import os
13
+ import json
14
+
15
+ class UserTemplateDialog(QDialog):
16
+ """ユーザーテンプレート管理ダイアログ"""
17
+
18
+ def __init__(self, main_window, parent=None):
19
+ super().__init__(parent)
20
+ self.main_window = main_window
21
+ self.user_templates = []
22
+ self.selected_template = None
23
+ self.init_ui()
24
+ self.load_user_templates()
25
+
26
+ def init_ui(self):
27
+ self.setWindowTitle("User Templates")
28
+ self.setModal(False) # モードレスに変更
29
+ self.resize(800, 600)
30
+
31
+ # ウィンドウを右上に配置
32
+ if self.parent():
33
+ parent_geometry = self.parent().geometry()
34
+ x = parent_geometry.right() - self.width() - 20
35
+ y = parent_geometry.top() + 50
36
+ self.move(x, y)
37
+
38
+ layout = QVBoxLayout(self)
39
+
40
+ # Instructions
41
+ instruction_label = QLabel("Create and manage your custom molecular templates. Click a template to use it in the editor.")
42
+ instruction_label.setWordWrap(True)
43
+ layout.addWidget(instruction_label)
44
+
45
+ # Template grid
46
+ self.template_widget = QWidget()
47
+ self.template_layout = QGridLayout(self.template_widget)
48
+ self.template_layout.setSpacing(10)
49
+
50
+ scroll_area = QScrollArea()
51
+ scroll_area.setWidget(self.template_widget)
52
+ scroll_area.setWidgetResizable(True)
53
+ scroll_area.setMinimumHeight(400)
54
+ layout.addWidget(scroll_area)
55
+
56
+ # Buttons
57
+ button_layout = QHBoxLayout()
58
+
59
+ self.save_current_button = QPushButton("Save Current 2D as Template")
60
+ self.save_current_button.clicked.connect(self.save_current_as_template)
61
+ button_layout.addWidget(self.save_current_button)
62
+
63
+ button_layout.addStretch()
64
+
65
+ self.delete_button = QPushButton("Delete Selected")
66
+ self.delete_button.clicked.connect(self.delete_selected_template)
67
+ self.delete_button.setEnabled(False)
68
+ button_layout.addWidget(self.delete_button)
69
+
70
+ close_button = QPushButton("Close")
71
+ close_button.clicked.connect(self.accept)
72
+ button_layout.addWidget(close_button)
73
+
74
+ layout.addLayout(button_layout)
75
+
76
+ def resizeEvent(self, event):
77
+ """ダイアログリサイズ時にテンプレートプレビューを再フィット"""
78
+ super().resizeEvent(event)
79
+ # Delay the refit to ensure proper widget sizing
80
+ QTimer.singleShot(100, self.refit_all_previews)
81
+
82
+ def refit_all_previews(self):
83
+ """すべてのテンプレートプレビューを再フィット"""
84
+ try:
85
+ for i in range(self.template_layout.count()):
86
+ item = self.template_layout.itemAt(i)
87
+ if item and item.widget():
88
+ widget = item.widget()
89
+ # Find the TemplatePreviewView within this widget
90
+ for child in widget.findChildren(TemplatePreviewView):
91
+ if hasattr(child, 'redraw_with_current_size'):
92
+ # Use redraw for better scaling adaptation
93
+ child.redraw_with_current_size()
94
+ elif hasattr(child, 'refit_view'):
95
+ child.refit_view()
96
+ except Exception as e:
97
+ print(f"Warning: Failed to refit template previews: {e}")
98
+
99
+ def showEvent(self, event):
100
+ """ダイアログ表示時にプレビューを適切にフィット"""
101
+ super().showEvent(event)
102
+ # Ensure all previews are properly fitted when dialog becomes visible
103
+ QTimer.singleShot(300, self.refit_all_previews)
104
+
105
+ def get_template_directory(self):
106
+ """テンプレートディレクトリのパスを取得"""
107
+ template_dir = os.path.join(self.main_window.settings_dir, 'user-templates')
108
+ if not os.path.exists(template_dir):
109
+ os.makedirs(template_dir)
110
+ return template_dir
111
+
112
+ def load_user_templates(self):
113
+ """ユーザーテンプレートを読み込み"""
114
+ template_dir = self.get_template_directory()
115
+ self.user_templates.clear()
116
+
117
+ try:
118
+ for filename in os.listdir(template_dir):
119
+ if filename.endswith('.pmetmplt'):
120
+ filepath = os.path.join(template_dir, filename)
121
+ template_data = self.load_template_file(filepath)
122
+ if template_data:
123
+ template_data['filename'] = filename
124
+ template_data['filepath'] = filepath
125
+ self.user_templates.append(template_data)
126
+ except Exception as e:
127
+ print(f"Error loading user templates: {e}")
128
+
129
+ self.update_template_grid()
130
+
131
+ def load_template_file(self, filepath):
132
+ """テンプレートファイルを読み込み"""
133
+ try:
134
+ with open(filepath, 'r', encoding='utf-8') as f:
135
+ return json.load(f)
136
+ except Exception as e:
137
+ print(f"Error loading template file {filepath}: {e}")
138
+ return None
139
+
140
+ def save_template_file(self, filepath, template_data):
141
+ """テンプレートファイルを保存"""
142
+ try:
143
+ with open(filepath, 'w', encoding='utf-8') as f:
144
+ json.dump(template_data, f, indent=2, ensure_ascii=False)
145
+ return True
146
+ except Exception as e:
147
+ print(f"Error saving template file {filepath}: {e}")
148
+ return False
149
+
150
+ def update_template_grid(self):
151
+ """テンプレートグリッドを更新"""
152
+ # Clear existing widgets
153
+ for i in reversed(range(self.template_layout.count())):
154
+ self.template_layout.itemAt(i).widget().setParent(None)
155
+
156
+ # Add template previews (left-to-right, top-to-bottom ordering)
157
+ cols = 4
158
+ for i, template in enumerate(self.user_templates):
159
+ row = i // cols
160
+ col = i % cols # Left-to-right ordering
161
+
162
+ preview_widget = self.create_template_preview(template)
163
+ self.template_layout.addWidget(preview_widget, row, col)
164
+
165
+ # Ensure all previews are properly fitted after grid update
166
+ QTimer.singleShot(200, self.refit_all_previews)
167
+
168
+ def create_template_preview(self, template_data):
169
+ """テンプレートプレビューウィジェットを作成"""
170
+ widget = QWidget()
171
+ widget.setFixedSize(180, 200)
172
+ widget.setStyleSheet("""
173
+ QWidget {
174
+ border: 2px solid #ccc;
175
+ border-radius: 8px;
176
+ background-color: white;
177
+ }
178
+ QWidget:hover {
179
+ border-color: #007acc;
180
+ background-color: #f0f8ff;
181
+ }
182
+ """)
183
+
184
+ layout = QVBoxLayout(widget)
185
+
186
+ # Preview graphics - use custom view class for better resize handling
187
+ preview_scene = QGraphicsScene()
188
+ preview_view = TemplatePreviewView(preview_scene)
189
+ preview_view.setFixedSize(160, 140)
190
+ preview_view.setRenderHint(QPainter.RenderHint.Antialiasing)
191
+ preview_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
192
+ preview_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
193
+
194
+ # Set template data for dynamic redrawing
195
+ preview_view.set_template_data(template_data, self)
196
+
197
+ # Draw template structure with view size for proper scaling
198
+ view_size = (preview_view.width(), preview_view.height())
199
+ self.draw_template_preview(preview_scene, template_data, view_size)
200
+
201
+ # Improved fitting approach with better error handling
202
+ bounding_rect = preview_scene.itemsBoundingRect()
203
+ if not bounding_rect.isEmpty() and bounding_rect.width() > 0 and bounding_rect.height() > 0:
204
+ # Calculate appropriate padding based on content size
205
+ content_size = max(bounding_rect.width(), bounding_rect.height())
206
+ padding = max(20, content_size * 0.2) # At least 20 units or 20% of content
207
+
208
+ padded_rect = bounding_rect.adjusted(-padding, -padding, padding, padding)
209
+ preview_scene.setSceneRect(padded_rect)
210
+
211
+ # Store original scene rect for proper fitting on resize
212
+ preview_view.original_scene_rect = padded_rect
213
+
214
+ # Use QTimer to ensure fitInView happens after widget is fully initialized
215
+ QTimer.singleShot(0, lambda: self.fit_preview_view_safely(preview_view, padded_rect))
216
+ else:
217
+ # Default view for empty or invalid content
218
+ default_rect = QRectF(-50, -50, 100, 100)
219
+ preview_scene.setSceneRect(default_rect)
220
+ preview_view.original_scene_rect = default_rect
221
+ QTimer.singleShot(0, lambda: self.fit_preview_view_safely(preview_view, default_rect))
222
+
223
+ layout.addWidget(preview_view)
224
+
225
+ # Template name
226
+ name = template_data.get('name', 'Unnamed Template')
227
+ name_label = QLabel(name)
228
+ name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
229
+ name_label.setWordWrap(True)
230
+ layout.addWidget(name_label)
231
+
232
+ # Mouse events
233
+ widget.mousePressEvent = lambda event: self.select_template(template_data, widget)
234
+ widget.mouseDoubleClickEvent = lambda event: self.use_template(template_data)
235
+
236
+ return widget
237
+
238
+ def fit_preview_view_safely(self, view, rect):
239
+ """プレビュービューを安全にフィット"""
240
+ try:
241
+ if view and not rect.isEmpty():
242
+ view.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
243
+ except Exception as e:
244
+ print(f"Warning: Failed to fit preview view: {e}")
245
+
246
+ def draw_template_preview(self, scene, template_data, view_size=None):
247
+ """テンプレートプレビューを描画 - fitInView縮小率に基づく動的スケーリング"""
248
+ atoms = template_data.get('atoms', [])
249
+ bonds = template_data.get('bonds', [])
250
+
251
+ if not atoms:
252
+ # Add placeholder text when no atoms
253
+ text = scene.addText("No structure", QFont('Arial', 12))
254
+ text.setDefaultTextColor(QColor('gray'))
255
+ return
256
+
257
+ # Calculate molecular dimensions
258
+ positions = [QPointF(atom['x'], atom['y']) for atom in atoms]
259
+ min_x = min(pos.x() for pos in positions)
260
+ max_x = max(pos.x() for pos in positions)
261
+ min_y = min(pos.y() for pos in positions)
262
+ max_y = max(pos.y() for pos in positions)
263
+
264
+ mol_width = max_x - min_x
265
+ mol_height = max_y - min_y
266
+ mol_size = max(mol_width, mol_height)
267
+
268
+ # Calculate fit scale factor (how much fitInView will shrink the content)
269
+ if view_size is None:
270
+ view_size = (160, 140) # Default preview view size
271
+
272
+ view_width, view_height = view_size
273
+
274
+ if mol_size > 0 and mol_width > 0 and mol_height > 0:
275
+ # Calculate the padding that will be added
276
+ padding = max(20, mol_size * 0.2)
277
+ padded_width = mol_width + 2 * padding
278
+ padded_height = mol_height + 2 * padding
279
+
280
+ # Calculate how much fitInView will scale down the content
281
+ # fitInView fits the padded rectangle into the view while maintaining aspect ratio
282
+ fit_scale_x = view_width / padded_width
283
+ fit_scale_y = view_height / padded_height
284
+ fit_scale = min(fit_scale_x, fit_scale_y) # fitInView uses the smaller scale
285
+
286
+ # Compensate for the fit scaling to maintain visual thickness
287
+ # When fit_scale is small (content heavily shrunk), we need thicker lines/fonts
288
+ if fit_scale > 0:
289
+ scale_factor = max(0.4, min(4.0, 1.0 / fit_scale))
290
+ else:
291
+ scale_factor = 4.0
292
+
293
+ # Debug info (can be removed in production)
294
+ # print(f"Mol size: {mol_size:.1f}, Fit scale: {fit_scale:.3f}, Scale factor: {scale_factor:.2f}")
295
+ else:
296
+ scale_factor = 1.0
297
+
298
+ # Base sizes that look good at 1:1 scale
299
+ base_bond_width = 1.8
300
+ base_font_size = 11
301
+ base_ellipse_width = 18
302
+ base_ellipse_height = 14
303
+ base_double_bond_offset = 3.5
304
+ base_triple_bond_offset = 2.5
305
+
306
+ # Apply inverse fit scaling to maintain visual consistency
307
+ bond_width = max(1.0, min(8.0, base_bond_width * scale_factor))
308
+ font_size = max(8, min(24, int(base_font_size * scale_factor)))
309
+ ellipse_width = max(10, min(40, base_ellipse_width * scale_factor))
310
+ ellipse_height = max(8, min(30, base_ellipse_height * scale_factor))
311
+ double_bond_offset = max(2.0, min(10.0, base_double_bond_offset * scale_factor))
312
+ triple_bond_offset = max(1.5, min(8.0, base_triple_bond_offset * scale_factor))
313
+
314
+ # Create atom ID to index mapping for bond drawing
315
+ atom_id_to_index = {}
316
+ for i, atom in enumerate(atoms):
317
+ atom_id = atom.get('id', i) # Use id if available, otherwise use index
318
+ atom_id_to_index[atom_id] = i
319
+
320
+ # Draw bonds first using original coordinates with dynamic sizing
321
+ for bond in bonds:
322
+ atom1_id, atom2_id = bond['atom1'], bond['atom2']
323
+
324
+ # Get atom indices from IDs
325
+ atom1_idx = atom_id_to_index.get(atom1_id)
326
+ atom2_idx = atom_id_to_index.get(atom2_id)
327
+
328
+ if atom1_idx is not None and atom2_idx is not None and atom1_idx < len(atoms) and atom2_idx < len(atoms):
329
+ pos1 = QPointF(atoms[atom1_idx]['x'], atoms[atom1_idx]['y'])
330
+ pos2 = QPointF(atoms[atom2_idx]['x'], atoms[atom2_idx]['y'])
331
+
332
+ # Draw bonds with proper order - dynamic thickness
333
+ bond_order = bond.get('order', 1)
334
+ pen = QPen(QColor('black'), bond_width)
335
+
336
+ if bond_order == 2:
337
+ # Double bond - draw two parallel lines
338
+ line = QLineF(pos1, pos2)
339
+ if line.length() > 0:
340
+ normal = line.normalVector()
341
+ normal.setLength(double_bond_offset)
342
+
343
+ line1 = QLineF(pos1 + normal.p2() - normal.p1(), pos2 + normal.p2() - normal.p1())
344
+ line2 = QLineF(pos1 - normal.p2() + normal.p1(), pos2 - normal.p2() + normal.p1())
345
+
346
+ scene.addLine(line1, pen)
347
+ scene.addLine(line2, pen)
348
+ else:
349
+ scene.addLine(line, pen)
350
+ elif bond_order == 3:
351
+ # Triple bond - draw three parallel lines
352
+ line = QLineF(pos1, pos2)
353
+ if line.length() > 0:
354
+ normal = line.normalVector()
355
+ normal.setLength(triple_bond_offset)
356
+
357
+ # Center line
358
+ scene.addLine(line, pen)
359
+ # Side lines
360
+ line1 = QLineF(pos1 + normal.p2() - normal.p1(), pos2 + normal.p2() - normal.p1())
361
+ line2 = QLineF(pos1 - normal.p2() + normal.p1(), pos2 - normal.p2() + normal.p1())
362
+
363
+ scene.addLine(line1, pen)
364
+ scene.addLine(line2, pen)
365
+ else:
366
+ scene.addLine(line, pen)
367
+ else:
368
+ # Single bond
369
+ scene.addLine(QLineF(pos1, pos2), pen)
370
+
371
+ # Draw only non-carbon atom labels with dynamic sizing
372
+ for i, atom in enumerate(atoms):
373
+ try:
374
+ pos = QPointF(atom['x'], atom['y'])
375
+ symbol = atom.get('symbol', 'C')
376
+
377
+ # Draw atoms - white ellipse background to hide bonds, then CPK colored text
378
+ if symbol != 'C':
379
+ # All non-carbon atoms including hydrogen: white background ellipse + CPK colored text
380
+ color = CPK_COLORS.get(symbol, CPK_COLORS.get('DEFAULT', QColor('#FF1493')))
381
+
382
+ # Add white background ellipse to hide bonds - dynamic size
383
+ pen = QPen(Qt.GlobalColor.white, 0) # No border
384
+ brush = QBrush(Qt.GlobalColor.white)
385
+ ellipse_x = pos.x() - ellipse_width/2
386
+ ellipse_y = pos.y() - ellipse_height/2
387
+ ellipse = scene.addEllipse(ellipse_x, ellipse_y, ellipse_width, ellipse_height, pen, brush)
388
+
389
+ # Add CPK colored text label on top - dynamic font size
390
+ font = QFont("Arial", font_size, QFont.Weight.Bold)
391
+ text = scene.addText(symbol, font)
392
+ text.setDefaultTextColor(color) # CPK colored text
393
+ text_rect = text.boundingRect()
394
+ text.setPos(pos.x() - text_rect.width()/2, pos.y() - text_rect.height()/2)
395
+
396
+ except Exception:
397
+ continue
398
+
399
+ def select_template(self, template_data, widget):
400
+ """テンプレートを選択してテンプレートモードに切り替え"""
401
+ # Clear previous selection styling
402
+ for i in range(self.template_layout.count()):
403
+ item = self.template_layout.itemAt(i)
404
+ if item and item.widget():
405
+ item.widget().setStyleSheet("""
406
+ QWidget {
407
+ border: 2px solid #ccc;
408
+ border-radius: 8px;
409
+ background-color: white;
410
+ }
411
+ QWidget:hover {
412
+ border-color: #007acc;
413
+ background-color: #f0f8ff;
414
+ }
415
+ """)
416
+
417
+ # Highlight selected widget - only border, no background change
418
+ widget.setStyleSheet("""
419
+ QWidget {
420
+ border: 3px solid #007acc;
421
+ border-radius: 8px;
422
+ background-color: white;
423
+ }
424
+ """)
425
+
426
+ self.selected_template = template_data
427
+ self.delete_button.setEnabled(True)
428
+
429
+ # Automatically switch to template mode when template is selected
430
+ template_name = template_data.get('name', 'user_template')
431
+ mode_name = f"template_user_{template_name}"
432
+
433
+ # Store template data for the scene to use
434
+ try:
435
+ self.main_window.scene.user_template_data = template_data
436
+ except Exception:
437
+ # Best-effort: ignore if scene or attribute missing
438
+ pass
439
+
440
+ # Force the main window into the template mode.
441
+ # Clear or uncheck any existing mode actions if present to avoid staying in another mode.
442
+ try:
443
+ # Uncheck all mode actions first (if a dict of QAction exists)
444
+ if hasattr(self.main_window, 'mode_actions') and isinstance(self.main_window.mode_actions, dict):
445
+ for act in self.main_window.mode_actions.values():
446
+ try:
447
+ act.setChecked(False)
448
+ except Exception:
449
+ continue
450
+
451
+ # If main_window has a set_mode method, call it. Otherwise, try to set a mode attribute.
452
+ if hasattr(self.main_window, 'set_mode') and callable(self.main_window.set_mode):
453
+ self.main_window.set_mode(mode_name)
454
+ else:
455
+ # Fallback: set an attribute and try to update UI
456
+ setattr(self.main_window, 'mode', mode_name)
457
+
458
+ # Update UI
459
+ try:
460
+ self.main_window.statusBar().showMessage(f"Template mode: {template_name}")
461
+ except Exception:
462
+ # ignore status bar failures
463
+ pass
464
+
465
+ # If there is a matching QAction in mode_actions, check it
466
+ try:
467
+ if hasattr(self.main_window, 'mode_actions') and f"template_user_{template_name}" in self.main_window.mode_actions:
468
+ self.main_window.mode_actions[f"template_user_{template_name}"].setChecked(True)
469
+ except Exception:
470
+ pass
471
+ except Exception as e:
472
+ print(f"Warning: Failed to switch main window to template mode: {e}")
473
+
474
+ def use_template(self, template_data):
475
+ """テンプレートを使用(エディタに適用)"""
476
+ try:
477
+ # Switch to template mode
478
+ template_name = template_data.get('name', 'user_template')
479
+ mode_name = f"template_user_{template_name}"
480
+
481
+ # Store template data for the scene to use
482
+ try:
483
+ self.main_window.scene.user_template_data = template_data
484
+ except Exception:
485
+ pass
486
+
487
+ # Force the main window into the template mode (same approach as select_template)
488
+ try:
489
+ if hasattr(self.main_window, 'mode_actions') and isinstance(self.main_window.mode_actions, dict):
490
+ for act in self.main_window.mode_actions.values():
491
+ try:
492
+ act.setChecked(False)
493
+ except Exception:
494
+ continue
495
+
496
+ if hasattr(self.main_window, 'set_mode') and callable(self.main_window.set_mode):
497
+ self.main_window.set_mode(mode_name)
498
+ else:
499
+ setattr(self.main_window, 'mode', mode_name)
500
+
501
+ try:
502
+ self.main_window.statusBar().showMessage(f"Template mode: {template_name}")
503
+ except Exception:
504
+ pass
505
+
506
+ # Mark selected and keep dialog open
507
+ self.selected_template = template_data
508
+ except Exception as e:
509
+ print(f"Warning: Failed to switch main window to template mode: {e}")
510
+
511
+ # Don't close dialog - keep it open for easy template switching
512
+ # self.accept()
513
+
514
+ except Exception as e:
515
+ QMessageBox.critical(self, "Error", f"Failed to apply template: {str(e)}")
516
+
517
+ def save_current_as_template(self):
518
+ """現在の2D構造をテンプレートとして保存"""
519
+ if not self.main_window.data.atoms:
520
+ QMessageBox.warning(self, "Warning", "No structure to save as template.")
521
+ return
522
+
523
+ # Get template name
524
+ name, ok = QInputDialog.getText(self, "Save Template", "Enter template name:")
525
+ if not ok or not name.strip():
526
+ return
527
+
528
+ name = name.strip()
529
+
530
+ try:
531
+ # Convert current structure to template format
532
+ template_data = self.convert_structure_to_template(name)
533
+
534
+ # Save to file
535
+ filename = f"{name.replace(' ', '_')}.pmetmplt"
536
+ filepath = os.path.join(self.get_template_directory(), filename)
537
+
538
+ if os.path.exists(filepath):
539
+ reply = QMessageBox.question(
540
+ self, "Overwrite Template",
541
+ f"Template '{name}' already exists. Overwrite?",
542
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
543
+ )
544
+ if reply != QMessageBox.StandardButton.Yes:
545
+ return
546
+
547
+ if self.save_template_file(filepath, template_data):
548
+ # Mark main window as saved
549
+ self.main_window.has_unsaved_changes = False
550
+ self.main_window.update_window_title()
551
+
552
+ QMessageBox.information(self, "Success", f"Template '{name}' saved successfully.")
553
+ self.load_user_templates() # Refresh the display
554
+ else:
555
+ QMessageBox.critical(self, "Error", "Failed to save template.")
556
+
557
+ except Exception as e:
558
+ QMessageBox.critical(self, "Error", f"Failed to save template: {str(e)}")
559
+
560
+ def convert_structure_to_template(self, name):
561
+ """現在の構造をテンプレート形式に変換"""
562
+ atoms_data = []
563
+ bonds_data = []
564
+
565
+ # Convert atoms
566
+ for atom_id, atom_info in self.main_window.data.atoms.items():
567
+ pos = atom_info['pos']
568
+ atoms_data.append({
569
+ 'id': atom_id,
570
+ 'symbol': atom_info['symbol'],
571
+ 'x': pos.x(),
572
+ 'y': pos.y(),
573
+ 'charge': atom_info.get('charge', 0),
574
+ 'radical': atom_info.get('radical', 0)
575
+ })
576
+
577
+ # Convert bonds
578
+ for (atom1_id, atom2_id), bond_info in self.main_window.data.bonds.items():
579
+ bonds_data.append({
580
+ 'atom1': atom1_id,
581
+ 'atom2': atom2_id,
582
+ 'order': bond_info['order'],
583
+ 'stereo': bond_info.get('stereo', 0)
584
+ })
585
+
586
+ # Create template data
587
+ template_data = {
588
+ 'format': "PME Template",
589
+ 'version': "1.0",
590
+ 'application': "MoleditPy",
591
+ 'application_version': VERSION,
592
+ 'name': name,
593
+ 'created': str(QDateTime.currentDateTime().toString()),
594
+ 'atoms': atoms_data,
595
+ 'bonds': bonds_data
596
+ }
597
+
598
+ return template_data
599
+
600
+ def delete_selected_template(self):
601
+ """選択されたテンプレートを削除"""
602
+ if not self.selected_template:
603
+ return
604
+
605
+ name = self.selected_template.get('name', 'Unknown')
606
+ reply = QMessageBox.question(
607
+ self, "Delete Template",
608
+ f"Are you sure you want to delete template '{name}'?",
609
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
610
+ )
611
+
612
+ if reply == QMessageBox.StandardButton.Yes:
613
+ try:
614
+ filepath = self.selected_template['filepath']
615
+ os.remove(filepath)
616
+ QMessageBox.information(self, "Success", f"Template '{name}' deleted successfully.")
617
+ self.load_user_templates() # Refresh the display
618
+ self.selected_template = None
619
+ self.delete_button.setEnabled(False)
620
+ except Exception as e:
621
+ QMessageBox.critical(self, "Error", f"Failed to delete template: {str(e)}")