MoleditPy 2.2.0a1__py3-none-any.whl → 2.2.0a2__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.
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QFont, QColor
16
16
  from rdkit import Chem
17
17
 
18
18
  #Version
19
- VERSION = '2.2.0a1'
19
+ VERSION = '2.2.0a2'
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -298,7 +298,7 @@ class MainWindowUiManager(object):
298
298
 
299
299
 
300
300
  def dragEnterEvent(self, event):
301
- """ウィンドウ全体で .pmeraw、.pmeprj、.mol、.sdf、.xyz ファイルのドラッグを受け入れる"""
301
+ """ウィンドウ全体でサポートされているファイルのドラッグを受け入れる"""
302
302
  # Accept if any dragged local file has a supported extension
303
303
  if event.mimeData().hasUrls():
304
304
  urls = event.mimeData().urls()
@@ -306,9 +306,28 @@ class MainWindowUiManager(object):
306
306
  try:
307
307
  if url.isLocalFile():
308
308
  file_path = url.toLocalFile()
309
- if file_path.lower().endswith(('.pmeraw', '.pmeprj', '.mol', '.sdf', '.xyz')):
309
+ file_lower = file_path.lower()
310
+
311
+ # Built-in extensions
312
+ if file_lower.endswith(('.pmeraw', '.pmeprj', '.mol', '.sdf', '.xyz')):
310
313
  event.acceptProposedAction()
311
314
  return
315
+
316
+ # Plugin-registered file openers
317
+ if self.plugin_manager and hasattr(self.plugin_manager, 'file_openers'):
318
+ for ext in self.plugin_manager.file_openers.keys():
319
+ if file_lower.endswith(ext):
320
+ event.acceptProposedAction()
321
+ return
322
+
323
+ # Plugin drop handlers (accept more liberally for custom logic)
324
+ # A plugin drop handler might handle it, so accept
325
+ if self.plugin_manager and hasattr(self.plugin_manager, 'drop_handlers'):
326
+ if len(self.plugin_manager.drop_handlers) > 0:
327
+ # Accept any file if drop handlers are registered
328
+ # They will check the file type in dropEvent
329
+ event.acceptProposedAction()
330
+ return
312
331
  except Exception:
313
332
  continue
314
333
  event.ignore()
@@ -9,7 +9,7 @@ import json
9
9
  __version__="2025.12.25"
10
10
  __author__="HiroYokoyama"
11
11
  PLUGIN_NAME = "ORCA xyz2inp GUI"
12
- SETTINGS_JSON = os.path.join(os.path.dirname(__file__), "orca_xyz2inp_settings.json")
12
+ SETTINGS_JSON = os.path.join(os.path.dirname(__file__), "orca_xyz2inp_gui.json")
13
13
 
14
14
  class OrcaInputDialog(QDialog):
15
15
  def __init__(self, main_window):
@@ -9,7 +9,6 @@ import traceback
9
9
  import sys
10
10
  import os
11
11
  import json
12
- import functools # Added for polite patching
13
12
 
14
13
  # Try importing from the installed package first (pip package structure)
15
14
  try:
@@ -31,7 +30,6 @@ class AtomColorizerWindow(QDialog):
31
30
  def __init__(self, main_window):
32
31
  super().__init__(parent=main_window)
33
32
  self.mw = main_window
34
- # self.dock = dock_widget # Removed as per instruction
35
33
  self.plotter = self.mw.plotter
36
34
 
37
35
  # Set window properties for modeless behavior
@@ -71,8 +69,6 @@ class AtomColorizerWindow(QDialog):
71
69
  self.le_indices.setPlaceholderText("e.g. 0, 1, 5")
72
70
  sel_layout.addWidget(self.le_indices)
73
71
 
74
- # 'Get Selection' button removed as per user request (auto-update is active)
75
-
76
72
  # Auto-update timer
77
73
  from PyQt6.QtCore import QTimer
78
74
  self.sel_timer = QTimer(self)
@@ -140,13 +136,6 @@ class AtomColorizerWindow(QDialog):
140
136
  if isinstance(item, int):
141
137
  indices.add(item)
142
138
 
143
- # 2D Selection logic removed as per request ("2Dはいらない")
144
-
145
- if not indices:
146
- # Silent return if auto-updating, or maybe clear?
147
- # If we invoke manually, we might want info, but generic message is okay if list is empty.
148
- pass
149
-
150
139
  # Update the line edit
151
140
  sorted_indices = sorted(list(indices))
152
141
  new_text = ",".join(map(str, sorted_indices))
@@ -155,9 +144,6 @@ class AtomColorizerWindow(QDialog):
155
144
 
156
145
  def _auto_update_selection(self):
157
146
  """Timer slot to auto-update selection."""
158
- # Only update if the user is not actively typing?
159
- # For now, just call get_selection_from_viewer which now checks for changes before setting text.
160
- # However, checking if le_indices has focus might be good.
161
147
  if self.le_indices.hasFocus():
162
148
  return
163
149
  self.get_selection_from_viewer()
@@ -169,51 +155,6 @@ class AtomColorizerWindow(QDialog):
169
155
  # Update button style
170
156
  self.btn_color.setStyleSheet(f"background-color: {c.name()}; color: {'black' if c.lightness() > 128 else 'white'};")
171
157
 
