Extract search-and-open UI into OpenItemWidget (open_search.py), a plain QWidget with item_selected/cancelled signals. Silo_Open now adds this widget as an MDI subwindow instead of running a blocking QDialog, matching the Silo_New tab pattern. Improvements over the old dialog: - Non-blocking: multiple search tabs can be open simultaneously - 500 ms debounce on search input reduces API load - Filter checkbox changes trigger immediate re-search
192 lines
7.7 KiB
Python
192 lines
7.7 KiB
Python
"""Search-and-open widget for Kindred Create.
|
|
|
|
Provides :class:`OpenItemWidget`, a plain ``QWidget`` that can be
|
|
embedded in an MDI sub-window. Searches both the Silo database and
|
|
local CAD files, presenting results in a unified table. Emits
|
|
``item_selected`` when the user picks an item and ``cancelled`` when
|
|
the user clicks Cancel.
|
|
"""
|
|
|
|
import FreeCAD
|
|
from PySide import QtCore, QtWidgets
|
|
|
|
|
|
class OpenItemWidget(QtWidgets.QWidget):
|
|
"""Search-and-open widget for embedding in an MDI subwindow.
|
|
|
|
Parameters
|
|
----------
|
|
client : SiloClient
|
|
Authenticated Silo API client instance.
|
|
search_local_fn : callable
|
|
Function that accepts a search term string and returns an
|
|
iterable of dicts with keys ``part_number``, ``description``,
|
|
``path``, ``modified``.
|
|
parent : QWidget, optional
|
|
Parent widget.
|
|
|
|
Signals
|
|
-------
|
|
item_selected(dict)
|
|
Emitted when the user selects an item. The dict contains
|
|
keys: *part_number*, *description*, *item_type*, *source*
|
|
(``"database"``, ``"local"``, or ``"both"``), *modified*,
|
|
and *path* (str or ``None``).
|
|
cancelled()
|
|
Emitted when the user clicks Cancel.
|
|
"""
|
|
|
|
item_selected = QtCore.Signal(dict)
|
|
cancelled = QtCore.Signal()
|
|
|
|
def __init__(self, client, search_local_fn, parent=None):
|
|
super().__init__(parent)
|
|
self._client = client
|
|
self._search_local = search_local_fn
|
|
self._results_data = []
|
|
|
|
self.setMinimumWidth(700)
|
|
self.setMinimumHeight(500)
|
|
|
|
self._build_ui()
|
|
|
|
# Debounced search timer (500 ms)
|
|
self._search_timer = QtCore.QTimer(self)
|
|
self._search_timer.setSingleShot(True)
|
|
self._search_timer.setInterval(500)
|
|
self._search_timer.timeout.connect(self._do_search)
|
|
|
|
# Populate on first display
|
|
QtCore.QTimer.singleShot(0, self._do_search)
|
|
|
|
# ---- UI construction ---------------------------------------------------
|
|
|
|
def _build_ui(self):
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
layout.setSpacing(8)
|
|
|
|
# Search row
|
|
self._search_input = QtWidgets.QLineEdit()
|
|
self._search_input.setPlaceholderText("Search by part number or description...")
|
|
self._search_input.textChanged.connect(self._on_search_changed)
|
|
layout.addWidget(self._search_input)
|
|
|
|
# Filter checkboxes
|
|
filter_layout = QtWidgets.QHBoxLayout()
|
|
self._db_checkbox = QtWidgets.QCheckBox("Database")
|
|
self._db_checkbox.setChecked(True)
|
|
self._local_checkbox = QtWidgets.QCheckBox("Local Files")
|
|
self._local_checkbox.setChecked(True)
|
|
self._db_checkbox.toggled.connect(self._on_filter_changed)
|
|
self._local_checkbox.toggled.connect(self._on_filter_changed)
|
|
filter_layout.addWidget(self._db_checkbox)
|
|
filter_layout.addWidget(self._local_checkbox)
|
|
filter_layout.addStretch()
|
|
layout.addLayout(filter_layout)
|
|
|
|
# Results table
|
|
self._results_table = QtWidgets.QTableWidget()
|
|
self._results_table.setColumnCount(5)
|
|
self._results_table.setHorizontalHeaderLabels(
|
|
["Part Number", "Description", "Type", "Source", "Modified"]
|
|
)
|
|
self._results_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
|
self._results_table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
|
self._results_table.horizontalHeader().setStretchLastSection(True)
|
|
self._results_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
|
self._results_table.doubleClicked.connect(self._open_selected)
|
|
layout.addWidget(self._results_table, 1)
|
|
|
|
# Buttons
|
|
btn_layout = QtWidgets.QHBoxLayout()
|
|
btn_layout.addStretch()
|
|
open_btn = QtWidgets.QPushButton("Open")
|
|
open_btn.clicked.connect(self._open_selected)
|
|
cancel_btn = QtWidgets.QPushButton("Cancel")
|
|
cancel_btn.clicked.connect(self.cancelled.emit)
|
|
btn_layout.addWidget(open_btn)
|
|
btn_layout.addWidget(cancel_btn)
|
|
layout.addLayout(btn_layout)
|
|
|
|
# ---- Search logic ------------------------------------------------------
|
|
|
|
def _on_search_changed(self, _text):
|
|
"""Restart debounce timer on each keystroke."""
|
|
self._search_timer.start()
|
|
|
|
def _on_filter_changed(self, _checked):
|
|
"""Re-run search immediately when filter checkboxes change."""
|
|
self._do_search()
|
|
|
|
def _do_search(self):
|
|
"""Execute search against database and/or local files."""
|
|
search_term = self._search_input.text().strip()
|
|
self._results_data = []
|
|
|
|
if self._db_checkbox.isChecked():
|
|
try:
|
|
for item in self._client.list_items(search=search_term):
|
|
self._results_data.append(
|
|
{
|
|
"part_number": item.get("part_number", ""),
|
|
"description": item.get("description", ""),
|
|
"item_type": item.get("item_type", ""),
|
|
"source": "database",
|
|
"modified": item.get("updated_at", "")[:10]
|
|
if item.get("updated_at")
|
|
else "",
|
|
"path": None,
|
|
}
|
|
)
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(f"DB search failed: {e}\n")
|
|
|
|
if self._local_checkbox.isChecked():
|
|
try:
|
|
for item in self._search_local(search_term):
|
|
existing = next(
|
|
(r for r in self._results_data if r["part_number"] == item["part_number"]),
|
|
None,
|
|
)
|
|
if existing:
|
|
existing["source"] = "both"
|
|
existing["path"] = item.get("path")
|
|
else:
|
|
self._results_data.append(
|
|
{
|
|
"part_number": item.get("part_number", ""),
|
|
"description": item.get("description", ""),
|
|
"item_type": "",
|
|
"source": "local",
|
|
"modified": item.get("modified", "")[:10]
|
|
if item.get("modified")
|
|
else "",
|
|
"path": item.get("path"),
|
|
}
|
|
)
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(f"Local search failed: {e}\n")
|
|
|
|
self._populate_table()
|
|
|
|
def _populate_table(self):
|
|
"""Refresh the results table from ``_results_data``."""
|
|
self._results_table.setRowCount(len(self._results_data))
|
|
for row, data in enumerate(self._results_data):
|
|
self._results_table.setItem(row, 0, QtWidgets.QTableWidgetItem(data["part_number"]))
|
|
self._results_table.setItem(row, 1, QtWidgets.QTableWidgetItem(data["description"]))
|
|
self._results_table.setItem(row, 2, QtWidgets.QTableWidgetItem(data["item_type"]))
|
|
self._results_table.setItem(row, 3, QtWidgets.QTableWidgetItem(data["source"]))
|
|
self._results_table.setItem(row, 4, QtWidgets.QTableWidgetItem(data["modified"]))
|
|
self._results_table.resizeColumnsToContents()
|
|
|
|
# ---- Selection ---------------------------------------------------------
|
|
|
|
def _open_selected(self):
|
|
"""Emit ``item_selected`` with the data from the selected row."""
|
|
selected = self._results_table.selectedItems()
|
|
if not selected:
|
|
return
|
|
row = selected[0].row()
|
|
self.item_selected.emit(dict(self._results_data[row]))
|