MoleditPy 1.15.1__py3-none-any.whl → 1.16.0a1__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/__init__.py +4 -0
  2. moleditpy/__main__.py +29 -19748
  3. moleditpy/main.py +40 -0
  4. moleditpy/modules/__init__.py +36 -0
  5. moleditpy/modules/about_dialog.py +92 -0
  6. moleditpy/modules/align_plane_dialog.py +281 -0
  7. moleditpy/modules/alignment_dialog.py +261 -0
  8. moleditpy/modules/analysis_window.py +197 -0
  9. moleditpy/modules/angle_dialog.py +428 -0
  10. moleditpy/modules/assets/icon.icns +0 -0
  11. moleditpy/modules/atom_item.py +336 -0
  12. moleditpy/modules/bond_item.py +303 -0
  13. moleditpy/modules/bond_length_dialog.py +368 -0
  14. moleditpy/modules/calculation_worker.py +754 -0
  15. moleditpy/modules/color_settings_dialog.py +309 -0
  16. moleditpy/modules/constants.py +76 -0
  17. moleditpy/modules/constrained_optimization_dialog.py +667 -0
  18. moleditpy/modules/custom_interactor_style.py +737 -0
  19. moleditpy/modules/custom_qt_interactor.py +49 -0
  20. moleditpy/modules/dialog3_d_picking_mixin.py +96 -0
  21. moleditpy/modules/dihedral_dialog.py +431 -0
  22. moleditpy/modules/main_window.py +827 -0
  23. moleditpy/modules/main_window_app_state.py +709 -0
  24. moleditpy/modules/main_window_compute.py +1203 -0
  25. moleditpy/modules/main_window_dialog_manager.py +454 -0
  26. moleditpy/modules/main_window_edit_3d.py +531 -0
  27. moleditpy/modules/main_window_edit_actions.py +1449 -0
  28. moleditpy/modules/main_window_export.py +744 -0
  29. moleditpy/modules/main_window_main_init.py +1560 -0
  30. moleditpy/modules/main_window_molecular_parsers.py +956 -0
  31. moleditpy/modules/main_window_project_io.py +416 -0
  32. moleditpy/modules/main_window_string_importers.py +270 -0
  33. moleditpy/modules/main_window_ui_manager.py +567 -0
  34. moleditpy/modules/main_window_view_3d.py +1163 -0
  35. moleditpy/modules/main_window_view_loaders.py +350 -0
  36. moleditpy/modules/mirror_dialog.py +110 -0
  37. moleditpy/modules/molecular_data.py +290 -0
  38. moleditpy/modules/molecule_scene.py +1895 -0
  39. moleditpy/modules/move_group_dialog.py +586 -0
  40. moleditpy/modules/periodic_table_dialog.py +72 -0
  41. moleditpy/modules/planarize_dialog.py +209 -0
  42. moleditpy/modules/settings_dialog.py +1034 -0
  43. moleditpy/modules/template_preview_item.py +148 -0
  44. moleditpy/modules/template_preview_view.py +62 -0
  45. moleditpy/modules/translation_dialog.py +353 -0
  46. moleditpy/modules/user_template_dialog.py +621 -0
  47. moleditpy/modules/zoomable_view.py +98 -0
  48. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/METADATA +1 -1
  49. moleditpy-1.16.0a1.dist-info/RECORD +54 -0
  50. moleditpy-1.15.1.dist-info/RECORD +0 -9
  51. /moleditpy/{assets → modules/assets}/icon.ico +0 -0
  52. /moleditpy/{assets → modules/assets}/icon.png +0 -0
  53. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/WHEEL +0 -0
  54. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/entry_points.txt +0 -0
  55. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1034 @@