172
- def _update_3d_actor(self):
173
- """Re-generate glyphs and update the actor to reflect color changes."""
174
- try:
175
- # 1. Re-run glyph filter to propagate color changes from glyph_source to mesh
176
- if hasattr(self.mw, 'glyph_source') and self.mw.glyph_source:
177
- # Read resolution from settings or default
178
- try:
179
- style = self.mw.current_3d_style
180
- if style == 'cpk':
181
- resolution = self.mw.settings.get('cpk_resolution', 32)
182
- elif style == 'stick':
183
- resolution = self.mw.settings.get('stick_resolution', 16)
184
- else: # ball_stick
185
- resolution = self.mw.settings.get('ball_stick_resolution', 16)
186
- except Exception:
187
- resolution = 16
188
-
189
- glyphs = self.mw.glyph_source.glyph(
190
- scale='radii',
191
- geom=pv.Sphere(radius=1.0, theta_resolution=resolution, phi_resolution=resolution),
192
- orient=False
193
- )
194
-
195
- # 2. Update the actor
196
- if hasattr(self.mw, 'atom_actor') and self.mw.atom_actor:
197
- self.mw.plotter.remove_actor(self.mw.atom_actor)
198
-
199
- # Re-add mesh (copying properties logic from main_window_view_3d roughly)
200
- is_lighting_enabled = self.mw.settings.get('lighting_enabled', True)
201
- mesh_props = dict(
202
- smooth_shading=True,
203
- specular=self.mw.settings.get('specular', 0.2),
204
- specular_power=self.mw.settings.get('specular_power', 20),
205
- lighting=is_lighting_enabled,
206
- )
207
-
208
- self.mw.atom_actor = self.mw.plotter.add_mesh(
209
- glyphs, scalars='colors', rgb=True, **mesh_props
210
- )
211
-
212
- self.mw.plotter.render()
213
- except Exception as e:
214
- print(f"Error updating 3D actor: {e}")
215
- traceback.print_exc()
216
-
217
158
  def apply_color(self):
218
159
  txt = self.le_indices.text().strip()
219
160
  if not txt:
@@ -227,321 +168,95 @@ class AtomColorizerWindow(QDialog):
227
168
  QMessageBox.warning(self, "Error", "Invalid indices format.")
228
169
  return
229
170
 
230
- if not hasattr(self.mw, 'glyph_source') or self.mw.glyph_source is None:
231
- QMessageBox.warning(self, "Error", "No 3D molecule found (glyph_source is None).")
171
+ if not self.mw.current_mol:
172
+ QMessageBox.warning(self, "Error", "No molecule loaded.")
232
173
  return
233
174
 
234
-
235
- # 1. Update glyph_source colors
236
175
  try:
237
- if not hasattr(self.mw, 'custom_atom_colors'):
238
- self.mw.custom_atom_colors = {}
239
-
240
- colors = self.mw.glyph_source.point_data['colors']
176
+ # Use the API to set atom colors
177
+ hex_color = self.current_color.name()
241
178
 
242
- # Helper to normalize color to whatever format 'colors' is using
243
- r, g, b = self.current_color.red(), self.current_color.green(), self.current_color.blue()
244
-
245
- # Store simple 0-255 list for persistence to avoid numpy type issues in JSON
246
- stored_color = [r, g, b]
247
-
248
- # Check if colors are float (0-1) or uint8 (0-255)
249
- is_float = (colors.dtype.kind == 'f')
250
-
251
- new_color_val = [r/255.0, g/255.0, b/255.0] if is_float else [r, g, b]
252
-
253
179
  for idx in target_indices:
254
- if 0 <= idx < len(colors):
255
- colors[idx] = new_color_val
256
- # Update persistent storage
257
- self.mw.custom_atom_colors[idx] = stored_color
258
-
259
- # 2. Force update of the actor
260
- self._update_3d_actor()
261
-
180
+ if 0 <= idx < self.mw.current_mol.GetNumAtoms():
181
+ # Access via main_window_view_3d proxy
182
+ if hasattr(self.mw, 'main_window_view_3d'):
183
+ self.mw.main_window_view_3d.update_atom_color_override(idx, hex_color)
184
+ else:
185
+ # Fallback if unproxied (unlikely in this architecture)
186
+ pass
187
+
262
188
  except Exception as e:
263
- # QMessageBox.critical(self, "Error", f"Failed to apply color: {e}")
189
+ QMessageBox.critical(self, "Error", f"Failed to apply color: {e}")
264
190
  traceback.print_exc()
265
191
 
266
192
  def reset_colors(self):
267
- if not hasattr(self.mw, 'glyph_source') or self.mw.glyph_source is None:
268
- return
269
193
  if not self.mw.current_mol:
270
194
  return
271
195
 
272
196
  try:
273
- # Clear persistent storage
274
- if hasattr(self.mw, 'custom_atom_colors'):
275
- self.mw.custom_atom_colors = {}
276
-
277
- colors = self.mw.glyph_source.point_data['colors']
278
- is_float = (colors.dtype.kind == 'f')
279
-
280
- # Iterate atoms and reset to CPK
197
+ # Clear all color overrides using the API
281
198
  for i in range(self.mw.current_mol.GetNumAtoms()):
