Files
silo-mod/freecad/open_search.py
forbes-0023 be8783bf0a feat(open): replace modal open dialog with MDI tab
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
2026-02-12 10:22:42 -06:00

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]))