MoleditPy 2.2.0a2__py3-none-any.whl → 2.2.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 (27) hide show
  1. moleditpy/modules/constants.py +1 -1
  2. moleditpy/modules/main_window_main_init.py +32 -14
  3. moleditpy/modules/plugin_interface.py +1 -10
  4. moleditpy/modules/plugin_manager.py +0 -3
  5. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/METADATA +1 -1
  6. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/RECORD +10 -27
  7. moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -919
  8. moleditpy/plugins/File/animated_xyz_giffer.py +0 -583
  9. moleditpy/plugins/File/cube_viewer.py +0 -689
  10. moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -1148
  11. moleditpy/plugins/File/mapped_cube_viewer.py +0 -552
  12. moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -1226
  13. moleditpy/plugins/File/paste_xyz.py +0 -336
  14. moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -930
  15. moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -1028
  16. moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +0 -286
  17. moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -65
  18. moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -268
  19. moleditpy/plugins/Optimization/conf_search.py +0 -224
  20. moleditpy/plugins/Utility/atom_colorizer.py +0 -262
  21. moleditpy/plugins/Utility/console.py +0 -163
  22. moleditpy/plugins/Utility/pubchem_ressolver.py +0 -244
  23. moleditpy/plugins/Utility/vdw_radii_overlay.py +0 -432
  24. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/WHEEL +0 -0
  25. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/entry_points.txt +0 -0
  26. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/licenses/LICENSE +0 -0
  27. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/top_level.txt +0 -0