282
- atom = self.mw.current_mol.GetAtomWithIdx(i)
283
- sym = atom.GetSymbol()
284
- # Get default color (float 0-1)
285
- base_col = CPK_COLORS_PV.get(sym, [0.5, 0.5, 0.5])
199
+ if hasattr(self.mw, 'main_window_view_3d'):
200
+ self.mw.main_window_view_3d.update_atom_color_override(i, None)
286
201
 
287
- if is_float:
288
- colors[i] = base_col
289
- else:
290
- colors[i] = [int(c*255) for c in base_col]
291
-
292
- self._update_3d_actor()
293
-
294
202
  except Exception as e:
295
203
  QMessageBox.critical(self, "Error", f"Failed to reset colors: {e}")
296
204
 
297
205
 
298
- def _restore_colors_from_file(self):
299
- """
300
- Check if the main window has a valid .pmeprj file open.
301
- If so, read it manually to find 'custom_atom_colors' and apply them.
302
- This handles the case where the file was loaded *before* this plugin started.
303
- """
304
- # If no file path or not a .pmeprj, ignore
305
- if not hasattr(self.mw, 'current_file_path') or not self.mw.current_file_path:
306
- return
307
- if not self.mw.current_file_path.lower().endswith('.pmeprj'):
308
- return
309
-
310
- # If we already have colors (unlikely if plugin just started, unless double-patched), skip
311
- if hasattr(self.mw, 'custom_atom_colors') and self.mw.custom_atom_colors:
312
- return
313
-
314
- try:
315
- with open(self.mw.current_file_path, 'r', encoding='utf-8') as f:
316
- data = json.load(f)
317
-
318
- if "3d_structure" in data and data["3d_structure"]:
319
- raw_colors = data["3d_structure"].get("custom_atom_colors")
320
- if raw_colors:
321
- custom_colors = {int(k): v for k, v in raw_colors.items()}
322
-
323
- # Apply to MainWindow
324
- self.mw.custom_atom_colors = custom_colors
325
-
326
- # Force update of 3D actor
327
- # We might need to ensure glyph_source is ready; assuming file load populated it.
328
- if hasattr(self.mw, 'glyph_source') and self.mw.glyph_source:
329
- # We need to manually inject these colors into the polydata
330
- colors = self.mw.glyph_source.point_data['colors']
331
- is_float = (colors.dtype.kind == 'f')
332
-
333
- for idx, col_val in custom_colors.items():
334
- if 0 <= idx < len(colors):
335
- if is_float:
336
- # If stored as 0-255 but buffer is float 0-1
337
- if any(c > 1.0 for c in col_val):
338
- colors[idx] = [c/255.0 for c in col_val]
339
- else:
340
- colors[idx] = col_val
341
- else:
342
- # If stored as float 0-1 but buffer is uint8
343
- if all(c <= 1.0 for c in col_val):
344
- colors[idx] = [int(c*255) for c in col_val]
345
- else:
346
- colors[idx] = col_val
347
-
348
- self._update_3d_actor()
349
- print(f"Atom Colorizer: Restored {len(custom_colors)} custom colors from file.")
350
- except Exception as e:
351
- print(f"Atom Colorizer: Failed to lazy-load colors from file: {e}")
352
- traceback.print_exc()
353
-
354
206
  # Global reference to keep window alive
355
207
  _atom_colorizer_window = None
356
- _patches_installed = False
357
208
 
358
209
  def run(mw):
359
- global _atom_colorizer_window, _patches_installed
360
-
361
- # Check if this is the first run (patches not installed)
362
- first_run = not _patches_installed
363
-
364
- # Install patches for persistence
365
- install_patches(mw)
366
-
367
210
  global _atom_colorizer_window
211
+
368
212
  # Check if window already exists
369
213
  if _atom_colorizer_window is None:
370
214
  _atom_colorizer_window = AtomColorizerWindow(mw)
371
215
  # Handle cleanup when window is closed
372
216
  _atom_colorizer_window.finished.connect(lambda: _cleanup_window())
373
217
 
374
- # Only restore from file if this is the first execution
375
- if first_run:
376
- _atom_colorizer_window._restore_colors_from_file()
377
-
378
218
  _atom_colorizer_window.show()
379
219
  _atom_colorizer_window.raise_()
380
220
  _atom_colorizer_window.activateWindow()
381
221
 
382
- # initialize removed as it only registered the menu action
383
-
384
222
  def _cleanup_window():
385
223
  global _atom_colorizer_window
386
224
  _atom_colorizer_window = None
387
225
 
388
- def install_patches(mw):
226
+
227
+ def initialize(context):
389
228
  """
390
- Install monkey patches to core modules to ensure color persistence.
391
- Checks `_patches_installed` to avoid double patching.
229
+ Register plugin save/load handlers for persistence.
392
230
  """
