MoleditPy-linux 4.10.0__tar.gz → 4.10.2__tar.gz

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 (85) hide show
  1. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/PKG-INFO +1 -1
  2. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/pyproject.toml +1 -1
  3. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
  4. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/plugins/plugin_manager_window.py +180 -56
  5. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/LICENSE +0 -0
  6. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/README.md +0 -0
  7. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/setup.cfg +0 -0
  8. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  9. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  10. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  11. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  12. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  13. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/__init__.py +0 -0
  14. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/__main__.py +0 -0
  15. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  16. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/assets/icon.icns +0 -0
  17. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/assets/icon.ico +0 -0
  18. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/assets/icon.png +0 -0
  19. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/core/__init__.py +0 -0
  20. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  21. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/core/molecular_data.py +0 -0
  22. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/main.py +0 -0
  23. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/plugins/__init__.py +0 -0
  24. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
  25. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
  26. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/__init__.py +0 -0
  27. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  28. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
  29. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  30. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  31. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
  32. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/app_state.py +0 -0
  33. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/atom_item.py +0 -0
  34. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/atom_picking.py +0 -0
  35. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  36. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/bond_item.py +0 -0
  37. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
  38. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
  39. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/chain_mixin.py +0 -0
  40. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  41. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/compute_logic.py +0 -0
  42. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
  43. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
  44. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  45. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
  46. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
  47. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
  48. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  49. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
  50. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/export_logic.py +0 -0
  51. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  52. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/io_logic.py +0 -0
  53. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/main_window.py +0 -0
  54. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/main_window_init.py +0 -0
  55. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  56. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
  57. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
  58. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
  59. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
  60. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  61. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
  62. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
  63. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/preview_molecule.py +0 -0
  64. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  65. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  66. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
  67. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
  68. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  69. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
  70. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  71. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/string_importers.py +0 -0
  72. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  73. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  74. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
  75. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/ui_manager.py +0 -0
  76. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
  77. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
  78. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  79. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/__init__.py +0 -0
  80. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/constants.py +0 -0
  81. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/default_settings.py +0 -0
  82. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/hit_radius.py +0 -0
  83. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  84. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/suppress_log.py +0 -0
  85. {moleditpy_linux-4.10.0 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.10.0
3
+ Version: 4.10.2
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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "4.10.0"
8
+ version = "4.10.2"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.10.0
3
+ Version: 4.10.2
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
@@ -13,17 +13,19 @@ DOI: 10.5281/zenodo.17268532
13
13
  import logging
14
14
  import os
15
15
  import shutil
16
- from typing import Any, Optional
16
+ from typing import Any, Optional, cast
17
17
 
18
18
 
19
19
  from PyQt6.QtCore import Qt, QUrl
20
20
  from PyQt6.QtGui import QDesktopServices, QDragEnterEvent, QDropEvent
21
21
  from PyQt6.QtWidgets import (
22
22
  QAbstractItemView,
23
+ QCheckBox,
23
24
  QDialog,
24
25
  QHBoxLayout,
25
26
  QHeaderView,
26
27
  QLabel,
28
+ QLineEdit,
27
29
  QMessageBox,
28
30
  QPushButton,
29
31
  QTableWidget,
@@ -41,39 +43,68 @@ class PluginManagerWindow(QDialog):
41
43
  super().__init__(parent)
42
44
  self.btn_remove: Any = None
43
45
  self.table: Any = None
46
+ self.search_input: Any = None
44
47
  self.plugin_manager = plugin_manager
45
48
  self._preferences_applied = False
46
49
  self.setWindowTitle("Plugin Manager")
47
- self.resize(800, 500)
50
+ self.resize(850, 520)
48
51
  self.setAcceptDrops(True) # Enable drag & drop for the whole window
49
52
 
50
53
  self.init_ui()
51
54
  self.refresh_plugin_list()
52
55
 
53
56
  def init_ui(self) -> None:
54
- """Build the plugin manager UI with table, buttons, and drag-and-drop support."""
57
+ """Build plugin manager UI with table, buttons, search box, and drop support."""
55
58
  layout = QVBoxLayout(self)
56
59
 
57
- lbl_info = QLabel("Drag & Drop .py or .zip files here to install plugins.")
60
+ lbl_info = QLabel(
61
+ "Drag & Drop .py or .zip files to install plugins. Use checkboxes to enable/disable."
62
+ )
58
63
  lbl_info.setStyleSheet("color: gray; font-style: italic;")
59
64
  layout.addWidget(lbl_info)
60
65
 
66
+ # Search Box
67
+ search_layout = QHBoxLayout()
68
+ search_label = QLabel("Search:")
69
+ self.search_input = QLineEdit()
70
+ self.search_input.setPlaceholderText(
71
+ "Search plugins by name, author, location, or description..."
72
+ )
73
+ self.search_input.setClearButtonEnabled(True)
74
+ self.search_input.textChanged.connect(self.filter_plugins)
75
+ search_layout.addWidget(search_label)
76
+ search_layout.addWidget(self.search_input)
77
+ layout.addLayout(search_layout)
78
+
61
79
  # Plugin Table
62
80
  self.table = QTableWidget()
63
- self.table.setColumnCount(6)
81
+ self.table.setColumnCount(7)
64
82
  self.table.setHorizontalHeaderLabels(
65
- ["Status", "Name", "Version", "Author", "Location", "Description"]
83
+ [
84
+ "Enabled",
85
+ "Status",
86
+ "Name",
87
+ "Version",
88
+ "Author",
89
+ "Location",
90
+ "Description",
91
+ ]
66
92
  )
67
93
  self.table.horizontalHeader().setSectionResizeMode(
68
94
  QHeaderView.ResizeMode.Interactive
69
95
  )
70
96
  self.table.horizontalHeader().setSectionResizeMode(
71
- 4, QHeaderView.ResizeMode.Interactive
97
+ 0, QHeaderView.ResizeMode.ResizeToContents
72
98
  )
73
99
  self.table.horizontalHeader().setSectionResizeMode(
74
- 5, QHeaderView.ResizeMode.Stretch
100
+ 5, QHeaderView.ResizeMode.Interactive
101
+ )
102
+ self.table.horizontalHeader().setSectionResizeMode(
103
+ 6, QHeaderView.ResizeMode.Stretch
75
104
  ) # Description stretches
76
- self.table.setColumnWidth(1, 200) # Make Name column wider
105
+ self.table.setColumnWidth(0, 70) # Enabled checkbox
106
+ self.table.setColumnWidth(1, 100) # Status
107
+ self.table.setColumnWidth(2, 180) # Name
77
108
  self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
78
109
  self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
79
110
  self.table.itemSelectionChanged.connect(self.update_button_state)
@@ -111,6 +142,20 @@ class PluginManagerWindow(QDialog):
111
142
 
112
143
  layout.addLayout(btn_layout)
113
144
 
145
+ def _create_checkbox_container(
146
+ self, checked: bool, tooltip: str = "Enable or disable this plugin"
147
+ ) -> tuple[QWidget, QCheckBox]:
148
+ """Create a centered QCheckBox container widget without custom stylesheets."""
149
+ container = QWidget()
150
+ box_layout = QHBoxLayout(container)
151
+ box_layout.setContentsMargins(0, 0, 0, 0)
152
+ box_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
153
+ checkbox = QCheckBox()
154
+ checkbox.setChecked(checked)
155
+ checkbox.setToolTip(tooltip)
156
+ box_layout.addWidget(checkbox)
157
+ return container, checkbox
158
+
114
159
  def refresh_plugin_list(self) -> None:
115
160
  """Repopulate the plugin table from the current plugin registry."""
116
161
  self.table.setRowCount(0)
@@ -118,20 +163,36 @@ class PluginManagerWindow(QDialog):
118
163
 
119
164
  self.table.setRowCount(len(plugins))
120
165
  for row, p in enumerate(plugins):
121
- status_item = QTableWidgetItem(str(p.get("status", "Unknown")))
166
+ # Column 0: Enabled Checkbox (Clean native check, centered)
167
+ is_enabled = not p.get("disabled", False)
168
+ container, _ = self._create_checkbox_container(is_enabled)
169
+ self.table.setCellWidget(row, 0, container)
170
+
171
+ # Store the plugin index in the item's UserRole for stable reference during filtering
172
+ enabled_item = QTableWidgetItem()
173
+ enabled_item.setData(Qt.ItemDataRole.UserRole, row)
174
+ self.table.setItem(row, 0, enabled_item)
175
+
176
+ # Column 1: Status (colored text)
177
+ status = str(p.get("status", "Unknown"))
178
+ status_item = QTableWidgetItem(status)
122
179
  status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
123
- status_item.setFlags(status_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
124
- status_item.setCheckState(
125
- Qt.CheckState.Unchecked
126
- if p.get("disabled", False)
127
- else Qt.CheckState.Checked
128
- )
129
- self.table.setItem(row, 0, status_item)
130
- self.table.setItem(row, 1, QTableWidgetItem(str(p.get("name", "Unknown"))))
131
- self.table.setItem(row, 2, QTableWidgetItem(str(p.get("version", ""))))
132
- self.table.setItem(row, 3, QTableWidgetItem(str(p.get("author", ""))))
180
+ if status.startswith("Error"):
181
+ status_item.setForeground(Qt.GlobalColor.red)
182
+ elif status == "Loaded":
183
+ status_item.setForeground(Qt.GlobalColor.darkGreen)
184
+ elif status in ("No Entry Point", "Disabled"):
185
+ status_item.setForeground(Qt.GlobalColor.gray)
186
+ self.table.setItem(row, 1, status_item)
133
187
 
134
- # Location (Relative Path)
188
+ # Column 2: Name
189
+ self.table.setItem(row, 2, QTableWidgetItem(str(p.get("name", "Unknown"))))
190
+ # Column 3: Version
191
+ self.table.setItem(row, 3, QTableWidgetItem(str(p.get("version", ""))))
192
+ # Column 4: Author
193
+ self.table.setItem(row, 4, QTableWidgetItem(str(p.get("author", ""))))
194
+
195
+ # Column 5: Location (Relative Path)
135
196
  full_path = p.get("filepath", "")
136
197
  rel_path = ""
137
198
  if full_path:
@@ -141,35 +202,85 @@ class PluginManagerWindow(QDialog):
141
202
  )
142
203
  except (AttributeError, RuntimeError, ValueError, TypeError):
143
204
  rel_path = os.path.basename(full_path)
144
- self.table.setItem(row, 4, QTableWidgetItem(str(rel_path)))
145
-
146
- self.table.setItem(row, 5, QTableWidgetItem(str(p.get("description", ""))))
147
-
148
- # Simple color coding for status
149
- status = str(p.get("status", ""))
150
- color = None
151
- if status.startswith("Error"):
152
- color = Qt.GlobalColor.red
153
- elif status == "Loaded":
154
- color = Qt.GlobalColor.darkGreen
155
- elif status in ("No Entry Point", "Disabled"):
156
- color = Qt.GlobalColor.gray
205
+ self.table.setItem(row, 5, QTableWidgetItem(str(rel_path)))
206
+
207
+ # Column 6: Description
208
+ self.table.setItem(row, 6, QTableWidgetItem(str(p.get("description", ""))))
209
+
210
+ if self.search_input and self.search_input.text():
211
+ self.filter_plugins(self.search_input.text())
212
+
213
+ def filter_plugins(self, text: str) -> None:
214
+ """Filter table rows based on search text."""
215
+ query = text.strip().lower()
216
+ for row in range(self.table.rowCount()):
217
+ if not query:
218
+ self.table.setRowHidden(row, False)
219
+ continue
220
+
221
+ # Check status, name, author, location, and description
222
+ status = (
223
+ self.table.item(row, 1).text().lower()
224
+ if self.table.item(row, 1)
225
+ else ""
226
+ )
227
+ name = (
228
+ self.table.item(row, 2).text().lower()
229
+ if self.table.item(row, 2)
230
+ else ""
231
+ )
232
+ author = (
233
+ self.table.item(row, 4).text().lower()
234
+ if self.table.item(row, 4)
235
+ else ""
236
+ )
237
+ location = (
238
+ self.table.item(row, 5).text().lower()
239
+ if self.table.item(row, 5)
240
+ else ""
241
+ )
242
+ description = (
243
+ self.table.item(row, 6).text().lower()
244
+ if self.table.item(row, 6)
245
+ else ""
246
+ )
157
247
 
158
- if color:
159
- self.table.item(row, 0).setForeground(color)
248
+ match = (
249
+ query in name
250
+ or query in author
251
+ or query in location
252
+ or query in description
253
+ or query in status
254
+ )
255
+ self.table.setRowHidden(row, not match)
256
+
257
+ def _status_checkbox(self, row: int) -> Optional[QCheckBox]:
258
+ """Return the visible status checkbox for a plugin table row."""
259
+ container = self.table.cellWidget(row, 0)
260
+ if isinstance(container, QCheckBox):
261
+ return container
262
+ if isinstance(container, QWidget):
263
+ checkbox = container.findChild(QCheckBox)
264
+ return checkbox
265
+ return None
160
266
 
161
267
  def _save_checkbox_preferences(self) -> set[str]:
162
268
  """Collect and persist checkbox choices."""
163
269
  disabled_paths = set()
164
- for row, plugin in enumerate(self.plugin_manager.plugins):
165
- status_item = self.table.item(row, 0)
166
- if (
167
- status_item is not None
168
- and status_item.checkState() == Qt.CheckState.Unchecked
270
+ for row in range(self.table.rowCount()):
271
+ item = self.table.item(row, 0)
272
+ plugin_idx = item.data(Qt.ItemDataRole.UserRole) if item else row
273
+ if plugin_idx is not None and 0 <= plugin_idx < len(
274
+ self.plugin_manager.plugins
169
275
  ):
170
- filepath = plugin.get("filepath")
171
- if filepath:
172
- disabled_paths.add(self.plugin_manager.plugin_path_key(filepath))
276
+ plugin = self.plugin_manager.plugins[plugin_idx]
277
+ status_checkbox = self._status_checkbox(row)
278
+ if status_checkbox is not None and not status_checkbox.isChecked():
279
+ filepath = plugin.get("filepath")
280
+ if filepath:
281
+ disabled_paths.add(
282
+ self.plugin_manager.plugin_path_key(filepath)
283
+ )
173
284
 
174
285
  self.plugin_manager.save_disabled_plugins(disabled_paths)
175
286
  return disabled_paths
@@ -212,6 +323,19 @@ class PluginManagerWindow(QDialog):
212
323
  self.plugin_manager.discover_plugins()
213
324
  self.refresh_plugin_list()
214
325
 
326
+ def _plugin_for_row(self, row: int) -> Optional[dict[str, Any]]:
327
+ """Retrieve plugin dict for a table row, accounting for search filtering."""
328
+ if row < 0 or row >= self.table.rowCount():
329
+ return None
330
+ item = self.table.item(row, 0)
331
+ idx = item.data(Qt.ItemDataRole.UserRole) if item else row
332
+ if idx is not None and 0 <= idx < len(self.plugin_manager.plugins):
333
+ plugin: dict[str, Any] = cast(
334
+ dict[str, Any], self.plugin_manager.plugins[idx]
335
+ )
336
+ return plugin
337
+ return None
338
+
215
339
  def on_remove_plugin(self) -> None:
216
340
  """Delete the selected plugin file or folder and reload."""
217
341
  row = self.table.currentRow()
@@ -219,9 +343,8 @@ class PluginManagerWindow(QDialog):
219
343
  QMessageBox.warning(self, "Warning", "Please select a plugin to remove.")
220
344
  return
221
345
 
222
- # Assuming table row index matches plugins list index (confirmed in refresh_plugin_list)
223
- if row < len(self.plugin_manager.plugins):
224
- plugin = self.plugin_manager.plugins[row]
346
+ plugin = self._plugin_for_row(row)
347
+ if plugin is not None:
225
348
  filepath = plugin.get("filepath")
226
349
 
227
350
  if filepath and os.path.exists(filepath):
@@ -266,15 +389,15 @@ class PluginManagerWindow(QDialog):
266
389
  def show_plugin_details(self, item: QTableWidgetItem) -> None:
267
390
  """Show a message box with full metadata for the double-clicked plugin."""
268
391
  row = item.row()
269
- if row < len(self.plugin_manager.plugins):
270
- p = self.plugin_manager.plugins[row]
392
+ plugin = self._plugin_for_row(row)
393
+ if plugin is not None:
271
394
  msg = (
272
- f"Name: {p.get('name', 'Unknown')}\n"
273
- f"Version: {p.get('version', 'Unknown')}\n"
274
- f"Author: {p.get('author', 'Unknown')}\n"
275
- f"Status: {p.get('status', 'Unknown')}\n"
276
- f"Location: {p.get('filepath', 'Unknown')}\n\n"
277
- f"Description:\n{p.get('description', 'No description available.')}"
395
+ f"Name: {plugin.get('name', 'Unknown')}\n"
396
+ f"Version: {plugin.get('version', 'Unknown')}\n"
397
+ f"Author: {plugin.get('author', 'Unknown')}\n"
398
+ f"Status: {plugin.get('status', 'Unknown')}\n"
399
+ f"Location: {plugin.get('filepath', 'Unknown')}\n\n"
400
+ f"Description:\n{plugin.get('description', 'No description available.')}"
278
401
  )
279
402
  QMessageBox.information(self, "Plugin Details", msg)
280
403
 
@@ -302,7 +425,8 @@ class PluginManagerWindow(QDialog):
302
425
  is_folder = False
303
426
 
304
427
  if os.path.isfile(file_path):
305
- # Special handling: If user drops __init__.py, assume they want to install the package (folder)
428
+ # Special handling: If user drops __init__.py,
429
+ # assume they want to install the package (folder)
306
430
  if os.path.basename(file_path) == "__init__.py":
307
431
  file_path = os.path.dirname(file_path)
308
432
  is_valid = True