MoleditPy-linux 4.10.1__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.1 → moleditpy_linux-4.10.2}/PKG-INFO +1 -1
  2. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/pyproject.toml +1 -1
  3. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
  4. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/plugins/plugin_manager_window.py +177 -67
  5. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/LICENSE +0 -0
  6. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/README.md +0 -0
  7. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/setup.cfg +0 -0
  8. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  9. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  10. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  11. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  12. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  13. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/__init__.py +0 -0
  14. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/__main__.py +0 -0
  15. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  16. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/assets/icon.icns +0 -0
  17. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/assets/icon.ico +0 -0
  18. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/assets/icon.png +0 -0
  19. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/core/__init__.py +0 -0
  20. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  21. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/core/molecular_data.py +0 -0
  22. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/main.py +0 -0
  23. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/plugins/__init__.py +0 -0
  24. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
  25. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
  26. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/__init__.py +0 -0
  27. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  28. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
  29. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  30. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  31. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
  32. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/app_state.py +0 -0
  33. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/atom_item.py +0 -0
  34. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/atom_picking.py +0 -0
  35. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  36. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/bond_item.py +0 -0
  37. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
  38. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
  39. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/chain_mixin.py +0 -0
  40. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  41. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/compute_logic.py +0 -0
  42. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
  43. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
  44. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  45. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
  46. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
  47. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
  48. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  49. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
  50. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/export_logic.py +0 -0
  51. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  52. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/io_logic.py +0 -0
  53. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/main_window.py +0 -0
  54. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/main_window_init.py +0 -0
  55. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  56. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
  57. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
  58. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
  59. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
  60. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  61. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
  62. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
  63. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/preview_molecule.py +0 -0
  64. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  65. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  66. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
  67. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
  68. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  69. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
  70. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  71. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/string_importers.py +0 -0
  72. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  73. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  74. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
  75. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/ui_manager.py +0 -0
  76. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
  77. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
  78. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  79. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/__init__.py +0 -0
  80. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/constants.py +0 -0
  81. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/default_settings.py +0 -0
  82. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/hit_radius.py +0 -0
  83. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  84. {moleditpy_linux-4.10.1 → moleditpy_linux-4.10.2}/src/moleditpy_linux/utils/suppress_log.py +0 -0
  85. {moleditpy_linux-4.10.1 → 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.1
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.1"
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.1
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,7 +13,7 @@ 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
@@ -25,6 +25,7 @@ from PyQt6.QtWidgets import (
25
25
  QHBoxLayout,
26
26
  QHeaderView,
27
27
  QLabel,
28
+ QLineEdit,
28
29
  QMessageBox,
29
30
  QPushButton,
30
31
  QTableWidget,
@@ -42,42 +43,68 @@ class PluginManagerWindow(QDialog):
42
43
  super().__init__(parent)
43
44
  self.btn_remove: Any = None
44
45
  self.table: Any = None
46
+ self.search_input: Any = None
45
47
  self.plugin_manager = plugin_manager
46
48
  self._preferences_applied = False
47
49
  self.setWindowTitle("Plugin Manager")
48
- self.resize(800, 500)
50
+ self.resize(850, 520)
49
51
  self.setAcceptDrops(True) # Enable drag & drop for the whole window
50
52
 
51
53
  self.init_ui()
52
54
  self.refresh_plugin_list()
53
55
 
54
56
  def init_ui(self) -> None:
55
- """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."""
56
58
  layout = QVBoxLayout(self)
57
59
 
58
60
  lbl_info = QLabel(
59
- "Drag & Drop .py or .zip files here to install plugins. Use the checkbox to enable/disable plugins."
61
+ "Drag & Drop .py or .zip files to install plugins. Use checkboxes to enable/disable."
60
62
  )
61
63
  lbl_info.setStyleSheet("color: gray; font-style: italic;")
62
64
  layout.addWidget(lbl_info)
63
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
+
64
79
  # Plugin Table
65
80
  self.table = QTableWidget()
66
- self.table.setColumnCount(6)
81
+ self.table.setColumnCount(7)
67
82
  self.table.setHorizontalHeaderLabels(
68
- ["Status", "Name", "Version", "Author", "Location", "Description"]
83
+ [
84
+ "Enabled",
85
+ "Status",
86
+ "Name",
87
+ "Version",
88
+ "Author",
89
+ "Location",
90
+ "Description",
91
+ ]
69
92
  )
70
93
  self.table.horizontalHeader().setSectionResizeMode(
71
94
  QHeaderView.ResizeMode.Interactive
72
95
  )
73
96
  self.table.horizontalHeader().setSectionResizeMode(
74
- 4, QHeaderView.ResizeMode.Interactive
97
+ 0, QHeaderView.ResizeMode.ResizeToContents
98
+ )
99
+ self.table.horizontalHeader().setSectionResizeMode(
100
+ 5, QHeaderView.ResizeMode.Interactive
75
101
  )
76
102
  self.table.horizontalHeader().setSectionResizeMode(
77
- 5, QHeaderView.ResizeMode.Stretch
103
+ 6, QHeaderView.ResizeMode.Stretch
78
104
  ) # Description stretches
79
- self.table.setColumnWidth(1, 200) # Make Name column wider
80
- self.table.setColumnWidth(0, 120) # Keep the status checkbox on one line
105
+ self.table.setColumnWidth(0, 70) # Enabled checkbox
106
+ self.table.setColumnWidth(1, 100) # Status
107
+ self.table.setColumnWidth(2, 180) # Name
81
108
  self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
82
109
  self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
83
110
  self.table.itemSelectionChanged.connect(self.update_button_state)
@@ -115,6 +142,20 @@ class PluginManagerWindow(QDialog):
115
142
 
116
143
  layout.addLayout(btn_layout)
117
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
+
118
159
  def refresh_plugin_list(self) -> None:
119
160
  """Repopulate the plugin table from the current plugin registry."""
120
161
  self.table.setRowCount(0)
@@ -122,22 +163,36 @@ class PluginManagerWindow(QDialog):
122
163
 
123
164
  self.table.setRowCount(len(plugins))
124
165
  for row, p in enumerate(plugins):
125
- # Keep an empty item for row selection; the checkbox owns visible status text.
126
- status_item = QTableWidgetItem()
127
- self.table.setItem(row, 0, status_item)
128
-
129
- # Use an explicit widget so macOS keeps the checkbox and status text
130
- # in a stable horizontal layout instead of stacking the item indicator.
131
- status_checkbox = QCheckBox(str(p.get("status", "Unknown")))
132
- status_checkbox.setChecked(not p.get("disabled", False))
133
- status_checkbox.setToolTip("Enable or disable this plugin")
134
- status_checkbox.setStyleSheet("QCheckBox { padding-left: 4px; }")
135
- self.table.setCellWidget(row, 0, status_checkbox)
136
- self.table.setItem(row, 1, QTableWidgetItem(str(p.get("name", "Unknown"))))
137
- self.table.setItem(row, 2, QTableWidgetItem(str(p.get("version", ""))))
138
- self.table.setItem(row, 3, QTableWidgetItem(str(p.get("author", ""))))
139
-
140
- # Location (Relative Path)
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)
179
+ status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
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)
187
+
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)
141
196
  full_path = p.get("filepath", "")