@@ -1,1148 +0,0 @@
1
- import os
2
- import re
3
- import numpy as np
4
- import traceback
5
- from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
6
- QListWidget, QSlider, QCheckBox, QFileDialog, QMessageBox,
7
- QDockWidget, QWidget, QSplitter, QApplication, QTreeWidget, QTreeWidgetItem, QHeaderView)
8
- from PyQt6.QtCore import Qt, QTimer, pyqtSignal
9
-
10
- # Try to import RDKit
11
- try:
12
- from rdkit import Chem
13
- from rdkit.Geometry import Point3D
14
- except ImportError:
15
- Chem = None
16
-
17
- PLUGIN_NAME = "Gaussian Freq Analyzer"
18
- __version__="2025.12.25"
19
- __author__="HiroYokoyama"
20
-
21
- class FCHKParser:
22
- def __init__(self):
23
- self.filename = ""
24
- self.atoms = [] # List of atomic numbers
25
- self.coords = [] # List of (x, y, z) in Angstrom
26
- self.frequencies = [] # List of frequencies in cm^-1
27
- self.vib_modes = [] # List of displacement vectors (natoms, 3)
28
- self.charge = 0
29
- self.multiplicity = 1
30
-
31
- def parse(self, filename):
32
- self.filename = filename
33
- self.atoms = []
34
- self.coords = []
35
- self.frequencies = []
36
- self.intensities = [] # IR Intensities
37
- self.vib_modes = [] # list of list of (dx, dy, dz)
38
-
39
- with open(filename, 'r') as f:
40
- lines = f.readlines()
41
-
42
- data = {}
43
- current_section = None
44
- current_values = []
45
-
46
- # Helper to process accumulated values
47
- def process_section(section, values):
48
- if section == "Atomic numbers":
49
- # Integers
50
- return [int(v) for v in values]
51
- elif section == "Current cartesian coordinates":
52
- # Floats, Bohr -> Angstrom? No, FCHK usually stores in Bohr.
53
- # Use standard conversion 0.529177
54
- return [float(v) for v in values]
55
- elif section == "Vib-E2":
56
- return [float(v) for v in values]
57
- elif section == "Vib-Modes":
58
- return [float(v) for v in values]
59
- elif section == "Dipole Derivatives":
60
- return [float(v) for v in values]
61
- elif section == "IR Inten":
62
- return [float(v) for v in values]
63
- elif section == "Charge":
64
- return [int(v) for v in values]
65
- elif section == "Multiplicity":
66
- return [int(v) for v in values]
67
- return values
68
-
69
- # Basic FCHK parsing
70
- i = 0
71
- while i < len(lines):
72
- line = lines[i].strip()
73
- if not line:
74
- i += 1
75
- continue
76
-
77
- # Section header?
78
- # Pattern: Name (starts upper) ... Type (I/R) ... N= (optional) ... Value/Count
79
- # Example: "Vib-Modes R N= 27"
80
- # Or: "Charge I 0"
81
- # We look for Capital Start, then some space, then I or R, then optional N=
82
-
83
- # Simple check: line starts with upper char
84
- if line[0].isupper():
85
- # Check for Type and N= signature more loosely but reliably
86
- # If " I " or " R " is present (at least 3 spaces before/after or N=)
87
- # Let's try Regex for robustness
88
- # Matches: Start with Word, spaces, I or R, spaces, (N= xxxxx)?
89
- match = re.search(r'^([A-Za-z0-9\-\s]+?)\s+([IR])\s+(?:N=\s+(\d+)|([0-9\-]+))', line)
90
-
91
- # Actually, simpler: check for " I " or " R " or " I " at specific columns?
92
- # FCHK is fixed format mostly.
93
- # Name: 0-40. Type: 43.
94
- # But let's trust " I" or " R" presence for now if regex is too complex to get right blindly.
95
- # However, the previous "R N=" failed. Maybe it was "R N=".
96
- # Let's use a regex that handles variable whitespace.
97
- if re.search(r'\s+[IR]\s+(N=)?\s+', line):
98
- # Store previous
99
- if current_section:
100
- data[current_section] = process_section(current_section, current_values)
101
-
102
- # Extract section name
103
- parts = line.split()
104
- # Name is usually first 1-N tokens until I/R
105
- # But Name can have spaces "Atomic numbers".
106
- # split by " I" or " R" is safest if exists.
107
-
108
- if " I" in line:
109
- current_section = line.split(" I")[0].strip()
110
- elif " R" in line:
111
- current_section = line.split(" R")[0].strip()
112
- else:
113
- # Fallback: scan tokens for I or R
114
- label_parts = []
115
- for p in parts:
116
- if p == 'I' or p == 'R':
117
- break
118
- label_parts.append(p)
119
- current_section = " ".join(label_parts)
120
-
121
- current_values = []
122
- i += 1
123
- continue
124
-
125
- # Data line (if not header)
126
-
127
- # Data line
128
- # Accumulated values
129
- # FCHK data lines are space separated
130
- tokens = line.split()
131
- current_values.extend(tokens)
132
- i += 1
133
-
134
- # Last section
135
- if current_section:
136
- data[current_section] = process_section(current_section, current_values)
137
-
138
- # Extract specific data
139
- if "Atomic numbers" in data:
140
- self.atoms = data["Atomic numbers"]
141
-
142
- BOHR_TO_ANG = 0.529177210903
143
-
144
- if "Current cartesian coordinates" in data:
145
- raw_coords = data["Current cartesian coordinates"]
146
- # Convert to list of tuples (x,y,z)
147
- # FCHK coords are X1, Y1, Z1, X2... in Bohr
148
- coords_ang = []
149
- for j in range(0, len(raw_coords), 3):
150
- if j+2 < len(raw_coords):
151
- x = raw_coords[j] * BOHR_TO_ANG
152
- y = raw_coords[j+1] * BOHR_TO_ANG
153
- z = raw_coords[j+2] * BOHR_TO_ANG
154
- coords_ang.append((x, y, z))
155
- self.coords = coords_ang
156
-
157
- # Parse Vib-E2 section properly
158
- # Vib-E2 contains blocks: [Frequencies, Reduced Masses, Force Constants, IR Intensities, ...]
159
- # Each block has n_modes values
160
- if "Vib-E2" in data:
161
- raw_e2 = data["Vib-E2"]
162
-
163
- # Determine n_modes from Vib-Modes section (safest approach)
164
- if "Vib-Modes" in data:
165
- n_atoms = len(self.atoms) if self.atoms else 0
166
- if n_atoms > 0:
167
- # Vib-Modes size = 3 * n_atoms * n_modes
168
- n_modes = len(data["Vib-Modes"]) // (3 * n_atoms)
169
- else:
170
- n_modes = 0
171
- else:
172
- # Fallback: estimate from 3N-6 (or 3N-5 for linear)
173
- n_atoms = len(self.atoms)
174
- n_modes = max(1, 3 * n_atoms - 6) if n_atoms > 2 else 1
175
-
176
- if n_modes > 0 and len(raw_e2) >= n_modes:
177
- # Block 1: Frequencies (0 to n_modes)
178
- self.frequencies = raw_e2[0:n_modes]
179
-
180
- # Block 4: IR Intensities (3*n_modes to 4*n_modes)
181
- # Already in km/mol units - NO conversion needed!
182
- if len(raw_e2) >= 4 * n_modes:
183
- ir_start = 3 * n_modes
184
- ir_end = 4 * n_modes
185
- self.intensities = raw_e2[ir_start:ir_end]
186
-
187
- # Get actual masses used by Gaussian
188
- self.masses = []
189
- if "Real atomic weights" in data:
190
- self.masses = [float(m) for m in data["Real atomic weights"]]
191
- elif "Atomic numbers" in data:
192
- # Fallback: RDKit average atomic weight (slight difference)
193
- if Chem:
194
- from rdkit.Chem import GetPeriodicTable
195
- pt = GetPeriodicTable()
196
- self.masses = [pt.GetAtomicWeight(int(z)) for z in data["Atomic numbers"]]
197
-
198
- # Fallback: Check for separate IR Inten section (uncommon but possible)
199
- # Gaussian's conversion factor: Atomic Units -> km/mol
200
- CONV_FACTOR = 974.868
201
-
202
- # Only look for separate IR section if we didn't get it from Vib-E2
203
- if not self.intensities or len(self.intensities) == 0:
204
- ir_key = None
205
- for key in data.keys():
206
- if key.strip().lower() in ["ir inten", "ir intensities"]:
207
- ir_key = key
208
- break
209
-
210
- if ir_key:
211
- raw_vals = [float(val) for val in data[ir_key]]
212
- # Always convert from a.u. to km/mol using Gaussian's factor
213
- self.intensities = [v * CONV_FACTOR for v in raw_vals]
214
-
215
- if "Dipole Derivatives" in data:
216
- self.dipole_derivs = data["Dipole Derivatives"]
217
-
218
- if "Vib-Modes" in data:
219
- # Modes are stored as X1, Y1, Z1... for mode 1, then mode 2...
220
- # Size should be 3*N_atoms * N_freqs
221
- raw_modes = data["Vib-Modes"]
222
- n_atoms = len(self.atoms)
223
- n_modes = len(self.frequencies)
224
- mode_len = n_atoms * 3
225
-
226
- parsed_modes = []
227
- for m in range(n_modes):
228
- start = m * mode_len
229
- end = start + mode_len
230
- if end <= len(raw_modes):
231
- mode_vec = raw_modes[start:end]
232
- # Structure as list of (dx, dy, dz)
233
- vecs = []
234
- for k in range(0, len(mode_vec), 3):
235
- dx = mode_vec[k]
236
- dy = mode_vec[k+1]
237
- dz = mode_vec[k+2]
238
- vecs.append((dx, dy, dz))
239
- parsed_modes.append(vecs)
240
- self.vib_modes = parsed_modes
241
-
242
- # Consistency Fix:
243
- # Sync frequencies, modes, and intensities to avoid mismatches
244
- if len(self.frequencies) != len(self.vib_modes):
245
- n_valid = min(len(self.frequencies), len(self.vib_modes))
246
- self.frequencies = self.frequencies[:n_valid]
247
- self.vib_modes = self.vib_modes[:n_valid]
248
- if self.intensities and len(self.intensities) > n_valid:
249
- self.intensities = self.intensities[:n_valid]
250
-
251
-
252
-
253
- if "Charge" in data and len(data["Charge"]) > 0:
254
- self.charge = data["Charge"][0]
255
-
256
- if "Multiplicity" in data and len(data["Multiplicity"]) > 0:
257
- self.multiplicity = data["Multiplicity"][0]
258
-
259
-
260
- from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
261
- QListWidget, QSlider, QCheckBox, QFileDialog, QMessageBox,
262
- QDockWidget, QWidget, QSplitter, QFormLayout, QDialogButtonBox, QSpinBox, QDoubleSpinBox)
263
- from PyQt6.QtGui import QImage, QPainter, QPen, QColor, QFont, QPaintEvent
264
- try:
265
- from PIL import Image
266
- HAS_PIL = True
267
- except ImportError:
268
- HAS_PIL = False
269
-
270
- # ... (Previous imports safely handled by original file or above)
271
-
272
-
273
- class GaussianFCHKFreqAnalyzer(QWidget):
274
- def __init__(self, main_window, dock_widget=None):
275
- super().__init__(main_window)
276
- self.mw = main_window
277
- self.dock = dock_widget
278
- self.setAcceptDrops(True)
279
-
280
- self.parser = None
281
- self.base_mol = None
282
- self.timer = QTimer()
283
- self.timer.timeout.connect(self.animate_frame)
284
- self.animation_step = 0
285
- self.is_playing = False
286
- self.vector_actor = None
287
-
288
- self.init_ui()
289
-
290
- def init_ui(self):
291
- layout = QVBoxLayout(self)
292
-
293
- # Info Label
294
- self.lbl_info = QLabel("Drop .fchk file here or click Open")
295
- self.lbl_info.setAlignment(Qt.AlignmentFlag.AlignCenter)
296
- self.lbl_info.setStyleSheet("border: 2px dashed #AAA; padding: 20px; color: #555;")
297
- layout.addWidget(self.lbl_info)
298
-
299
- # Open Button
300
- btn_open = QPushButton("Open FCHK File")
301
- btn_open.clicked.connect(self.open_file_dialog)
302
- layout.addWidget(btn_open)
303
-
304
- # Frequency List
305
- # Frequency List
306
- layout.addWidget(QLabel("Vibrational Frequencies:"))
307
- self.list_freq = QTreeWidget()
308
- self.list_freq.setColumnCount(3)
309
- self.list_freq.setHeaderLabels(["No.", "Frequency (cm⁻¹)", "Intensity (km/mol)"])
310
- self.list_freq.header().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
311
- self.list_freq.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
312
- self.list_freq.headerItem().setTextAlignment(0, Qt.AlignmentFlag.AlignCenter) # Center No. header
313
- self.list_freq.headerItem().setTextAlignment(1, Qt.AlignmentFlag.AlignCenter) # Center Frequency header
314
- self.list_freq.headerItem().setTextAlignment(2, Qt.AlignmentFlag.AlignCenter) # Center Intensity header
315
- self.list_freq.currentItemChanged.connect(self.on_freq_selected)
316
- layout.addWidget(self.list_freq)
317
-
318
- # Spectrum Button
319
- btn_spectrum = QPushButton("Show Spectrum")
320
- btn_spectrum.clicked.connect(self.show_spectrum)
321
- layout.addWidget(btn_spectrum)
322
-
323
- # Animation Controls
324
- anim_layout = QVBoxLayout()
325
-
326
- # Vector Controls
327
- vec_layout = QHBoxLayout()
328
- self.chk_vectors = QCheckBox("Show Vectors")
329
- self.chk_vectors.setChecked(False)
330
- self.chk_vectors.stateChanged.connect(lambda state: self.update_vectors())
331
- vec_layout.addWidget(self.chk_vectors)
332
-
333
- vec_layout.addWidget(QLabel("Scale:"))
334
- self.spin_vec_scale = QDoubleSpinBox()
335
- self.spin_vec_scale.setRange(0.1, 200.0)
336
- self.spin_vec_scale.setSingleStep(1.0)
337
- self.spin_vec_scale.setValue(2.0)
338
- self.spin_vec_scale.valueChanged.connect(lambda val: self.update_vectors())
339
- vec_layout.addWidget(self.spin_vec_scale)
340
- vec_layout.addStretch()
341
-
342
- anim_layout.addLayout(vec_layout)
343
-
344
- # Amplitude
345
- amp_layout = QHBoxLayout()
346
- amp_layout.addWidget(QLabel("Amplitude:"))
347
- self.slider_amp = QSlider(Qt.Orientation.Horizontal)
348
- self.slider_amp.setRange(1, 20)
349
- self.slider_amp.setValue(5)
350
-
351
- self.lbl_amp_val = QLabel("5")
352
- self.slider_amp.valueChanged.connect(lambda v: self.lbl_amp_val.setText(str(v)))
353
-
354
- amp_layout.addWidget(self.slider_amp)
355
- amp_layout.addWidget(self.lbl_amp_val)
356
- anim_layout.addLayout(amp_layout)
357
-
358
- # FPS (Speed)
359
- speed_layout = QHBoxLayout()
360
- speed_layout.addWidget(QLabel("FPS:"))
361
- self.slider_speed = QSlider(Qt.Orientation.Horizontal)
362
- self.slider_speed.setRange(1, 60)
363
- self.slider_speed.setValue(20)
364
-
365
- self.lbl_speed_val = QLabel("20")
366
- self.slider_speed.valueChanged.connect(lambda v: self.lbl_speed_val.setText(str(v)))
367
-
368
- self.slider_speed.valueChanged.connect(self.update_timer_interval)
369
- speed_layout.addWidget(self.slider_speed)
370
- speed_layout.addWidget(self.lbl_speed_val)
371
- anim_layout.addLayout(speed_layout)
372
-
373
- # Buttons
374
- btn_layout = QHBoxLayout()
375
- self.btn_play = QPushButton("Play")
376
- self.btn_play.clicked.connect(self.toggle_play)
377
- self.btn_stop = QPushButton("Stop")
378
- self.btn_stop.clicked.connect(self.stop_play)
379
-
380
- btn_layout.addWidget(self.btn_play)
381
- btn_layout.addWidget(self.btn_stop)
382
- anim_layout.addLayout(btn_layout)
383
-
384
- layout.addLayout(anim_layout)
385
-
386
- # Export GIF Button
387
- self.btn_gif = QPushButton("Export GIF")
388
- self.btn_gif.clicked.connect(self.save_as_gif)
389
- self.btn_gif.setEnabled(HAS_PIL)
390
- layout.addWidget(self.btn_gif)
391
-
392
- # Metadata Info
393
- self.lbl_meta = QLabel("")
394
- layout.addWidget(self.lbl_meta)
395
-
396
- layout.addStretch()
397
-
398
-
399
- # Close Button
400
- btn_close = QPushButton("Close")
401
- def close_action():
402
- self.stop_play()
403
- self.reset_geometry()
404
- self.remove_vectors()
405
- if self.dock:
406
- self.dock.close()
407
- else:
408
- self.close()
409
- btn_close.clicked.connect(close_action)
410
- layout.addWidget(btn_close)
411
-
412
- self.setLayout(layout)
413
-
414
- def dragEnterEvent(self, event):
415
- if event.mimeData().hasUrls():
416
- for url in event.mimeData().urls():
417
- fname = url.toLocalFile().lower()
418
- if fname.endswith(".fchk") or fname.endswith(".fck"):
419
- event.acceptProposedAction()
420
- return
421
- event.ignore()
422
-
423
- def dropEvent(self, event):
424
- for url in event.mimeData().urls():
425
- file_path = url.toLocalFile()
426
- if file_path.lower().endswith((".fchk", ".fck")):
427
- self.load_file(file_path)
428
- event.acceptProposedAction()
429
- break
430
-
431
- def open_file_dialog(self):
432
- fname, _ = QFileDialog.getOpenFileName(self, "Open Gaussian FCHK", "", "FCHK Files (*.fchk *.fck)")
433
- if fname:
434
- self.load_file(fname)
435
-
436
- def load_file(self, filename):
437
- self.parser = FCHKParser()
438
- try:
439
- self.parser.parse(filename)
440
- self.update_ui_after_load()
441
- self.lbl_info.setText(os.path.basename(filename))
442
- self.lbl_info.setStyleSheet("border: 2px solid #4CAF50; padding: 10px; color: #4CAF50;")
443
-
444
- # Update Main Window Context
445
- if hasattr(self.mw, 'current_file_path'):
446
- self.mw.current_file_path = filename
447
- if hasattr(self.mw, 'update_window_title'):
448
- self.mw.update_window_title()
449
- else:
450
- self.mw.setWindowTitle(f"{os.path.basename(filename)} - MoleditPy")
451
-
452
- except Exception as e:
453
- QMessageBox.critical(self, "Error", f"Failed to parse FCHK:\n{e}")
454
- traceback.print_exc()
455
-
456
- def update_ui_after_load(self):
457
- self.list_freq.clear()
458
- # Ensure freqs match modes logic handled in parser consistency check
459
- if hasattr(self.parser, 'frequencies'):
460
- for i, freq in enumerate(self.parser.frequencies):
461
- item = QTreeWidgetItem()
462
- item.setText(0, str(i + 1)) # Mode number
463
- item.setText(1, f"{freq:.2f}")
464
-
465
- if hasattr(self.parser, 'intensities') and i < len(self.parser.intensities):
466
- inten = self.parser.intensities[i]
467
- # Higher precision to see small values and compare with LOG
468
- item.setText(2, f"{inten:.4f}")
469
- else:
470
- item.setText(2, "-")
471
- item.setTextAlignment(0, Qt.AlignmentFlag.AlignCenter) # Center mode number
472
- item.setTextAlignment(1, Qt.AlignmentFlag.AlignCenter) # Center frequency
473
- item.setTextAlignment(2, Qt.AlignmentFlag.AlignCenter) # Center intensity
474
- self.list_freq.addTopLevelItem(item)
475
-
476
- self.lbl_meta.setText(f"Charge: {self.parser.charge}, Multiplicity: {self.parser.multiplicity}\nAtoms: {len(self.parser.atoms)}")
477
-
478
- # Load molecule into main window
479
- if len(self.parser.atoms) > 0 and Chem:
480
- self.create_base_molecule()
481
-
482
- def create_base_molecule(self):
483
- if not self.parser: return
484
-
485
- mol = Chem.RWMol()
486
-
487
- # Mapping atomic number to symbol?
488
- # Needed: FCHK gives Atomic Numbers
489
- pt = Chem.GetPeriodicTable()
490
-
491
- for ans in self.parser.atoms:
492
- sym = pt.GetElementSymbol(int(ans))
493
- atom = Chem.Atom(sym)
494
- mol.AddAtom(atom)
495
-
496
- conf = Chem.Conformer(len(self.parser.atoms))
497
- for idx, (x, y, z) in enumerate(self.parser.coords):
498
- conf.SetAtomPosition(idx, Point3D(x, y, z))
499
- mol.AddConformer(conf)
500
-
501
- if hasattr(self.mw, 'estimate_bonds_from_distances'):
502
- self.mw.estimate_bonds_from_distances(mol)
503
-
504
- self.base_mol = mol.GetMol()
505
- self.mw.current_mol = self.base_mol
506
-
507
- if hasattr(self.mw, '_enter_3d_viewer_ui_mode'):
508
- self.mw._enter_3d_viewer_ui_mode()
509
-
510
- self.mw.draw_molecule_3d(self.base_mol)
511
- if hasattr(self.mw, 'plotter'):
512
- self.mw.plotter.reset_camera()
513
-
514
- def on_freq_selected(self, current, previous):
515
- if self.is_playing:
516
- # Transition smoothly to new selected mode
517
- pass
518
- else:
519
- self.update_vectors()
520
-
521
- def toggle_play(self):
522
- curr = self.list_freq.currentItem()
523
- if not curr:
524
- return
525
- # Row index logic
526
- row = self.list_freq.indexOfTopLevelItem(curr)
527
- if row < 0: return
528
-
529
- if self.is_playing:
530
- # Pause logic
531
- self.is_playing = False
532
- self.timer.stop()
533
- self.btn_play.setText("Play")
534
- # Do NOT reset geometry
535
- return
536
-
537
- self.is_playing = True
538
- self.btn_play.setText("Pause")
539
- self.timer.start(50)
540
- self.update_timer_interval()
541
-
542
- def stop_play(self):
543
- self.is_playing = False
544
- self.timer.stop()
545
- self.btn_play.setText("Play")
546
-
547
- self.reset_geometry()
548
- QApplication.processEvents()
549
-
550
- def update_timer_interval(self):
551
- fps = self.slider_speed.value()
552
- if fps <= 0: fps = 1
553
- interval = 1000 / fps
554
- self.timer.setInterval(int(interval))
555
-
556
- def reset_geometry(self):
557
- if not self.base_mol or not self.parser: return
558
- conf = self.base_mol.GetConformer()
559
- for idx, (x, y, z) in enumerate(self.parser.coords):
560
- conf.SetAtomPosition(idx, Point3D(x, y, z))
561
- self.mw.draw_molecule_3d(self.base_mol)
562
-
563
- self.update_vectors()
564
-
565
- if hasattr(self.mw, 'plotter'):
566
- self.mw.plotter.render()
567
-
568
- def animate_frame(self):
569
- if not self.parser or not self.base_mol:
570
- self.stop_play()
571
- return
572
-
573
- curr = self.list_freq.currentItem()
574
- if not curr: return
575
- row = self.list_freq.indexOfTopLevelItem(curr)
576
- if row < 0 or row >= len(self.parser.vib_modes):
577
- return
578
-
579
- mode_vecs = self.parser.vib_modes[row]
580
-
581
- self.animation_step += 1
582
- # Use 20 steps per cycle
583
- cycle_pos = (self.animation_step % 20) / 20.0
584
- phase = cycle_pos * 2 * np.pi
585
-
586
- scale = self.slider_amp.value() / 20.0
587
- factor = np.sin(phase) * scale
588
-
589
- self.apply_displacement(mode_vecs, factor)
590
- self.mw.draw_molecule_3d(self.base_mol)
591
- self.update_vectors(mode_vecs=mode_vecs, scale_factor=factor)
592
-
593
- def apply_displacement(self, mode_vecs, factor):
594
- conf = self.base_mol.GetConformer()
595
- base_coords = self.parser.coords
596
- for idx, (bx, by, bz) in enumerate(base_coords):
597
- if idx < len(mode_vecs):
598
- dx, dy, dz = mode_vecs[idx]
599
- nx = bx + dx * factor
600
- ny = by + dy * factor
601
- nz = bz + dz * factor
602
- conf.SetAtomPosition(idx, Point3D(nx, ny, nz))
603
-
604
- def remove_vectors(self):
605
- if self.vector_actor and hasattr(self.mw, 'plotter'):
606
- try:
607
- self.mw.plotter.remove_actor(self.vector_actor)
608
- except: pass
609
- self.vector_actor = None
610
-
611
- def update_vectors(self, mode_vecs=None, scale_factor=0.0):
612
- # Clean up existing vectors
613
- self.remove_vectors()
614
-
615
- if not self.chk_vectors.isChecked():
616
- return
617
-
618
- if not self.parser or not self.base_mol or not hasattr(self.mw, 'plotter'):
619
- return
620
-
621
- # Get current frequency
622
- curr = self.list_freq.currentItem()
623
- if not curr: return
624
- row = self.list_freq.indexOfTopLevelItem(curr)
625
- if row < 0 or row >= len(self.parser.vib_modes): return
626
-
627
- # Get vectors if not provided
628
- if mode_vecs is None:
629
- mode_vecs = self.parser.vib_modes[row]
630
-
631
- # Current coords from molecule conformer
632
- conf = self.base_mol.GetConformer()
633
- coords = []
634
- vectors = []
635
-
636
- # Amplitude for vector length scaling
637
- # Now decoupled from animation amplitude
638
- vis_scale = self.spin_vec_scale.value()
639
-
640
- for idx in range(len(mode_vecs)):
641
- pos = conf.GetAtomPosition(idx)
642
- coords.append([pos.x, pos.y, pos.z])
643
-
644
- dx, dy, dz = mode_vecs[idx]
645
- vectors.append([dx, dy, dz])
646
-
647
- if not coords: return
648
-
649
- coords = np.array(coords)
650
- vectors = np.array(vectors)
651
-
652
- try:
653
- self.vector_actor = self.mw.plotter.add_arrows(coords, vectors, mag=vis_scale, color='lightgreen', show_scalar_bar=False)
654
- except Exception as e:
655
- print(f"Error adding arrows: {e}")
656
-
657
- def save_as_gif(self):
658
- if not self.parser or not self.base_mol: return
659
-
660
- # Pause to configure
661
- was_playing = self.is_playing
662
- if self.is_playing:
663
- self.toggle_play() # Pause
664
-
665
- curr = self.list_freq.currentItem()
666
- if not curr:
667
- QMessageBox.warning(self, "Select Frequency", "Please select a frequency to export.")
668
- return
669
- row = self.list_freq.indexOfTopLevelItem(curr)
670
-
671
- dialog = QDialog(self)
672
- dialog.setWindowTitle("Export GIF Settings")
673
- form = QFormLayout(dialog)
674
-
675
- # Calculate current FPS
676
- # Slider value is now FPS directly
677
- current_fps = self.slider_speed.value()
678
-
679
- spin_fps = QSpinBox()
680
- spin_fps.setRange(1, 60)
681
- spin_fps.setValue(current_fps)
682
-
683
- chk_transparent = QCheckBox()
684
- chk_transparent.setChecked(True)
685
-
686
- chk_hq = QCheckBox()
687
- chk_hq.setChecked(True)
688
-
689
- form.addRow("FPS:", spin_fps)
690
- form.addRow("Transparent Background:", chk_transparent)
691
- form.addRow("High Quality (Adaptive):", chk_hq)
692
-
693
- btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
694
- btns.accepted.connect(dialog.accept)
695
- btns.rejected.connect(dialog.reject)
696
- form.addRow(btns)
697
-
698
- if dialog.exec() != QDialog.DialogCode.Accepted:
699
- if was_playing: self.toggle_play()
700
- return # Cancel
701
-
702
- target_fps = spin_fps.value()
703
- use_transparent = chk_transparent.isChecked()
704
- use_hq = chk_hq.isChecked()
705
-
706
- file_path, _ = QFileDialog.getSaveFileName(self, "Save GIF", "", "GIF Files (*.gif)")
707
- if not file_path:
708
- if was_playing: self.toggle_play()
709
- return
710
-
711
- if not file_path.lower().endswith('.gif'):
712
- file_path += '.gif'
713
-
714
- # Generate Frames
715
- # 1 Cycle = 20 steps
716
- images = []
717
- mode_vecs = self.parser.vib_modes[row]
718
-
719
- import copy
720
- # Store current geometry to restore later
721
- self.reset_geometry() # align to base
722
-
723
- try:
724
- for i in range(20):
725
- cycle_pos = i / 20.0
726
- phase = cycle_pos * 2 * np.pi
727
- scale = self.slider_amp.value() / 20.0
728
- factor = np.sin(phase) * scale # Calculate factor here
729
- self.apply_displacement(mode_vecs, factor)
730
- self.mw.draw_molecule_3d(self.base_mol)
731
- self.update_vectors(mode_vecs, factor)
732
- self.mw.plotter.render()
733
-
734
- img_array = self.mw.plotter.screenshot(transparent_background=use_transparent, return_img=True)
735
- if img_array is not None:
736
- img = Image.fromarray(img_array)
737
- images.append(img)
738
-
739
- if images:
740
- duration_ms = int(1000 / target_fps)
741
-
742
- processed_images = []
743
- for img in images:
744
- if use_hq:
745
- if use_transparent:
746
- # Alpha preservation with adaptive palette wrapper
747
- alpha = img.split()[3]
748
- img_rgb = img.convert("RGB")
749
- # Quantize to 255 colors to leave room for transparency
750
- img_p = img_rgb.convert('P', palette=Image.Palette.ADAPTIVE, colors=255)
751
- # Set simple transparency
752
- mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
753
- img_p.paste(255, mask)
754
- img_p.info['transparency'] = 255
755
- processed_images.append(img_p)
756
- else:
757
- processed_images.append(img.convert("P", palette=Image.Palette.ADAPTIVE, colors=256))
758
- else:
759
- if use_transparent:
760
- img = img.convert("RGBA")
761
- processed_images.append(img)
762
- else:
763
- processed_images.append(img.convert("RGB"))
764
-
765
- processed_images[0].save(file_path, save_all=True, append_images=processed_images[1:], duration=duration_ms, loop=0, disposal=2)
766
- QMessageBox.information(self, "Success", f"Saved GIF to:\n{file_path}")
767
-
768
- except Exception as e:
769
- QMessageBox.critical(self, "Error", f"Failed to save GIF: {e}")
770
- traceback.print_exc()
771
- finally:
772
- self.reset_geometry()
773
- # Restore play state if needed, or leave paused
774
- # User might want to inspect
775
- if was_playing:
776
- self.toggle_play()
777
-
778
- def on_dock_visibility_changed(self, visible):
779
- if not visible and self.is_playing:
780
- self.stop_play()
781
-
782
- def close_plugin(self):
783
- self.stop_play()
784
- self.remove_vectors()
785
- self.animation_step = 0
786
- def show_spectrum(self):
787
- if not self.parser or not self.parser.frequencies:
788
- return
789
-
790
- # Filter out low frequencies (translations/rotations)
791
- freqs = []
792
- intensities = []
793
-
794
- parser_intensities = self.parser.intensities if hasattr(self.parser, 'intensities') and self.parser.intensities else [1.0]*len(self.parser.frequencies)
795
-
796
- for i, freq in enumerate(self.parser.frequencies):
797
- # Use abs() to preserve imaginary frequencies (negative values)
798
- # Only exclude low-frequency modes (translations/rotations)
799
- if abs(freq) > 10.0:
800
- freqs.append(freq)
801
- if i < len(parser_intensities):
802
- intensities.append(parser_intensities[i])
803
- else:
804
- intensities.append(1.0)
805
-
806
- dlg = SpectrumDialog(freqs, intensities, self)
807
- dlg.exec()
808
-
809
-
810
- class SpectrumDialog(QDialog):
811
- def __init__(self, freqs, intensities, parent=None):
812
- super().__init__(parent)
813
- self.setWindowTitle("IR Spectrum")
814
- self.resize(800, 600)
815
-
816
- self.freqs = np.array(freqs)
817
- self.intensities = np.array(intensities)
818
- self.scaling_factor = 1.0
819
-
820
- # Layout
821
- layout = QVBoxLayout(self)
822
-
823
- # Plot Area
824
- self.plot_widget = SpectrumPlotWidget(self.freqs, self.intensities)
825
- layout.addWidget(self.plot_widget)
826
-
827
- # Controls
828
- controls = QHBoxLayout()
829
-
830
- # Scaling Factor
831
- controls.addWidget(QLabel("Scaling Factor:"))
832
- from PyQt6.QtWidgets import QDoubleSpinBox
833
- self.spin_scale = QDoubleSpinBox()
834
- self.spin_scale.setRange(0.5, 1.5)
835
- self.spin_scale.setValue(1.0)
836
- self.spin_scale.setSingleStep(0.01)
837
- self.spin_scale.setDecimals(3)
838
- self.spin_scale.valueChanged.connect(self.on_scaling_changed)
839
- controls.addWidget(self.spin_scale)
840
-
841
- controls.addWidget(QLabel("FWHM (cm⁻¹):"))
842
- self.spin_fwhm = QSpinBox()
843
- self.spin_fwhm.setRange(1, 500)
844
- self.spin_fwhm.setValue(50)
845
- self.spin_fwhm.valueChanged.connect(self.on_fwhm_changed)
846
- controls.addWidget(self.spin_fwhm)
847
-
848
- # Axis Range
849
- controls.addWidget(QLabel("Min WN:"))
850
- self.spin_min = QSpinBox()
851
- self.spin_min.setRange(0, 5000)
852
- self.spin_min.setValue(0)
853
- self.spin_min.setSingleStep(100)
854
- self.spin_min.valueChanged.connect(self.on_range_changed)
855
- controls.addWidget(self.spin_min)
856
-
857
- controls.addWidget(QLabel("Max WN:"))
858
- self.spin_max = QSpinBox()
859
- self.spin_max.setRange(0, 5000)
860
- self.spin_max.setValue(4000)
861
- self.spin_max.setSingleStep(100)
862
- self.spin_max.valueChanged.connect(self.on_range_changed)
863
- controls.addWidget(self.spin_max)
864
-
865
- btn_csv = QPushButton("Export CSV")
866
- btn_csv.clicked.connect(self.export_csv)
867
- controls.addWidget(btn_csv)
868
-
869
- btn_png = QPushButton("Export Image")
870
- btn_png.clicked.connect(self.export_png)
871
- controls.addWidget(btn_png)
872
-
873
- btn_close = QPushButton("Close")
874
- btn_close.clicked.connect(self.accept)
875
- controls.addWidget(btn_close)
876
-
877
- layout.addLayout(controls)
878
- self.setLayout(layout)
879
-
880
- # Initial Plot
881
- self.on_range_changed()
882
-
883
- def on_scaling_changed(self, val):
884
- self.scaling_factor = val
885
- scaled_freqs = self.freqs * self.scaling_factor
886
- self.plot_widget.set_frequencies(scaled_freqs)
887
-
888
- def on_fwhm_changed(self, val):
889
- self.plot_widget.set_fwhm(val)
890
-
891
- def on_range_changed(self):
892
- mn = self.spin_min.value()
893
- mx = self.spin_max.value()
894
- if mx > mn:
895
- self.plot_widget.set_range(mn, mx)
896
-
897
- def export_csv(self):
898
- fname, _ = QFileDialog.getSaveFileName(self, "Save Spectrum Data", "", "CSV Files (*.csv)")
899
- if fname:
900
- if not fname.lower().endswith('.csv'): fname += '.csv'
901
- try:
902
- x, y = self.plot_widget.get_curve_data()
903
- with open(fname, 'w') as f:
904
- f.write("Frequency,Intensity\n")
905
- for xi, yi in zip(x, y):
906
- f.write(f"{xi:.2f},{yi:.4f}\n")
907
- QMessageBox.information(self, "Success", "Saved CSV.")
908
- except Exception as e:
909
- QMessageBox.critical(self, "Error", str(e))
910
-
911
- def export_png(self):
912
- fname, _ = QFileDialog.getSaveFileName(self, "Save Spectrum Image", "", "PNG Files (*.png)")
913
- if fname:
914
- if not fname.lower().endswith('.png'): fname += '.png'
915
- try:
916
- # Capture the widget
917
- pixmap = self.plot_widget.grab()
918
- pixmap.save(fname)
919
- QMessageBox.information(self, "Success", "Saved Image.")
920
- except Exception as e:
921
- QMessageBox.critical(self, "Error", str(e))
922
-
923
- class SpectrumPlotWidget(QWidget):
924
- def __init__(self, freqs, intensities, parent=None):
925
- super().__init__(parent)
926
- self.freqs = freqs
927
- self.intensities = intensities
928
- self.fwhm = 80.0
929
- self.curve_x = []
930
- self.curve_y = []
931
-
932
- self.setAutoFillBackground(True)
933
- self.setStyleSheet("background-color: white;")
934
-
935
- self.min_x = 0.0
936
- self.max_x = 4000.0
937
-
938
- def set_fwhm(self, val):
939
- self.fwhm = val
940
- self.recalc_curve()
941
- self.update()
942
-
943
- def set_frequencies(self, freqs):
944
- self.freqs = freqs
945
- self.recalc_curve()
946
- self.update()
947
-
948
- def set_range(self, mn, mx):
949
- self.min_x = float(mn)
950
- self.max_x = float(mx)
951
- self.recalc_curve()
952
- self.update()
953
-
954
- def get_curve_data(self):
955
- return self.curve_x, self.curve_y
956
-
957
- def recalc_curve(self):
958
- # Determine range
959
- if len(self.freqs) == 0: return
960
-
961
- # X resolution based on custom range
962
- self.curve_x = np.linspace(self.min_x, self.max_x, 1000)
963
- self.curve_y = np.zeros_like(self.curve_x)
964
-
965
- # Sum Gaussians weighted by intensity
966
- sigma = self.fwhm / 2.35482
967
-
968
- for f, i in zip(self.freqs, self.intensities):
969
- self.curve_y += i * np.exp(-(self.curve_x - f)**2 / (2 * sigma**2))
970
-
971
- # Do NOT normalize - preserve actual intensity values
972
-
973
-
974
- def paintEvent(self, event):
975
- painter = QPainter(self)
976
- painter.setRenderHint(QPainter.RenderHint.Antialiasing)
977
-
978
- w = self.width()
979
- h = self.height()
980
-
981
- # Margins
982
- margin_l = 50
983
- margin_r = 20
984
- margin_t = 20
985
- margin_b = 60 # Increased from 40 for better spacing
986
-
987
- plot_w = w - margin_l - margin_r
988
- plot_h = h - margin_t - margin_b
989
-
990
- if len(self.curve_x) == 0:
991
- painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "No Data")
992
- return
993
-
994
- # Calculate max_y from both curve AND raw intensities to prevent stick normalization
995
- # Add 10% padding for better visibility
996
- max_curve = np.max(self.curve_y) if len(self.curve_y) > 0 else 1.0
997
- max_intensity = np.max(self.intensities) if len(self.intensities) > 0 else 1.0
998
- max_y = max(max_curve, max_intensity) * 1.1 # 1.1x for padding
999
- if max_y == 0: max_y = 1.0
1000
- min_x = np.min(self.curve_x)
1001
- max_x = np.max(self.curve_x)
1002
- range_x = max_x - min_x
1003
- if range_x == 0: range_x = 100
1004
-
1005
- # Helper to transform
1006
- # Inverted X: Max (left) -> Min (right)
1007
- # Inverted Y: 0 (top) -> Max (bottom) - peaks point down
1008
-
1009
- def to_screen(x, y):
1010
- # X: margin_l corresponds to max_x, w - margin_r corresponds to min_x
1011
- sx = margin_l + (max_x - x) / range_x * plot_w
1012
-
1013
- # Y: margin_t corresponds to 0, h - margin_b corresponds to max_y
1014
- sy = margin_t + (y / max_y) * plot_h
1015
- return sx, sy
1016
-
1017
- # Draw Axes
1018
- painter.setPen(QPen(Qt.GlobalColor.black, 2))
1019
- painter.drawLine(margin_l, margin_t, w - margin_r, margin_t) # X-axis at top (Baseline)
1020
- painter.drawLine(margin_l, h - margin_b, w - margin_r, h - margin_b) # X-axis at bottom
1021
-
1022
- painter.drawLine(margin_l, h - margin_b, margin_l, margin_t) # Y (Left)
1023
- painter.drawLine(w - margin_r, h - margin_b, w - margin_r, margin_t) # Y (Right border)
1024
-
1025
- # Draw Ticks / Labels (Simplified)
1026
- font = painter.font()
1027
- font.setPointSize(12) # Increased from 8
1028
- painter.setFont(font)
1029
-
1030
- # X Ticks (approx 5)
1031
- # Inverted: Left is Max, Right is Min
1032
- n_ticks = 5
1033
- for i in range(n_ticks + 1):
1034
- # val goes from max_x to min_x
1035
- val = max_x - (range_x * i / n_ticks)
1036
- px, py = to_screen(val, 0)
1037
- # Label at bottom
1038
- painter.drawText(int(px)-20, h - margin_b + 5, 40, 20, Qt.AlignmentFlag.AlignCenter, f"{int(val)}")
1039
- painter.drawLine(int(px), h - margin_b, int(px), h - margin_b + 5)
1040
-
1041
- # Labels
1042
- font.setPointSize(14) # Increased from 10
1043
- font.setBold(True)
1044
- painter.setFont(font)
1045
- painter.drawText(0, h-25, w, 20, Qt.AlignmentFlag.AlignCenter, "Wavenumber (cm⁻¹)")
1046
-
1047
- # Draw baseline at y=0
1048
- baseline_x_start, baseline_y = to_screen(max_x, 0)
1049
- baseline_x_end, _ = to_screen(min_x, 0)
1050
- painter.setPen(QPen(QColor(150, 150, 150), 1, Qt.PenStyle.DashLine))
1051
- painter.drawLine(int(baseline_x_start), int(baseline_y), int(baseline_x_end), int(baseline_y))
1052
-
1053
- # Draw Curve
1054
- painter.setPen(QPen(Qt.GlobalColor.blue, 2))
1055
- path_points = []
1056
- for x, y in zip(self.curve_x, self.curve_y):
1057
- sx, sy = to_screen(x, y)
1058
- path_points.append( (sx, sy) )
1059
-
1060
- if len(path_points) > 1:
1061
- from PyQt6.QtCore import QPointF
1062
- qpoints = [QPointF(x, y) for x, y in path_points]
1063
- painter.drawPolyline(qpoints)
1064
-
1065
- # Draw Sticks (Bars) for original frequencies
1066
- painter.setPen(QPen(QColor(255, 0, 0, 100), 1))
1067
- for f, i in zip(self.freqs, self.intensities):
1068
- sx, sy = to_screen(f, i)
1069
- px_base, py_base = to_screen(f, 0)
1070
- painter.drawLine(int(sx), int(py_base), int(sx), int(sy))
1071
- # If we need to remove self from layout or close dock?
1072
- # Done by caller usually.
1073
-
1074
- # def closeEvent(self, event):
1075
- # self.close_plugin()
1076
- # super().closeEvent(event)
1077
-
1078
-
1079
-
1080
- def on_dock_visibility_changed(self, visible):
1081
- if not visible:
1082
- self.stop_play()
1083
- self.animation_step = 0
1084
- if self.base_mol:
1085
- self.reset_geometry()
1086
- # Force redraw
1087
- if hasattr(self.mw, 'plotter'):
1088
- self.mw.plotter.render()
1089
-
1090
- def load_from_file(main_window, fname):
1091
- # Check for existing dock
1092
- dock = None
1093
- analyzer = None
1094
-
1095
- # Check existing docks
1096
- for d in main_window.findChildren(QDockWidget):
1097
- if d.windowTitle() == "Gaussian Freq Analyzer":
1098
- dock = d
1099
- analyzer = d.widget()
1100
- break
1101
-
1102
- if not dock:
1103
- dock = QDockWidget("Gaussian Freq Analyzer", main_window)
1104
- dock.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
1105
- analyzer = GaussianFCHKFreqAnalyzer(main_window, dock)
1106
- dock.setWidget(analyzer)
1107
- main_window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)
1108
-
1109
- # Connect visibility change
1110
- dock.visibilityChanged.connect(analyzer.on_dock_visibility_changed)
1111
-
1112
- dock.show()
1113
- dock.raise_()
1114
-
1115
- if analyzer:
1116
- analyzer.load_file(fname)
1117
-
1118
- def run(mw):
1119
- # Smart Open Logic
1120
- if hasattr(mw, 'current_file_path') and mw.current_file_path:
1121
- fpath = mw.current_file_path.lower()
1122
- if fpath.endswith((".fchk", ".fck")):
1123
- load_from_file(mw, mw.current_file_path)
1124
- return
1125
-
1126
- fname, _ = QFileDialog.getOpenFileName(mw, "Open Gaussian FCHK", "", "Gaussian FCHK (*.fchk *.fck);;All Files (*)")
1127
- if fname:
1128
- load_from_file(mw, fname)
1129
-
1130
- def initialize(context):
1131
- mw = context.get_main_window()
1132
-
1133
- def load_wrapper(fname):
1134
- load_from_file(mw, fname)
1135
-
1136
- # 1. Register File Openers
1137
- context.register_file_opener('.fchk', load_wrapper)
1138
- context.register_file_opener('.fck', load_wrapper)
1139
-
1140
- # 2. Register Drop Handler
1141
- def drop_handler(file_path):
1142
- if file_path.lower().endswith(('.fchk', '.fck')):
1143
- load_from_file(mw, file_path)
1144
- return True
1145
- return False
1146
-
1147
- if hasattr(context, 'register_drop_handler'):
1148
- context.register_drop_handler(drop_handler, priority=10)