393
- global _patches_installed
394
- if _patches_installed:
395
- return
396
-
397
- # Initialize persistent storage on MainWindow if not present
398
- if not hasattr(mw, 'custom_atom_colors'):
399
- mw.custom_atom_colors = {}
400
-
401
- # --- Patch 1: MainWindowView3d.draw_molecule_3d ---
402
- # Purpose: Re-apply colors after any redraw (e.g. style change, molecular edit)
403
- # We patch the instance method on 'mw' which is the entry point for other modules.
404
-
405
- original_draw_3d = mw.draw_molecule_3d
406
-
407
- def patched_draw_3d(mol):
408
- # Call original
409
- res = original_draw_3d(mol)
410
-
411
- # Apply custom colors if they exist
412
- if hasattr(mw, 'custom_atom_colors') and mw.custom_atom_colors and hasattr(mw, 'glyph_source') and mw.glyph_source:
413
- try:
414
- import pyvista as pv # Ensure pyvista is available inside closure if needed
415
-
416
- colors = mw.glyph_source.point_data['colors']
417
- is_float = (colors.dtype.kind == 'f')
418
-
419
- # 1. Update the source colors
420
- for idx, col_val in mw.custom_atom_colors.items():
421
- if isinstance(idx, str): idx = int(idx)
422
- # Check index bounds
423
- if 0 <= idx < len(colors):
424
- if is_float:
425
- if any(c > 1.0 for c in col_val):
426
- colors[idx] = [c/255.0 for c in col_val]
427
- else:
428
- colors[idx] = col_val
429
- else:
430
- if all(c <= 1.0 for c in col_val):
431
- colors[idx] = [int(c*255) for c in col_val]
432
- else:
433
- colors[idx] = col_val
434
-
435
- # 2. Re-generate the actor (Glyph filter)
436
- # Mimic common 3D view logic to respect resolution settings
437
- try:
438
- style = getattr(mw, 'current_3d_style', 'cpk')
439
- if style == 'cpk':
440
- resolution = mw.settings.get('cpk_resolution', 32)
441
- elif style == 'stick':
442
- resolution = mw.settings.get('stick_resolution', 16)
443
- else: # ball_stick
444
- resolution = mw.settings.get('ball_stick_resolution', 16)
445
- except Exception:
446
- resolution = 16
447
-
448
- glyphs = mw.glyph_source.glyph(
449
- scale='radii',
450
- geom=pv.Sphere(radius=1.0, theta_resolution=resolution, phi_resolution=resolution),
451
- orient=False
452
- )
453
-
454
- # Remove old actor
455
- if hasattr(mw, 'atom_actor') and mw.atom_actor:
456
- mw.plotter.remove_actor(mw.atom_actor)
457
-
458
- # Add new actor
459
- is_lighting_enabled = mw.settings.get('lighting_enabled', True)
460
- mesh_props = dict(
461
- smooth_shading=True,
462
- specular=mw.settings.get('specular', 0.2),
463
- specular_power=mw.settings.get('specular_power', 20),
464
- lighting=is_lighting_enabled,
465
- )
466
-
467
- mw.atom_actor = mw.plotter.add_mesh(
468
- glyphs, scalars='colors', rgb=True, **mesh_props
469
- )
470
-
471
- # Force render
472
- if hasattr(mw, 'plotter'):
473
- mw.plotter.render()
474
-
475
- except Exception as e:
476
- print(f"Patched draw_3d error: {e}")
477
- traceback.print_exc()
478
- return res
479
-
480
- mw.draw_molecule_3d = patched_draw_3d
481
-
482
- # --- Patch 2: MainWindowAppState.create_json_data ---
483
- # Purpose: Save colors to .pmeprj
484
-
485
- original_create_json = mw.create_json_data
486
-
487
- def patched_create_json():
488
- data = original_create_json()
489
- if hasattr(mw, 'custom_atom_colors') and mw.custom_atom_colors:
490
- if "3d_structure" in data and data["3d_structure"]:
491
- data["3d_structure"]["custom_atom_colors"] = mw.custom_atom_colors
492
- return data
493
-
494
- mw.create_json_data = patched_create_json
495
-
496
- # --- Patch 3: MainWindowAppState.load_from_json_data ---
497
- # Purpose: Load colors from .pmeprj
231
+ mw = context.get_main_window()
498
232
 
499
- original_load_json = mw.load_from_json_data
500
-
501
- def patched_load_json(json_data):
502
- # Extract custom colors
503
- custom_colors = {}
504
- if "3d_structure" in json_data and json_data["3d_structure"]:
505
- raw_colors = json_data["3d_structure"].get("custom_atom_colors")
506
- if raw_colors:
507
- # Ensure keys are ints (JSON keys are strings)
508
- custom_colors = {int(k): v for k, v in raw_colors.items()}
509
-
510
- # Set colors to mw BEFORE calling original logic
511
- # (because original logic calls draw_molecule_3d, which uses our patch)
512
- mw.custom_atom_colors = custom_colors
513
-
514
- return original_load_json(json_data)
233
+ def save_handler():
234
+ """Save color overrides to project file."""
235
+ # _plugin_color_overrides is stored on the MainWindow instance by the API
236
+ if not hasattr(mw, '_plugin_color_overrides'):
237
+ return {}
515
238
 
516
- mw.load_from_json_data = patched_load_json
517
-
518
- # --- Patch 4: MainWindow.clear_all (New / Clear) ---
519
- # Purpose: Reset colors when starting fresh
520
-
521
- original_clear_all = mw.clear_all
239
+ # Convert color overrides to JSON-serializable format
240
+ return {
241
+ "atom_colors": {str(k): v for k, v in mw._plugin_color_overrides.items()}
242
+ }
522
243
 