1
+ from PyQt6.QtWidgets import (
2
+ QDialog, QVBoxLayout, QTabWidget, QWidget, QFormLayout, QPushButton, QHBoxLayout,
3
+ QCheckBox, QComboBox, QLabel, QColorDialog, QSlider, QFrame, QMessageBox
4
+ )
5
+ from PyQt6.QtWidgets import QApplication
6
+ from PyQt6.QtCore import Qt
7
+ from PyQt6.QtGui import QColor
8
+ try:
9
+ from .constants import CPK_COLORS
10
+ except Exception:
11
+ from modules.constants import CPK_COLORS
12
+
13
+
14
+ class SettingsDialog(QDialog):
15
+ def __init__(self, current_settings, parent=None):
16
+ super().__init__(parent)
17
+ self.setWindowTitle("3D View Settings")
18
+ self.setMinimumSize(500, 600)
19
+
20
+ # 親ウィンドウの参照を保存(Apply機能のため)
21
+ self.parent_window = parent
22
+
23
+ # デフォルト設定をクラス内で定義
24
+ # Multi-bond settings are model-specific now (ball_stick, cpk, wireframe, stick)
25
+ self.default_settings = {
26
+ 'background_color': '#919191',
27
+ 'projection_mode': 'Perspective',
28
+ 'lighting_enabled': True,
29
+ 'specular': 0.20,
30
+ 'specular_power': 20,
31
+ 'light_intensity': 1.0,
32
+ 'show_3d_axes': True,
33
+ # Ball and Stick model parameters
34
+ 'ball_stick_atom_scale': 1.0,
35
+ 'ball_stick_bond_radius': 0.1,
36
+ 'ball_stick_resolution': 16,
37
+ # CPK (Space-filling) model parameters
38
+ 'cpk_atom_scale': 1.0,
39
+ 'cpk_resolution': 32,
40
+ # Wireframe model parameters
41
+ 'wireframe_bond_radius': 0.01,
42
+ 'wireframe_resolution': 6,
43
+ # Stick model parameters
44
+ 'stick_atom_radius': 0.15,
45
+ 'stick_bond_radius': 0.15,
46
+ 'stick_resolution': 16,
47
+ # Multiple bond offset parameters (per-model)
48
+ 'ball_stick_double_bond_offset_factor': 2.0,
49
+ 'ball_stick_triple_bond_offset_factor': 2.0,
50
+ 'ball_stick_double_bond_radius_factor': 0.8,
51
+ 'ball_stick_triple_bond_radius_factor': 0.75,
52
+ 'wireframe_double_bond_offset_factor': 3.0,
53
+ 'wireframe_triple_bond_offset_factor': 3.0,
54
+ 'wireframe_double_bond_radius_factor': 0.8,
55
+ 'wireframe_triple_bond_radius_factor': 0.75,
56
+ 'stick_double_bond_offset_factor': 1.5,
57
+ 'stick_triple_bond_offset_factor': 1.0,
58
+ 'stick_double_bond_radius_factor': 0.6,
59
+ 'stick_triple_bond_radius_factor': 0.4,
60
+ # If True, attempts to be permissive when RDKit raises chemical/sanitization errors
61
+ # during file import (useful for viewing malformed XYZ/MOL files). When enabled,
62
+ # element symbol recognition will be coerced where possible and Chem.SanitizeMol
63
+ # failures will be ignored so the 3D viewer can still display the structure.
64
+ 'skip_chemistry_checks': False,
65
+ # 3D conversion/optimization defaults
66
+ '3d_conversion_mode': 'fallback',
67
+ 'optimization_method': 'MMFF_RDKIT',
68
+ 'ball_stick_bond_color': '#7F7F7F',
69
+ 'cpk_colors': {},
70
+ }
71
+
72
+ # --- 選択された色を管理する専用のインスタンス変数 ---
73
+ self.current_bg_color = None
74
+
75
+ # --- UI要素の作成 ---
76
+ layout = QVBoxLayout(self)
77
+
78
+ # タブウィジェットを作成
79
+ self.tab_widget = QTabWidget()
80
+ layout.addWidget(self.tab_widget)
81
+
82
+ # Scene設定タブ
83
+ self.create_scene_tab()
84
+
85
+ # Ball and Stick設定タブ
86
+ self.create_ball_stick_tab()
87
+
88
+ # CPK設定タブ
89
+ self.create_cpk_tab()
90
+
91
+ # Wireframe設定タブ
92
+ self.create_wireframe_tab()
93
+
94
+ # Stick設定タブ
95
+ self.create_stick_tab()
96
+
97
+ # Other設定タブ
98
+ self.create_other_tab()
99
+
100
+ # 渡された設定でUIと内部変数を初期化
101
+ self.update_ui_from_settings(current_settings)
102
+
103
+ # --- ボタンの配置 ---
104
+ buttons = QHBoxLayout()
105
+
106
+ # タブごとのリセットボタン
107
+ reset_tab_button = QPushButton("Reset Current Tab")
108
+ reset_tab_button.clicked.connect(self.reset_current_tab)
109
+ reset_tab_button.setToolTip("Reset settings for the currently selected tab only")
110
+ buttons.addWidget(reset_tab_button)
111
+
112
+ # 全体リセットボタン
113
+ reset_all_button = QPushButton("Reset All")
114
+ reset_all_button.clicked.connect(self.reset_all_settings)
115
+ reset_all_button.setToolTip("Reset all settings to defaults")
116
+ buttons.addWidget(reset_all_button)
117
+
118
+ buttons.addStretch(1)
119
+
120
+ # Applyボタンを追加
121
+ apply_button = QPushButton("Apply")
122
+ apply_button.clicked.connect(self.apply_settings)
123
+ apply_button.setToolTip("Apply settings without closing dialog")
124
+ buttons.addWidget(apply_button)
125
+
126
+ ok_button = QPushButton("OK")
127
+ cancel_button = QPushButton("Cancel")
128
+ ok_button.clicked.connect(self.accept)
129
+ cancel_button.clicked.connect(self.reject)
130
+
131
+ buttons.addWidget(ok_button)
132
+ buttons.addWidget(cancel_button)
133
+ layout.addLayout(buttons)
134
+
135
+ def create_scene_tab(self):
136
+ """基本設定タブを作成"""
137
+ scene_widget = QWidget()
138
+ form_layout = QFormLayout(scene_widget)
139
+
140
+ # 1. 背景色
141
+ self.bg_button = QPushButton()
142
+ self.bg_button.setToolTip("Click to select a color")
143
+ self.bg_button.clicked.connect(self.select_color)
144
+ form_layout.addRow("Background Color:", self.bg_button)
145
+
146
+ # 1a. 軸の表示/非表示
147
+ self.axes_checkbox = QCheckBox()
148
+ form_layout.addRow("Show 3D Axes:", self.axes_checkbox)
149
+
150
+ # 2. ライトの有効/無効
151
+ self.light_checkbox = QCheckBox()
152
+ form_layout.addRow("Enable Lighting:", self.light_checkbox)
153
+
154
+ # 光の強さスライダーを追加
155
+ self.intensity_slider = QSlider(Qt.Orientation.Horizontal)
156
+ self.intensity_slider.setRange(0, 200) # 0.0 ~ 2.0 の範囲
157
+ self.intensity_label = QLabel("1.0")
158
+ self.intensity_slider.valueChanged.connect(lambda v: self.intensity_label.setText(f"{v/100:.2f}"))
159
+ intensity_layout = QHBoxLayout()
160
+ intensity_layout.addWidget(self.intensity_slider)
161
+ intensity_layout.addWidget(self.intensity_label)
162
+ form_layout.addRow("Light Intensity:", intensity_layout)
163
+
164
+ # 3. 光沢 (Specular)
165
+ self.specular_slider = QSlider(Qt.Orientation.Horizontal)
166
+ self.specular_slider.setRange(0, 100)
167
+ self.specular_label = QLabel("0.20")
168
+ self.specular_slider.valueChanged.connect(lambda v: self.specular_label.setText(f"{v/100:.2f}"))
169
+ specular_layout = QHBoxLayout()
170
+ specular_layout.addWidget(self.specular_slider)
171
+ specular_layout.addWidget(self.specular_label)
172
+ form_layout.addRow("Shininess (Specular):", specular_layout)
173
+
174
+ # 4. 光沢の強さ (Specular Power)
175
+ self.spec_power_slider = QSlider(Qt.Orientation.Horizontal)
176
+ self.spec_power_slider.setRange(0, 100)
177
+ self.spec_power_label = QLabel("20")
178
+ self.spec_power_slider.valueChanged.connect(lambda v: self.spec_power_label.setText(str(v)))
179
+ spec_power_layout = QHBoxLayout()
180
+ spec_power_layout.addWidget(self.spec_power_slider)
181
+ spec_power_layout.addWidget(self.spec_power_label)
182
+ form_layout.addRow("Shininess Power:", spec_power_layout)
183
+
184
+ # Projection mode (Perspective / Orthographic)
185
+ self.projection_combo = QComboBox()
186
+ self.projection_combo.addItem("Perspective")
187
+ self.projection_combo.addItem("Orthographic")
188
+ self.projection_combo.setToolTip("Choose camera projection mode: Perspective (default) or Orthographic")
189
+ form_layout.addRow("Projection Mode:", self.projection_combo)
190
+
191
+ self.tab_widget.addTab(scene_widget, "Scene")
192
+
193
+ def create_other_tab(self):
194
+ """other設定タブを作成"""
195
+ self.other_widget = QWidget()
196
+ self.other_form_layout = QFormLayout(self.other_widget)
197
+
198
+ # 化学チェックスキップオプション(otherタブに移動)
199
+ self.skip_chem_checks_checkbox = QCheckBox()
200
+ self.skip_chem_checks_checkbox.setToolTip("When enabled, file import will try to ignore chemical/sanitization errors and allow viewing malformed files.")
201
+ # Immediately persist change to settings when user toggles the checkbox
202
+ try:
203
+ self.skip_chem_checks_checkbox.stateChanged.connect(lambda s: self._on_skip_chem_checks_changed(s))
204
+ except Exception:
205
+ pass
206
+
207
+ # Add the checkbox to the other tab's form
208
+ try:
209
+ self.other_form_layout.addRow("Skip chemistry checks on import xyz file:", self.skip_chem_checks_checkbox)
210
+ except Exception:
211
+ pass
212
+
213
+ # Add Other tab to the tab widget
214
+ self.tab_widget.addTab(self.other_widget, "Other")
215
+
216
+ def refresh_ui(self):
217
+ """Refresh periodic table / BS button visuals using current settings.
218
+
219
+ Called when settings change externally (e.g., Reset All in main settings) so
220
+ the dialog reflects the current stored overrides.
221
+ """
222
+ try:
223
+ # Update element button colors from parent.settings cpks
224
+ overrides = self.parent_window.settings.get('cpk_colors', {}) if self.parent_window and hasattr(self.parent_window, 'settings') else {}
225
+ for s, btn in self.element_buttons.items():
226
+ try:
227
+ override = overrides.get(s)
228
+ q_color = QColor(override) if override else CPK_COLORS.get(s, CPK_COLORS['DEFAULT'])
229
+ brightness = (q_color.red() * 299 + q_color.green() * 587 + q_color.blue() * 114) / 1000
230
+ text_color = 'white' if brightness < 128 else 'black'
231
+ btn.setStyleSheet(f"background-color: {q_color.name()}; color: {text_color}; border: 1px solid #555; font-weight: bold;")
232
+ except Exception:
233
+ pass
234
+ # Update BS color button from parent settings
235
+ try:
236
+ if hasattr(self, 'bs_button') and self.parent_window and hasattr(self.parent_window, 'settings'):
237
+ bs_hex = self.parent_window.settings.get('ball_stick_bond_color', self.parent_window.default_settings.get('ball_stick_bond_color', '#7F7F7F'))
238
+ self.bs_button.setStyleSheet(f"background-color: {bs_hex}; border: 1px solid #888;")
239
+ self.bs_button.setToolTip(bs_hex)
240
+ except Exception:
241
+ pass
242
+ except Exception:
243
+ pass
244
+ # Avoid circular import: import ColorSettingsDialog only inside method using it
245
+
246
+ # NOTE: Multi-bond offset/thickness settings moved to per-model tabs to allow
247
+ # independent configuration for Ball&Stick/CPK/Wireframe/Stick.
248
+
249
+ # 'Other' tab is created in create_other_tab; nothing to do here.
250
+
251
+ def create_ball_stick_tab(self):
252
+ """Ball and Stick設定タブを作成"""
253
+ ball_stick_widget = QWidget()
254
+ form_layout = QFormLayout(ball_stick_widget)
255
+
256
+ info_label = QLabel("Ball & Stick model shows atoms as spheres and bonds as cylinders.")
257
+ info_label.setWordWrap(True)
258
+ info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
259
+ form_layout.addRow(info_label)
260
+
261
+ # 原子サイズスケール
262
+ self.bs_atom_scale_slider = QSlider(Qt.Orientation.Horizontal)
263
+ self.bs_atom_scale_slider.setRange(10, 200) # 0.1 ~ 2.0
264
+ self.bs_atom_scale_label = QLabel("1.00")
265
+ self.bs_atom_scale_slider.valueChanged.connect(lambda v: self.bs_atom_scale_label.setText(f"{v/100:.2f}"))
266
+ atom_scale_layout = QHBoxLayout()
267
+ atom_scale_layout.addWidget(self.bs_atom_scale_slider)
268
+ atom_scale_layout.addWidget(self.bs_atom_scale_label)
269
+ form_layout.addRow("Atom Size Scale:", atom_scale_layout)
270
+
271
+ # ボンド半径
272
+ self.bs_bond_radius_slider = QSlider(Qt.Orientation.Horizontal)
273
+ self.bs_bond_radius_slider.setRange(1, 50) # 0.01 ~ 0.5
274
+ self.bs_bond_radius_label = QLabel("0.10")
275
+ self.bs_bond_radius_slider.valueChanged.connect(lambda v: self.bs_bond_radius_label.setText(f"{v/100:.2f}"))
276
+ bond_radius_layout = QHBoxLayout()
277
+ bond_radius_layout.addWidget(self.bs_bond_radius_slider)
278
+ bond_radius_layout.addWidget(self.bs_bond_radius_label)
279
+ form_layout.addRow("Bond Radius:", bond_radius_layout)
280
+
281
+ # --- 区切り線(水平ライン) ---
282
+ line = QFrame()
283
+ line.setFrameShape(QFrame.Shape.HLine)
284
+ line.setFrameShadow(QFrame.Shadow.Sunken)
285
+ form_layout.addRow(line)
286
+
287
+ # --- Per-model multi-bond controls (Ball & Stick) ---
288
+ # 二重/三重結合のオフセット倍率(Ball & Stick)
289
+ self.bs_double_offset_slider = QSlider(Qt.Orientation.Horizontal)
290
+ self.bs_double_offset_slider.setRange(100, 400)
291
+ self.bs_double_offset_label = QLabel("2.00")
292
+ self.bs_double_offset_slider.valueChanged.connect(lambda v: self.bs_double_offset_label.setText(f"{v/100:.2f}"))
293
+ bs_double_offset_layout = QHBoxLayout()
294
+ bs_double_offset_layout.addWidget(self.bs_double_offset_slider)
295
+ bs_double_offset_layout.addWidget(self.bs_double_offset_label)
296
+ form_layout.addRow("Double Bond Offset (Ball & Stick):", bs_double_offset_layout)
297
+
298
+ self.bs_triple_offset_slider = QSlider(Qt.Orientation.Horizontal)
299
+ self.bs_triple_offset_slider.setRange(100, 400)
300
+ self.bs_triple_offset_label = QLabel("2.00")
301
+ self.bs_triple_offset_slider.valueChanged.connect(lambda v: self.bs_triple_offset_label.setText(f"{v/100:.2f}"))
302
+ bs_triple_offset_layout = QHBoxLayout()
303
+ bs_triple_offset_layout.addWidget(self.bs_triple_offset_slider)
304
+ bs_triple_offset_layout.addWidget(self.bs_triple_offset_label)
305
+ form_layout.addRow("Triple Bond Offset (Ball & Stick):", bs_triple_offset_layout)
306
+
307
+ # 半径倍率
308
+ self.bs_double_radius_slider = QSlider(Qt.Orientation.Horizontal)
309
+ self.bs_double_radius_slider.setRange(50, 100)
310
+ self.bs_double_radius_label = QLabel("0.80")
311
+ self.bs_double_radius_slider.valueChanged.connect(lambda v: self.bs_double_radius_label.setText(f"{v/100:.2f}"))
312
+ bs_double_radius_layout = QHBoxLayout()
313
+ bs_double_radius_layout.addWidget(self.bs_double_radius_slider)
314
+ bs_double_radius_layout.addWidget(self.bs_double_radius_label)
315
+ form_layout.addRow("Double Bond Thickness (Ball & Stick):", bs_double_radius_layout)
316
+
317
+ self.bs_triple_radius_slider = QSlider(Qt.Orientation.Horizontal)
318
+ self.bs_triple_radius_slider.setRange(50, 100)
319
+ self.bs_triple_radius_label = QLabel("0.70")
320
+ self.bs_triple_radius_slider.valueChanged.connect(lambda v: self.bs_triple_radius_label.setText(f"{v/100:.2f}"))
321
+ bs_triple_radius_layout = QHBoxLayout()
322
+ bs_triple_radius_layout.addWidget(self.bs_triple_radius_slider)
323
+ bs_triple_radius_layout.addWidget(self.bs_triple_radius_label)
324
+ form_layout.addRow("Triple Bond Thickness (Ball & Stick):", bs_triple_radius_layout)
325
+
326
+ # --- 区切り線(水平ライン) ---
327
+ line = QFrame()
328
+ line.setFrameShape(QFrame.Shape.HLine)
329
+ line.setFrameShadow(QFrame.Shadow.Sunken)
330
+ form_layout.addRow(line)
331
+
332
+ # 解像度
333
+ self.bs_resolution_slider = QSlider(Qt.Orientation.Horizontal)
334
+ self.bs_resolution_slider.setRange(6, 32)
335
+ self.bs_resolution_label = QLabel("16")
336
+ self.bs_resolution_slider.valueChanged.connect(lambda v: self.bs_resolution_label.setText(str(v)))
337
+ resolution_layout = QHBoxLayout()
338
+ resolution_layout.addWidget(self.bs_resolution_slider)
339
+ resolution_layout.addWidget(self.bs_resolution_label)
340
+ form_layout.addRow("Resolution (Quality):", resolution_layout)
341
+
342
+ # --- Ball & Stick bond color ---
343
+ self.bs_bond_color_button = QPushButton()
344
+ self.bs_bond_color_button.setFixedSize(36, 24)
345
+ self.bs_bond_color_button.clicked.connect(self.pick_bs_bond_color)
346
+ self.bs_bond_color_button.setToolTip("Choose the uniform bond color for Ball & Stick model (3D)")
347
+ form_layout.addRow("Ball & Stick bond color:", self.bs_bond_color_button)
348
+
349
+ self.tab_widget.addTab(ball_stick_widget, "Ball & Stick")
350
+
351
+ def create_cpk_tab(self):
352
+ """CPK設定タブを作成"""
353
+ cpk_widget = QWidget()
354
+ form_layout = QFormLayout(cpk_widget)
355
+
356
+ info_label = QLabel("CPK model shows atoms as space-filling spheres using van der Waals radii.")
357
+ info_label.setWordWrap(True)
358
+ info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
359
+ form_layout.addRow(info_label)
360
+
361
+ # 原子サイズスケール
362
+ self.cpk_atom_scale_slider = QSlider(Qt.Orientation.Horizontal)
363
+ self.cpk_atom_scale_slider.setRange(50, 200) # 0.5 ~ 2.0
364
+ self.cpk_atom_scale_label = QLabel("1.00")
365
+ self.cpk_atom_scale_slider.valueChanged.connect(lambda v: self.cpk_atom_scale_label.setText(f"{v/100:.2f}"))
366
+ atom_scale_layout = QHBoxLayout()
367
+ atom_scale_layout.addWidget(self.cpk_atom_scale_slider)
368
+ atom_scale_layout.addWidget(self.cpk_atom_scale_label)
369
+ form_layout.addRow("Atom Size Scale:", atom_scale_layout)
370
+
371
+ # 解像度
372
+ self.cpk_resolution_slider = QSlider(Qt.Orientation.Horizontal)
373
+ self.cpk_resolution_slider.setRange(8, 64)
374
+ self.cpk_resolution_label = QLabel("32")
375
+ self.cpk_resolution_slider.valueChanged.connect(lambda v: self.cpk_resolution_label.setText(str(v)))
376
+ resolution_layout = QHBoxLayout()
377
+ resolution_layout.addWidget(self.cpk_resolution_slider)
378
+ resolution_layout.addWidget(self.cpk_resolution_label)
379
+ form_layout.addRow("Resolution (Quality):", resolution_layout)
380
+
381
+ self.tab_widget.addTab(cpk_widget, "CPK (Space-filling)")
382
+
383
+ def create_wireframe_tab(self):
384
+ """Wireframe設定タブを作成"""
385
+ wireframe_widget = QWidget()
386
+ form_layout = QFormLayout(wireframe_widget)
387
+
388
+ info_label = QLabel("Wireframe model shows molecular structure with thin lines only.")
389
+ info_label.setWordWrap(True)
390
+ info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
391
+ form_layout.addRow(info_label)
392
+
393
+ # ボンド半径
394
+ self.wf_bond_radius_slider = QSlider(Qt.Orientation.Horizontal)
395
+ self.wf_bond_radius_slider.setRange(1, 10) # 0.01 ~ 0.1
396
+ self.wf_bond_radius_label = QLabel("0.01")
397
+ self.wf_bond_radius_slider.valueChanged.connect(lambda v: self.wf_bond_radius_label.setText(f"{v/100:.2f}"))
398
+ bond_radius_layout = QHBoxLayout()
399
+ bond_radius_layout.addWidget(self.wf_bond_radius_slider)
400
+ bond_radius_layout.addWidget(self.wf_bond_radius_label)
401
+ form_layout.addRow("Bond Radius:", bond_radius_layout)
402
+
403
+
404
+ # --- 区切り線(水平ライン) ---
405
+ line = QFrame()
406
+ line.setFrameShape(QFrame.Shape.HLine)
407
+ line.setFrameShadow(QFrame.Shadow.Sunken)
408
+ form_layout.addRow(line)
409
+
410
+ # --- Per-model multi-bond controls (Wireframe) ---
411
+ self.wf_double_offset_slider = QSlider(Qt.Orientation.Horizontal)
412
+ self.wf_double_offset_slider.setRange(100, 400)
413
+ self.wf_double_offset_label = QLabel("2.00")
414
+ self.wf_double_offset_slider.valueChanged.connect(lambda v: self.wf_double_offset_label.setText(f"{v/100:.2f}"))
415
+ wf_double_offset_layout = QHBoxLayout()
416
+ wf_double_offset_layout.addWidget(self.wf_double_offset_slider)
417
+ wf_double_offset_layout.addWidget(self.wf_double_offset_label)
418
+ form_layout.addRow("Double Bond Offset (Wireframe):", wf_double_offset_layout)
419
+
420
+ self.wf_triple_offset_slider = QSlider(Qt.Orientation.Horizontal)
421
+ self.wf_triple_offset_slider.setRange(100, 400)
422
+ self.wf_triple_offset_label = QLabel("2.00")
423
+ self.wf_triple_offset_slider.valueChanged.connect(lambda v: self.wf_triple_offset_label.setText(f"{v/100:.2f}"))
424
+ wf_triple_offset_layout = QHBoxLayout()
425
+ wf_triple_offset_layout.addWidget(self.wf_triple_offset_slider)
426
+ wf_triple_offset_layout.addWidget(self.wf_triple_offset_label)
427
+ form_layout.addRow("Triple Bond Offset (Wireframe):", wf_triple_offset_layout)
428
+
429
+ self.wf_double_radius_slider = QSlider(Qt.Orientation.Horizontal)
430
+ self.wf_double_radius_slider.setRange(50, 100)
431
+ self.wf_double_radius_label = QLabel("0.80")
432
+ self.wf_double_radius_slider.valueChanged.connect(lambda v: self.wf_double_radius_label.setText(f"{v/100:.2f}"))
433
+ wf_double_radius_layout = QHBoxLayout()
434
+ wf_double_radius_layout.addWidget(self.wf_double_radius_slider)
435
+ wf_double_radius_layout.addWidget(self.wf_double_radius_label)
436
+ form_layout.addRow("Double Bond Thickness (Wireframe):", wf_double_radius_layout)
437
+
438
+ self.wf_triple_radius_slider = QSlider(Qt.Orientation.Horizontal)
439
+ self.wf_triple_radius_slider.setRange(50, 100)
440
+ self.wf_triple_radius_label = QLabel("0.70")
441
+ self.wf_triple_radius_slider.valueChanged.connect(lambda v: self.wf_triple_radius_label.setText(f"{v/100:.2f}"))
442
+ wf_triple_radius_layout = QHBoxLayout()
443
+ wf_triple_radius_layout.addWidget(self.wf_triple_radius_slider)
444
+ wf_triple_radius_layout.addWidget(self.wf_triple_radius_label)
445
+ form_layout.addRow("Triple Bond Thickness (Wireframe):", wf_triple_radius_layout)
446
+
447
+ # --- 区切り線(水平ライン) ---
448
+ line = QFrame()
449
+ line.setFrameShape(QFrame.Shape.HLine)
450
+ line.setFrameShadow(QFrame.Shadow.Sunken)
451
+ form_layout.addRow(line)
452
+
453
+ # 解像度
454
+ self.wf_resolution_slider = QSlider(Qt.Orientation.Horizontal)
455
+ self.wf_resolution_slider.setRange(4, 16)
456
+ self.wf_resolution_label = QLabel("6")
457
+ self.wf_resolution_slider.valueChanged.connect(lambda v: self.wf_resolution_label.setText(str(v)))
458
+ resolution_layout = QHBoxLayout()
459
+ resolution_layout.addWidget(self.wf_resolution_slider)
460
+ resolution_layout.addWidget(self.wf_resolution_label)
461
+ form_layout.addRow("Resolution (Quality):", resolution_layout)
462
+
463
+ self.tab_widget.addTab(wireframe_widget, "Wireframe")
464
+
465
+ def create_stick_tab(self):
466
+ """Stick設定タブを作成"""
467
+ stick_widget = QWidget()
468
+ form_layout = QFormLayout(stick_widget)
469
+
470
+ info_label = QLabel("Stick model shows bonds as thick cylinders with atoms as small spheres.")
471
+ info_label.setWordWrap(True)
472
+ info_label.setStyleSheet("color: #666; font-style: italic; margin-top: 10px;")
473
+ form_layout.addRow(info_label)
474
+
475
+ # 原子半径
476
+ self.stick_atom_radius_slider = QSlider(Qt.Orientation.Horizontal)
477
+ self.stick_atom_radius_slider.setRange(5, 50) # 0.05 ~ 0.5
478
+ self.stick_atom_radius_label = QLabel("0.15")
479
+ self.stick_atom_radius_slider.valueChanged.connect(lambda v: self.stick_atom_radius_label.setText(f"{v/100:.2f}"))
480
+ atom_radius_layout = QHBoxLayout()
481
+ atom_radius_layout.addWidget(self.stick_atom_radius_slider)
482
+ atom_radius_layout.addWidget(self.stick_atom_radius_label)
483
+ form_layout.addRow("Atom Radius:", atom_radius_layout)
484
+
485
+ # ボンド半径
486
+ self.stick_bond_radius_slider = QSlider(Qt.Orientation.Horizontal)
487
+ self.stick_bond_radius_slider.setRange(5, 50) # 0.05 ~ 0.5
488
+ self.stick_bond_radius_label = QLabel("0.15")
489
+ self.stick_bond_radius_slider.valueChanged.connect(lambda v: self.stick_bond_radius_label.setText(f"{v/100:.2f}"))
490
+ bond_radius_layout = QHBoxLayout()
491
+ bond_radius_layout.addWidget(self.stick_bond_radius_slider)
492
+ bond_radius_layout.addWidget(self.stick_bond_radius_label)
493
+ form_layout.addRow("Bond Radius:", bond_radius_layout)
494
+
495
+ # --- 区切り線(水平ライン) ---
496
+ line = QFrame()
497
+ line.setFrameShape(QFrame.Shape.HLine)
498
+ line.setFrameShadow(QFrame.Shadow.Sunken)
499
+ form_layout.addRow(line)
500
+
501
+ # --- Per-model multi-bond controls (Stick) ---
502
+ self.stick_double_offset_slider = QSlider(Qt.Orientation.Horizontal)
503
+ self.stick_double_offset_slider.setRange(50, 400)
504
+ self.stick_double_offset_label = QLabel("2.00")
505
+ self.stick_double_offset_slider.valueChanged.connect(lambda v: self.stick_double_offset_label.setText(f"{v/100:.2f}"))
506
+ stick_double_offset_layout = QHBoxLayout()
507
+ stick_double_offset_layout.addWidget(self.stick_double_offset_slider)
508
+ stick_double_offset_layout.addWidget(self.stick_double_offset_label)
509
+ form_layout.addRow("Double Bond Offset (Stick):", stick_double_offset_layout)
510
+
511
+ self.stick_triple_offset_slider = QSlider(Qt.Orientation.Horizontal)
512
+ self.stick_triple_offset_slider.setRange(50, 400)
513
+ self.stick_triple_offset_label = QLabel("2.00")
514
+ self.stick_triple_offset_slider.valueChanged.connect(lambda v: self.stick_triple_offset_label.setText(f"{v/100:.2f}"))
515
+ stick_triple_offset_layout = QHBoxLayout()
516
+ stick_triple_offset_layout.addWidget(self.stick_triple_offset_slider)
517
+ stick_triple_offset_layout.addWidget(self.stick_triple_offset_label)
518
+ form_layout.addRow("Triple Bond Offset (Stick):", stick_triple_offset_layout)
519
+
520
+ self.stick_double_radius_slider = QSlider(Qt.Orientation.Horizontal)
521
+ self.stick_double_radius_slider.setRange(20, 100)
522
+ self.stick_double_radius_label = QLabel("0.80")
523
+ self.stick_double_radius_slider.valueChanged.connect(lambda v: self.stick_double_radius_label.setText(f"{v/100:.2f}"))
524
+ stick_double_radius_layout = QHBoxLayout()
525
+ stick_double_radius_layout.addWidget(self.stick_double_radius_slider)
526
+ stick_double_radius_layout.addWidget(self.stick_double_radius_label)
527
+ form_layout.addRow("Double Bond Thickness (Stick):", stick_double_radius_layout)
528
+
529
+ self.stick_triple_radius_slider = QSlider(Qt.Orientation.Horizontal)
530
+ self.stick_triple_radius_slider.setRange(20, 100)
531
+ self.stick_triple_radius_label = QLabel("0.70")
532
+ self.stick_triple_radius_slider.valueChanged.connect(lambda v: self.stick_triple_radius_label.setText(f"{v/100:.2f}"))
533
+ stick_triple_radius_layout = QHBoxLayout()
534
+ stick_triple_radius_layout.addWidget(self.stick_triple_radius_slider)
535
+ stick_triple_radius_layout.addWidget(self.stick_triple_radius_label)
536
+ form_layout.addRow("Triple Bond Thickness (Stick):", stick_triple_radius_layout)
537
+
538
+ # --- 区切り線(水平ライン) ---
539
+ line = QFrame()
540
+ line.setFrameShape(QFrame.Shape.HLine)
541
+ line.setFrameShadow(QFrame.Shadow.Sunken)
542
+ form_layout.addRow(line)
543
+
544
+ # 解像度
545
+ self.stick_resolution_slider = QSlider(Qt.Orientation.Horizontal)
546
+ self.stick_resolution_slider.setRange(6, 32)
547
+ self.stick_resolution_label = QLabel("16")
548
+ self.stick_resolution_slider.valueChanged.connect(lambda v: self.stick_resolution_label.setText(str(v)))
549
+ resolution_layout = QHBoxLayout()
550
+ resolution_layout.addWidget(self.stick_resolution_slider)
551
+ resolution_layout.addWidget(self.stick_resolution_label)
552
+ form_layout.addRow("Resolution (Quality):", resolution_layout)
553
+
554
+
555
+
556
+
557
+ self.tab_widget.addTab(stick_widget, "Stick")
558
+
559
+ def reset_current_tab(self):
560
+ """現在選択されているタブの設定のみをデフォルトに戻す"""
561
+ current_tab_index = self.tab_widget.currentIndex()
562
+ tab_name = self.tab_widget.tabText(current_tab_index)
563
+
564
+ # 各タブの設定項目を定義
565
+ # Note: tab labels must match those added to the QTabWidget ("Scene", "Ball & Stick",
566
+ # "CPK (Space-filling)", "Wireframe", "Stick", "Other"). Use the per-model
567
+ # multi-bond keys present in self.default_settings.
568
+ tab_settings = {
569
+ "Scene": {
570
+ 'background_color': self.default_settings['background_color'],
571
+ 'projection_mode': self.default_settings['projection_mode'],
572
+ 'show_3d_axes': self.default_settings['show_3d_axes'],
573
+ 'lighting_enabled': self.default_settings['lighting_enabled'],
574
+ 'light_intensity': self.default_settings['light_intensity'],
575
+ 'specular': self.default_settings['specular'],
576
+ 'specular_power': self.default_settings['specular_power']
577
+ },
578
+ "Other": {
579
+ # other options
580
+ 'skip_chemistry_checks': self.default_settings.get('skip_chemistry_checks', False),
581
+ },
582
+ "Ball & Stick": {
583
+ 'ball_stick_atom_scale': self.default_settings['ball_stick_atom_scale'],
584
+ 'ball_stick_bond_radius': self.default_settings['ball_stick_bond_radius'],
585
+ 'ball_stick_resolution': self.default_settings['ball_stick_resolution'],
586
+ 'ball_stick_double_bond_offset_factor': self.default_settings.get('ball_stick_double_bond_offset_factor', 2.0),
587
+ 'ball_stick_triple_bond_offset_factor': self.default_settings.get('ball_stick_triple_bond_offset_factor', 2.0),
588
+ 'ball_stick_double_bond_radius_factor': self.default_settings.get('ball_stick_double_bond_radius_factor', 0.8),
589
+ 'ball_stick_triple_bond_radius_factor': self.default_settings.get('ball_stick_triple_bond_radius_factor', 0.75)
590
+ },
591
+ "CPK (Space-filling)": {
592
+ 'cpk_atom_scale': self.default_settings['cpk_atom_scale'],
593
+ 'cpk_resolution': self.default_settings['cpk_resolution'],
594
+ },
595
+ "Wireframe": {
596
+ 'wireframe_bond_radius': self.default_settings['wireframe_bond_radius'],
597
+ 'wireframe_resolution': self.default_settings['wireframe_resolution'],
598
+ 'wireframe_double_bond_offset_factor': self.default_settings.get('wireframe_double_bond_offset_factor', 3.0),
599
+ 'wireframe_triple_bond_offset_factor': self.default_settings.get('wireframe_triple_bond_offset_factor', 3.0),
600
+ 'wireframe_double_bond_radius_factor': self.default_settings.get('wireframe_double_bond_radius_factor', 0.8),
601
+ 'wireframe_triple_bond_radius_factor': self.default_settings.get('wireframe_triple_bond_radius_factor', 0.75)
602
+ },
603
+ "Stick": {
604
+ 'stick_atom_radius': self.default_settings['stick_atom_radius'],
605
+ 'stick_bond_radius': self.default_settings['stick_bond_radius'],
606
+ 'stick_resolution': self.default_settings['stick_resolution'],
607
+ 'stick_double_bond_offset_factor': self.default_settings.get('stick_double_bond_offset_factor', 1.5),
608
+ 'stick_triple_bond_offset_factor': self.default_settings.get('stick_triple_bond_offset_factor', 1.0),
609
+ 'stick_double_bond_radius_factor': self.default_settings.get('stick_double_bond_radius_factor', 0.6),
610
+ 'stick_triple_bond_radius_factor': self.default_settings.get('stick_triple_bond_radius_factor', 0.4)
611
+ }
612
+ }
613
+
614
+ # 選択されたタブの設定のみを適用
615
+ if tab_name in tab_settings:
616
+ tab_defaults = tab_settings[tab_name]
617
+
618
+ # 現在の設定を取得
619
+ current_settings = self.get_current_ui_settings()
620
+
621
+ # 選択されたタブの項目のみをデフォルト値で更新
622
+ updated_settings = current_settings.copy()
623
+ updated_settings.update(tab_defaults)
624
+
625
+ # UIを更新
626
+ self.update_ui_from_settings(updated_settings)
627
+
628
+ # ユーザーへのフィードバック
629
+ QMessageBox.information(self, "Reset Complete", f"Settings for '{tab_name}' tab have been reset to defaults.")
630
+ else:
631
+ QMessageBox.warning(self, "Error", f"Unknown tab: {tab_name}")
632
+
633
+ def reset_all_settings(self):
634
+ """すべての設定をデフォルトに戻す"""
635
+ reply = QMessageBox.question(
636
+ self,
637
+ "Reset All Settings",
638
+ "Are you sure you want to reset all settings to defaults?",
639
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
640
+ QMessageBox.StandardButton.No
641
+ )
642
+
643
+ if reply == QMessageBox.StandardButton.Yes:
644
+ # Update the dialog UI
645
+ self.update_ui_from_settings(self.default_settings)
646
+
647
+ # Also persist defaults to the application-level settings if parent is available
648
+ try:
649
+ if self.parent_window and hasattr(self.parent_window, 'settings'):
650
+ # Update parent settings and save
651
+ self.parent_window.settings.update(self.default_settings)
652
+ # defer writing to disk; mark dirty so closeEvent will persist
653
+ try:
654
+ self.parent_window.settings_dirty = True
655
+ except Exception:
656
+ pass
657
+ # Also ensure color settings return to defaults and UI reflects them
658
+ try:
659
+ # Remove any CPK overrides to restore defaults
660
+ if 'cpk_colors' in self.parent_window.settings:
661
+ # Reset to defaults (empty override dict)
662
+ self.parent_window.settings['cpk_colors'] = {}
663
+ # 2D bond color is fixed and not part of the settings; do not modify it here
664
+ # Reset 3D Ball & Stick uniform bond color to default
665
+ self.parent_window.settings['ball_stick_bond_color'] = self.default_settings.get('ball_stick_bond_color', '#7F7F7F')
666
+ # Update global CPK colors and reapply 3D settings immediately
667
+ try:
668
+ self.parent_window.update_cpk_colors_from_settings()
669
+ except Exception:
670
+ pass
671
+ try:
672
+ self.parent_window.apply_3d_settings()
673
+ except Exception:
674
+ pass
675
+ # Re-draw current 3D molecule if any
676
+ try:
677
+ if hasattr(self.parent_window, 'current_mol') and self.parent_window.current_mol:
678
+ self.parent_window.draw_molecule_3d(self.parent_window.current_mol)
679
+ except Exception:
680
+ pass
681
+ # Update 2D scene items to reflect color reset
682
+ try:
683
+ if hasattr(self.parent_window, 'scene'):
684
+ for it in self.parent_window.scene.items():
685
+ try:
686
+ if hasattr(it, 'update_style'):
687
+ it.update_style()
688
+ except Exception:
689
+ pass
690
+ except Exception:
691
+ pass
692
+ # Mark settings dirty so they'll be saved on exit
693
+ try:
694
+ self.parent_window.settings_dirty = True
695
+ except Exception:
696
+ pass
697
+ except Exception:
698
+ pass
699
+
700
+ # Refresh parent's optimization and conversion menu/action states
701
+ try:
702
+ # Optimization method
703
+ if hasattr(self.parent_window, 'optimization_method'):
704
+ self.parent_window.optimization_method = self.parent_window.settings.get('optimization_method', 'MMFF_RDKIT')
705
+ if hasattr(self.parent_window, 'opt3d_actions'):
706
+ for k, act in self.parent_window.opt3d_actions.items():
707
+ try:
708
+ act.setChecked(k.upper() == (self.parent_window.optimization_method or '').upper())
709
+ except Exception:
710
+ pass
711
+
712
+ # Conversion mode
713
+ conv_mode = self.parent_window.settings.get('3d_conversion_mode', 'fallback')
714
+ if hasattr(self.parent_window, 'conv_actions'):
715
+ for k, act in self.parent_window.conv_actions.items():
716
+ try:
717
+ act.setChecked(k == conv_mode)
718
+ except Exception:
719
+ pass
720
+ except Exception:
721
+ pass
722
+ except Exception:
723
+ pass
724
+
725
+ QMessageBox.information(self, "Reset Complete", "All settings have been reset to defaults.")
726
+
727
+ def get_current_ui_settings(self):
728
+ """現在のUIから設定値を取得"""
729
+ return {
730
+ 'background_color': self.current_bg_color,
731
+ 'show_3d_axes': self.axes_checkbox.isChecked(),
732
+ 'lighting_enabled': self.light_checkbox.isChecked(),
733
+ 'light_intensity': self.intensity_slider.value() / 100.0,
734
+ 'specular': self.specular_slider.value() / 100.0,
735
+ 'specular_power': self.spec_power_slider.value(),
736
+ # Ball and Stick settings
737
+ 'ball_stick_atom_scale': self.bs_atom_scale_slider.value() / 100.0,
738
+ 'ball_stick_bond_radius': self.bs_bond_radius_slider.value() / 100.0,
739
+ 'ball_stick_resolution': self.bs_resolution_slider.value(),
740
+ # CPK settings
741
+ 'cpk_atom_scale': self.cpk_atom_scale_slider.value() / 100.0,
742
+ 'cpk_resolution': self.cpk_resolution_slider.value(),
743
+ # Wireframe settings
744
+ 'wireframe_bond_radius': self.wf_bond_radius_slider.value() / 100.0,
745
+ 'wireframe_resolution': self.wf_resolution_slider.value(),
746
+ # Stick settings
747
+ 'stick_atom_radius': self.stick_atom_radius_slider.value() / 100.0,
748
+ 'stick_bond_radius': self.stick_bond_radius_slider.value() / 100.0,
749
+ 'stick_resolution': self.stick_resolution_slider.value(),
750
+ # Multi-bond settings (per-model)
751
+ 'ball_stick_double_bond_offset_factor': self.bs_double_offset_slider.value() / 100.0,
752
+ 'ball_stick_triple_bond_offset_factor': self.bs_triple_offset_slider.value() / 100.0,
753
+ 'ball_stick_double_bond_radius_factor': self.bs_double_radius_slider.value() / 100.0,
754
+ 'ball_stick_triple_bond_radius_factor': self.bs_triple_radius_slider.value() / 100.0,
755
+ 'wireframe_double_bond_offset_factor': self.wf_double_offset_slider.value() / 100.0,
756
+ 'wireframe_triple_bond_offset_factor': self.wf_triple_offset_slider.value() / 100.0,
757
+ 'wireframe_double_bond_radius_factor': self.wf_double_radius_slider.value() / 100.0,
758
+ 'wireframe_triple_bond_radius_factor': self.wf_triple_radius_slider.value() / 100.0,
759
+ 'stick_double_bond_offset_factor': self.stick_double_offset_slider.value() / 100.0,
760
+ 'stick_triple_bond_offset_factor': self.stick_triple_offset_slider.value() / 100.0,
761
+ 'stick_double_bond_radius_factor': self.stick_double_radius_slider.value() / 100.0,
762
+ 'stick_triple_bond_radius_factor': self.stick_triple_radius_slider.value() / 100.0,
763
+ }
764
+
765
+ def reset_to_defaults(self):
766
+ """UIをデフォルト設定に戻す(後方互換性のため残存)"""
767
+ self.reset_all_settings()
768
+
769
+ def update_ui_from_settings(self, settings_dict):
770
+ # 基本設定
771
+ self.current_bg_color = settings_dict.get('background_color', self.default_settings['background_color'])
772
+ self.update_color_button(self.current_bg_color)
773
+ self.axes_checkbox.setChecked(settings_dict.get('show_3d_axes', self.default_settings['show_3d_axes']))
774
+ self.light_checkbox.setChecked(settings_dict.get('lighting_enabled', self.default_settings['lighting_enabled']))
775
+
776
+ # スライダーの値を設定
777
+ intensity_val = int(settings_dict.get('light_intensity', self.default_settings['light_intensity']) * 100)
778
+ self.intensity_slider.setValue(intensity_val)
779
+ self.intensity_label.setText(f"{intensity_val/100:.2f}")
780
+
781
+ specular_val = int(settings_dict.get('specular', self.default_settings['specular']) * 100)
782
+ self.specular_slider.setValue(specular_val)
783
+ self.specular_label.setText(f"{specular_val/100:.2f}")
784
+
785
+ self.spec_power_slider.setValue(settings_dict.get('specular_power', self.default_settings['specular_power']))
786
+ self.spec_power_label.setText(str(settings_dict.get('specular_power', self.default_settings['specular_power'])))
787
+
788
+ # Ball and Stick設定
789
+ bs_atom_scale = int(settings_dict.get('ball_stick_atom_scale', self.default_settings['ball_stick_atom_scale']) * 100)
790
+ self.bs_atom_scale_slider.setValue(bs_atom_scale)
791
+ self.bs_atom_scale_label.setText(f"{bs_atom_scale/100:.2f}")
792
+
793
+ bs_bond_radius = int(settings_dict.get('ball_stick_bond_radius', self.default_settings['ball_stick_bond_radius']) * 100)
794
+ self.bs_bond_radius_slider.setValue(bs_bond_radius)
795
+ self.bs_bond_radius_label.setText(f"{bs_bond_radius/100:.2f}")
796
+
797
+ self.bs_resolution_slider.setValue(settings_dict.get('ball_stick_resolution', self.default_settings['ball_stick_resolution']))
798
+ self.bs_resolution_label.setText(str(settings_dict.get('ball_stick_resolution', self.default_settings['ball_stick_resolution'])))
799
+ # Ball & Stick bond color (uniform gray color for ball-and-stick)
800
+ bs_bond_color = settings_dict.get('ball_stick_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F'))
801
+ try:
802
+ self.bs_bond_color = QColor(bs_bond_color).name()
803
+ except Exception:
804
+ self.bs_bond_color = self.default_settings.get('ball_stick_bond_color', '#7F7F7F')
805
+ # Ensure color button exists and update its appearance
806
+ try:
807
+ if hasattr(self, 'bs_bond_color_button'):
808
+ self.bs_bond_color_button.setStyleSheet(f"background-color: {self.bs_bond_color}; border: 1px solid #888;")
809
+ try:
810
+ self.bs_bond_color_button.setToolTip(self.bs_bond_color)
811
+ except Exception:
812
+ pass
813
+ except Exception:
814
+ pass
815
+
816
+ # CPK設定
817
+ cpk_atom_scale = int(settings_dict.get('cpk_atom_scale', self.default_settings['cpk_atom_scale']) * 100)
818
+ self.cpk_atom_scale_slider.setValue(cpk_atom_scale)
819
+ self.cpk_atom_scale_label.setText(f"{cpk_atom_scale/100:.2f}")
820
+
821
+ self.cpk_resolution_slider.setValue(settings_dict.get('cpk_resolution', self.default_settings['cpk_resolution']))
822
+ self.cpk_resolution_label.setText(str(settings_dict.get('cpk_resolution', self.default_settings['cpk_resolution'])))
823
+
824
+ # Wireframe設定
825
+ wf_bond_radius = int(settings_dict.get('wireframe_bond_radius', self.default_settings['wireframe_bond_radius']) * 100)
826
+ self.wf_bond_radius_slider.setValue(wf_bond_radius)
827
+ self.wf_bond_radius_label.setText(f"{wf_bond_radius/100:.2f}")
828
+
829
+ self.wf_resolution_slider.setValue(settings_dict.get('wireframe_resolution', self.default_settings['wireframe_resolution']))
830
+ self.wf_resolution_label.setText(str(settings_dict.get('wireframe_resolution', self.default_settings['wireframe_resolution'])))
831
+
832
+ # Stick設定
833
+ stick_atom_radius = int(settings_dict.get('stick_atom_radius', self.default_settings['stick_atom_radius']) * 100)
834
+ self.stick_atom_radius_slider.setValue(stick_atom_radius)
835
+ self.stick_atom_radius_label.setText(f"{stick_atom_radius/100:.2f}")
836
+
837
+ stick_bond_radius = int(settings_dict.get('stick_bond_radius', self.default_settings['stick_bond_radius']) * 100)
838
+ self.stick_bond_radius_slider.setValue(stick_bond_radius)
839
+ self.stick_bond_radius_label.setText(f"{stick_bond_radius/100:.2f}")
840
+
841
+ self.stick_resolution_slider.setValue(settings_dict.get('stick_resolution', self.default_settings['stick_resolution']))
842
+ self.stick_resolution_label.setText(str(settings_dict.get('stick_resolution', self.default_settings['stick_resolution'])))
843
+
844
+ # 多重結合設定(モデル毎) — 後方互換のため既存のグローバルキーがあればフォールバック
845
+ # Ball & Stick
846
+ 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)
847
+ self.bs_double_offset_slider.setValue(bs_double_offset)
848
+ self.bs_double_offset_label.setText(f"{bs_double_offset/100:.2f}")
849
+
850
+ 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)
851
+ self.bs_triple_offset_slider.setValue(bs_triple_offset)
852
+ self.bs_triple_offset_label.setText(f"{bs_triple_offset/100:.2f}")
853
+
854
+ 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)
855
+ self.bs_double_radius_slider.setValue(bs_double_radius)
856
+ self.bs_double_radius_label.setText(f"{bs_double_radius/100:.2f}")
857
+
858
+ 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)
859
+ self.bs_triple_radius_slider.setValue(bs_triple_radius)
860
+ self.bs_triple_radius_label.setText(f"{bs_triple_radius/100:.2f}")
861
+
862
+ # Wireframe
863
+ 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)
864
+ self.wf_double_offset_slider.setValue(wf_double_offset)
865
+ self.wf_double_offset_label.setText(f"{wf_double_offset/100:.2f}")
866
+
867
+ 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)
868
+ self.wf_triple_offset_slider.setValue(wf_triple_offset)
869
+ self.wf_triple_offset_label.setText(f"{wf_triple_offset/100:.2f}")
870
+
871
+ 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)
872
+ self.wf_double_radius_slider.setValue(wf_double_radius)
873
+ self.wf_double_radius_label.setText(f"{wf_double_radius/100:.2f}")
874
+
875
+ 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)
876
+ self.wf_triple_radius_slider.setValue(wf_triple_radius)
877
+ self.wf_triple_radius_label.setText(f"{wf_triple_radius/100:.2f}")
878
+
879
+ # Stick
880
+ 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)
881
+ self.stick_double_offset_slider.setValue(stick_double_offset)
882
+ self.stick_double_offset_label.setText(f"{stick_double_offset/100:.2f}")
883
+
884
+ 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)
885
+ self.stick_triple_offset_slider.setValue(stick_triple_offset)
886
+ self.stick_triple_offset_label.setText(f"{stick_triple_offset/100:.2f}")
887
+
888
+ 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)
889
+ self.stick_double_radius_slider.setValue(stick_double_radius)
890
+ self.stick_double_radius_label.setText(f"{stick_double_radius/100:.2f}")
891
+
892
+ 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)
893
+ self.stick_triple_radius_slider.setValue(stick_triple_radius)
894
+ self.stick_triple_radius_label.setText(f"{stick_triple_radius/100:.2f}")
895
+
896
+ # Projection mode
897
+ proj_mode = settings_dict.get('projection_mode', self.default_settings.get('projection_mode', 'Perspective'))
898
+ idx = self.projection_combo.findText(proj_mode)
899
+ self.projection_combo.setCurrentIndex(idx if idx != -1 else 0)
900
+ # skip chemistry checks
901
+ self.skip_chem_checks_checkbox.setChecked(settings_dict.get('skip_chemistry_checks', self.default_settings.get('skip_chemistry_checks', False)))
902
+
903
+ def select_color(self):
904
+ """カラーピッカーを開き、選択された色を内部変数とUIに反映させる"""
905
+ # 内部変数から現在の色を取得してカラーピッカーを初期化
906
+ color = QColorDialog.getColor(QColor(self.current_bg_color), self)
907
+ if color.isValid():
908
+ # 内部変数を更新
909
+ self.current_bg_color = color.name()
910
+ # UIの見た目を更新
911
+ self.update_color_button(self.current_bg_color)
912
+
913
+ def update_color_button(self, color_hex):
914
+ """ボタンの背景色と境界線を設定する"""
915
+ self.bg_button.setStyleSheet(f"background-color: {color_hex}; border: 1px solid #888;")
916
+
917
+ def get_settings(self):
918
+ return {
919
+ 'background_color': self.current_bg_color,
920
+ 'projection_mode': self.projection_combo.currentText(),
921
+ 'show_3d_axes': self.axes_checkbox.isChecked(),
922
+ 'lighting_enabled': self.light_checkbox.isChecked(),
923
+ 'light_intensity': self.intensity_slider.value() / 100.0,
924
+ 'specular': self.specular_slider.value() / 100.0,
925
+ 'specular_power': self.spec_power_slider.value(),
926
+ # Ball and Stick settings
927
+ 'ball_stick_atom_scale': self.bs_atom_scale_slider.value() / 100.0,
928
+ 'ball_stick_bond_radius': self.bs_bond_radius_slider.value() / 100.0,
929
+ 'ball_stick_resolution': self.bs_resolution_slider.value(),
930
+ # CPK settings
931
+ 'cpk_atom_scale': self.cpk_atom_scale_slider.value() / 100.0,
932
+ 'cpk_resolution': self.cpk_resolution_slider.value(),
933
+ # Wireframe settings
934
+ 'wireframe_bond_radius': self.wf_bond_radius_slider.value() / 100.0,
935
+ 'wireframe_resolution': self.wf_resolution_slider.value(),
936
+ # Stick settings
937
+ 'stick_atom_radius': self.stick_atom_radius_slider.value() / 100.0,
938
+ 'stick_bond_radius': self.stick_bond_radius_slider.value() / 100.0,
939
+ 'stick_resolution': self.stick_resolution_slider.value(),
940
+ # Multiple bond offset settings (per-model)
941
+ 'ball_stick_double_bond_offset_factor': self.bs_double_offset_slider.value() / 100.0,
942
+ 'ball_stick_triple_bond_offset_factor': self.bs_triple_offset_slider.value() / 100.0,
943
+ 'ball_stick_double_bond_radius_factor': self.bs_double_radius_slider.value() / 100.0,
944
+ 'ball_stick_triple_bond_radius_factor': self.bs_triple_radius_slider.value() / 100.0,
945
+ 'wireframe_double_bond_offset_factor': self.wf_double_offset_slider.value() / 100.0,
946
+ 'wireframe_triple_bond_offset_factor': self.wf_triple_offset_slider.value() / 100.0,
947
+ 'wireframe_double_bond_radius_factor': self.wf_double_radius_slider.value() / 100.0,
948
+ 'wireframe_triple_bond_radius_factor': self.wf_triple_radius_slider.value() / 100.0,
949
+ 'stick_double_bond_offset_factor': self.stick_double_offset_slider.value() / 100.0,
950
+ 'stick_triple_bond_offset_factor': self.stick_triple_offset_slider.value() / 100.0,
951
+ 'stick_double_bond_radius_factor': self.stick_double_radius_slider.value() / 100.0,
952
+ 'stick_triple_bond_radius_factor': self.stick_triple_radius_slider.value() / 100.0,
953
+ 'skip_chemistry_checks': self.skip_chem_checks_checkbox.isChecked(),
954
+ # Ball & Stick bond color (3D grey/uniform color)
955
+ 'ball_stick_bond_color': getattr(self, 'bs_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F')),
956
+ }
957
+
958
+ def pick_bs_bond_color(self):
959
+ """Open QColorDialog to pick Ball & Stick bond color (3D)."""
960
+ cur = getattr(self, 'bs_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F'))
961
+ color = QColorDialog.getColor(QColor(cur), self)
962
+ if color.isValid():
963
+ self.bs_bond_color = color.name()
964
+ try:
965
+ self.bs_bond_color_button.setStyleSheet(f"background-color: {self.bs_bond_color}; border: 1px solid #888;")
966
+ self.bs_bond_color_button.setToolTip(self.bs_bond_color)
967
+ except Exception:
968
+ pass
969
+
970
+ def apply_settings(self):
971
+ """設定を適用(ダイアログは開いたまま)"""
972
+ # 親ウィンドウの設定を更新
973
+ if self.parent_window:
974
+ settings = self.get_settings()
975
+ self.parent_window.settings.update(settings)
976
+ # Mark settings dirty; persist on exit to avoid frequent disk writes
977
+ try:
978
+ self.parent_window.settings_dirty = True
979
+ except Exception:
980
+ pass
981
+ # 3Dビューの設定を適用
982
+ self.parent_window.apply_3d_settings()
983
+ # Update CPK colors from settings if present (no-op otherwise)
984
+ try:
985
+ self.parent_window.update_cpk_colors_from_settings()
986
+ except Exception:
987
+ pass
988
+ # Refresh any open CPK color dialogs so they update their UI
989
+ try:
990
+ for w in QApplication.topLevelWidgets():
991
+ try:
992
+ # import locally to avoid circular import
993
+ try:
994
+ from .color_settings_dialog import ColorSettingsDialog
995
+ except Exception:
996
+ from modules.color_settings_dialog import ColorSettingsDialog
997
+ if isinstance(w, ColorSettingsDialog):
998
+ try:
999
+ w.refresh_ui()
1000
+ except Exception:
1001
+ pass
1002
+ except Exception:
1003
+ pass
1004
+ except Exception:
1005
+ pass
1006
+ # 現在の分子を再描画(設定変更を反映)
1007
+ if hasattr(self.parent_window, 'current_mol') and self.parent_window.current_mol:
1008
+ self.parent_window.draw_molecule_3d(self.parent_window.current_mol)
1009
+ # ステータスバーに適用完了を表示
1010
+ self.parent_window.statusBar().showMessage("Settings applied successfully")
1011
+
1012
+ def _on_skip_chem_checks_changed(self, state):
1013
+ """Handle user toggling of skip chemistry checks: persist and update UI.
1014
+
1015
+ state: Qt.Checked (2) or Qt.Unchecked (0)
1016
+ """
1017
+ try:
1018
+ enabled = bool(state)
1019
+ self.settings['skip_chemistry_checks'] = enabled
1020
+ # mark dirty instead of immediate save
1021
+ try:
1022
+ self.settings_dirty = True
1023
+ except Exception:
1024
+ pass
1025
+ # If skip is enabled, allow Optimize button; otherwise, respect chem_check flags
1026
+
1027
+ except Exception:
1028
+ pass
1029
+
1030
+ def accept(self):
1031
+ """ダイアログの設定を適用してから閉じる"""
1032
+ # apply_settingsを呼び出して設定を適用
1033
+ self.apply_settings()
1034
+ super().accept()