feat: add silo-aware start panel #10
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user