523
- @functools.wraps(original_clear_all)
524
- def patched_clear_all():
525
- # Reset colors BEFORE clearing logic
526
- if hasattr(mw, 'custom_atom_colors'):
527
- mw.custom_atom_colors = {}
528
- return original_clear_all()
244
+ def load_handler(data):
245
+ """Load color overrides from project file."""
246
+ if not data:
247
+ return
529
248
 
530
- mw.clear_all = patched_clear_all
531
-
532
- # --- Patch 5: MainWindow.trigger_conversion (2D -> 3D) ---
533
- # Purpose: Reset colors when generating new 3D structure
534
-
535
- original_trigger_conversion = mw.trigger_conversion
536
-
537
- @functools.wraps(original_trigger_conversion)
538
- def patched_trigger_conversion():
539
- # Reset colors because structure is being regenerated
540
- if hasattr(mw, 'custom_atom_colors'):
541
- mw.custom_atom_colors = {}
542
- return original_trigger_conversion()
249
+ atom_colors = data.get("atom_colors", {})
543
250
 
544
- mw.trigger_conversion = patched_trigger_conversion
251
+ # Restore color overrides using the API
252
+ if hasattr(mw, 'main_window_view_3d'):
253
+ for atom_idx_str, hex_color in atom_colors.items():
254
+ try:
255
+ atom_idx = int(atom_idx_str)
256
+ mw.main_window_view_3d.update_atom_color_override(atom_idx, hex_color)
257
+ except Exception as e:
258
+ print(f"Failed to restore color for atom {atom_idx_str}: {e}")
545
259
 
546
- _patches_installed = True
547
- print("Atom Colorizer patches installed.")
260
+ # Register handlers
261
+ context.register_save_handler(save_handler)
262
+ context.register_load_handler(load_handler)
@@ -7,8 +7,8 @@ import numpy as np
7
7
  import functools
8
8
  import types
9
9
  from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QLabel,
10
- QSlider, QHBoxLayout, QPushButton)
11
- from PyQt6.QtGui import QAction
10
+ QSlider, QHBoxLayout, QPushButton, QDoubleSpinBox)
11
+ from PyQt6.QtGui import QAction, QColor
12
12
  from PyQt6.QtCore import Qt, QTimer
13
13
 
14
14
  # Try to import VDW radii from constants, fallback if needed
@@ -35,7 +35,8 @@ SETTINGS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vdw_ra
35
35
  # Global State
36
36
  _config_window = None
37
37
  _vdw_settings = {
38
- "occupancy": 0.3 # Opacity (0.0 - 1.0)
38
+ "occupancy": 0.3, # Opacity (0.0 - 1.0)
39
+ "resolution": 0.125 # Voxel spacing in Angstroms
39
40
  }
40
41
 
41
42
  def load_settings():
@@ -44,9 +45,10 @@ def load_settings():
44
45
  if os.path.exists(SETTINGS_FILE):
45
46
  with open(SETTINGS_FILE, 'r') as f:
46
47
  saved = json.load(f)
47
- # Filter to only keep occupancy
48
48
  if "occupancy" in saved:
49
49
  _vdw_settings["occupancy"] = float(saved["occupancy"])
50
+ if "resolution" in saved:
51
+ _vdw_settings["resolution"] = float(saved["resolution"])
50
52
  except Exception as e:
51
53
  print(f"Error loading VDW settings: {e}")
52
54
 
@@ -63,7 +65,7 @@ class VDWConfigWindow(QDialog):
63
65
  self.mw = main_window
64
66
  self.setWindowTitle("VDW Overlay Settings")
65
67
  self.setModal(False)
66
- self.resize(300, 100)
68
+ self.resize(350, 150)
67
69
  self.init_ui()
68
70
 
69
71
  def init_ui(self):
@@ -74,14 +76,45 @@ class VDWConfigWindow(QDialog):
74
76
  occ_layout.addWidget(QLabel("Occupancy:"))
75
77
  self.slider_occ = QSlider(Qt.Orientation.Horizontal)
76
78
  self.slider_occ.setRange(0, 100)
77
- current_occ = int(_vdw_settings.get("occupancy", 0.3) * 100)
78
- self.slider_occ.setValue(current_occ)
79
- self.slider_occ.valueChanged.connect(self.on_occupancy_changed)
79
+ current_occ = _vdw_settings.get("occupancy", 0.3)
80
+ self.slider_occ.setValue(int(current_occ * 100))
81
+ self.slider_occ.valueChanged.connect(self.on_occupancy_slider_changed)
80
82
  occ_layout.addWidget(self.slider_occ)
81
- self.lbl_occ_val = QLabel(f"{current_occ}%")
82
- occ_layout.addWidget(self.lbl_occ_val)
83
+
84
+ self.spin_occ = QDoubleSpinBox()
85
+ self.spin_occ.setRange(0.0, 1.0)
86
+ self.spin_occ.setSingleStep(0.05)
87
+ self.spin_occ.setValue(current_occ)
88
+ self.spin_occ.valueChanged.connect(self.on_occupancy_spin_changed)
89
+ occ_layout.addWidget(self.spin_occ)
90
+
83
91
  layout.addLayout(occ_layout)
