Merge pull request 'feat: add silo-aware start panel' (#10) from feature/start-panel into main

Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
2026-02-08 22:11:55 +00:00
2 changed files with 254 additions and 0 deletions

View File

@@ -47,6 +47,7 @@ class SiloWorkbench(FreeCADGui.Workbench):
"Separator",
"Silo_Settings",
"Silo_Auth",
"Silo_StartPanel",
]
self.appendMenu("Silo", self.menu_commands)
@@ -54,6 +55,7 @@ class SiloWorkbench(FreeCADGui.Workbench):
def Activated(self):
"""Called when workbench is activated."""
FreeCAD.Console.PrintMessage("Kindred Silo workbench activated\n")
FreeCADGui.runCommand("Silo_StartPanel", 0)
def Deactivated(self):
pass

View File

@@ -2940,6 +2940,257 @@ class Silo_Auth:
return True
# ---------------------------------------------------------------------------
# Start panel
# ---------------------------------------------------------------------------
class SiloStartPanel:
"""Content widget for the Silo Start Panel dock."""
def __init__(self):
from PySide import QtCore, QtGui
self.widget = QtGui.QWidget()
self._build_ui()
self._refresh()
self._timer = QtCore.QTimer(self.widget)
self._timer.timeout.connect(self._refresh)
self._timer.start(60000) # Refresh every 60 seconds
def _build_ui(self):
from PySide import QtCore, QtGui
layout = QtGui.QVBoxLayout(self.widget)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(6)
# Connection status badge
status_row = QtGui.QHBoxLayout()
status_row.setSpacing(6)
self._conn_dot = QtGui.QLabel("\u2b24")
self._conn_dot.setFixedWidth(16)
self._conn_dot.setAlignment(QtCore.Qt.AlignCenter)
self._conn_label = QtGui.QLabel("Checking...")
self._conn_label.setStyleSheet("font-weight: bold;")
status_row.addWidget(self._conn_dot)
status_row.addWidget(self._conn_label)
status_row.addStretch()
layout.addLayout(status_row)
layout.addSpacing(8)
# My Checkouts section
checkout_header = QtGui.QLabel("My Checkouts")
checkout_header.setStyleSheet("font-weight: bold; font-size: 12px;")
layout.addWidget(checkout_header)
self._checkout_list = QtGui.QListWidget()
self._checkout_list.setMaximumHeight(160)
self._checkout_list.setAlternatingRowColors(True)
self._checkout_list.itemDoubleClicked.connect(self._on_checkout_clicked)
layout.addWidget(self._checkout_list)
layout.addSpacing(8)
# Recent Silo Activity section
activity_header = QtGui.QLabel("Recent Silo Activity")
activity_header.setStyleSheet("font-weight: bold; font-size: 12px;")
layout.addWidget(activity_header)
self._activity_list = QtGui.QListWidget()
self._activity_list.setMaximumHeight(200)
self._activity_list.setAlternatingRowColors(True)
self._activity_list.itemDoubleClicked.connect(self._on_activity_clicked)
layout.addWidget(self._activity_list)
layout.addSpacing(8)
# Local Recent Files section
local_header = QtGui.QLabel("Local Recent Files")
local_header.setStyleSheet("font-weight: bold; font-size: 12px;")
layout.addWidget(local_header)
self._local_list = QtGui.QListWidget()
self._local_list.setMaximumHeight(160)
self._local_list.setAlternatingRowColors(True)
self._local_list.itemDoubleClicked.connect(self._on_local_clicked)
layout.addWidget(self._local_list)
layout.addStretch()
def _refresh(self):
self._refresh_connection()
self._refresh_checkouts()
self._refresh_activity()
self._refresh_local()
def _refresh_connection(self):
try:
reachable, _ = _client.check_connection()
except Exception:
reachable = False
if reachable:
api_url = _get_api_url()
parsed = urllib.parse.urlparse(api_url)
hostname = parsed.hostname or api_url
self._conn_dot.setStyleSheet("color: #4CAF50; font-size: 10px;")
self._conn_label.setText(f"Connected to {hostname}")
else:
self._conn_dot.setStyleSheet("color: #888; font-size: 10px;")
self._conn_label.setText("Disconnected")
def _refresh_checkouts(self):
self._checkout_list.clear()
try:
local_files = search_local_files()
for item in local_files[:15]:
pn = item.get("part_number", "")
desc = item.get("description", "")
modified = (item.get("modified") or "")[:10]
label = f"{pn} {desc}"
if modified:
label += f" ({modified})"
list_item = self._checkout_list.addItem(label)
except Exception:
self._checkout_list.addItem("(Unable to scan local files)")
if self._checkout_list.count() == 0:
self._checkout_list.addItem("(No local checkouts)")
def _refresh_activity(self):
self._activity_list.clear()
try:
reachable, _ = _client.check_connection()
except Exception:
reachable = False
if not reachable:
self._activity_list.addItem("(Not connected)")
return
try:
items = _client.list_items()
if isinstance(items, list):
# Collect local part numbers for badge comparison
local_pns = set()
try:
for lf in search_local_files():
local_pns.add(lf.get("part_number", ""))
except Exception:
pass
for item in items[:10]:
pn = item.get("part_number", "")
desc = item.get("description", "")
updated = (item.get("updated_at") or "")[:10]
badge = "\u2713 " if pn in local_pns else ""
label = f"{badge}{pn} {desc}"
if updated:
label += f" ({updated})"
self._activity_list.addItem(label)
if self._activity_list.count() == 0:
self._activity_list.addItem("(No items in database)")
except Exception:
self._activity_list.addItem("(Unable to fetch activity)")
def _refresh_local(self):
from PySide import QtGui
self._local_list.clear()
try:
param = FreeCAD.ParamGet("User parameter:BaseApp/RecentFiles")
count = param.GetInt("RecentFiles", 0)
for i in range(min(count, 10)):
path = param.GetString(f"MRU{i}", "")
if path:
name = Path(path).name
item = QtGui.QListWidgetItem(name)
item.setToolTip(path)
item.setData(256, path) # Qt.UserRole = 256
self._local_list.addItem(item)
except Exception:
pass
if self._local_list.count() == 0:
self._local_list.addItem("(No recent files)")
def _on_checkout_clicked(self, item):
"""Open a local checkout file."""
text = item.text()
if text.startswith("("):
return
pn = text.split()[0] if text else ""
if not pn:
return
local_path = find_file_by_part_number(pn)
if local_path and local_path.exists():
FreeCAD.openDocument(str(local_path))
def _on_activity_clicked(self, item):
"""Open/checkout a remote item."""
text = item.text()
if text.startswith("("):
return
# Strip badge if present
text = text.lstrip("\u2713 ")
pn = text.split()[0] if text else ""
if not pn:
return
local_path = find_file_by_part_number(pn)
if local_path and local_path.exists():
FreeCAD.openDocument(str(local_path))
else:
_sync.open_item(pn)
def _on_local_clicked(self, item):
"""Open a local recent file."""
path = item.data(256) # Qt.UserRole
if path and Path(path).exists():
FreeCAD.openDocument(path)
class Silo_StartPanel:
"""Show the Silo Start Panel."""
def GetResources(self):
return {
"MenuText": "Start Panel",
"ToolTip": "Show Silo start panel with checkouts and recent activity",
"Pixmap": _icon("open"),
}
def Activated(self):
from PySide import QtCore, QtGui
mw = FreeCADGui.getMainWindow()
if mw is None:
return
# Reuse existing panel if it exists
panel = mw.findChild(QtGui.QDockWidget, "SiloStartPanel")
if panel:
panel.show()
panel.raise_()
return
# Create new dock widget
content = SiloStartPanel()
dock = QtGui.QDockWidget("Silo", mw)
dock.setObjectName("SiloStartPanel")
dock.setWidget(content.widget)
dock.setAllowedAreas(
QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea
)
mw.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock)
def IsActive(self):
return True
# Register commands
FreeCADGui.addCommand("Silo_Open", Silo_Open())
FreeCADGui.addCommand("Silo_New", Silo_New())
@@ -2955,3 +3206,4 @@ FreeCADGui.addCommand("Silo_SetStatus", Silo_SetStatus())
FreeCADGui.addCommand("Silo_Settings", Silo_Settings())
FreeCADGui.addCommand("Silo_Auth", Silo_Auth())
FreeCADGui.addCommand("Silo_StartPanel", Silo_StartPanel())