142
197
  rel_path = ""
143
198
  if full_path:
@@ -147,43 +202,85 @@ class PluginManagerWindow(QDialog):
147
202
  )
148
203
  except (AttributeError, RuntimeError, ValueError, TypeError):
149
204
  rel_path = os.path.basename(full_path)
150
- self.table.setItem(row, 4, QTableWidgetItem(str(rel_path)))
151
-
152
- self.table.setItem(row, 5, QTableWidgetItem(str(p.get("description", ""))))
153
-
154
- # Simple color coding for status
155
- status = str(p.get("status", ""))
156
- color = None
157
- checkbox_color = None
158
- if status.startswith("Error"):
159
- color = Qt.GlobalColor.red
160
- checkbox_color = "red"
161
- elif status == "Loaded":
162
- color = Qt.GlobalColor.darkGreen
163
- checkbox_color = "darkgreen"
164
- elif status in ("No Entry Point", "Disabled"):
165
- color = Qt.GlobalColor.gray
166
- checkbox_color = "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
+ )
167
247
 
168
- if color:
169
- status_checkbox.setStyleSheet(
170
- f"QCheckBox {{ padding-left: 4px; color: {checkbox_color}; }}"
171
- )
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)
172
256
 
173
257
  def _status_checkbox(self, row: int) -> Optional[QCheckBox]:
174
258
  """Return the visible status checkbox for a plugin table row."""
175
- checkbox = self.table.cellWidget(row, 0)
176
- return checkbox if isinstance(checkbox, QCheckBox) else None
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
177
266
 
178
267
  def _save_checkbox_preferences(self) -> set[str]:
179
268
  """Collect and persist checkbox choices."""
180
269
  disabled_paths = set()
181
- for row, plugin in enumerate(self.plugin_manager.plugins):
182
- status_checkbox = self._status_checkbox(row)
183
- if status_checkbox is not None and not status_checkbox.isChecked():
184
- filepath = plugin.get("filepath")
185
- if filepath:
186
- disabled_paths.add(self.plugin_manager.plugin_path_key(filepath))
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
275
+ ):
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
+ )
187
284
 
188
285
  self.plugin_manager.save_disabled_plugins(disabled_paths)
189
286
  return disabled_paths
@@ -226,6 +323,19 @@ class PluginManagerWindow(QDialog):
226
323
  self.plugin_manager.discover_plugins()
227
324
  self.refresh_plugin_list()
228
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
+
229
339
  def on_remove_plugin(self) -> None:
230
340
  """Delete the selected plugin file or folder and reload."""
231
341
  row = self.table.currentRow()
@@ -233,9 +343,8 @@ class PluginManagerWindow(QDialog):
233
343
  QMessageBox.warning(self, "Warning", "Please select a plugin to remove.")
234
344
  return
235
345
 
236
- # Assuming table row index matches plugins list index (confirmed in refresh_plugin_list)
237
- if row < len(self.plugin_manager.plugins):
238
- plugin = self.plugin_manager.plugins[row]
346
+ plugin = self._plugin_for_row(row)
347
+ if plugin is not None:
239
348
  filepath = plugin.get("filepath")
240
349
 
241
350
  if filepath and os.path.exists(filepath):
@@ -280,15 +389,15 @@ class PluginManagerWindow(QDialog):
280
389
  def show_plugin_details(self, item: QTableWidgetItem) -> None:
281
390
  """Show a message box with full metadata for the double-clicked plugin."""
282
391
  row = item.row()
283
- if row < len(self.plugin_manager.plugins):
284
- p = self.plugin_manager.plugins[row]
392
+ plugin = self._plugin_for_row(row)
393
+ if plugin is not None:
285
394
  msg = (
286
- f"Name: {p.get('name', 'Unknown')}\n"
287
- f"Version: {p.get('version', 'Unknown')}\n"
288
- f"Author: {p.get('author', 'Unknown')}\n"
289
- f"Status: {p.get('status', 'Unknown')}\n"
290
- f"Location: {p.get('filepath', 'Unknown')}\n\n"
291
- 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.')}"
292
401
  )
293
402
  QMessageBox.information(self, "Plugin Details", msg)
294
403
 
@@ -316,7 +425,8 @@ class PluginManagerWindow(QDialog):
316
425
  is_folder = False
317
426
 
318
427
  if os.path.isfile(file_path):
319
- # 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)
320
430
  if os.path.basename(file_path) == "__init__.py":
321
431
  file_path = os.path.dirname(file_path)
322
432
  is_valid = True