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,1503 @@
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, QTabWidget, QWidget, QFormLayout, QPushButton, QHBoxLayout,
15
+ QCheckBox, QComboBox, QLabel, QColorDialog, QSlider, QFrame, QMessageBox
16
+ )
17
+ from PyQt6.QtWidgets import QApplication
18
+ from PyQt6.QtCore import Qt
19
+ from PyQt6.QtGui import QColor
20
+ try:
21
+ from .constants import CPK_COLORS
22
+ except Exception:
23
+ from modules.constants import CPK_COLORS
24
+
25
+
26
+ class SettingsDialog(QDialog):
27
+ def __init__(self, current_settings, parent=None):
28
+ super().__init__(parent)
29
+ self.setWindowTitle("Settings")
30
+ self.setMinimumSize(700, 800)
31
+
32
+ # 親ウィンドウの参照を保存(Apply機能のため)
33
+ self.parent_window = parent
34
+
35
+ # デフォルト設定をクラス内で定義
36
+ # Multi-bond settings are model-specific now (ball_stick, cpk, wireframe, stick)
37
+ self.default_settings = {
38
+ 'background_color': '#919191',
39
+ 'projection_mode': 'Perspective',
40
+ 'lighting_enabled': True,
41
+ 'specular': 0.20,
42
+ 'specular_power': 20,
43
+ 'light_intensity': 1.0,
44
+ 'show_3d_axes': True,
45
+ # Ball and Stick model parameters
46
+ 'ball_stick_atom_scale': 1.0,
47
+ 'ball_stick_bond_radius': 0.1,
48
+ 'ball_stick_resolution': 16,
49
+ # CPK (Space-filling) model parameters
50
+ 'cpk_atom_scale': 1.0,
51
+ 'cpk_resolution': 32,
52
+ # Wireframe model parameters
53
+ 'wireframe_bond_radius': 0.01,
54
+ 'wireframe_resolution': 6,
55
+ # Stick model parameters
56
+ 'stick_bond_radius': 0.15,
57
+ 'stick_resolution': 16,
58
+ # Multiple bond offset parameters (per-model)
59
+ 'ball_stick_double_bond_offset_factor': 2.0,
60
+ 'ball_stick_triple_bond_offset_factor': 2.0,
61
+ 'ball_stick_double_bond_radius_factor': 0.8,
62
+ 'ball_stick_triple_bond_radius_factor': 0.75,
63
+ 'wireframe_double_bond_offset_factor': 3.0,
64
+ 'wireframe_triple_bond_offset_factor': 3.0,
65
+ 'wireframe_double_bond_radius_factor': 0.8,
66
+ 'wireframe_triple_bond_radius_factor': 0.75,
67
+ 'stick_double_bond_offset_factor': 1.5,
68
+ 'stick_triple_bond_offset_factor': 1.0,
69
+ 'stick_double_bond_radius_factor': 0.6,
70
+ 'stick_triple_bond_radius_factor': 0.4,
71
+ 'aromatic_torus_thickness_factor': 0.6,
72
+ # Whether to draw an aromatic circle inside rings in 3D
73
+ 'display_aromatic_circles_3d': False,
74
+ # If True, attempts to be permissive when RDKit raises chemical/sanitization errors
75
+ # during file import (useful for viewing malformed XYZ/MOL files). When enabled,
76
+ # element symbol recognition will be coerced where possible and Chem.SanitizeMol
77
+ # failures will be ignored so the 3D viewer can still display the structure.
78
+ 'skip_chemistry_checks': False,
79
+ # When True, always prompt the user for molecular charge on XYZ import
80
+ # instead of silently trying charge=0 first. Default True to disable
81
+ # the silent 'charge=0' test.
82
+ 'always_ask_charge': False,
83
+ # 3D conversion/optimization defaults
84
+ '3d_conversion_mode': 'fallback',
85
+ 'optimization_method': 'MMFF_RDKIT',
86
+ 'ball_stick_bond_color': '#7F7F7F',
87
+ 'cpk_colors': {},
88
+ # If True, RDKit will attempt to kekulize aromatic systems for 3D display
89
+ # (shows alternating single/double bonds rather than aromatic circles)
90
+ 'display_kekule_3d': False,
91
+ 'ball_stick_use_cpk_bond_color': False,
92
+
93
+ # --- 2D Settings Defaults ---
94
+ 'bond_width_2d': 2.0,
95
+ 'bond_spacing_double_2d': 3.5,
96
+ 'bond_spacing_triple_2d': 3.5,
97
+ 'atom_font_size_2d': 20,
98
+ 'background_color_2d': '#FFFFFF',
99
+ 'bond_color_2d': '#222222', # Almost black
100
+ 'atom_use_bond_color_2d': False,
101
+ 'bond_cap_style_2d': 'Round',
102
+ }
103
+
104
+ # --- 選択された色を管理する専用のインスタンス変数 ---
105
+ self.current_bg_color = None
106
+
107
+ # --- UI要素の作成 ---
108
+ layout = QVBoxLayout(self)
109
+
110
+ # タブウィジェットを作成
111
+ self.tab_widget = QTabWidget()
112
+ layout.addWidget(self.tab_widget)
113
+
114
+ # 2D Settings Tab (First)
115
+ self.create_2d_settings_tab()
116
+
117
+ # Scene設定タブ
118
+ self.create_scene_tab()
119
+
120
+ # Ball and Stick設定タブ
121
+ self.create_ball_stick_tab()
122
+
123
+ # CPK設定タブ
124
+ self.create_cpk_tab()
125
+
126
+ # Wireframe設定タブ
127
+ self.create_wireframe_tab()
128
+
129
+ # Stick設定タブ
130
+ self.create_stick_tab()
131
+
132
+ # Other設定タブ
133
+ self.create_other_tab()
134
+
135
+ # 渡された設定でUIと内部変数を初期化
136
+ self.update_ui_from_settings(current_settings)
137
+
138
+ # Initialize aromatic circle checkbox and torus thickness from settings
139
+ self.aromatic_circle_checkbox.setChecked(current_settings.get('display_aromatic_circles_3d', self.default_settings.get('display_aromatic_circles_3d', False)))
140
+ # Thickness factor is stored as a multiplier (e.g., 1.0), slider uses integer 0-300 representing 0.1x-3.0x
141
+ thickness_factor = current_settings.get('aromatic_torus_thickness_factor', self.default_settings.get('aromatic_torus_thickness_factor', 1.0))
142
+ self.aromatic_torus_thickness_slider.setValue(int(thickness_factor * 100))
143
+ self.aromatic_torus_thickness_label.setText(f"{thickness_factor:.1f}")
144
+
145
+ # Initialize internal 2D colors
146
+ self.current_bg_color_2d = current_settings.get('background_color_2d', self.default_settings['background_color_2d'])
147
+ self.current_bond_color_2d = current_settings.get('bond_color_2d', self.default_settings['bond_color_2d'])
148
+
149
+ # Apply initial 2D button styles
150
+ self.update_2d_color_buttons()
151
+
152
+ # --- ボタンの配置 ---
153
+ buttons = QHBoxLayout()
154
+
155
+ # タブごとのリセットボタン
156
+ reset_tab_button = QPushButton("Reset Current Tab")
157
+ reset_tab_button.clicked.connect(self.reset_current_tab)
158
+ reset_tab_button.setToolTip("Reset settings for the currently selected tab only")
159
+ buttons.addWidget(reset_tab_button)
160
+
161
+ # 全体リセットボタン
162
+ reset_all_button = QPushButton("Reset All")
163
+ reset_all_button.clicked.connect(self.reset_all_settings)
164
+ reset_all_button.setToolTip("Reset all settings to defaults")
165
+ buttons.addWidget(reset_all_button)
166
+
167
+ buttons.addStretch(1)
168
+
169
+ # Applyボタンを追加
170
+ apply_button = QPushButton("Apply")
171
+ apply_button.clicked.connect(self.apply_settings)
172
+ apply_button.setToolTip("Apply settings without closing dialog")
173
+ buttons.addWidget(apply_button)
174
+
175
+ ok_button = QPushButton("OK")
176
+ cancel_button = QPushButton("Cancel")
177
+ ok_button.clicked.connect(self.accept)
178
+ cancel_button.clicked.connect(self.reject)
179
+
180
+ buttons.addWidget(ok_button)
181
+ buttons.addWidget(cancel_button)
182
+ layout.addLayout(buttons)
183
+
184
+ def create_2d_settings_tab(self):
185
+ """2D Settings Tab"""
186
+ widget = QWidget()
187
+ form_layout = QFormLayout(widget)
188
+
189
+ # --- View Settings ---
190
+ form_layout.addRow(QLabel("<b>View Appearance</b>"))
191
+
192
+ # Background Color
193
+ self.bg_color_2d_button = QPushButton()
194
+ self.bg_color_2d_button.setFixedSize(60, 24)
195
+ self.bg_color_2d_button.clicked.connect(self.pick_bg_color_2d)
196
+ form_layout.addRow("Background Color:", self.bg_color_2d_button)
197
+
198
+ line = QFrame()
199
+ line.setFrameShape(QFrame.Shape.HLine)
200
+ line.setFrameShadow(QFrame.Shadow.Sunken)
201
+ form_layout.addRow(line)
202
+
203
+ # --- Bond Settings ---
204
+ form_layout.addRow(QLabel("<b>Bond Settings</b>"))
205
+
206
+ # Bond Color
207
+ self.bond_color_2d_button = QPushButton()
208
+ self.bond_color_2d_button.setFixedSize(60, 24)
209
+ self.bond_color_2d_button.clicked.connect(self.pick_bond_color_2d)
210
+ form_layout.addRow("Bond Color:", self.bond_color_2d_button)
211
+
212
+ # Bond Width
213
+ self.bond_width_2d_slider = QSlider(Qt.Orientation.Horizontal)
214
+ self.bond_width_2d_slider.setRange(10, 200) # 1.0 - 20.0
215
+ self.bond_width_2d_label = QLabel("2.0")
216
+ self.bond_width_2d_slider.valueChanged.connect(lambda v: self.bond_width_2d_label.setText(f"{v/10:.1f}"))
217
+ bw_layout = QHBoxLayout()
218
+ bw_layout.addWidget(self.bond_width_2d_slider)
219
+ bw_layout.addWidget(self.bond_width_2d_label)
220
+ form_layout.addRow("Bond Width:", bw_layout)
221
+
222
+ # Double Bond Spacing
223
+ self.bond_spacing_double_2d_slider = QSlider(Qt.Orientation.Horizontal)
224
+ self.bond_spacing_double_2d_slider.setRange(10, 200) # 1.0 - 20.0
225
+ self.bond_spacing_double_2d_label = QLabel("3.5")
226
+ self.bond_spacing_double_2d_slider.valueChanged.connect(lambda v: self.bond_spacing_double_2d_label.setText(f"{v/10:.1f}"))
227
+ bsd_layout = QHBoxLayout()
228
+ bsd_layout.addWidget(self.bond_spacing_double_2d_slider)
229
+ bsd_layout.addWidget(self.bond_spacing_double_2d_label)
230
+ form_layout.addRow("Double Bond Spacing:", bsd_layout)
231
+
232
+ # Triple Bond Spacing
233
+ self.bond_spacing_triple_2d_slider = QSlider(Qt.Orientation.Horizontal)
234
+ self.bond_spacing_triple_2d_slider.setRange(10, 200) # 1.0 - 20.0
235
+ self.bond_spacing_triple_2d_label = QLabel("3.5")
236
+ self.bond_spacing_triple_2d_slider.valueChanged.connect(lambda v: self.bond_spacing_triple_2d_label.setText(f"{v/10:.1f}"))
237
+ bst_layout = QHBoxLayout()
238
+ bst_layout.addWidget(self.bond_spacing_triple_2d_slider)
239
+ bst_layout.addWidget(self.bond_spacing_triple_2d_label)
240
+ form_layout.addRow("Triple Bond Spacing:", bst_layout)
241
+
242
+ # Bond Cap Style
243
+ self.bond_cap_style_2d_combo = QComboBox()
244
+ self.bond_cap_style_2d_combo.addItems(['Round', 'Flat', 'Square'])
245
+ form_layout.addRow("Bond Cap Style:", self.bond_cap_style_2d_combo)
246
+
247
+
248
+ line2 = QFrame()
249
+ line2.setFrameShape(QFrame.Shape.HLine)
250
+ line2.setFrameShadow(QFrame.Shadow.Sunken)
251
+ form_layout.addRow(line2)
252
+
253
+ # --- Atom Settings ---
254
+ form_layout.addRow(QLabel("<b>Atom Settings</b>"))
255
+
256
+ # Font Size
257
+ self.atom_font_size_2d_slider = QSlider(Qt.Orientation.Horizontal)
258
+ self.atom_font_size_2d_slider.setRange(8, 72)
259
+ self.atom_font_size_2d_label = QLabel("20")
260
+ self.atom_font_size_2d_slider.valueChanged.connect(lambda v: self.atom_font_size_2d_label.setText(str(v)))
261
+ fs_layout = QHBoxLayout()
262
+ fs_layout.addWidget(self.atom_font_size_2d_slider)
263
+ fs_layout.addWidget(self.atom_font_size_2d_label)
264
+ form_layout.addRow("Atom Label Font Size:", fs_layout)
265
+
266
+ # Use Bond Color Checkbox
267
+ self.atom_use_bond_color_2d_checkbox = QCheckBox()
268
+ self.atom_use_bond_color_2d_checkbox.setToolTip("If checked, atoms will use the unified Bond Color instead of element-specific colors (CPK).")
269
+ form_layout.addRow("Use Bond Color for Atoms:", self.atom_use_bond_color_2d_checkbox)
270
+
271
+ self.tab_widget.addTab(widget, "2D Settings")
272
+
273
+
274
+
275
+ # Initialize internal variable for combo updates
276
+ # (Usually done in update_ui_from_settings, but we add direct access here for simple binding if needed)
277
+ # We rely on update_ui_from_settings to set current selection.
278
+
279
+
280
+ def pick_bg_color_2d(self):
281
+ color = QColorDialog.getColor(QColor(self.current_bg_color_2d), self, "Select 2D Background Color")
282
+ if color.isValid():
283
+ self.current_bg_color_2d = color.name()
284
+ self.update_2d_color_buttons()
285
+
286
+ def pick_bond_color_2d(self):
287
+ color = QColorDialog.getColor(QColor(self.current_bond_color_2d), self, "Select 2D Bond Color")
288
+ if color.isValid():
289
+ self.current_bond_color_2d = color.name()
290
+ self.update_2d_color_buttons()
291
+
292
+ def update_2d_color_buttons(self):
293
+ try:
294
+ self.bg_color_2d_button.setStyleSheet(f"background-color: {self.current_bg_color_2d}; border: 1px solid #888;")
295
+ self.bond_color_2d_button.setStyleSheet(f"background-color: {self.current_bond_color_2d}; border: 1px solid #888;")
296
+ except Exception:
297
+ pass
298
+
299
+ def create_scene_tab(self):
300
+ """基本設定タブを作成"""
301
+ scene_widget = QWidget()
302
+ form_layout = QFormLayout(scene_widget)
303
+
304
+ # 1. 背景色
305
+ self.bg_button = QPushButton()
306
+ self.bg_button.setToolTip("Click to select a color")
307
+ self.bg_button.clicked.connect(self.select_color)
308
+ form_layout.addRow("Background Color:", self.bg_button)
309
+
310
+ # 1a. 軸の表示/非表示
311
+ self.axes_checkbox = QCheckBox()
312
+ form_layout.addRow("Show 3D Axes:", self.axes_checkbox)
313
+
314
+ # 2. ライトの有効/無効
315
+ self.light_checkbox = QCheckBox()
316
+ form_layout.addRow("Enable Lighting:", self.light_checkbox)
317
+
318
+ # 光の強さスライダーを追加
319
+ self.intensity_slider = QSlider(Qt.Orientation.Horizontal)
320
+ self.intensity_slider.setRange(0, 200) # 0.0 ~ 2.0 の範囲
321
+ self.intensity_label = QLabel("1.0")
322
+ self.intensity_slider.valueChanged.connect(lambda v: self.intensity_label.setText(f"{v/100:.2f}"))
323
+ intensity_layout = QHBoxLayout()
324
+ intensity_layout.addWidget(self.intensity_slider)
325
+ intensity_layout.addWidget(self.intensity_label)
326
+ form_layout.addRow("Light Intensity:", intensity_layout)
327
+
328
+ # 3. 光沢 (Specular)
329
+ self.specular_slider = QSlider(Qt.Orientation.Horizontal)
330
+ self.specular_slider.setRange(0, 100)
331
+ self.specular_label = QLabel("0.20")
332
+ self.specular_slider.valueChanged.connect(lambda v: self.specular_label.setText(f"{v/100:.2f}"))
333
+ specular_layout = QHBoxLayout()
334
+ specular_layout.addWidget(self.specular_slider)
335
+ specular_layout.addWidget(self.specular_label)
336
+ form_layout.addRow("Shininess (Specular):", specular_layout)
337
+
338
+ # 4. 光沢の強さ (Specular Power)
339
+ self.spec_power_slider = QSlider(Qt.Orientation.Horizontal)
340
+ self.spec_power_slider.setRange(0, 100)
341
+ self.spec_power_label = QLabel("20")
342
+ self.spec_power_slider.valueChanged.connect(lambda v: self.spec_power_label.setText(str(v)))
343
+ spec_power_layout = QHBoxLayout()
344
+ spec_power_layout.addWidget(self.spec_power_slider)
345
+ spec_power_layout.addWidget(self.spec_power_label)
346
+ form_layout.addRow("Shininess Power:", spec_power_layout)
347
+
348
+ # Projection mode (Perspective / Orthographic)
349
+ self.projection_combo = QComboBox()
350
+ self.projection_combo.addItem("Perspective")
351
+ self.projection_combo.addItem("Orthographic")
352
+ self.projection_combo.setToolTip("Choose camera projection mode: Perspective (default) or Orthographic")
353
+ form_layout.addRow("Projection Mode:", self.projection_combo)
354
+
355
+ self.tab_widget.addTab(scene_widget, "3D Scene")
356
+
357
+ def create_other_tab(self):
358
+ """other設定タブを作成"""
359
+ self.other_widget = QWidget()
360
+ self.other_form_layout = QFormLayout(self.other_widget)
361
+
362
+ # 化学チェックスキップオプション(otherタブに移動)
363
+ self.skip_chem_checks_checkbox = QCheckBox()
364
+ self.skip_chem_checks_checkbox.setToolTip("When enabled, XYZ file import will try to ignore chemical/sanitization errors and allow viewing malformed files.")
365
+ # Immediately persist change to settings when user toggles the checkbox
366
+ try:
367
+ self.skip_chem_checks_checkbox.stateChanged.connect(lambda s: self._on_skip_chem_checks_changed(s))
368
+ except Exception:
369
+ pass
370
+
371
+ # Add the checkbox to the other tab's form
372
+ try:
373
+ self.other_form_layout.addRow("Skip chemistry checks on import XYZ file:", self.skip_chem_checks_checkbox)
374
+ except Exception:
375
+ pass
376
+
377
+ # 3D Kekule display option (under Other) will be added below the
378
+ # 'Always ask molecular charge on import' option so ordering is clear
379
+ # in the UI.
380
+ self.kekule_3d_checkbox = QCheckBox()
381
+ self.kekule_3d_checkbox.setToolTip("When enabled, aromatic bonds will be kekulized in the 3D view (show alternating single/double bonds).")
382
+ # Don't persist kekule state immediately; Apply/OK should commit setting.
383
+ # Always ask charge on XYZ import (skip silent charge=0 test)
384
+ self.always_ask_charge_checkbox = QCheckBox()
385
+ self.always_ask_charge_checkbox.setToolTip("Prompt for overall molecular charge when importing XYZ files instead of silently trying charge=0 first.")
386
+ try:
387
+ self.other_form_layout.addRow("Always ask molecular charge on import XYZ file:", self.always_ask_charge_checkbox)
388
+ except Exception:
389
+ pass
390
+
391
+ # Add separator after Kekule bonds option
392
+ separator = QFrame()
393
+ separator.setFrameShape(QFrame.Shape.HLine)
394
+ separator.setFrameShadow(QFrame.Shadow.Sunken)
395
+ self.other_form_layout.addRow(separator)
396
+
397
+ # Place the Kekulé option after the always-ask-charge option
398
+ try:
399
+ self.other_form_layout.addRow("Display Kekulé bonds in 3D:", self.kekule_3d_checkbox)
400
+ except Exception:
401
+ pass
402
+
403
+
404
+ # Aromatic ring circle display option
405
+ self.aromatic_circle_checkbox = QCheckBox()
406
+ self.aromatic_circle_checkbox.setToolTip("When enabled, aromatic rings will be displayed with a circle inside the ring in 3D view.")
407
+ try:
408
+ self.other_form_layout.addRow("Display aromatic rings as circles in 3D:", self.aromatic_circle_checkbox)
409
+ except Exception:
410
+ pass
411
+
412
+ # Aromatic torus thickness factor
413
+ self.aromatic_torus_thickness_slider = QSlider(Qt.Orientation.Horizontal)
414
+ self.aromatic_torus_thickness_slider.setRange(10, 300) # 0.1x to 3.0x
415
+ self.aromatic_torus_thickness_slider.setValue(60) # Default 0.6x
416
+ self.aromatic_torus_thickness_label = QLabel("0.6")
417
+ self.aromatic_torus_thickness_slider.valueChanged.connect(
418
+ lambda v: self.aromatic_torus_thickness_label.setText(f"{v/100:.1f}")
419
+ )
420
+ thickness_layout = QHBoxLayout()
421
+ thickness_layout.addWidget(self.aromatic_torus_thickness_slider)
422
+ thickness_layout.addWidget(self.aromatic_torus_thickness_label)
423
+ try:
424
+ self.other_form_layout.addRow("Aromatic torus thickness (× bond radius):", thickness_layout)
425
+ except Exception:
426
+ pass
427
+
428
+ # Add Other tab to the tab widget
429
+ self.tab_widget.addTab(self.other_widget, "Other")
430
+
431
+ def refresh_ui(self):
432
+ """Refresh periodic table / BS button visuals using current settings.
433
+
434
+ Called when settings change externally (e.g., Reset All in main settings) so
435
+ the dialog reflects the current stored overrides.
436
+ """
437
+ try:
438
+ # Update element button colors from parent.settings cpks
439
+ overrides = self.parent_window.settings.get('cpk_colors', {}) if self.parent_window and hasattr(self.parent_window, 'settings') else {}
440
+ for s, btn in self.element_buttons.items():
441
+ try:
442
+ override = overrides.get(s)
443
+ q_color = QColor(override) if override else CPK_COLORS.get(s, CPK_COLORS['DEFAULT'])
444
+ brightness = (q_color.red() * 299 + q_color.green() * 587 + q_color.blue() * 114) / 1000
445
+ text_color = 'white' if brightness < 128 else 'black'
446
+ btn.setStyleSheet(f"background-color: {q_color.name()}; color: {text_color}; border: 1px solid #555; font-weight: bold;")
447
+ except Exception:
448
+ pass
449
+ # Update BS color button from parent settings
450
+ try:
451
+ if hasattr(self, 'bs_button') and self.parent_window and hasattr(self.parent_window, 'settings'):
452
+ bs_hex = self.parent_window.settings.get('ball_stick_bond_color', self.parent_window.default_settings.get('ball_stick_bond_color', '#7F7F7F'))
453
+ self.bs_button.setStyleSheet(f"background-color: {bs_hex}; border: 1px solid #888;")
454
+ self.bs_button.setToolTip(bs_hex)
455
+ except Exception:
456
+ pass
457
+ except Exception:
458
+ pass
459
+ # Avoid circular import: import ColorSettingsDialog only inside method using it
460
+
461
+ # NOTE: Multi-bond offset/thickness settings moved to per-model tabs to allow
462
+ # independent configuration for Ball&Stick/CPK/Wireframe/Stick.
463
+
464
+ # 'Other' tab is created in create_other_tab; nothing to do here.
465
+
466
+ def create_ball_stick_tab(self):
467
+ """Ball and Stick設定タブを作成"""
468
+ ball_stick_widget = QWidget()
469
+ form_layout = QFormLayout(ball_stick_widget)
470
+
471
+ info_label = QLabel("Ball & Stick model shows atoms as spheres and bonds as cylinders.")
472
+ info_label.setWordWrap(True)
473
+ info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
474
+ form_layout.addRow(info_label)
475
+
476
+ # 原子サイズスケール
477
+ self.bs_atom_scale_slider = QSlider(Qt.Orientation.Horizontal)
478
+ self.bs_atom_scale_slider.setRange(10, 200) # 0.1 ~ 2.0
479
+ self.bs_atom_scale_label = QLabel("1.00")
480
+ self.bs_atom_scale_slider.valueChanged.connect(lambda v: self.bs_atom_scale_label.setText(f"{v/100:.2f}"))
481
+ atom_scale_layout = QHBoxLayout()
482
+ atom_scale_layout.addWidget(self.bs_atom_scale_slider)
483
+ atom_scale_layout.addWidget(self.bs_atom_scale_label)
484
+ form_layout.addRow("Atom Size Scale:", atom_scale_layout)
485
+
486
+ # ボンド半径
487
+ self.bs_bond_radius_slider = QSlider(Qt.Orientation.Horizontal)
488
+ self.bs_bond_radius_slider.setRange(1, 50) # 0.01 ~ 0.5
489
+ self.bs_bond_radius_label = QLabel("0.10")
490
+ self.bs_bond_radius_slider.valueChanged.connect(lambda v: self.bs_bond_radius_label.setText(f"{v/100:.2f}"))
491
+ bond_radius_layout = QHBoxLayout()
492
+ bond_radius_layout.addWidget(self.bs_bond_radius_slider)
493
+ bond_radius_layout.addWidget(self.bs_bond_radius_label)
494
+ form_layout.addRow("Bond Radius:", bond_radius_layout)
495
+
496
+ # --- 区切り線(水平ライン) ---
497
+ line = QFrame()
498
+ line.setFrameShape(QFrame.Shape.HLine)
499
+ line.setFrameShadow(QFrame.Shadow.Sunken)
500
+ form_layout.addRow(line)
501
+
502
+ # --- Per-model multi-bond controls (Ball & Stick) ---
503
+ # 二重/三重結合のオフセット倍率(Ball & Stick)
504
+ self.bs_double_offset_slider = QSlider(Qt.Orientation.Horizontal)
505
+ self.bs_double_offset_slider.setRange(100, 400)
506
+ self.bs_double_offset_label = QLabel("2.00")
507
+ self.bs_double_offset_slider.valueChanged.connect(lambda v: self.bs_double_offset_label.setText(f"{v/100:.2f}"))
508
+ bs_double_offset_layout = QHBoxLayout()
509
+ bs_double_offset_layout.addWidget(self.bs_double_offset_slider)
510
+ bs_double_offset_layout.addWidget(self.bs_double_offset_label)
511
+ form_layout.addRow("Double Bond Offset (Ball & Stick):", bs_double_offset_layout)
512
+
513
+ self.bs_triple_offset_slider = QSlider(Qt.Orientation.Horizontal)
514
+ self.bs_triple_offset_slider.setRange(100, 400)
515
+ self.bs_triple_offset_label = QLabel("2.00")
516
+ self.bs_triple_offset_slider.valueChanged.connect(lambda v: self.bs_triple_offset_label.setText(f"{v/100:.2f}"))
517
+ bs_triple_offset_layout = QHBoxLayout()
518
+ bs_triple_offset_layout.addWidget(self.bs_triple_offset_slider)
519
+ bs_triple_offset_layout.addWidget(self.bs_triple_offset_label)
520
+ form_layout.addRow("Triple Bond Offset (Ball & Stick):", bs_triple_offset_layout)
521
+
522
+ # 半径倍率
523
+ self.bs_double_radius_slider = QSlider(Qt.Orientation.Horizontal)
524
+ self.bs_double_radius_slider.setRange(50, 100)
525
+ self.bs_double_radius_label = QLabel("0.80")
526
+ self.bs_double_radius_slider.valueChanged.connect(lambda v: self.bs_double_radius_label.setText(f"{v/100:.2f}"))
527
+ bs_double_radius_layout = QHBoxLayout()
528
+ bs_double_radius_layout.addWidget(self.bs_double_radius_slider)
529
+ bs_double_radius_layout.addWidget(self.bs_double_radius_label)
530
+ form_layout.addRow("Double Bond Thickness (Ball & Stick):", bs_double_radius_layout)
531
+
532
+ self.bs_triple_radius_slider = QSlider(Qt.Orientation.Horizontal)
533
+ self.bs_triple_radius_slider.setRange(50, 100)
534
+ self.bs_triple_radius_label = QLabel("0.70")
535
+ self.bs_triple_radius_slider.valueChanged.connect(lambda v: self.bs_triple_radius_label.setText(f"{v/100:.2f}"))
536
+ bs_triple_radius_layout = QHBoxLayout()
537
+ bs_triple_radius_layout.addWidget(self.bs_triple_radius_slider)
538
+ bs_triple_radius_layout.addWidget(self.bs_triple_radius_label)
539
+ form_layout.addRow("Triple Bond Thickness (Ball & Stick):", bs_triple_radius_layout)
540
+
541
+ # --- 区切り線(水平ライン) ---
542
+ line = QFrame()
543
+ line.setFrameShape(QFrame.Shape.HLine)
544
+ line.setFrameShadow(QFrame.Shadow.Sunken)
545
+ form_layout.addRow(line)
546
+
547
+ # 解像度
548
+ self.bs_resolution_slider = QSlider(Qt.Orientation.Horizontal)
549
+ self.bs_resolution_slider.setRange(6, 32)
550
+ self.bs_resolution_label = QLabel("16")
551
+ self.bs_resolution_slider.valueChanged.connect(lambda v: self.bs_resolution_label.setText(str(v)))
552
+ resolution_layout = QHBoxLayout()
553
+ resolution_layout.addWidget(self.bs_resolution_slider)
554
+ resolution_layout.addWidget(self.bs_resolution_label)
555
+ form_layout.addRow("Resolution (Quality):", resolution_layout)
556
+
557
+ # --- 区切り線(水平ライン) ---
558
+ line = QFrame()
559
+ line.setFrameShape(QFrame.Shape.HLine)
560
+ line.setFrameShadow(QFrame.Shadow.Sunken)
561
+ form_layout.addRow(line)
562
+
563
+ # --- Ball & Stick bond color ---
564
+ self.bs_bond_color_button = QPushButton()
565
+ self.bs_bond_color_button.setFixedSize(36, 24)
566
+ self.bs_bond_color_button.clicked.connect(self.pick_bs_bond_color)
567
+ self.bs_bond_color_button.setToolTip("Choose the uniform bond color for Ball & Stick model (3D)")
568
+ form_layout.addRow("Ball & Stick bond color:", self.bs_bond_color_button)
569
+
570
+ # Use CPK colors for bonds option
571
+ self.bs_use_cpk_bond_checkbox = QCheckBox()
572
+ self.bs_use_cpk_bond_checkbox.setToolTip("If checked, bonds will be colored using the atom colors (split bonds). If unchecked, a uniform color is used.")
573
+ form_layout.addRow("Use CPK colors for bonds:", self.bs_use_cpk_bond_checkbox)
574
+
575
+ self.tab_widget.addTab(ball_stick_widget, "Ball & Stick")
576
+
577
+ def create_cpk_tab(self):
578
+ """CPK設定タブを作成"""
579
+ cpk_widget = QWidget()
580
+ form_layout = QFormLayout(cpk_widget)
581
+
582
+ info_label = QLabel("CPK model shows atoms as space-filling spheres using van der Waals radii.")
583
+ info_label.setWordWrap(True)
584
+ info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
585
+ form_layout.addRow(info_label)
586
+
587
+ # 原子サイズスケール
588
+ self.cpk_atom_scale_slider = QSlider(Qt.Orientation.Horizontal)
589
+ self.cpk_atom_scale_slider.setRange(50, 200) # 0.5 ~ 2.0
590
+ self.cpk_atom_scale_label = QLabel("1.00")
591
+ self.cpk_atom_scale_slider.valueChanged.connect(lambda v: self.cpk_atom_scale_label.setText(f"{v/100:.2f}"))
592
+ atom_scale_layout = QHBoxLayout()
593
+ atom_scale_layout.addWidget(self.cpk_atom_scale_slider)
594
+ atom_scale_layout.addWidget(self.cpk_atom_scale_label)
595
+ form_layout.addRow("Atom Size Scale:", atom_scale_layout)
596
+
597
+ # 解像度
598
+ self.cpk_resolution_slider = QSlider(Qt.Orientation.Horizontal)
599
+ self.cpk_resolution_slider.setRange(8, 64)
600
+ self.cpk_resolution_label = QLabel("32")
601
+ self.cpk_resolution_slider.valueChanged.connect(lambda v: self.cpk_resolution_label.setText(str(v)))
602
+ resolution_layout = QHBoxLayout()
603
+ resolution_layout.addWidget(self.cpk_resolution_slider)
604
+ resolution_layout.addWidget(self.cpk_resolution_label)
605
+ form_layout.addRow("Resolution (Quality):", resolution_layout)
606
+
607
+ self.tab_widget.addTab(cpk_widget, "CPK (Space-filling)")
608
+
609
+ def create_wireframe_tab(self):
610
+ """Wireframe設定タブを作成"""
611
+ wireframe_widget = QWidget()
612
+ form_layout = QFormLayout(wireframe_widget)
613
+
614
+ info_label = QLabel("Wireframe model shows molecular structure with thin lines only.")
615
+ info_label.setWordWrap(True)
616
+ info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
617
+ form_layout.addRow(info_label)
618
+
619
+ # ボンド半径
620
+ self.wf_bond_radius_slider = QSlider(Qt.Orientation.Horizontal)
621
+ self.wf_bond_radius_slider.setRange(1, 10) # 0.01 ~ 0.1
622
+ self.wf_bond_radius_label = QLabel("0.01")
623
+ self.wf_bond_radius_slider.valueChanged.connect(lambda v: self.wf_bond_radius_label.setText(f"{v/100:.2f}"))
624
+ bond_radius_layout = QHBoxLayout()
625
+ bond_radius_layout.addWidget(self.wf_bond_radius_slider)
626
+ bond_radius_layout.addWidget(self.wf_bond_radius_label)
627
+ form_layout.addRow("Bond Radius:", bond_radius_layout)
628
+
629
+
630
+ # --- 区切り線(水平ライン) ---
631
+ line = QFrame()
632
+ line.setFrameShape(QFrame.Shape.HLine)
633
+ line.setFrameShadow(QFrame.Shadow.Sunken)
634
+ form_layout.addRow(line)
635
+
636
+ # --- Per-model multi-bond controls (Wireframe) ---
637
+ self.wf_double_offset_slider = QSlider(Qt.Orientation.Horizontal)
638
+ self.wf_double_offset_slider.setRange(100, 400)
639
+ self.wf_double_offset_label = QLabel("2.00")
640
+ self.wf_double_offset_slider.valueChanged.connect(lambda v: self.wf_double_offset_label.setText(f"{v/100:.2f}"))
641
+ wf_double_offset_layout = QHBoxLayout()
642
+ wf_double_offset_layout.addWidget(self.wf_double_offset_slider)
643
+ wf_double_offset_layout.addWidget(self.wf_double_offset_label)
644
+ form_layout.addRow("Double Bond Offset (Wireframe):", wf_double_offset_layout)
645
+
646
+ self.wf_triple_offset_slider = QSlider(Qt.Orientation.Horizontal)
647
+ self.wf_triple_offset_slider.setRange(100, 400)
648
+ self.wf_triple_offset_label = QLabel("2.00")
649
+ self.wf_triple_offset_slider.valueChanged.connect(lambda v: self.wf_triple_offset_label.setText(f"{v/100:.2f}"))
650
+ wf_triple_offset_layout = QHBoxLayout()
651
+ wf_triple_offset_layout.addWidget(self.wf_triple_offset_slider)
652
+ wf_triple_offset_layout.addWidget(self.wf_triple_offset_label)
653
+ form_layout.addRow("Triple Bond Offset (Wireframe):", wf_triple_offset_layout)
654
+
655
+ self.wf_double_radius_slider = QSlider(Qt.Orientation.Horizontal)
656
+ self.wf_double_radius_slider.setRange(50, 100)
657
+ self.wf_double_radius_label = QLabel("0.80")
658
+ self.wf_double_radius_slider.valueChanged.connect(lambda v: self.wf_double_radius_label.setText(f"{v/100:.2f}"))
659
+ wf_double_radius_layout = QHBoxLayout()
660
+ wf_double_radius_layout.addWidget(self.wf_double_radius_slider)
661
+ wf_double_radius_layout.addWidget(self.wf_double_radius_label)
662
+ form_layout.addRow("Double Bond Thickness (Wireframe):", wf_double_radius_layout)
663
+
664
+ self.wf_triple_radius_slider = QSlider(Qt.Orientation.Horizontal)
665
+ self.wf_triple_radius_slider.setRange(50, 100)
666
+ self.wf_triple_radius_label = QLabel("0.70")
667
+ self.wf_triple_radius_slider.valueChanged.connect(lambda v: self.wf_triple_radius_label.setText(f"{v/100:.2f}"))
668
+ wf_triple_radius_layout = QHBoxLayout()
669
+ wf_triple_radius_layout.addWidget(self.wf_triple_radius_slider)
670
+ wf_triple_radius_layout.addWidget(self.wf_triple_radius_label)
671
+ form_layout.addRow("Triple Bond Thickness (Wireframe):", wf_triple_radius_layout)
672
+
673
+ # --- 区切り線(水平ライン) ---
674
+ line = QFrame()
675
+ line.setFrameShape(QFrame.Shape.HLine)
676
+ line.setFrameShadow(QFrame.Shadow.Sunken)
677
+ form_layout.addRow(line)
678
+
679
+ # 解像度
680
+ self.wf_resolution_slider = QSlider(Qt.Orientation.Horizontal)
681
+ self.wf_resolution_slider.setRange(4, 16)
682
+ self.wf_resolution_label = QLabel("6")
683
+ self.wf_resolution_slider.valueChanged.connect(lambda v: self.wf_resolution_label.setText(str(v)))
684
+ resolution_layout = QHBoxLayout()
685
+ resolution_layout.addWidget(self.wf_resolution_slider)
686
+ resolution_layout.addWidget(self.wf_resolution_label)
687
+ form_layout.addRow("Resolution (Quality):", resolution_layout)
688
+
689
+ self.tab_widget.addTab(wireframe_widget, "Wireframe")
690
+
691
+ def create_stick_tab(self):
692
+ """Stick設定タブを作成"""
693
+ stick_widget = QWidget()
694
+ form_layout = QFormLayout(stick_widget)
695
+
696
+ info_label = QLabel("Stick model shows bonds as thick cylinders with atoms as small spheres.")
697
+ info_label.setWordWrap(True)
698
+ info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
699
+ form_layout.addRow(info_label)
700
+
701
+ # ボンド半径(原子半径も同じ値を使用)
702
+ self.stick_bond_radius_slider = QSlider(Qt.Orientation.Horizontal)
703
+ self.stick_bond_radius_slider.setRange(5, 50) # 0.05 ~ 0.5
704
+ self.stick_bond_radius_label = QLabel("0.15")
705
+ self.stick_bond_radius_slider.valueChanged.connect(lambda v: self.stick_bond_radius_label.setText(f"{v/100:.2f}"))
706
+ bond_radius_layout = QHBoxLayout()
707
+ bond_radius_layout.addWidget(self.stick_bond_radius_slider)
708
+ bond_radius_layout.addWidget(self.stick_bond_radius_label)
709
+ form_layout.addRow("Bond Radius:", bond_radius_layout)
710
+
711
+ # --- 区切り線(水平ライン) ---
712
+ line = QFrame()
713
+ line.setFrameShape(QFrame.Shape.HLine)
714
+ line.setFrameShadow(QFrame.Shadow.Sunken)
715
+ form_layout.addRow(line)
716
+
717
+ # --- Per-model multi-bond controls (Stick) ---
718
+ self.stick_double_offset_slider = QSlider(Qt.Orientation.Horizontal)
719
+ self.stick_double_offset_slider.setRange(50, 400)
720
+ self.stick_double_offset_label = QLabel("2.00")
721
+ self.stick_double_offset_slider.valueChanged.connect(lambda v: self.stick_double_offset_label.setText(f"{v/100:.2f}"))
722
+ stick_double_offset_layout = QHBoxLayout()
723
+ stick_double_offset_layout.addWidget(self.stick_double_offset_slider)
724
+ stick_double_offset_layout.addWidget(self.stick_double_offset_label)
725
+ form_layout.addRow("Double Bond Offset (Stick):", stick_double_offset_layout)
726
+
727
+ self.stick_triple_offset_slider = QSlider(Qt.Orientation.Horizontal)
728
+ self.stick_triple_offset_slider.setRange(50, 400)
729
+ self.stick_triple_offset_label = QLabel("2.00")
730
+ self.stick_triple_offset_slider.valueChanged.connect(lambda v: self.stick_triple_offset_label.setText(f"{v/100:.2f}"))
731
+ stick_triple_offset_layout = QHBoxLayout()
732
+ stick_triple_offset_layout.addWidget(self.stick_triple_offset_slider)
733
+ stick_triple_offset_layout.addWidget(self.stick_triple_offset_label)
734
+ form_layout.addRow("Triple Bond Offset (Stick):", stick_triple_offset_layout)
735
+
736
+ self.stick_double_radius_slider = QSlider(Qt.Orientation.Horizontal)
737
+ self.stick_double_radius_slider.setRange(20, 100)
738
+ self.stick_double_radius_label = QLabel("0.80")
739
+ self.stick_double_radius_slider.valueChanged.connect(lambda v: self.stick_double_radius_label.setText(f"{v/100:.2f}"))
740
+ stick_double_radius_layout = QHBoxLayout()
741
+ stick_double_radius_layout.addWidget(self.stick_double_radius_slider)
742
+ stick_double_radius_layout.addWidget(self.stick_double_radius_label)
743
+ form_layout.addRow("Double Bond Thickness (Stick):", stick_double_radius_layout)
744
+
745
+ self.stick_triple_radius_slider = QSlider(Qt.Orientation.Horizontal)
746
+ self.stick_triple_radius_slider.setRange(20, 100)
747
+ self.stick_triple_radius_label = QLabel("0.70")
748
+ self.stick_triple_radius_slider.valueChanged.connect(lambda v: self.stick_triple_radius_label.setText(f"{v/100:.2f}"))
749
+ stick_triple_radius_layout = QHBoxLayout()
750
+ stick_triple_radius_layout.addWidget(self.stick_triple_radius_slider)
751
+ stick_triple_radius_layout.addWidget(self.stick_triple_radius_label)
752
+ form_layout.addRow("Triple Bond Thickness (Stick):", stick_triple_radius_layout)
753
+
754
+ # --- 区切り線(水平ライン) ---
755
+ line = QFrame()
756
+ line.setFrameShape(QFrame.Shape.HLine)
757
+ line.setFrameShadow(QFrame.Shadow.Sunken)
758
+ form_layout.addRow(line)
759
+
760
+ # 解像度
761
+ self.stick_resolution_slider = QSlider(Qt.Orientation.Horizontal)
762
+ self.stick_resolution_slider.setRange(6, 32)
763
+ self.stick_resolution_label = QLabel("16")
764
+ self.stick_resolution_slider.valueChanged.connect(lambda v: self.stick_resolution_label.setText(str(v)))
765
+ resolution_layout = QHBoxLayout()
766
+ resolution_layout.addWidget(self.stick_resolution_slider)
767
+ resolution_layout.addWidget(self.stick_resolution_label)
768
+ form_layout.addRow("Resolution (Quality):", resolution_layout)
769
+
770
+
771
+
772
+
773
+ self.tab_widget.addTab(stick_widget, "Stick")
774
+
775
+ def reset_current_tab(self):
776
+ """現在選択されているタブの設定のみをデフォルトに戻す"""
777
+ current_tab_index = self.tab_widget.currentIndex()
778
+ tab_name = self.tab_widget.tabText(current_tab_index)
779
+
780
+ # 各タブの設定項目を定義
781
+ # Each tab settings
782
+ # Note: tab labels must match those added to the QTabWidget ("2D Settings", "Scene", "Ball & Stick", ...
783
+ # "CPK (Space-filling)", "Wireframe", "Stick", "Other"). Use the per-model
784
+ # multi-bond keys present in self.default_settings.
785
+ tab_settings = {
786
+ "2D Settings": {
787
+ 'bond_width_2d': self.default_settings['bond_width_2d'],
788
+ 'bond_spacing_double_2d': self.default_settings['bond_spacing_double_2d'],
789
+ 'bond_spacing_triple_2d': self.default_settings['bond_spacing_triple_2d'],
790
+ 'atom_font_size_2d': self.default_settings['atom_font_size_2d'],
791
+ 'background_color_2d': self.default_settings['background_color_2d'],
792
+ 'bond_color_2d': self.default_settings['bond_color_2d'],
793
+ 'atom_use_bond_color_2d': self.default_settings['atom_use_bond_color_2d'],
794
+ 'bond_cap_style_2d': self.default_settings['bond_cap_style_2d']
795
+ },
796
+ "3D Scene": {
797
+ 'background_color': self.default_settings['background_color'],
798
+ 'projection_mode': self.default_settings['projection_mode'],
799
+ 'show_3d_axes': self.default_settings['show_3d_axes'],
800
+ 'lighting_enabled': self.default_settings['lighting_enabled'],
801
+ 'light_intensity': self.default_settings['light_intensity'],
802
+ 'specular': self.default_settings['specular'],
803
+ 'specular_power': self.default_settings['specular_power']
804
+ },
805
+ "Other": {
806
+ # other options
807
+ 'skip_chemistry_checks': self.default_settings.get('skip_chemistry_checks', False),
808
+ 'display_kekule_3d': self.default_settings.get('display_kekule_3d', False),
809
+ 'always_ask_charge': self.default_settings.get('always_ask_charge', False),
810
+ 'display_aromatic_circles_3d': self.default_settings.get('display_aromatic_circles_3d', False),
811
+ 'aromatic_torus_thickness_factor': self.default_settings.get('aromatic_torus_thickness_factor', 0.6),
812
+ },
813
+ "Ball & Stick": {
814
+ 'ball_stick_atom_scale': self.default_settings['ball_stick_atom_scale'],
815
+ 'ball_stick_bond_radius': self.default_settings['ball_stick_bond_radius'],
816
+ 'ball_stick_resolution': self.default_settings['ball_stick_resolution'],
817
+ 'ball_stick_double_bond_offset_factor': self.default_settings.get('ball_stick_double_bond_offset_factor', 2.0),
818
+ 'ball_stick_triple_bond_offset_factor': self.default_settings.get('ball_stick_triple_bond_offset_factor', 2.0),
819
+ 'ball_stick_double_bond_radius_factor': self.default_settings.get('ball_stick_double_bond_radius_factor', 0.8),
820
+ 'ball_stick_triple_bond_radius_factor': self.default_settings.get('ball_stick_triple_bond_radius_factor', 0.75),
821
+ 'ball_stick_use_cpk_bond_color': self.default_settings['ball_stick_use_cpk_bond_color'],
822
+ 'ball_stick_bond_color': self.default_settings.get('ball_stick_bond_color', '#7F7F7F')
823
+ },
824
+ "CPK (Space-filling)": {
825
+ 'cpk_atom_scale': self.default_settings['cpk_atom_scale'],
826
+ 'cpk_resolution': self.default_settings['cpk_resolution'],
827
+ 'cpk_colors': {}
828
+ },
829
+ "Wireframe": {
830
+ 'wireframe_bond_radius': self.default_settings['wireframe_bond_radius'],
831
+ 'wireframe_resolution': self.default_settings['wireframe_resolution'],
832
+ 'wireframe_double_bond_offset_factor': self.default_settings.get('wireframe_double_bond_offset_factor', 3.0),
833
+ 'wireframe_triple_bond_offset_factor': self.default_settings.get('wireframe_triple_bond_offset_factor', 3.0),
834
+ 'wireframe_double_bond_radius_factor': self.default_settings.get('wireframe_double_bond_radius_factor', 0.8),
835
+ 'wireframe_triple_bond_radius_factor': self.default_settings.get('wireframe_triple_bond_radius_factor', 0.75)
836
+ },
837
+ "Stick": {
838
+ 'stick_bond_radius': self.default_settings['stick_bond_radius'],
839
+ 'stick_resolution': self.default_settings['stick_resolution'],
840
+ 'stick_double_bond_offset_factor': self.default_settings.get('stick_double_bond_offset_factor', 1.5),
841
+ 'stick_triple_bond_offset_factor': self.default_settings.get('stick_triple_bond_offset_factor', 1.0),
842
+ 'stick_double_bond_radius_factor': self.default_settings.get('stick_double_bond_radius_factor', 0.6),
843
+ 'stick_triple_bond_radius_factor': self.default_settings.get('stick_triple_bond_radius_factor', 0.4)
844
+ }
845
+ }
846
+
847
+ # 選択されたタブの設定のみを適用
848
+ if tab_name in tab_settings:
849
+ tab_defaults = tab_settings[tab_name]
850
+
851
+ # 現在の設定を取得
852
+ current_settings = self.get_current_ui_settings()
853
+
854
+ # 選択されたタブの項目のみをデフォルト値で更新
855
+ updated_settings = current_settings.copy()
856
+ updated_settings.update(tab_defaults)
857
+
858
+ # UIを更新
859
+ self.update_ui_from_settings(updated_settings)
860
+
861
+ # If 2D settings were reset, update internal color variables too
862
+ if tab_name == "2D Settings":
863
+ self.current_bg_color_2d = updated_settings.get('background_color_2d', self.default_settings['background_color_2d'])
864
+ self.current_bond_color_2d = updated_settings.get('bond_color_2d', self.default_settings['bond_color_2d'])
865
+ self.update_2d_color_buttons()
866
+
867
+ # CPK tab: do not change parent/settings immediately; let Apply/OK persist any changes
868
+
869
+ # ユーザーへのフィードバック
870
+ QMessageBox.information(self, "Reset Complete", f"Settings for '{tab_name}' tab have been reset to defaults.")
871
+ else:
872
+ QMessageBox.warning(self, "Error", f"Unknown tab: {tab_name}")
873
+
874
+ def reset_all_settings(self):
875
+ """すべての設定をデフォルトに戻す"""
876
+ reply = QMessageBox.question(
877
+ self,
878
+ "Reset All Settings",
879
+ "Are you sure you want to reset all settings to defaults?",
880
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
881
+ QMessageBox.StandardButton.No
882
+ )
883
+
884
+ if reply == QMessageBox.StandardButton.Yes:
885
+ # Update the dialog UI
886
+ self.update_ui_from_settings(self.default_settings)
887
+
888
+ # Also persist defaults to the application-level settings if parent is available
889
+ try:
890
+ if self.parent_window and hasattr(self.parent_window, 'settings'):
891
+ # Update parent settings and save
892
+ self.parent_window.settings.update(self.default_settings)
893
+ # defer writing to disk; mark dirty so closeEvent will persist
894
+ try:
895
+ self.parent_window.settings_dirty = True
896
+ except Exception:
897
+ pass
898
+ # Also ensure color settings return to defaults and UI reflects them
899
+ try:
900
+ # Remove any CPK overrides to restore defaults
901
+ if 'cpk_colors' in self.parent_window.settings:
902
+ # Reset to defaults (empty override dict)
903
+ self.parent_window.settings['cpk_colors'] = {}
904
+
905
+ # Reset 2D Settings (colors need manual refresh in parent scene/view usually handled by apply)
906
+
907
+ # Reset 3D Ball & Stick uniform bond color to default
908
+ self.parent_window.settings['ball_stick_bond_color'] = self.default_settings.get('ball_stick_bond_color', '#7F7F7F')
909
+ # Update global CPK colors and reapply 3D settings immediately
910
+ try:
911
+ self.parent_window.update_cpk_colors_from_settings()
912
+ except Exception:
913
+ pass
914
+ try:
915
+ self.parent_window.apply_3d_settings()
916
+ except Exception:
917
+ pass
918
+ # Re-draw current 3D molecule if any
919
+ try:
920
+ if hasattr(self.parent_window, 'current_mol') and self.parent_window.current_mol:
921
+ self.parent_window.draw_molecule_3d(self.parent_window.current_mol)
922
+ except Exception:
923
+ pass
924
+ # Update 2D scene items to reflect color reset
925
+ try:
926
+ if hasattr(self.parent_window, 'scene'):
927
+ for it in self.parent_window.scene.items():
928
+ try:
929
+ if hasattr(it, 'update_style'):
930
+ it.update_style()
931
+ except Exception:
932
+ pass
933
+ except Exception:
934
+ pass
935
+ # Mark settings dirty so they'll be saved on exit
936
+ try:
937
+ self.parent_window.settings_dirty = True
938
+ except Exception:
939
+ pass
940
+ except Exception:
941
+ pass
942
+
943
+ # Refresh parent's optimization and conversion menu/action states
944
+ try:
945
+ # Optimization method
946
+ if hasattr(self.parent_window, 'optimization_method'):
947
+ self.parent_window.optimization_method = self.parent_window.settings.get('optimization_method', 'MMFF_RDKIT')
948
+ if hasattr(self.parent_window, 'opt3d_actions'):
949
+ for k, act in self.parent_window.opt3d_actions.items():
950
+ try:
951
+ act.setChecked(k.upper() == (self.parent_window.optimization_method or '').upper())
952
+ except Exception:
953
+ pass
954
+
955
+ # Conversion mode
956
+ conv_mode = self.parent_window.settings.get('3d_conversion_mode', 'fallback')
957
+ if hasattr(self.parent_window, 'conv_actions'):
958
+ for k, act in self.parent_window.conv_actions.items():
959
+ try:
960
+ act.setChecked(k == conv_mode)
961
+ except Exception:
962
+ pass
963
+ except Exception:
964
+ pass
965
+ except Exception:
966
+ pass
967
+
968
+ QMessageBox.information(self, "Reset Complete", "All settings have been reset to defaults.")
969
+
970
+ def get_current_ui_settings(self):
971
+ """現在のUIから設定値を取得"""
972
+ return {
973
+ 'background_color': self.current_bg_color,
974
+ 'show_3d_axes': self.axes_checkbox.isChecked(),
975
+ 'lighting_enabled': self.light_checkbox.isChecked(),
976
+ 'light_intensity': self.intensity_slider.value() / 100.0,
977
+ 'specular': self.specular_slider.value() / 100.0,
978
+ 'specular_power': self.spec_power_slider.value(),
979
+ # Ball and Stick settings
980
+ 'ball_stick_atom_scale': self.bs_atom_scale_slider.value() / 100.0,
981
+ 'ball_stick_bond_radius': self.bs_bond_radius_slider.value() / 100.0,
982
+ 'ball_stick_resolution': self.bs_resolution_slider.value(),
983
+ # CPK settings
984
+ 'cpk_atom_scale': self.cpk_atom_scale_slider.value() / 100.0,
985
+ 'cpk_resolution': self.cpk_resolution_slider.value(),
986
+ # Wireframe settings
987
+ 'wireframe_bond_radius': self.wf_bond_radius_slider.value() / 100.0,
988
+ 'wireframe_resolution': self.wf_resolution_slider.value(),
989
+ # Stick settings
990
+ 'stick_bond_radius': self.stick_bond_radius_slider.value() / 100.0,
991
+ 'stick_resolution': self.stick_resolution_slider.value(),
992
+ # Multi-bond settings (per-model)
993
+ 'ball_stick_double_bond_offset_factor': self.bs_double_offset_slider.value() / 100.0,
994
+ 'ball_stick_triple_bond_offset_factor': self.bs_triple_offset_slider.value() / 100.0,
995
+ 'ball_stick_double_bond_radius_factor': self.bs_double_radius_slider.value() / 100.0,
996
+ 'ball_stick_triple_bond_radius_factor': self.bs_triple_radius_slider.value() / 100.0,
997
+ 'wireframe_double_bond_offset_factor': self.wf_double_offset_slider.value() / 100.0,
998
+ 'wireframe_triple_bond_offset_factor': self.wf_triple_offset_slider.value() / 100.0,
999
+ 'wireframe_double_bond_radius_factor': self.wf_double_radius_slider.value() / 100.0,
1000
+ 'wireframe_triple_bond_radius_factor': self.wf_triple_radius_slider.value() / 100.0,
1001
+ 'stick_double_bond_offset_factor': self.stick_double_offset_slider.value() / 100.0,
1002
+ 'stick_triple_bond_offset_factor': self.stick_triple_offset_slider.value() / 100.0,
1003
+ 'stick_double_bond_radius_factor': self.stick_double_radius_slider.value() / 100.0,
1004
+ 'stick_triple_bond_radius_factor': self.stick_triple_radius_slider.value() / 100.0,
1005
+ # Projection mode
1006
+ 'projection_mode': self.projection_combo.currentText(),
1007
+ # Kekule / aromatic / torus settings
1008
+ 'display_kekule_3d': self.kekule_3d_checkbox.isChecked(),
1009
+ 'display_aromatic_circles_3d': self.aromatic_circle_checkbox.isChecked(),
1010
+ 'aromatic_torus_thickness_factor': self.aromatic_torus_thickness_slider.value() / 100.0,
1011
+ 'skip_chemistry_checks': self.skip_chem_checks_checkbox.isChecked(),
1012
+ 'always_ask_charge': self.always_ask_charge_checkbox.isChecked(),
1013
+ # Ball & Stick bond color and use-cpk option
1014
+ 'ball_stick_bond_color': getattr(self, 'bs_bond_color', self.default_settings.get('ball_stick_bond_color')),
1015
+ 'ball_stick_use_cpk_bond_color': self.bs_use_cpk_bond_checkbox.isChecked(),
1016
+ }
1017
+
1018
+ def reset_to_defaults(self):
1019
+ """UIをデフォルト設定に戻す(後方互換性のため残存)"""
1020
+ self.reset_all_settings()
1021
+
1022
+ def update_ui_from_settings(self, settings_dict):
1023
+ # 基本設定
1024
+ self.current_bg_color = settings_dict.get('background_color', self.default_settings['background_color'])
1025
+ self.update_color_button(self.current_bg_color)
1026
+ self.axes_checkbox.setChecked(settings_dict.get('show_3d_axes', self.default_settings['show_3d_axes']))
1027
+ self.light_checkbox.setChecked(settings_dict.get('lighting_enabled', self.default_settings['lighting_enabled']))
1028
+
1029
+ # スライダーの値を設定
1030
+ intensity_val = int(settings_dict.get('light_intensity', self.default_settings['light_intensity']) * 100)
1031
+ self.intensity_slider.setValue(intensity_val)
1032
+ self.intensity_label.setText(f"{intensity_val/100:.2f}")
1033
+
1034
+ specular_val = int(settings_dict.get('specular', self.default_settings['specular']) * 100)
1035
+ self.specular_slider.setValue(specular_val)
1036
+ self.specular_label.setText(f"{specular_val/100:.2f}")
1037
+
1038
+ self.spec_power_slider.setValue(settings_dict.get('specular_power', self.default_settings['specular_power']))
1039
+ self.spec_power_label.setText(str(settings_dict.get('specular_power', self.default_settings['specular_power'])))
1040
+
1041
+ # Ball and Stick設定
1042
+ bs_atom_scale = int(settings_dict.get('ball_stick_atom_scale', self.default_settings['ball_stick_atom_scale']) * 100)
1043
+ self.bs_atom_scale_slider.setValue(bs_atom_scale)
1044
+ self.bs_atom_scale_label.setText(f"{bs_atom_scale/100:.2f}")
1045
+
1046
+ bs_bond_radius = int(settings_dict.get('ball_stick_bond_radius', self.default_settings['ball_stick_bond_radius']) * 100)
1047
+ self.bs_bond_radius_slider.setValue(bs_bond_radius)
1048
+ self.bs_bond_radius_label.setText(f"{bs_bond_radius/100:.2f}")
1049
+
1050
+ self.bs_resolution_slider.setValue(settings_dict.get('ball_stick_resolution', self.default_settings['ball_stick_resolution']))
1051
+ self.bs_resolution_label.setText(str(settings_dict.get('ball_stick_resolution', self.default_settings['ball_stick_resolution'])))
1052
+ # Ball & Stick bond color (uniform gray color for ball-and-stick)
1053
+ bs_bond_color = settings_dict.get('ball_stick_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F'))
1054
+ try:
1055
+ self.bs_bond_color = QColor(bs_bond_color).name()
1056
+ except Exception:
1057
+ self.bs_bond_color = self.default_settings.get('ball_stick_bond_color', '#7F7F7F')
1058
+ # Ensure color button exists and update its appearance
1059
+ try:
1060
+ if hasattr(self, 'bs_bond_color_button'):
1061
+ self.bs_bond_color_button.setStyleSheet(f"background-color: {self.bs_bond_color}; border: 1px solid #888;")
1062
+ try:
1063
+ self.bs_bond_color_button.setToolTip(self.bs_bond_color)
1064
+ except Exception:
1065
+ pass
1066
+ except Exception:
1067
+ pass
1068
+
1069
+ # Ball & Stick CPK bond color option
1070
+ self.bs_use_cpk_bond_checkbox.setChecked(settings_dict.get('ball_stick_use_cpk_bond_color', self.default_settings['ball_stick_use_cpk_bond_color']))
1071
+
1072
+ # CPK設定
1073
+ cpk_atom_scale = int(settings_dict.get('cpk_atom_scale', self.default_settings['cpk_atom_scale']) * 100)
1074
+ self.cpk_atom_scale_slider.setValue(cpk_atom_scale)
1075
+ self.cpk_atom_scale_label.setText(f"{cpk_atom_scale/100:.2f}")
1076
+
1077
+ self.cpk_resolution_slider.setValue(settings_dict.get('cpk_resolution', self.default_settings['cpk_resolution']))
1078
+ self.cpk_resolution_label.setText(str(settings_dict.get('cpk_resolution', self.default_settings['cpk_resolution'])))
1079
+
1080
+ # Wireframe設定
1081
+ wf_bond_radius = int(settings_dict.get('wireframe_bond_radius', self.default_settings['wireframe_bond_radius']) * 100)
1082
+ self.wf_bond_radius_slider.setValue(wf_bond_radius)
1083
+ self.wf_bond_radius_label.setText(f"{wf_bond_radius/100:.2f}")
1084
+
1085
+ self.wf_resolution_slider.setValue(settings_dict.get('wireframe_resolution', self.default_settings['wireframe_resolution']))
1086
+ self.wf_resolution_label.setText(str(settings_dict.get('wireframe_resolution', self.default_settings['wireframe_resolution'])))
1087
+
1088
+ # Stick設定
1089
+ stick_bond_radius = int(settings_dict.get('stick_bond_radius', self.default_settings['stick_bond_radius']) * 100)
1090
+ self.stick_bond_radius_slider.setValue(stick_bond_radius)
1091
+ self.stick_bond_radius_label.setText(f"{stick_bond_radius/100:.2f}")
1092
+
1093
+ self.stick_resolution_slider.setValue(settings_dict.get('stick_resolution', self.default_settings['stick_resolution']))
1094
+ self.stick_resolution_label.setText(str(settings_dict.get('stick_resolution', self.default_settings['stick_resolution'])))
1095
+
1096
+ # 多重結合設定(モデル毎) — 後方互換のため既存のグローバルキーがあればフォールバック
1097
+ # Ball & Stick
1098
+ bs_double_offset = int(settings_dict.get('ball_stick_double_bond_offset_factor', settings_dict.get('double_bond_offset_factor', self.default_settings.get('ball_stick_double_bond_offset_factor', 2.0))) * 100)
1099
+ self.bs_double_offset_slider.setValue(bs_double_offset)
1100
+ self.bs_double_offset_label.setText(f"{bs_double_offset/100:.2f}")
1101
+
1102
+ bs_triple_offset = int(settings_dict.get('ball_stick_triple_bond_offset_factor', settings_dict.get('triple_bond_offset_factor', self.default_settings.get('ball_stick_triple_bond_offset_factor', 2.0))) * 100)
1103
+ self.bs_triple_offset_slider.setValue(bs_triple_offset)
1104
+ self.bs_triple_offset_label.setText(f"{bs_triple_offset/100:.2f}")
1105
+
1106
+ bs_double_radius = int(settings_dict.get('ball_stick_double_bond_radius_factor', settings_dict.get('double_bond_radius_factor', self.default_settings.get('ball_stick_double_bond_radius_factor', 1.0))) * 100)
1107
+ self.bs_double_radius_slider.setValue(bs_double_radius)
1108
+ self.bs_double_radius_label.setText(f"{bs_double_radius/100:.2f}")
1109
+
1110
+ bs_triple_radius = int(settings_dict.get('ball_stick_triple_bond_radius_factor', settings_dict.get('triple_bond_radius_factor', self.default_settings.get('ball_stick_triple_bond_radius_factor', 0.75))) * 100)
1111
+ self.bs_triple_radius_slider.setValue(bs_triple_radius)
1112
+ self.bs_triple_radius_label.setText(f"{bs_triple_radius/100:.2f}")
1113
+
1114
+ # Wireframe
1115
+ wf_double_offset = int(settings_dict.get('wireframe_double_bond_offset_factor', settings_dict.get('double_bond_offset_factor', self.default_settings.get('wireframe_double_bond_offset_factor', 3.0))) * 100)
1116
+ self.wf_double_offset_slider.setValue(wf_double_offset)
1117
+ self.wf_double_offset_label.setText(f"{wf_double_offset/100:.2f}")
1118
+
1119
+ wf_triple_offset = int(settings_dict.get('wireframe_triple_bond_offset_factor', settings_dict.get('triple_bond_offset_factor', self.default_settings.get('wireframe_triple_bond_offset_factor', 3.0))) * 100)
1120
+ self.wf_triple_offset_slider.setValue(wf_triple_offset)
1121
+ self.wf_triple_offset_label.setText(f"{wf_triple_offset/100:.2f}")
1122
+
1123
+ wf_double_radius = int(settings_dict.get('wireframe_double_bond_radius_factor', settings_dict.get('double_bond_radius_factor', self.default_settings.get('wireframe_double_bond_radius_factor', 1.0))) * 100)
1124
+ self.wf_double_radius_slider.setValue(wf_double_radius)
1125
+ self.wf_double_radius_label.setText(f"{wf_double_radius/100:.2f}")
1126
+
1127
+ wf_triple_radius = int(settings_dict.get('wireframe_triple_bond_radius_factor', settings_dict.get('triple_bond_radius_factor', self.default_settings.get('wireframe_triple_bond_radius_factor', 0.75))) * 100)
1128
+ self.wf_triple_radius_slider.setValue(wf_triple_radius)
1129
+ self.wf_triple_radius_label.setText(f"{wf_triple_radius/100:.2f}")
1130
+
1131
+ # Stick
1132
+ stick_double_offset = int(settings_dict.get('stick_double_bond_offset_factor', settings_dict.get('double_bond_offset_factor', self.default_settings.get('stick_double_bond_offset_factor', 1.5))) * 100)
1133
+ self.stick_double_offset_slider.setValue(stick_double_offset)
1134
+ self.stick_double_offset_label.setText(f"{stick_double_offset/100:.2f}")
1135
+
1136
+ stick_triple_offset = int(settings_dict.get('stick_triple_bond_offset_factor', settings_dict.get('triple_bond_offset_factor', self.default_settings.get('stick_triple_bond_offset_factor', 1.0))) * 100)
1137
+ self.stick_triple_offset_slider.setValue(stick_triple_offset)
1138
+ self.stick_triple_offset_label.setText(f"{stick_triple_offset/100:.2f}")
1139
+
1140
+ stick_double_radius = int(settings_dict.get('stick_double_bond_radius_factor', settings_dict.get('double_bond_radius_factor', self.default_settings.get('stick_double_bond_radius_factor', 0.60))) * 100)
1141
+ self.stick_double_radius_slider.setValue(stick_double_radius)
1142
+ self.stick_double_radius_label.setText(f"{stick_double_radius/100:.2f}")
1143
+
1144
+ stick_triple_radius = int(settings_dict.get('stick_triple_bond_radius_factor', settings_dict.get('triple_bond_radius_factor', self.default_settings.get('stick_triple_bond_radius_factor', 0.40))) * 100)
1145
+ self.stick_triple_radius_slider.setValue(stick_triple_radius)
1146
+ self.stick_triple_radius_label.setText(f"{stick_triple_radius/100:.2f}")
1147
+
1148
+ # Projection mode
1149
+ proj_mode = settings_dict.get('projection_mode', self.default_settings.get('projection_mode', 'Perspective'))
1150
+ idx = self.projection_combo.findText(proj_mode)
1151
+ self.projection_combo.setCurrentIndex(idx if idx != -1 else 0)
1152
+ # skip chemistry checks
1153
+ self.skip_chem_checks_checkbox.setChecked(settings_dict.get('skip_chemistry_checks', self.default_settings.get('skip_chemistry_checks', False)))
1154
+ # kekule setting
1155
+ self.kekule_3d_checkbox.setChecked(settings_dict.get('display_kekule_3d', self.default_settings.get('display_kekule_3d', False)))
1156
+ # always ask for charge on XYZ imports
1157
+ self.always_ask_charge_checkbox.setChecked(settings_dict.get('always_ask_charge', self.default_settings.get('always_ask_charge', False)))
1158
+ # Aromatic ring circle display and torus thickness factor
1159
+ self.aromatic_circle_checkbox.setChecked(settings_dict.get('display_aromatic_circles_3d', self.default_settings.get('display_aromatic_circles_3d', False)))
1160
+ thickness_factor = float(settings_dict.get('aromatic_torus_thickness_factor', self.default_settings.get('aromatic_torus_thickness_factor', 0.6)))
1161
+ try:
1162
+ self.aromatic_torus_thickness_slider.setValue(int(thickness_factor * 100))
1163
+ self.aromatic_torus_thickness_label.setText(f"{thickness_factor:.1f}")
1164
+ except Exception:
1165
+ pass
1166
+
1167
+ def select_color(self):
1168
+ """カラーピッカーを開き、選択された色を内部変数とUIに反映させる"""
1169
+ # 内部変数から現在の色を取得してカラーピッカーを初期化
1170
+ color = QColorDialog.getColor(QColor(self.current_bg_color), self)
1171
+ if color.isValid():
1172
+ # 内部変数を更新
1173
+ self.current_bg_color = color.name()
1174
+ # UIの見た目を更新
1175
+ self.update_color_button(self.current_bg_color)
1176
+
1177
+ def update_color_button(self, color_hex):
1178
+ """ボタンの背景色と境界線を設定する"""
1179
+ self.bg_button.setStyleSheet(f"background-color: {color_hex}; border: 1px solid #888;")
1180
+
1181
+ def get_settings(self):
1182
+ return {
1183
+ 'background_color': self.current_bg_color,
1184
+ 'projection_mode': self.projection_combo.currentText(),
1185
+ 'show_3d_axes': self.axes_checkbox.isChecked(),
1186
+ 'lighting_enabled': self.light_checkbox.isChecked(),
1187
+ 'light_intensity': self.intensity_slider.value() / 100.0,
1188
+ 'specular': self.specular_slider.value() / 100.0,
1189
+ 'specular_power': self.spec_power_slider.value(),
1190
+ # Ball and Stick settings
1191
+ 'ball_stick_atom_scale': self.bs_atom_scale_slider.value() / 100.0,
1192
+ 'ball_stick_bond_radius': self.bs_bond_radius_slider.value() / 100.0,
1193
+ 'ball_stick_resolution': self.bs_resolution_slider.value(),
1194
+ # CPK settings
1195
+ 'cpk_atom_scale': self.cpk_atom_scale_slider.value() / 100.0,
1196
+ 'cpk_resolution': self.cpk_resolution_slider.value(),
1197
+ # Wireframe settings
1198
+ 'wireframe_bond_radius': self.wf_bond_radius_slider.value() / 100.0,
1199
+ 'wireframe_resolution': self.wf_resolution_slider.value(),
1200
+ # Stick settings
1201
+ 'stick_bond_radius': self.stick_bond_radius_slider.value() / 100.0,
1202
+ 'stick_resolution': self.stick_resolution_slider.value(),
1203
+ # Multiple bond offset settings (per-model)
1204
+ 'ball_stick_double_bond_offset_factor': self.bs_double_offset_slider.value() / 100.0,
1205
+ 'ball_stick_triple_bond_offset_factor': self.bs_triple_offset_slider.value() / 100.0,
1206
+ 'ball_stick_double_bond_radius_factor': self.bs_double_radius_slider.value() / 100.0,
1207
+ 'ball_stick_triple_bond_radius_factor': self.bs_triple_radius_slider.value() / 100.0,
1208
+ 'wireframe_double_bond_offset_factor': self.wf_double_offset_slider.value() / 100.0,
1209
+ 'wireframe_triple_bond_offset_factor': self.wf_triple_offset_slider.value() / 100.0,
1210
+ 'wireframe_double_bond_radius_factor': self.wf_double_radius_slider.value() / 100.0,
1211
+ 'wireframe_triple_bond_radius_factor': self.wf_triple_radius_slider.value() / 100.0,
1212
+ 'stick_double_bond_offset_factor': self.stick_double_offset_slider.value() / 100.0,
1213
+ 'stick_triple_bond_offset_factor': self.stick_triple_offset_slider.value() / 100.0,
1214
+ 'stick_double_bond_radius_factor': self.stick_double_radius_slider.value() / 100.0,
1215
+ 'stick_triple_bond_radius_factor': self.stick_triple_radius_slider.value() / 100.0,
1216
+ 'display_kekule_3d': self.kekule_3d_checkbox.isChecked(),
1217
+ 'display_aromatic_circles_3d': self.aromatic_circle_checkbox.isChecked(),
1218
+ 'aromatic_torus_thickness_factor': self.aromatic_torus_thickness_slider.value() / 100.0,
1219
+ 'skip_chemistry_checks': self.skip_chem_checks_checkbox.isChecked(),
1220
+ 'always_ask_charge': self.always_ask_charge_checkbox.isChecked(),
1221
+ 'ball_stick_bond_color': getattr(self, 'bs_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F')),
1222
+ 'ball_stick_use_cpk_bond_color': self.bs_use_cpk_bond_checkbox.isChecked(),
1223
+ # 2D Settings
1224
+ 'bond_width_2d': self.bond_width_2d_slider.value() / 10.0,
1225
+ 'bond_spacing_double_2d': self.bond_spacing_double_2d_slider.value() / 10.0,
1226
+ 'bond_spacing_triple_2d': self.bond_spacing_triple_2d_slider.value() / 10.0,
1227
+ 'atom_font_size_2d': self.atom_font_size_2d_slider.value(),
1228
+ 'background_color_2d': self.current_bg_color_2d,
1229
+ 'bond_color_2d': self.current_bond_color_2d,
1230
+ 'atom_use_bond_color_2d': self.atom_use_bond_color_2d_checkbox.isChecked(),
1231
+ 'bond_cap_style_2d': self.bond_cap_style_2d_combo.currentText(),
1232
+ }
1233
+
1234
+ def pick_bs_bond_color(self):
1235
+ """Open QColorDialog to pick Ball & Stick bond color (3D)."""
1236
+ cur = getattr(self, 'bs_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F'))
1237
+ color = QColorDialog.getColor(QColor(cur), self)
1238
+ if color.isValid():
1239
+ self.bs_bond_color = color.name()
1240
+ try:
1241
+ self.bs_bond_color_button.setStyleSheet(f"background-color: {self.bs_bond_color}; border: 1px solid #888;")
1242
+ self.bs_bond_color_button.setToolTip(self.bs_bond_color)
1243
+ except Exception:
1244
+ pass
1245
+
1246
+ def apply_settings(self):
1247
+ """設定を適用(ダイアログは開いたまま)"""
1248
+ # 親ウィンドウの設定を更新
1249
+ if self.parent_window:
1250
+ settings = self.get_settings()
1251
+ self.parent_window.settings.update(settings)
1252
+ # Mark settings dirty; persist on exit to avoid frequent disk writes
1253
+ try:
1254
+ self.parent_window.settings_dirty = True
1255
+ except Exception:
1256
+ pass
1257
+ # 3Dビューの設定を適用
1258
+ self.parent_window.apply_3d_settings()
1259
+ # Update CPK colors from settings if present (no-op otherwise)
1260
+ try:
1261
+ self.parent_window.update_cpk_colors_from_settings()
1262
+ except Exception:
1263
+ pass
1264
+ # Refresh any open CPK color dialogs so they update their UI
1265
+ try:
1266
+ for w in QApplication.topLevelWidgets():
1267
+ try:
1268
+ # import locally to avoid circular import
1269
+ try:
1270
+ from .color_settings_dialog import ColorSettingsDialog
1271
+ except Exception:
1272
+ from modules.color_settings_dialog import ColorSettingsDialog
1273
+ if isinstance(w, ColorSettingsDialog):
1274
+ try:
1275
+ w.refresh_ui()
1276
+ except Exception:
1277
+ pass
1278
+ except Exception:
1279
+ pass
1280
+ except Exception:
1281
+ pass
1282
+ # 現在の分子を再描画(設定変更を反映)
1283
+ if hasattr(self.parent_window, 'current_mol') and self.parent_window.current_mol:
1284
+ self.parent_window.draw_molecule_3d(self.parent_window.current_mol)
1285
+
1286
+ # 2Dビューの設定適用 (背景色、結合スタイルなど)
1287
+ # update_style() will read the new settings from parent_window.settings
1288
+ try:
1289
+ if hasattr(self.parent_window, 'scene') and self.parent_window.scene:
1290
+ # Update Background
1291
+ bg_col_2d = self.parent_window.settings.get('background_color_2d', '#FFFFFF')
1292
+ self.parent_window.scene.setBackgroundBrush(QColor(bg_col_2d))
1293
+
1294
+ # Update Items
1295
+ for item in self.parent_window.scene.items():
1296
+ if hasattr(item, 'update_style'):
1297
+ item.update_style()
1298
+ elif hasattr(item, 'update'):
1299
+ item.update()
1300
+
1301
+ if hasattr(self.parent_window.view_2d, 'viewport'):
1302
+ self.parent_window.view_2d.viewport().update()
1303
+ except Exception:
1304
+ pass
1305
+
1306
+ # ステータスバーに適用完了を表示
1307
+ self.parent_window.statusBar().showMessage("Settings applied successfully")
1308
+
1309
+ def _on_skip_chem_checks_changed(self, state):
1310
+ """Handle user toggling of skip chemistry checks: persist and update UI.
1311
+
1312
+ state: Qt.Checked (2) or Qt.Unchecked (0)
1313
+ """
1314
+ try:
1315
+ enabled = bool(state)
1316
+ self.settings['skip_chemistry_checks'] = enabled
1317
+ # mark dirty instead of immediate save
1318
+ try:
1319
+ self.settings_dirty = True
1320
+ except Exception:
1321
+ pass
1322
+ # If skip is enabled, allow Optimize button; otherwise, respect chem_check flags
1323
+
1324
+ except Exception:
1325
+ pass
1326
+
1327
+ # Note: Kekule display is applied only when user clicks Apply/OK.
1328
+
1329
+ def accept(self):
1330
+ """ダイアログの設定を適用してから閉じる"""
1331
+ # apply_settingsを呼び出して設定を適用
1332
+ self.apply_settings()
1333
+ super().accept()
1334
+
1335
+ def update_ui_from_settings(self, settings_dict):
1336
+ """設定辞書に基づいてUIを更新"""
1337
+ # 1. Scene settings
1338
+ self.current_bg_color = settings_dict.get('background_color', self.default_settings['background_color'])
1339
+ self.update_color_button(self.current_bg_color)
1340
+
1341
+ self.axes_checkbox.setChecked(settings_dict.get('show_3d_axes', self.default_settings['show_3d_axes']))
1342
+ self.light_checkbox.setChecked(settings_dict.get('lighting_enabled', self.default_settings['lighting_enabled']))
1343
+
1344
+ intensity = settings_dict.get('light_intensity', self.default_settings['light_intensity'])
1345
+ self.intensity_slider.setValue(int(intensity * 100))
1346
+ self.intensity_label.setText(f"{intensity:.2f}")
1347
+
1348
+ specular = settings_dict.get('specular', self.default_settings['specular'])
1349
+ self.specular_slider.setValue(int(specular * 100))
1350
+ self.specular_label.setText(f"{specular:.2f}")
1351
+
1352
+ spec_power = settings_dict.get('specular_power', self.default_settings['specular_power'])
1353
+ self.spec_power_slider.setValue(int(spec_power))
1354
+ self.spec_power_label.setText(str(spec_power))
1355
+
1356
+ proj_mode = settings_dict.get('projection_mode', self.default_settings['projection_mode'])
1357
+ idx = self.projection_combo.findText(proj_mode)
1358
+ if idx >= 0:
1359
+ self.projection_combo.setCurrentIndex(idx)
1360
+
1361
+ # 2. Ball & Stick settings
1362
+ bs_atom_scale = settings_dict.get('ball_stick_atom_scale', self.default_settings['ball_stick_atom_scale'])
1363
+ self.bs_atom_scale_slider.setValue(int(bs_atom_scale * 100))
1364
+ self.bs_atom_scale_label.setText(f"{bs_atom_scale:.2f}")
1365
+
1366
+ bs_bond_radius = settings_dict.get('ball_stick_bond_radius', self.default_settings['ball_stick_bond_radius'])
1367
+ self.bs_bond_radius_slider.setValue(int(bs_bond_radius * 100))
1368
+ self.bs_bond_radius_label.setText(f"{bs_bond_radius:.2f}")
1369
+
1370
+ # Multi-bond offsets (Ball & Stick)
1371
+ bs_db_offset = settings_dict.get('ball_stick_double_bond_offset_factor', self.default_settings.get('ball_stick_double_bond_offset_factor', 2.0))
1372
+ self.bs_double_offset_slider.setValue(int(bs_db_offset * 100))
1373
+ self.bs_double_offset_label.setText(f"{bs_db_offset:.2f}")
1374
+
1375
+ bs_tr_offset = settings_dict.get('ball_stick_triple_bond_offset_factor', self.default_settings.get('ball_stick_triple_bond_offset_factor', 2.0))
1376
+ self.bs_triple_offset_slider.setValue(int(bs_tr_offset * 100))
1377
+ self.bs_triple_offset_label.setText(f"{bs_tr_offset:.2f}")
1378
+
1379
+ bs_db_rad = settings_dict.get('ball_stick_double_bond_radius_factor', self.default_settings.get('ball_stick_double_bond_radius_factor', 0.8))
1380
+ self.bs_double_radius_slider.setValue(int(bs_db_rad * 100))
1381
+ self.bs_double_radius_label.setText(f"{bs_db_rad:.2f}")
1382
+
1383
+ bs_tr_rad = settings_dict.get('ball_stick_triple_bond_radius_factor', self.default_settings.get('ball_stick_triple_bond_radius_factor', 0.75))
1384
+ self.bs_triple_radius_slider.setValue(int(bs_tr_rad * 100))
1385
+ self.bs_triple_radius_label.setText(f"{bs_tr_rad:.2f}")
1386
+
1387
+ bs_res = settings_dict.get('ball_stick_resolution', self.default_settings['ball_stick_resolution'])
1388
+ self.bs_resolution_slider.setValue(int(bs_res))
1389
+ self.bs_resolution_label.setText(str(bs_res))
1390
+
1391
+ # Ball & Stick bond color
1392
+ self.bs_bond_color = settings_dict.get('ball_stick_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F'))
1393
+ try:
1394
+ self.bs_bond_color_button.setStyleSheet(f"background-color: {self.bs_bond_color}; border: 1px solid #888;")
1395
+ self.bs_bond_color_button.setToolTip(self.bs_bond_color)
1396
+ except Exception:
1397
+ pass
1398
+
1399
+ self.bs_use_cpk_bond_checkbox.setChecked(settings_dict.get('ball_stick_use_cpk_bond_color', self.default_settings.get('ball_stick_use_cpk_bond_color', False)))
1400
+
1401
+ # 3. CPK settings
1402
+ cpk_atom_scale = settings_dict.get('cpk_atom_scale', self.default_settings['cpk_atom_scale'])
1403
+ self.cpk_atom_scale_slider.setValue(int(cpk_atom_scale * 100))
1404
+ self.cpk_atom_scale_label.setText(f"{cpk_atom_scale:.2f}")
1405
+
1406
+ cpk_res = settings_dict.get('cpk_resolution', self.default_settings['cpk_resolution'])
1407
+ self.cpk_resolution_slider.setValue(int(cpk_res))
1408
+ self.cpk_resolution_label.setText(str(cpk_res))
1409
+
1410
+ # 4. Wireframe settings
1411
+ wf_bond_radius = settings_dict.get('wireframe_bond_radius', self.default_settings['wireframe_bond_radius'])
1412
+ self.wf_bond_radius_slider.setValue(int(wf_bond_radius * 100))
1413
+ self.wf_bond_radius_label.setText(f"{wf_bond_radius:.2f}")
1414
+
1415
+ wf_res = settings_dict.get('wireframe_resolution', self.default_settings['wireframe_resolution'])
1416
+ self.wf_resolution_slider.setValue(int(wf_res))
1417
+ self.wf_resolution_label.setText(str(wf_res))
1418
+
1419
+ # Multi-bond offsets (Wireframe)
1420
+ wf_db_offset = settings_dict.get('wireframe_double_bond_offset_factor', self.default_settings.get('wireframe_double_bond_offset_factor', 3.0))
1421
+ self.wf_double_offset_slider.setValue(int(wf_db_offset * 100))
1422
+ self.wf_double_offset_label.setText(f"{wf_db_offset:.2f}")
1423
+
1424
+ wf_tr_offset = settings_dict.get('wireframe_triple_bond_offset_factor', self.default_settings.get('wireframe_triple_bond_offset_factor', 3.0))
1425
+ self.wf_triple_offset_slider.setValue(int(wf_tr_offset * 100))
1426
+ self.wf_triple_offset_label.setText(f"{wf_tr_offset:.2f}")
1427
+
1428
+ wf_db_rad = settings_dict.get('wireframe_double_bond_radius_factor', self.default_settings.get('wireframe_double_bond_radius_factor', 0.8))
1429
+ self.wf_double_radius_slider.setValue(int(wf_db_rad * 100))
1430
+ self.wf_double_radius_label.setText(f"{wf_db_rad:.2f}")
1431
+
1432
+ wf_tr_rad = settings_dict.get('wireframe_triple_bond_radius_factor', self.default_settings.get('wireframe_triple_bond_radius_factor', 0.75))
1433
+ self.wf_triple_radius_slider.setValue(int(wf_tr_rad * 100))
1434
+ self.wf_triple_radius_label.setText(f"{wf_tr_rad:.2f}")
1435
+
1436
+ # 5. Stick settings
1437
+ stick_bond_radius = settings_dict.get('stick_bond_radius', self.default_settings['stick_bond_radius'])
1438
+ self.stick_bond_radius_slider.setValue(int(stick_bond_radius * 100))
1439
+ self.stick_bond_radius_label.setText(f"{stick_bond_radius:.2f}")
1440
+
1441
+ stick_res = settings_dict.get('stick_resolution', self.default_settings['stick_resolution'])
1442
+ self.stick_resolution_slider.setValue(int(stick_res))
1443
+ self.stick_resolution_label.setText(str(stick_res))
1444
+
1445
+ # Multi-bond offsets (Stick)
1446
+ stick_db_offset = settings_dict.get('stick_double_bond_offset_factor', self.default_settings.get('stick_double_bond_offset_factor', 1.5))
1447
+ self.stick_double_offset_slider.setValue(int(stick_db_offset * 100))
1448
+ self.stick_double_offset_label.setText(f"{stick_db_offset:.2f}")
1449
+
1450
+ stick_tr_offset = settings_dict.get('stick_triple_bond_offset_factor', self.default_settings.get('stick_triple_bond_offset_factor', 1.0))
1451
+ self.stick_triple_offset_slider.setValue(int(stick_tr_offset * 100))
1452
+ self.stick_triple_offset_label.setText(f"{stick_tr_offset:.2f}")
1453
+
1454
+ stick_db_rad = settings_dict.get('stick_double_bond_radius_factor', self.default_settings.get('stick_double_bond_radius_factor', 0.6))
1455
+ self.stick_double_radius_slider.setValue(int(stick_db_rad * 100))
1456
+ self.stick_double_radius_label.setText(f"{stick_db_rad:.2f}")
1457
+
1458
+ stick_tr_rad = settings_dict.get('stick_triple_bond_radius_factor', self.default_settings.get('stick_triple_bond_radius_factor', 0.4))
1459
+ self.stick_triple_radius_slider.setValue(int(stick_tr_rad * 100))
1460
+ self.stick_triple_radius_label.setText(f"{stick_tr_rad:.2f}")
1461
+
1462
+ # 6. Other settings
1463
+ self.skip_chem_checks_checkbox.setChecked(settings_dict.get('skip_chemistry_checks', self.default_settings.get('skip_chemistry_checks', False)))
1464
+ self.kekule_3d_checkbox.setChecked(settings_dict.get('display_kekule_3d', self.default_settings.get('display_kekule_3d', False)))
1465
+ # always ask for charge on XYZ imports
1466
+ self.always_ask_charge_checkbox.setChecked(settings_dict.get('always_ask_charge', self.default_settings.get('always_ask_charge', False)))
1467
+ # Aromatic ring circle display and torus thickness factor
1468
+ self.aromatic_circle_checkbox.setChecked(settings_dict.get('display_aromatic_circles_3d', self.default_settings.get('display_aromatic_circles_3d', False)))
1469
+ thickness_factor = float(settings_dict.get('aromatic_torus_thickness_factor', self.default_settings.get('aromatic_torus_thickness_factor', 0.6)))
1470
+ try:
1471
+ self.aromatic_torus_thickness_slider.setValue(int(thickness_factor * 100))
1472
+ self.aromatic_torus_thickness_label.setText(f"{thickness_factor:.1f}")
1473
+ except Exception:
1474
+ pass
1475
+
1476
+ # 7. 2D Settings
1477
+ bw_2d = settings_dict.get('bond_width_2d', self.default_settings['bond_width_2d'])
1478
+ self.bond_width_2d_slider.setValue(int(bw_2d * 10))
1479
+ self.bond_width_2d_label.setText(f"{bw_2d:.1f}")
1480
+
1481
+ bsd_2d = settings_dict.get('bond_spacing_double_2d', self.default_settings['bond_spacing_double_2d'])
1482
+ self.bond_spacing_double_2d_slider.setValue(int(bsd_2d * 10))
1483
+ self.bond_spacing_double_2d_label.setText(f"{bsd_2d:.1f}")
1484
+
1485
+ bst_2d = settings_dict.get('bond_spacing_triple_2d', self.default_settings['bond_spacing_triple_2d'])
1486
+ self.bond_spacing_triple_2d_slider.setValue(int(bst_2d * 10))
1487
+ self.bond_spacing_triple_2d_label.setText(f"{bst_2d:.1f}")
1488
+
1489
+ fs_2d = settings_dict.get('atom_font_size_2d', self.default_settings['atom_font_size_2d'])
1490
+ self.atom_font_size_2d_slider.setValue(int(fs_2d))
1491
+ self.atom_font_size_2d_label.setText(str(fs_2d))
1492
+
1493
+ self.atom_use_bond_color_2d_checkbox.setChecked(settings_dict.get('atom_use_bond_color_2d', self.default_settings.get('atom_use_bond_color_2d', False)))
1494
+
1495
+ self.current_bg_color_2d = settings_dict.get('background_color_2d', self.default_settings['background_color_2d'])
1496
+
1497
+ # Load Bond Cap Style
1498
+ cap_style = settings_dict.get('bond_cap_style_2d', self.default_settings.get('bond_cap_style_2d', 'Round'))
1499
+ index = self.bond_cap_style_2d_combo.findText(cap_style)
1500
+ if index >= 0:
1501
+ self.bond_cap_style_2d_combo.setCurrentIndex(index)
1502
+ self.current_bond_color_2d = settings_dict.get('bond_color_2d', self.default_settings['bond_color_2d'])
1503
+ self.update_2d_color_buttons()