92
+
93
+ # Resolution Slider
94
+ res_layout = QHBoxLayout()
95
+ res_layout.addWidget(QLabel("Resolution (Å):"))
96
+ self.slider_res = QSlider(Qt.Orientation.Horizontal)
97
+ self.slider_res.setRange(5, 50) # 0.05 to 0.50
98
+ current_res = _vdw_settings.get("resolution", 0.125)
99
+ self.slider_res.setValue(int(current_res * 100))
100
+ self.slider_res.valueChanged.connect(self.on_resolution_slider_changed)
101
+ res_layout.addWidget(self.slider_res)
102
+
103
+ self.spin_res = QDoubleSpinBox()
104
+ self.spin_res.setRange(0.05, 0.50)
105
+ self.spin_res.setSingleStep(0.005)
106
+ self.spin_res.setDecimals(3)
107
+ self.spin_res.setValue(current_res)
108
+ self.spin_res.valueChanged.connect(self.on_resolution_spin_changed)
109
+ res_layout.addWidget(self.spin_res)
84
110
 
111
+ layout.addLayout(res_layout)
112
+
113
+ # Reset Button
114
+ btn_reset = QPushButton("Reset to Defaults")
115
+ btn_reset.clicked.connect(self.reset_defaults)
116
+ layout.addWidget(btn_reset)
117
+
85
118
  # Close Button
86
119
  btn_close = QPushButton("Close")
87
120
  btn_close.clicked.connect(self.close)
@@ -89,13 +122,93 @@ class VDWConfigWindow(QDialog):
89
122
 
90
123
  self.setLayout(layout)
91
124
 
92
- def on_occupancy_changed(self, value):
93
- opacity = value / 100.0
94
- _vdw_settings["occupancy"] = opacity
95
- self.lbl_occ_val.setText(f"{value}%")
125
+ def on_occupancy_slider_changed(self, value):
126
+ val_float = value / 100.0
127
+ self.spin_occ.blockSignals(True)
128
+ self.spin_occ.setValue(val_float)
129
+ self.spin_occ.blockSignals(False)
130
+ self._update_occupancy(val_float)
131
+
132
+ def on_occupancy_spin_changed(self, value):
133
+ val_int = int(value * 100)
134
+ self.slider_occ.blockSignals(True)
135
+ self.slider_occ.setValue(val_int)
136
+ self.slider_occ.blockSignals(False)
137
+ self._update_occupancy(value)
138
+
139
+ def _update_occupancy(self, value):
140
+ _vdw_settings["occupancy"] = value
141
+ save_settings()
142
+ self.update_view()
143
+
144
+ def on_resolution_slider_changed(self, value):
145
+ val_float = value / 100.0
146
+ self.spin_res.blockSignals(True)
147
+ self.spin_res.setValue(val_float)
148
+ self.spin_res.blockSignals(False)
149
+ self._update_resolution(val_float)
150
+
151
+ def on_resolution_spin_changed(self, value):
152
+ val_int = int(value * 100)
153
+ self.slider_res.blockSignals(True)
154
+ self.slider_res.setValue(val_int)
155
+ self.slider_res.blockSignals(False)
156
+ self._update_resolution(value)
157
+
158
+ def _update_resolution(self, value):
159
+ _vdw_settings["resolution"] = value
96
160
  save_settings()
97
161
  self.update_view()
98
162
 
163
+ def reset_defaults(self):
164
+ # Default values
165
+ def_occ = 0.3
166
+ def_res = 0.125
167
+
168
+ # Block signals to prevent redundant updates/saves during setting
169
+ self.slider_occ.blockSignals(True)
170
+ self.spin_occ.blockSignals(True)
171
+ self.slider_res.blockSignals(True)
172
+ self.spin_res.blockSignals(True)
173
+
174
+ # Set values
175
+ self.slider_occ.setValue(int(def_occ * 100))
176
+ self.spin_occ.setValue(def_occ)
177
+ self.slider_res.setValue(int(def_res * 100))
178
+ self.spin_res.setValue(def_res)
179
+
180
+ # Unblock
181
+ self.slider_occ.blockSignals(False)
182
+ self.spin_occ.blockSignals(False)
183
+ self.slider_res.blockSignals(False)
184
+ self.spin_res.blockSignals(False)
185
+
186
+ # Update settings and view once
187
+ _vdw_settings["occupancy"] = def_occ
188
+ _vdw_settings["resolution"] = def_res
189
+ save_settings()
190
+ self.update_view()
191
+
192
+ def refresh_ui_values(self):
193
+ """Update UI elements from global settings."""
194
+ occ = _vdw_settings.get("occupancy", 0.3)
195
+ res = _vdw_settings.get("resolution", 0.125)
196
+
197
+ self.slider_occ.blockSignals(True)
198
+ self.spin_occ.blockSignals(True)
199
+ self.slider_res.blockSignals(True)
200
+ self.spin_res.blockSignals(True)
201
+
202
+ self.slider_occ.setValue(int(occ * 100))
203
+ self.spin_occ.setValue(occ)
204
+ self.slider_res.setValue(int(res * 100))
205
+ self.spin_res.setValue(res)
206
+
207
+ self.slider_occ.blockSignals(False)
208
+ self.spin_occ.blockSignals(False)
209
+ self.slider_res.blockSignals(False)
210
+ self.spin_res.blockSignals(False)
211
+
99
212
  def update_view(self):
100
213
  # Trigger redraw if we are in the correct mode
