MoleditPy-linux 4.1.2__py3-none-any.whl → 4.1.4__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.
@@ -8,8 +8,6 @@ Author: Hiromichi Yokoyama
8
8
  License: GPL-3.0 license
9
9
  Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
-
12
- Top-level package for moleditpy_linux.
13
11
  """
14
12
 
15
13
  import importlib.util # noqa: F401
@@ -0,0 +1,11 @@
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
+ """
@@ -190,11 +190,9 @@ class PluginContext:
190
190
  Get or set the current molecule (RDKit Mol object). Shortcut for current_molecule.
191
191
  """
192
192
  mw = self.get_main_window()
193
- return (
194
- mw.view_3d_manager.current_mol
195
- if mw and hasattr(mw, "view_3d_manager")
196
- else None
197
- )
193
+ if mw and hasattr(mw, "view_3d_manager"):
194
+ return mw.view_3d_manager.current_mol
195
+ return None
198
196
 
199
197
  @current_mol.setter
200
198
  def current_mol(self, mol: Any) -> None:
@@ -206,9 +206,10 @@ class PluginManager:
206
206
  shutil.copy2(file_path, dest_path)
207
207
  msg = f"Installed {filename}"
208
208
 
209
- # Reload plugins after install
209
+ # Reload plugins after install and rebuild UI immediately
210
210
  if self.main_window:
211
211
  self.discover_plugins(self.main_window)
212
+ self.rebuild_plugin_menus()
212
213
  return True, msg
213
214
  except (
214
215
  AttributeError,
@@ -315,6 +316,13 @@ class PluginManager:
315
316
  stub.__package__ = parent_name
316
317
  sys.modules[parent_name] = stub
317
318
 
319
+ # Purge stale cache so package submodules reload from disk on re-exec
320
+ for _cached_key in list(sys.modules.keys()):
321
+ if _cached_key == unique_module_name or _cached_key.startswith(
322
+ unique_module_name + "."
323
+ ):
324
+ del sys.modules[_cached_key]
325
+
318
326
  spec = importlib.util.spec_from_file_location(
319
327
  unique_module_name,
320
328
  filepath,
@@ -588,7 +596,8 @@ class PluginManager:
588
596
  selected_atom_ids.add(item.atom_id)
589
597
 
590
598
  # Now map these editor IDs to RDKit indices
591
- mol = getattr(self.main_window, "current_mol", None)
599
+ v3d = getattr(self.main_window, "view_3d_manager", None)
600
+ mol = v3d.current_mol if v3d else None
592
601
  if mol and selected_atom_ids:
593
602
  for i in range(mol.GetNumAtoms()):
594
603
  atom = mol.GetAtomWithIdx(i)
@@ -156,13 +156,12 @@ class PluginManagerWindow(QDialog):
156
156
  self.btn_remove.setEnabled(has_selection)
157
157
 
158
158
  def on_reload(self, silent: bool = False) -> None:
159
- """Reload all plugins from disk and refresh the table."""
160
- # Trigger reload in main manager
159
+ """Reload all plugins from disk, rebuild main-window UI, and refresh the table."""
161
160
  if self.plugin_manager.main_window:
162
161
  self.plugin_manager.discover_plugins(self.plugin_manager.main_window)
162
+ self.plugin_manager.rebuild_plugin_menus()
163
163
  self.refresh_plugin_list()
164
164
 
165
- # For immediate feedback:
166
165
  if not silent:
167
166
  QMessageBox.information(self, "Reloaded", "Plugins have been reloaded.")
168
167
  else:
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
19
19
  from rdkit import Chem
20
20
 
21
21
  # PyQt6 Modules
22
+ import copy
22
23
  from PyQt6.QtCore import pyqtSignal
23
24
  from PyQt6.QtWidgets import QMainWindow, QMessageBox
24
25
 
@@ -254,8 +255,6 @@ class MainWindow(QMainWindow):
254
255
 
255
256
  def save_state_snapshot(self) -> None:
256
257
  """Create a deep copy snapshot of the current state for undo/redo comparison."""
257
- import copy
258
-
259
258
  try:
260
259
  self.state_manager.saved_state = copy.deepcopy(
261
260
  self.state_manager.get_current_state()
@@ -56,6 +56,8 @@ class MoveSelectedAtomsDialog(BasePickingDialog):
56
56
  self.mouse_moved_during_drag: bool = False
57
57
  self._consume_next_left_release: bool = False
58
58
  self.highlight_actor: Optional[pv.Actor] = None
59
+ self.original_style: Optional[Any] = None
60
+ self._click_press_pos: Optional[Any] = None
59
61
 
60
62
  self.widgets: dict[str, Any] = {}
61
63
 
@@ -211,6 +213,13 @@ class MoveSelectedAtomsDialog(BasePickingDialog):
211
213
  def _init_buttons_ui(self, layout: QVBoxLayout) -> None:
212
214
  """Initialize bottom buttons."""
213
215
  button_layout = QHBoxLayout()
216
+
217
+ box_select_btn = QPushButton("Box Selection: OFF")
218
+ box_select_btn.setCheckable(True)
219
+ box_select_btn.clicked.connect(self.toggle_box_selection)
220
+ self.widgets["box_select_btn"] = box_select_btn
221
+ button_layout.addWidget(box_select_btn)
222
+
214
223
  clear_btn = QPushButton("Clear Selection")
215
224
  clear_btn.clicked.connect(self.clear_selection)
216
225
  self.widgets["clear_button"] = clear_btn
@@ -238,6 +247,24 @@ class MoveSelectedAtomsDialog(BasePickingDialog):
238
247
 
239
248
  e_type = event.type()
240
249
 
250
+ btn = self.widgets.get("box_select_btn")
251
+ box_selection_on = btn is not None and btn.isChecked()
252
+
253
+ if e_type == QEvent.Type.MouseButtonPress:
254
+ self._click_press_pos = getattr(event, "pos", lambda: None)()
255
+ elif e_type == QEvent.Type.MouseButtonRelease:
256
+ if hasattr(self, "_click_press_pos") and self._click_press_pos is not None:
257
+ pos = getattr(event, "pos", lambda: None)()
258
+ if pos:
259
+ diff = pos - self._click_press_pos
260
+ if diff.manhattanLength() < 3:
261
+ if box_selection_on:
262
+ self.clear_selection()
263
+ self._click_press_pos = None
264
+
265
+ if box_selection_on:
266
+ return False
267
+
241
268
  if e_type == QEvent.Type.MouseButtonDblClick:
242
269
  # Ignore double clicks and reset state
243
270
  self.is_dragging_group = False
@@ -645,3 +672,77 @@ class MoveSelectedAtomsDialog(BasePickingDialog):
645
672
  self.update_display()
646
673
  self.is_dragging_group = False
647
674
  self.drag_start_pos = None
675
+
676
+ def toggle_box_selection(self, checked: bool) -> None:
677
+ """Toggle Box Selection mode using PyVista's rectangle picker."""
678
+ plotter = self.main_window.view_3d_manager.plotter
679
+ if plotter is None or plotter.interactor is None:
680
+ return
681
+
682
+ btn = self.widgets["box_select_btn"]
683
+ if checked:
684
+ btn.setText("Box Selection: ON")
685
+ # Save original style if not saved
686
+ if self.original_style is None:
687
+ self.original_style = plotter.interactor.GetInteractorStyle()
688
+
689
+ # Using PyVista's built-in picking which reliably draws the box correctly
690
+ plotter.enable_rectangle_picking(
691
+ callback=self.on_rectangle_picked,
692
+ show_message=False,
693
+ start=True,
694
+ color="red",
695
+ )
696
+ else:
697
+ btn.setText("Box Selection: OFF")
698
+ plotter.disable_picking()
699
+ # Restore original style
700
+ if self.original_style is not None:
701
+ plotter.interactor.SetInteractorStyle(self.original_style)
702
+
703
+ def on_rectangle_picked(self, selection: Any) -> None:
704
+ """Handle PyVista rectangle picking callback."""
705
+ if not hasattr(selection, "viewport"):
706
+ return
707
+
708
+ x0, y0, x1, y1 = selection.viewport
709
+ x_min = min(x0, x1)
710
+ x_max = max(x0, x1)
711
+ y_min = min(y0, y1)
712
+ y_max = max(y0, y1)
713
+
714
+ # If the drag box is small, treat it as a single click to clear selection
715
+ if abs(x_max - x_min) < 15 and abs(y_max - y_min) < 15:
716
+ self.clear_selection()
717
+ return
718
+
719
+ plotter = self.main_window.view_3d_manager.plotter
720
+ if plotter is None:
721
+ return
722
+ renderer = plotter.renderer
723
+
724
+ positions = self.main_window.view_3d_manager.atom_positions_3d
725
+ if positions is not None:
726
+ added = False
727
+ for atom_idx, pos in enumerate(positions):
728
+ renderer.SetWorldPoint(float(pos[0]), float(pos[1]), float(pos[2]), 1.0)
729
+ renderer.WorldToDisplay()
730
+ display = renderer.GetDisplayPoint()
731
+
732
+ if x_min <= display[0] <= x_max and y_min <= display[1] <= y_max:
733
+ if atom_idx not in self.selected_atoms:
734
+ self.selected_atoms.add(atom_idx)
735
+ added = True
736
+
737
+ if added:
738
+ self.show_atom_labels()
739
+ self.update_display()
740
+
741
+ def reject(self) -> None:
742
+ """Clean up when dialog closes."""
743
+ # Ensure we restore the interactor style if it was left in Box Selection mode
744
+ btn = self.widgets.get("box_select_btn")
745
+ if btn and btn.isChecked():
746
+ btn.setChecked(False)
747
+ self.toggle_box_selection(False)
748
+ super().reject()
@@ -2,7 +2,7 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  """
5
- MoleditPy  EA Python-based molecular editing software
5
+ MoleditPy — A Python-based molecular editing software
6
6
 
7
7
  Author: Hiromichi Yokoyama
8
8
  License: GPL-3.0 license
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.1.2
3
+ Version: 4.1.4
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
@@ -1,17 +1,17 @@
1
- moleditpy_linux/__init__.py,sha256=QkGj1Nbkjg98UGBnaK5MaKZX98Vlb_xhbO2MDRrjvE4,504
1
+ moleditpy_linux/__init__.py,sha256=QYE14Hc9eAwykvTIDIXvur6NXlGcZ0hg2nUZesj6soI,462
2
2
  moleditpy_linux/__main__.py,sha256=VHCD-CCX6iKUbmUyRv2BGVXCjHWEaISpmjO7_yh9AaU,654
3
3
  moleditpy_linux/main.py,sha256=0QYEi0JEOyjeY9ntLMGJpbxV69MGeog3jRtnkSJK7bI,7221
4
4
  moleditpy_linux/assets/file_icon.ico,sha256=yyVj084A7HuMNbV073cE_Ag3Ne405qgOP3Mia1ZqLpE,101632
5
5
  moleditpy_linux/assets/icon.icns,sha256=wD5R6-Vw7K662tVKhu2E1ImN0oUuyAP4youesEQsn9c,139863
6
6
  moleditpy_linux/assets/icon.ico,sha256=RfgFcx7-dHY_2STdsOQCQziY5SNhDr3gPnjO6jzEDPI,147975
7
7
  moleditpy_linux/assets/icon.png,sha256=kCFN1WacYIdy0GN6SFEbNA00ef39pCczBnFdkkBI8Bs,147110
8
- moleditpy_linux/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ moleditpy_linux/core/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
9
9
  moleditpy_linux/core/mol_geometry.py,sha256=GrzQfUZ9RVgwBeJEglQWvKhJMMkMVI6Hc6fUm2BG8UU,21548
10
10
  moleditpy_linux/core/molecular_data.py,sha256=QzOnO_qhGp8VqREj89MLnVEgMMzuDoQqKTbuhgIwusc,17834
11
11
  moleditpy_linux/plugins/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
12
- moleditpy_linux/plugins/plugin_interface.py,sha256=TMtnSdSVIEcEYLYPqoj9NQEO4LM_KPrg1Gn1eYBiOik,22043
13
- moleditpy_linux/plugins/plugin_manager.py,sha256=ty9judAFzOF8bErzPM5WE4S8f3Z83zepmwn5EurRUVE,30892
14
- moleditpy_linux/plugins/plugin_manager_window.py,sha256=5d0VWc5u5G_vsRoFawA5N5jI0q88tbKSzVEG_oAWbl8,13366
12
+ moleditpy_linux/plugins/plugin_interface.py,sha256=nCxmT7EXqL3SrRoltZ-A2d_PES5zggn5I1y28OUkPJ0,22016
13
+ moleditpy_linux/plugins/plugin_manager.py,sha256=46S51i6YpRZSiWgKRHH0CkgBiJ-RPe1lruUelNTlRdg,31364
14
+ moleditpy_linux/plugins/plugin_manager_window.py,sha256=hwJiT33KmoQG7AvGJ-1EM3CoKcVhmvQN2UQ-NiPPARc,13366
15
15
  moleditpy_linux/ui/__init__.py,sha256=_BslFbhSM4Ba4GDzH1XpvlqlGgvi4OFkcuU3pPeIz3s,751
16
16
  moleditpy_linux/ui/about_dialog.py,sha256=OQgCBR-5cIh_S5VWuBLfiss_xeatju0kGWlIIiEzpzg,4547
17
17
  moleditpy_linux/ui/align_plane_dialog.py,sha256=JuAhiw1wFdDRAwOJrtCWAOCRUXZELq2NH19ssOP6L38,11140
@@ -38,13 +38,13 @@ moleditpy_linux/ui/edit_actions_logic.py,sha256=rDtsrEAtBGVCww0Den7er33MjliaDIUX
38
38
  moleditpy_linux/ui/export_logic.py,sha256=WWE8WACoW_8s2UMSdLx9580FJguSZ6QgKEE7Vx-f4TE,43113
39
39
  moleditpy_linux/ui/geometry_base_dialog.py,sha256=cltYZZEd38j9M5tQTtxzPFh-uKqsBJnM_K8lWAPYXro,4399
40
40
  moleditpy_linux/ui/io_logic.py,sha256=45Qb9L0UuENs-ciBiSFL-7S8BXA5L5KZ9vJCmh6ChX4,44724
41
- moleditpy_linux/ui/main_window.py,sha256=lD4GHOlYvoWNavXBczrDyR1onruoar941YMtW2f43XQ,12491
41
+ moleditpy_linux/ui/main_window.py,sha256=_6Ie539dGrtyJYsKSqx8Y7m0_gufZ6gAsP2FV9RGb4U,12481
42
42
  moleditpy_linux/ui/main_window_init.py,sha256=Qz03pjS7IemAL-RhcaqKmiRalV1uHgD-5UnNUz9H878,75833
43
43
  moleditpy_linux/ui/mirror_dialog.py,sha256=qcyR9wshVKWs6wvCyZ6ZN09CL_m2oVNWH4U_UyUZpWE,5182
44
44
  moleditpy_linux/ui/molecular_scene_handler.py,sha256=4u170Hmu2mJoRi6FjV0uqEfH27Lbyu0TTF5Q5T9D3PU,68511
45
45
  moleditpy_linux/ui/molecule_scene.py,sha256=tuvtLNctiXuNSwC7W89nr8kKumoGNJl3brH2TGT3Aew,44004
46
46
  moleditpy_linux/ui/move_group_dialog.py,sha256=Lj_kmP5kNvk31sVra0MQ0l71d_n8AaWxLGLU56nsOGM,26777
47
- moleditpy_linux/ui/move_selected_atoms_dialog.py,sha256=3zAnknQ9bqyyzwoy2YSFdb4UYZuHaQfeMMwoxwDEjbc,25220
47
+ moleditpy_linux/ui/move_selected_atoms_dialog.py,sha256=Pj_em_Bz0sMb4pxkGNeQJq9Y1Q1OAZHBVKni-LQTcUo,29302
48
48
  moleditpy_linux/ui/periodic_table_dialog.py,sha256=ZEGreZmfi4p7ihPkuEfJhgq2gD8mMrPWbitrNcJ1h8M,6106
49
49
  moleditpy_linux/ui/planarize_dialog.py,sha256=yAG97zoRN_3B01nsy53stHFjV48Qk-L-jqqCxN_Y0YM,7811
50
50
  moleditpy_linux/ui/plugin_menu_manager.py,sha256=0NfXBETHMKthI50lrqe63xYzUrLmUP49Apm8g0JuNYQ,18986
@@ -55,7 +55,7 @@ moleditpy_linux/ui/template_preview_view.py,sha256=PM14bgCQHGpmJ_yuJPtkPxCv3QadT
55
55
  moleditpy_linux/ui/translation_dialog.py,sha256=ITZr53BWEoEdJZtaS5nPSgNd5TC0Po_fcGbkLD9_BXY,16595
56
56
  moleditpy_linux/ui/ui_manager.py,sha256=sMx5-qp5vAr2Am9Du9lHvw93kixG3vBwpRZiHYcAQkc,26070
57
57
  moleditpy_linux/ui/user_template_dialog.py,sha256=y-Psn4yIMbwJYNbVDAJGgeV2tf-1MoidT3Bb6BMnuqg,29246
58
- moleditpy_linux/ui/view_3d_logic.py,sha256=gdc824MWnzr7oqThwm4od5oCxo6m1yiXZg8-lJcWP90,90839
58
+ moleditpy_linux/ui/view_3d_logic.py,sha256=7_jd49EvRmhNqes1xuI7hapf_knhe9mjLwkQ8Xa4P8A,90839
59
59
  moleditpy_linux/ui/zoomable_view.py,sha256=5PbSJW3pUCBaEXGhvy6BlqLpKrRpTry2AGwZcSXSgfI,5926
60
60
  moleditpy_linux/ui/settings_tabs/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
61
61
  moleditpy_linux/ui/settings_tabs/settings_2d_tab.py,sha256=QB03CTbHUtTLdJqgCq3_JUa4Ie-yJ94KljvCSFcqhdM,15249
@@ -67,9 +67,9 @@ moleditpy_linux/utils/constants.py,sha256=zG1BY8IkCG-4JJxjvXFXVYlGCW2vWd-mW2faFq
67
67
  moleditpy_linux/utils/default_settings.py,sha256=6Q3Gw1vyG7uk-35yBtr1sKeJVnOjb4Sq2Hy8iR3MYK4,2894
68
68
  moleditpy_linux/utils/sip_isdeleted_safe.py,sha256=My6IJqDewbYY6SoRYNk6XwFUQ9_yihaR3Ym7EOETuAw,1189
69
69
  moleditpy_linux/utils/system_utils.py,sha256=K5c9cJRgMFqtUtLefSu3w2hy2drgxNqfT200bSIFy2k,2325
70
- moleditpy_linux-4.1.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
71
- moleditpy_linux-4.1.2.dist-info/METADATA,sha256=wXIbmkOuENNKBrmJ48Bhq3kSnsCjqXADcCE6sPVowuU,64017
72
- moleditpy_linux-4.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
73
- moleditpy_linux-4.1.2.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
74
- moleditpy_linux-4.1.2.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
75
- moleditpy_linux-4.1.2.dist-info/RECORD,,
70
+ moleditpy_linux-4.1.4.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
71
+ moleditpy_linux-4.1.4.dist-info/METADATA,sha256=92pb0Hcrh7l8bOFe7oIR1hZ7zpvDSPVHCYyhzLCpQ_g,64017
72
+ moleditpy_linux-4.1.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
73
+ moleditpy_linux-4.1.4.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
74
+ moleditpy_linux-4.1.4.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
75
+ moleditpy_linux-4.1.4.dist-info/RECORD,,