MoleditPy-linux 2.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from PyQt6.QtWidgets import (
14
+ QDialog, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
15
+ )
16
+ from PyQt6.QtCore import Qt
17
+ from PyQt6.QtWidgets import QMessageBox
18
+ import numpy as np
19
+
20
+ try:
21
+ from .dialog3_d_picking_mixin import Dialog3DPickingMixin
22
+ except Exception:
23
+ from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
24
+
25
+ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
26
+ def __init__(self, mol, main_window, axis, preselected_atoms=None, parent=None):
27
+ QDialog.__init__(self, parent)
28
+ Dialog3DPickingMixin.__init__(self)
29
+ self.mol = mol
30
+ self.main_window = main_window
31
+ self.axis = axis
32
+ self.selected_atoms = set()
33
+
34
+ # 事前選択された原子を追加(最大2個まで)
35
+ if preselected_atoms:
36
+ self.selected_atoms.update(preselected_atoms[:2])
37
+
38
+ self.init_ui()
39
+
40
+ # 事前選択された原子にラベルを追加
41
+ if self.selected_atoms:
42
+ for i, atom_idx in enumerate(sorted(self.selected_atoms), 1):
43
+ self.add_selection_label(atom_idx, f"Atom {i}")
44
+ self.update_display()
45
+
46
+ def init_ui(self):
47
+ axis_names = {'x': 'X-axis', 'y': 'Y-axis', 'z': 'Z-axis'}
48
+ self.setWindowTitle(f"Align to {axis_names[self.axis]}")
49
+ self.setModal(False) # モードレスにしてクリックを阻害しない
50
+ layout = QVBoxLayout(self)
51
+
52
+ # Instructions
53
+ instruction_label = QLabel(f"Click atoms in the 3D view to select them for alignment to the {axis_names[self.axis]}. Exactly 2 atoms are required. The first atom will be moved to the origin, and the second atom will be positioned on the {axis_names[self.axis]}.")
54
+ instruction_label.setWordWrap(True)
55
+ layout.addWidget(instruction_label)
56
+
57
+ # Selected atoms display
58
+ self.selection_label = QLabel("No atoms selected")
59
+ layout.addWidget(self.selection_label)
60
+
61
+ # Buttons
62
+ button_layout = QHBoxLayout()
63
+ self.clear_button = QPushButton("Clear Selection")
64
+ self.clear_button.clicked.connect(self.clear_selection)
65
+ button_layout.addWidget(self.clear_button)
66
+
67
+ button_layout.addStretch()
68
+
69
+ self.apply_button = QPushButton("Apply Alignment")
70
+ self.apply_button.clicked.connect(self.apply_alignment)
71
+ self.apply_button.setEnabled(False)
72
+ button_layout.addWidget(self.apply_button)
73
+
74
+ close_button = QPushButton("Close")
75
+ close_button.clicked.connect(self.reject)
76
+ button_layout.addWidget(close_button)
77
+
78
+ layout.addLayout(button_layout)
79
+
80
+ # Connect to main window's picker
81
+ self.picker_connection = None
82
+ self.enable_picking()
83
+
84
+ def enable_picking(self):
85
+ """3Dビューでの原子選択を有効にする"""
86
+ # Dialog3DPickingMixinの機能を使用
87
+ super().enable_picking()
88
+
89
+ def disable_picking(self):
90
+ """3Dビューでの原子選択を無効にする"""
91
+ # Dialog3DPickingMixinの機能を使用
92
+ super().disable_picking()
93
+
94
+ def on_atom_picked(self, atom_idx):
95
+ """原子がクリックされた時の処理"""
96
+ if self.main_window.current_mol is None:
97
+ return
98
+
99
+ if atom_idx in self.selected_atoms:
100
+ # 既に選択されている場合は選択解除
101
+ self.selected_atoms.remove(atom_idx)
102
+ self.remove_atom_label(atom_idx)
103
+ else:
104
+ # 2つまでしか選択できない
105
+ if len(self.selected_atoms) < 2:
106
+ self.selected_atoms.add(atom_idx)
107
+ # ラベルの順番を示す
108
+ label_text = f"Atom {len(self.selected_atoms)}"
109
+ self.add_selection_label(atom_idx, label_text)
110
+
111
+ self.update_display()
112
+
113
+ def update_display(self):
114
+ """選択状態の表示を更新"""
115
+ if len(self.selected_atoms) == 0:
116
+ self.selection_label.setText("Click atoms to select for alignment (exactly 2 required)")
117
+ self.apply_button.setEnabled(False)
118
+ elif len(self.selected_atoms) == 1:
119
+ selected_list = list(self.selected_atoms)
120
+ atom = self.mol.GetAtomWithIdx(selected_list[0])
121
+ self.selection_label.setText(f"Selected 1 atom: {atom.GetSymbol()}{selected_list[0]+1}")
122
+ self.apply_button.setEnabled(False)
123
+ elif len(self.selected_atoms) == 2:
124
+ selected_list = sorted(list(self.selected_atoms))
125
+ atom1 = self.mol.GetAtomWithIdx(selected_list[0])
126
+ atom2 = self.mol.GetAtomWithIdx(selected_list[1])
127
+ self.selection_label.setText(f"Selected 2 atoms: {atom1.GetSymbol()}{selected_list[0]+1}, {atom2.GetSymbol()}{selected_list[1]+1}")
128
+ self.apply_button.setEnabled(True)
129
+
130
+ def clear_selection(self):
131
+ """選択をクリア"""
132
+ self.clear_selection_labels()
133
+ self.selected_atoms.clear()
134
+ self.update_display()
135
+
136
+ def add_selection_label(self, atom_idx, label_text):
137
+ """選択された原子にラベルを追加"""
138
+ if not hasattr(self, 'selection_labels'):
139
+ self.selection_labels = []
140
+
141
+ # 原子の位置を取得
142
+ pos = self.main_window.atom_positions_3d[atom_idx]
143
+
144
+ # ラベルを追加
145
+ label_actor = self.main_window.plotter.add_point_labels(
146
+ [pos], [label_text],
147
+ point_size=20,
148
+ font_size=12,
149
+ text_color='yellow',
150
+ always_visible=True
151
+ )
152
+ self.selection_labels.append(label_actor)
153
+
154
+ def remove_atom_label(self, atom_idx):
155
+ """特定の原子のラベルを削除"""
156
+ # 簡単化のため、全ラベルをクリアして再描画
157
+ self.clear_selection_labels()
158
+ for i, idx in enumerate(sorted(self.selected_atoms), 1):
159
+ if idx != atom_idx:
160
+ self.add_selection_label(idx, f"Atom {i}")
161
+
162
+ def clear_selection_labels(self):
163
+ """選択ラベルをクリア"""
164
+ if hasattr(self, 'selection_labels'):
165
+ for label_actor in self.selection_labels:
166
+ try:
167
+ self.main_window.plotter.remove_actor(label_actor)
168
+ except Exception:
169
+ pass
170
+ self.selection_labels = []
171
+
172
+ def apply_alignment(self):
173
+ """アライメントを適用"""
174
+ if len(self.selected_atoms) != 2:
175
+ QMessageBox.warning(self, "Warning", "Please select exactly 2 atoms for alignment.")
176
+ return
177
+ try:
178
+
179
+ selected_list = sorted(list(self.selected_atoms))
180
+ atom1_idx, atom2_idx = selected_list[0], selected_list[1]
181
+
182
+ conf = self.mol.GetConformer()
183
+
184
+ # 原子の現在位置を取得
185
+ pos1 = np.array(conf.GetAtomPosition(atom1_idx))
186
+ pos2 = np.array(conf.GetAtomPosition(atom2_idx))
187
+
188
+ # 最初に全分子を移動して、atom1を原点に配置
189
+ translation = -pos1
190
+ for i in range(self.mol.GetNumAtoms()):
191
+ current_pos = np.array(conf.GetAtomPosition(i))
192
+ new_pos = current_pos + translation
193
+ conf.SetAtomPosition(i, new_pos.tolist())
194
+
195
+ # atom2の新しい位置を取得(移動後)
196
+ pos2_translated = pos2 + translation
197
+
198
+ # atom2を選択した軸上に配置するための回転を計算
199
+ axis_vectors = {
200
+ 'x': np.array([1.0, 0.0, 0.0]),
201
+ 'y': np.array([0.0, 1.0, 0.0]),
202
+ 'z': np.array([0.0, 0.0, 1.0])
203
+ }
204
+ target_axis = axis_vectors[self.axis]
205
+
206
+ # atom2から原点への方向ベクトル
207
+ current_vector = pos2_translated
208
+ current_length = np.linalg.norm(current_vector)
209
+
210
+ if current_length > 1e-10: # ゼロベクトルでない場合
211
+ current_vector_normalized = current_vector / current_length
212
+
213
+ # 回転軸と角度を計算
214
+ rotation_axis = np.cross(current_vector_normalized, target_axis)
215
+ rotation_axis_length = np.linalg.norm(rotation_axis)
216
+
217
+ if rotation_axis_length > 1e-10: # 回転が必要
218
+ rotation_axis = rotation_axis / rotation_axis_length
219
+ cos_angle = np.dot(current_vector_normalized, target_axis)
220
+ cos_angle = np.clip(cos_angle, -1.0, 1.0)
221
+ rotation_angle = np.arccos(cos_angle)
222
+
223
+ # ロドリゲスの回転公式を使用
224
+ def rodrigues_rotation(v, k, theta):
225
+ cos_theta = np.cos(theta)
226
+ sin_theta = np.sin(theta)
227
+ return (v * cos_theta +
228
+ np.cross(k, v) * sin_theta +
229
+ k * np.dot(k, v) * (1 - cos_theta))
230
+
231
+ # 全ての原子に回転を適用
232
+ for i in range(self.mol.GetNumAtoms()):
233
+ current_pos = np.array(conf.GetAtomPosition(i))
234
+ rotated_pos = rodrigues_rotation(current_pos, rotation_axis, rotation_angle)
235
+ conf.SetAtomPosition(i, rotated_pos.tolist())
236
+
237
+ # 3D座標を更新
238
+ self.main_window.atom_positions_3d = np.array([
239
+ list(conf.GetAtomPosition(i)) for i in range(self.mol.GetNumAtoms())
240
+ ])
241
+
242
+ # 3Dビューを更新
243
+ self.main_window.draw_molecule_3d(self.mol)
244
+
245
+ # キラルラベルを更新
246
+ self.main_window.update_chiral_labels()
247
+
248
+ # Undo状態を保存
249
+ self.main_window.push_undo_state()
250
+
251
+ QMessageBox.information(self, "Success", f"Alignment to {self.axis.upper()}-axis completed.")
252
+
253
+ except Exception as e:
254
+ QMessageBox.critical(self, "Error", f"Failed to apply alignment: {str(e)}")
255
+
256
+ def closeEvent(self, event):
257
+ """ダイアログが閉じられる時の処理"""
258
+ self.clear_selection_labels()
259
+ self.disable_picking()
260
+ super().closeEvent(event)
261
+
262
+ def reject(self):
263
+ """キャンセル時の処理"""
264
+ self.clear_selection_labels()
265
+ self.disable_picking()
266
+ super().reject()
267
+
268
+ def accept(self):
269
+ """OK時の処理"""
270
+ self.clear_selection_labels()
271
+ self.disable_picking()
272
+ super().accept()
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from PyQt6.QtWidgets import (
14
+ QDialog, QVBoxLayout, QGridLayout, QLabel, QLineEdit, QPushButton, QApplication
15
+ )
16
+ from PyQt6.QtCore import Qt
17
+ from rdkit import Chem
18
+ from rdkit.Chem import Descriptors, rdMolDescriptors
19
+ from rdkit.Chem import inchi as rd_inchi
20
+
21
+ class AnalysisWindow(QDialog):
22
+ def __init__(self, mol, parent=None, is_xyz_derived=False):
23
+ super().__init__(parent)
24
+ self.mol = mol
25
+ self.is_xyz_derived = is_xyz_derived # XYZ由来かどうかのフラグ
26
+ self.setWindowTitle("Molecule Analysis")
27
+ self.setMinimumWidth(400)
28
+ self.init_ui()
29
+
30
+ def init_ui(self):
31
+ main_layout = QVBoxLayout(self)
32
+ grid_layout = QGridLayout()
33
+
34
+ # --- 分子特性を計算 ---
35
+ try:
36
+ # RDKitのモジュールをインポート
37
+
38
+
39
+ if self.is_xyz_derived:
40
+ # XYZ由来の場合:元のXYZファイルの原子情報から直接計算
41
+ # (結合推定の影響を受けない)
42
+
43
+ # XYZファイルから読み込んだ元の原子情報を取得
44
+ if hasattr(self.mol, '_xyz_atom_data'):
45
+ xyz_atoms = self.mol._xyz_atom_data
46
+ else:
47
+ # フォールバック: RDKitオブジェクトから取得
48
+ xyz_atoms = [(atom.GetSymbol(), 0, 0, 0) for atom in self.mol.GetAtoms()]
49
+
50
+ # 原子数と元素種を集計
51
+ atom_counts = {}
52
+ total_atoms = len(xyz_atoms)
53
+ num_heavy_atoms = 0
54
+
55
+ for symbol, x, y, z in xyz_atoms:
56
+ atom_counts[symbol] = atom_counts.get(symbol, 0) + 1
57
+ if symbol != 'H': # 水素以外
58
+ num_heavy_atoms += 1
59
+
60
+ # 化学式を手動で構築(元素順序を考慮)
61
+ element_order = ['C', 'H', 'N', 'O', 'P', 'S', 'F', 'Cl', 'Br', 'I']
62
+ formula_parts = []
63
+
64
+ # 定義された順序で元素を追加
65
+ remaining_counts = atom_counts.copy()
66
+ for element in element_order:
67
+ if element in remaining_counts:
68
+ count = remaining_counts[element]
69
+ if count == 1:
70
+ formula_parts.append(element)
71
+ else:
72
+ formula_parts.append(f"{element}{count}")
73
+ del remaining_counts[element]
74
+
75
+ # 残りの元素をアルファベット順で追加
76
+ for element in sorted(remaining_counts.keys()):
77
+ count = remaining_counts[element]
78
+ if count == 1:
79
+ formula_parts.append(element)
80
+ else:
81
+ formula_parts.append(f"{element}{count}")
82
+
83
+ mol_formula = ''.join(formula_parts)
84
+
85
+ # 分子量と精密質量をRDKitから取得
86
+
87
+ mol_wt = 0.0
88
+ exact_mw = 0.0
89
+ pt = Chem.GetPeriodicTable()
90
+
91
+ for symbol, count in atom_counts.items():
92
+ try:
93
+ # RDKitの周期表から原子量と精密質量を取得
94
+ atomic_num = pt.GetAtomicNumber(symbol)
95
+ atomic_weight = pt.GetAtomicWeight(atomic_num)
96
+ exact_mass = pt.GetMostCommonIsotopeMass(atomic_num)
97
+
98
+ mol_wt += atomic_weight * count
99
+ exact_mw += exact_mass * count
100
+ except (ValueError, RuntimeError):
101
+ # 認識されない元素の場合はスキップ
102
+ print(f"Warning: Unknown element {symbol}, skipping in mass calculation")
103
+ continue
104
+
105
+ # 表示するプロパティを辞書にまとめる(XYZ元データから計算)
106
+ properties = {
107
+ "Molecular Formula:": mol_formula,
108
+ "Molecular Weight:": f"{mol_wt:.4f}",
109
+ "Exact Mass:": f"{exact_mw:.4f}",
110
+ "Heavy Atoms:": str(num_heavy_atoms),
111
+ "Total Atoms:": str(total_atoms),
112
+ }
113
+
114
+ # 注意メッセージを追加
115
+ note_label = QLabel("<i>Note: SMILES and structure-dependent properties are not available for XYZ-derived structures due to potential bond estimation inaccuracies.</i>")
116
+ note_label.setWordWrap(True)
117
+ main_layout.addWidget(note_label)
118
+
119
+ else:
120
+ # 通常の分子(MOLファイルや2Dエディタ由来)の場合:全てのプロパティを計算
121
+
122
+ # SMILES生成用に、一時的に水素原子を取り除いた分子オブジェクトを作成
123
+ mol_for_smiles = Chem.RemoveHs(self.mol)
124
+ # 水素を取り除いた分子からSMILESを生成(常に簡潔な表記になる)
125
+ smiles = Chem.MolToSmiles(mol_for_smiles, isomericSmiles=True)
126
+
127
+ # 各種プロパティを計算
128
+ mol_formula = rdMolDescriptors.CalcMolFormula(self.mol)
129
+ mol_wt = Descriptors.MolWt(self.mol)
130
+ exact_mw = Descriptors.ExactMolWt(self.mol)
131
+ num_heavy_atoms = self.mol.GetNumHeavyAtoms()
132
+ num_rings = rdMolDescriptors.CalcNumRings(self.mol)
133
+ log_p = Descriptors.MolLogP(self.mol)
134
+ tpsa = Descriptors.TPSA(self.mol)
135
+ num_h_donors = rdMolDescriptors.CalcNumHBD(self.mol)
136
+ num_h_acceptors = rdMolDescriptors.CalcNumHBA(self.mol)
137
+
138
+ # InChIを生成
139
+ try:
140
+ inchi = Chem.MolToInchi(self.mol)
141
+ except Exception:
142
+ inchi = "N/A"
143
+
144
+ # InChIKeyを生成(RDKitのinchi APIが無い場合に備えてフォールバック)
145
+ try:
146
+ # Prefer Chem.MolToInchiKey when available
147
+ inchi_key = None
148
+ try:
149
+ inchi_key = Chem.MolToInchiKey(self.mol)
150
+ except Exception:
151
+ # Fallback to rdkit.Chem.inchi if present
152
+ try:
153
+ inchi_key = rd_inchi.MolToInchiKey(self.mol)
154
+ except Exception:
155
+ inchi_key = None
156
+
157
+ if not inchi_key:
158
+ inchi_key = "N/A"
159
+ except Exception:
160
+ inchi_key = "N/A"
161
+
162
+ # 表示するプロパティを辞書にまとめる
163
+ properties = {
164
+ "SMILES:": smiles,
165
+ "InChI:": inchi,
166
+ "InChIKey:": inchi_key,
167
+ "Molecular Formula:": mol_formula,
168
+ "Molecular Weight:": f"{mol_wt:.4f}",
169
+ "Exact Mass:": f"{exact_mw:.4f}",
170
+ "Heavy Atoms:": str(num_heavy_atoms),
171
+ "Ring Count:": str(num_rings),
172
+ "LogP (o/w):": f"{log_p:.3f}",
173
+ "TPSA (Ų):": f"{tpsa:.2f}",
174
+ "H-Bond Donors:": str(num_h_donors),
175
+ "H-Bond Acceptors:": str(num_h_acceptors),
176
+ }
177
+ except Exception as e:
178
+ main_layout.addWidget(QLabel(f"Error calculating properties: {e}"))
179
+ return
180
+
181
+ # --- 計算結果をUIに表示 ---
182
+ row = 0
183
+ for label_text, value_text in properties.items():
184
+ label = QLabel(f"<b>{label_text}</b>")
185
+ value = QLineEdit(value_text)
186
+ value.setReadOnly(True)
187
+
188
+ copy_btn = QPushButton("Copy")
189
+ copy_btn.clicked.connect(lambda _, v=value: self.copy_to_clipboard(v.text()))
190
+
191
+ grid_layout.addWidget(label, row, 0)
192
+ grid_layout.addWidget(value, row, 1)
193
+ grid_layout.addWidget(copy_btn, row, 2)
194
+ row += 1
195
+
196
+ main_layout.addLayout(grid_layout)
197
+
198
+ # --- OKボタン ---
199
+ ok_button = QPushButton("OK")
200
+ ok_button.clicked.connect(self.accept)
201
+ main_layout.addWidget(ok_button, 0, Qt.AlignmentFlag.AlignCenter)
202
+
203
+ self.setLayout(main_layout)
204
+
205
+ def copy_to_clipboard(self, text):
206
+ clipboard = QApplication.clipboard()
207
+ clipboard.setText(text)
208
+ if self.parent() and hasattr(self.parent(), 'statusBar'):
209
+ self.parent().statusBar().showMessage(f"Copied '{text}' to clipboard.", 2000)