101
214
  if hasattr(self.mw, 'current_3d_style') and self.mw.current_3d_style == "vdw_overlay":
@@ -130,8 +243,10 @@ def draw_vdw_overlay(mw, mol):
130
243
  radii = []
131
244
  atom_colors = []
132
245
 
133
- # Use custom colors if available, otherwise CPK
134
- custom_map = getattr(mw, 'custom_atom_colors', {})
246
+ # Use custom colors if available (API-based or legacy)
247
+ custom_map = getattr(mw, '_plugin_color_overrides', {})
248
+ if not custom_map:
249
+ custom_map = getattr(mw, 'custom_atom_colors', {})
135
250
 
136
251
  if mol.GetNumConformers() > 0:
137
252
  conf = mol.GetConformer()
@@ -149,10 +264,18 @@ def draw_vdw_overlay(mw, mol):
149
264
 
150
265
  # Color handling
151
266
  if i in custom_map:
152
- c = custom_map[i]
153
- # Normalize 0-255 to 0-1 if needed
154
- if any(x > 1.0 for x in c):
155
- c = [x/255.0 for x in c]
267
+ val = custom_map[i]
268
+ # Handling new API (Hex string) vs Legacy (List/Tuple)
269
+ if isinstance(val, str) and val.startswith('#'):
270
+ # Convert Hex to RGB [0-1]
271
+ qc = QColor(val)
272
+ c = [qc.redF(), qc.greenF(), qc.blueF()]
273
+ else:
274
+ # Assume legacy list/tuple
275
+ c = val
276
+ # Normalize 0-255 to 0-1 if needed
277
+ if any(x > 1.0 for x in c):
278
+ c = [x/255.0 for x in c]
156
279
  else:
157
280
  c = CPK_COLORS_PV.get(sym, [0.8, 0.8, 0.8]) # Default grey if missing
158
281
  atom_colors.append(c)
@@ -170,7 +293,10 @@ def draw_vdw_overlay(mw, mol):
170
293
  max_bounds = positions.max(axis=0) + padding
171
294
 
172
295
  # Resolution (voxel size in Angstroms)
173
- spacing = (0.125, 0.125, 0.125)
296
+ res_val = _vdw_settings.get("resolution", 0.125)
297
+ # Clamp to safe limits just in case
298
+ if res_val < 0.01: res_val = 0.01
299
+ spacing = (res_val, res_val, res_val)
174
300
 
175
301
  dims = np.ceil((max_bounds - min_bounds) / spacing).astype(int)
176
302
 
@@ -242,6 +368,9 @@ def run(mw):
242
368
  _config_window = VDWConfigWindow(mw)
243
369
  _config_window.finished.connect(lambda: _cleanup_config())
244
370
 
371
+ # Ensure UI reflects the loaded settings (important if window was already open or reused)
372
+ _config_window.refresh_ui_values()
373
+
245
374
  _config_window.show()
246
375
  _config_window.raise_()
247
376
  _config_window.activateWindow()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.2.0a1
3
+ Version: 2.2.0a2
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -12,7 +12,7 @@ moleditpy/modules/bond_item.py,sha256=eVkEeKvM4igYI67DYxpey3FllqDyt_iWDo4VPYMhaP
12
12
  moleditpy/modules/bond_length_dialog.py,sha256=k5x_DhK9Q8CSwouKhEo_kLRRdaYHDaK84KDNmuDNLvY,14868
13
13
  moleditpy/modules/calculation_worker.py,sha256=KiGQY7i-QCQofEoE0r65KoQgpEGFcbhmxWv6egfkUdc,42324
14
14
  moleditpy/modules/color_settings_dialog.py,sha256=Ow44BhCOLo0AFb6klO001k6B4drOgKX9DeNBQhZLp5o,15474
15
- moleditpy/modules/constants.py,sha256=FeDinXswiCE3mVvn04ts1K8_wT2lsxft6PqIEAGHnSA,4704
15
+ moleditpy/modules/constants.py,sha256=7m9mLA8eKnYh2PPXu5pXMtRndPdZVQdSej_WjMGpsrI,4704
16
16
  moleditpy/modules/constrained_optimization_dialog.py,sha256=IEdNVhFoNSEMeA5ABpUH9Q88-YzDXFloQM2gwnPwnHY,30150
17
17
  moleditpy/modules/custom_interactor_style.py,sha256=NjsXE2a43IDNEanZBlcG9eR4ZIERT1MsQC6lbfesapQ,38453
18
18
  moleditpy/modules/custom_qt_interactor.py,sha256=MFaTuDh-FPeFBS4303CqxsxmsOIOW4QXUz6USwI8PHQ,2451
@@ -29,7 +29,7 @@ moleditpy/modules/main_window_main_init.py,sha256=2MWBLY_u4UoSC4orjhDh1WcfR8FoWE
29
29
  moleditpy/modules/main_window_molecular_parsers.py,sha256=Ex4-urYsKf6PyHp4XToOhgXzuYWa_n7q--QmHci4OCU,48401
30
30
  moleditpy/modules/main_window_project_io.py,sha256=q1vEmWQDqla32HVkmk8-j0OY9ut5TI5NJ4ikahewkEo,17259
