diff --git a/freecad/InitGui.py b/freecad/InitGui.py index f07896d..db03bd6 100644 --- a/freecad/InitGui.py +++ b/freecad/InitGui.py @@ -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 diff --git a/freecad/silo_commands.py b/freecad/silo_commands.py index ac89c72..aad817c 100644 --- a/freecad/silo_commands.py +++ b/freecad/silo_commands.py @@ -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())