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