31
31
  moleditpy/modules/main_window_string_importers.py,sha256=mQVDv2Dj4MwnPgMRe2IqdAAKnB_quE6QfYeAgCjfv28,10892
32
- moleditpy/modules/main_window_ui_manager.py,sha256=dqnBzvYZdE5Da6x4IAeGYSgwHp4EUO13THudf2hUePo,22843
32
+ moleditpy/modules/main_window_ui_manager.py,sha256=HofI6T9EvcSSzPbsdPqkYEEDoB6Hui1Uj2Ll-wwczGA,24016
33
33
  moleditpy/modules/main_window_view_3d.py,sha256=TKRerktpCTYxX9HU-dSOnIhx4OyZaVrRYj4pEOUXmGc,74088
34
34
  moleditpy/modules/main_window_view_loaders.py,sha256=Dbdgv4TY_ZkX8Qyaevwr-mBJYJ59CBzRTEks-U1FiGw,14462
35
35
  moleditpy/modules/mirror_dialog.py,sha256=c3v4qY6R4FAljzk4EPaDjL9ZdZMjLQSFLqDMXz2fBUk,4696
@@ -59,17 +59,17 @@ moleditpy/plugins/File/orca_out_freq_analyzer.py,sha256=6opjTpcK3B2XUIPk8fpOKlZ_
59
59
  moleditpy/plugins/File/paste_xyz.py,sha256=kV-_CMmXLcqMIyFNRAeiwsOECPE64GCizT3hDCNfaXk,15112
60
60
  moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py,sha256=3EXzxcxmJmgxVF9F-jFOPYJksubUdxZxvmNoUx7DTW4,37769
61
61
  moleditpy/plugins/Input Generator/orca_input_generator_neo.py,sha256=vqMpwJ9NaUw2SLcSqgIhWpdUaaJjnBvWHc4S0vtA7FI,40013
62
- moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py,sha256=bWwkLwauyXZkjxGMztdCgESnD3fZw0WsvqdNwnxDLes,11050
62
+ moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py,sha256=6d6v_UC9s-2bJihGqCX3usIplw-lOUVq96kEBf32S3g,11045
63
63
  moleditpy/plugins/Optimization/all-trans_optimizer.py,sha256=_7zAnYIRShPquIOFUf8_5N1o0g7iIXKFmcD0Eey8Lac,2532
64
64
  moleditpy/plugins/Optimization/complex_molecule_untangler.py,sha256=kGJ1nIWdLCcSxmKJOHLhTW4SAQSrESMkKsILeDqKbhA,10837
65
65
  moleditpy/plugins/Optimization/conf_search.py,sha256=equ6W02Yf2DAYSj1jSNeVrQSYPPpdlNrPDt3lm_Bek8,9110
66
- moleditpy/plugins/Utility/atom_colorizer.py,sha256=vysv-nmNZFVgfDA5tbJtMvEU2qkeHxrwNU7BxaEvePY,22824
66
+ moleditpy/plugins/Utility/atom_colorizer.py,sha256=BUO-EkIeWwkGE6DtOuXgw0QkDABet_YfNXfpPZFWBlQ,9936
67
67
  moleditpy/plugins/Utility/console.py,sha256=4_WgDiEHhalDFOLJGmYNRa1odzxdtg46qr9DUz_ht4I,5943
68
68
  moleditpy/plugins/Utility/pubchem_ressolver.py,sha256=w1Zfm_2LTDhuU6uYKQSSaGpyU36-14kvDssc0L8hsDc,10048
69
- moleditpy/plugins/Utility/vdw_radii_overlay.py,sha256=TtufVe8Gowmpd2O-JmN4nECBoCphfcwXy4LlpVt_LMc,11390
70
- moleditpy-2.2.0a1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
- moleditpy-2.2.0a1.dist-info/METADATA,sha256=IE4vrtPdPiLDEfMQfz_vV5bazWJvsglrxiN6gs8OKwk,59277
72
- moleditpy-2.2.0a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- moleditpy-2.2.0a1.dist-info/entry_points.txt,sha256=yH1h9JjALhok1foXT3-hYrC4ufoZt8b7oiBcsdnGNNM,54
74
- moleditpy-2.2.0a1.dist-info/top_level.txt,sha256=ARICrS4ihlPXqywlKl6o-oJa3Qz3gZRWu_VZsQ3_c44,10
75
- moleditpy-2.2.0a1.dist-info/RECORD,,
69
+ moleditpy/plugins/Utility/vdw_radii_overlay.py,sha256=C7cD82AiJIeaQk12gKjIt0oneJny2fDRAbw-t01vpEg,16533
70
+ moleditpy-2.2.0a2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
+ moleditpy-2.2.0a2.dist-info/METADATA,sha256=0pjsuxHg7SWfSJWfdNf-0zi2R5j4fbG0s8KSPYWeE3s,59277
72
+ moleditpy-2.2.0a2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
+ moleditpy-2.2.0a2.dist-info/entry_points.txt,sha256=yH1h9JjALhok1foXT3-hYrC4ufoZt8b7oiBcsdnGNNM,54
74
+ moleditpy-2.2.0a2.dist-info/top_level.txt,sha256=ARICrS4ihlPXqywlKl6o-oJa3Qz3gZRWu_VZsQ3_c44,10
75
+ moleditpy-2.2.0a2.dist-info/RECORD,,