Merge branch 'main' into feature/server-mode-ui

This commit is contained in:
2026-02-08 22:10:14 +00:00
3 changed files with 48 additions and 171 deletions

View File

@@ -35,28 +35,25 @@ class SiloWorkbench(FreeCADGui.Workbench):
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not register Silo origin: {e}\n")
self.toolbar_commands = [
"Silo_ToggleMode",
"Separator",
"Silo_Open",
"Silo_New",
"Silo_Save",
"Silo_Commit",
"Silo_Pull",
"Silo_Push",
# Silo menu provides admin/management commands.
# File operations (New/Open/Save) are handled by the standard File
# toolbar via the origin system -- no separate Silo toolbar needed.
self.menu_commands = [
"Silo_Info",
"Silo_BOM",
"Silo_TagProjects",
"Silo_SetStatus",
"Silo_Rollback",
"Separator",
"Silo_Settings",
"Silo_Auth",
]
self.appendToolbar("Silo", self.toolbar_commands)
self.appendMenu("Silo", self.toolbar_commands)
self.appendMenu("Silo", self.menu_commands)
def Activated(self):
"""Called when workbench is activated."""
FreeCAD.Console.PrintMessage("Kindred Silo workbench activated\n")
self._show_shortcut_recommendations()
def Deactivated(self):
pass
@@ -64,39 +61,6 @@ class SiloWorkbench(FreeCADGui.Workbench):
def GetClassName(self):
return "Gui::PythonWorkbench"
def _show_shortcut_recommendations(self):
"""Show keyboard shortcut recommendations dialog on first activation."""
try:
param_group = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/KindredSilo")
if param_group.GetBool("ShortcutsShown", False):
return
param_group.SetBool("ShortcutsShown", True)
from PySide import QtGui
msg = """<h3>Welcome to Kindred Silo!</h3>
<p>For the best experience, set up these keyboard shortcuts:</p>
<table style="margin: 10px 0;">
<tr><td><b>Ctrl+O</b></td><td> - </td><td>Silo_Open (Search & Open)</td></tr>
<tr><td><b>Ctrl+N</b></td><td> - </td><td>Silo_New (Register new item)</td></tr>
<tr><td><b>Ctrl+S</b></td><td> - </td><td>Silo_Save (Save & upload)</td></tr>
<tr><td><b>Ctrl+Shift+S</b></td><td> - </td><td>Silo_Commit (Save with comment)</td></tr>
</table>
<p><b>To set shortcuts:</b> Tools > Customize > Keyboard</p>
<p style="color: #888;">This message appears once.</p>"""
dialog = QtGui.QMessageBox()
dialog.setWindowTitle("Silo Keyboard Shortcuts")
dialog.setTextFormat(QtGui.Qt.RichText)
dialog.setText(msg)
dialog.setIcon(QtGui.QMessageBox.Information)
dialog.addButton("Set Up Now", QtGui.QMessageBox.AcceptRole)
dialog.addButton("Later", QtGui.QMessageBox.RejectRole)
if dialog.exec_() == 0:
FreeCADGui.runCommand("Std_DlgCustomize", 0)
except Exception as e:
FreeCAD.Console.PrintWarning("Silo shortcuts dialog: " + str(e) + "\n")
FreeCADGui.addWorkbench(SiloWorkbench())
FreeCAD.Console.PrintMessage("Silo workbench registered\n")

View File

@@ -106,6 +106,10 @@ def _get_auth_source() -> str:
return param.GetString("AuthSource", "")
def _get_auth_token() -> str:
return _fc_settings.get_api_token()
# Thin wrappers so command classes can call these without refactoring.
# They delegate to the settings adapter or the shared client.
@@ -881,9 +885,6 @@ class Silo_Save:
# Check if document has unsaved changes
gui_doc = FreeCADGui.getDocument(doc.Name)
is_modified = gui_doc.Modified if gui_doc else True
FreeCAD.Console.PrintMessage(
f"[DEBUG] Modified={is_modified}, FileName={doc.FileName}\n"
)
if gui_doc and not is_modified and doc.FileName:
FreeCAD.Console.PrintMessage("No changes to save.\n")
@@ -891,17 +892,9 @@ class Silo_Save:
# Collect properties BEFORE saving to avoid dirtying the document
# (accessing Shape properties can trigger recompute)
FreeCAD.Console.PrintMessage("[DEBUG] Collecting properties...\n")
properties = collect_document_properties(doc)
# Check modified state after collecting properties
is_modified_after_props = gui_doc.Modified if gui_doc else True
FreeCAD.Console.PrintMessage(
f"[DEBUG] After collect_properties: Modified={is_modified_after_props}\n"
)
# Save locally
FreeCAD.Console.PrintMessage("[DEBUG] Saving to canonical path...\n")
file_path = _sync.save_to_canonical_path(doc, force_rename=True)
if not file_path:
# Fallback to regular save if canonical path fails
@@ -912,24 +905,12 @@ class Silo_Save:
FreeCAD.Console.PrintError("Could not determine save path\n")
return
# Check modified state after save
is_modified_after_save = gui_doc.Modified if gui_doc else True
FreeCAD.Console.PrintMessage(
f"[DEBUG] After save: Modified={is_modified_after_save}\n"
)
# Force clear modified flag if save succeeded (needed for assemblies)
if is_modified_after_save and gui_doc:
FreeCAD.Console.PrintMessage(
"[DEBUG] Attempting to clear Modified flag...\n"
)
if gui_doc and gui_doc.Modified:
try:
gui_doc.Modified = False
FreeCAD.Console.PrintMessage(
f"[DEBUG] After force clear: Modified={gui_doc.Modified}\n"
)
except Exception as e:
FreeCAD.Console.PrintMessage(f"[DEBUG] Could not clear Modified: {e}\n")
except Exception:
pass
FreeCAD.Console.PrintMessage(f"Saved: {file_path}\n")
@@ -942,12 +923,6 @@ class Silo_Save:
new_rev = result["revision_number"]
FreeCAD.Console.PrintMessage(f"Uploaded as revision {new_rev}\n")
# Check modified state after upload
is_modified_after_upload = gui_doc.Modified if gui_doc else True
FreeCAD.Console.PrintMessage(
f"[DEBUG] After upload: Modified={is_modified_after_upload}\n"
)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Upload failed: {e}\n")
FreeCAD.Console.PrintMessage("File saved locally but not uploaded.\n")
@@ -2374,85 +2349,6 @@ class Silo_BOM:
return FreeCAD.ActiveDocument is not None
# ---------------------------------------------------------------------------
# Silo Mode toggle - swap Ctrl+O/S/N between standard and Silo commands
# ---------------------------------------------------------------------------
# Stored original shortcuts so they can be restored on toggle-off
_original_shortcuts: Dict[str, Any] = {}
def _swap_shortcuts(mapping, enable_silo):
"""Swap keyboard shortcuts between standard and Silo commands.
mapping: list of (std_cmd, silo_cmd, shortcut) tuples
enable_silo: True to assign shortcuts to Silo commands, False to restore.
"""
from PySide import QtGui
mw = FreeCADGui.getMainWindow()
if mw is None:
return
for std_cmd, silo_cmd, shortcut in mapping:
if enable_silo:
# Save and clear the standard command's shortcut
std_action = mw.findChild(QtGui.QAction, std_cmd)
if std_action:
_original_shortcuts[std_cmd] = std_action.shortcut().toString()
std_action.setShortcut("")
# Assign the shortcut to the Silo command
silo_action = mw.findChild(QtGui.QAction, silo_cmd)
if silo_action:
silo_action.setShortcut(shortcut)
else:
# Clear the Silo command's shortcut
silo_action = mw.findChild(QtGui.QAction, silo_cmd)
if silo_action:
silo_action.setShortcut("")
# Restore the standard command's original shortcut
std_action = mw.findChild(QtGui.QAction, std_cmd)
if std_action and std_cmd in _original_shortcuts:
std_action.setShortcut(_original_shortcuts.pop(std_cmd))
_SHORTCUT_MAP = [
("Std_Open", "Silo_Open", "Ctrl+O"),
("Std_Save", "Silo_Save", "Ctrl+S"),
("Std_New", "Silo_New", "Ctrl+N"),
]
class Silo_ToggleMode:
"""Toggle between standard file operations and Silo equivalents."""
def GetResources(self):
return {
"MenuText": "Silo Mode",
"ToolTip": (
"Toggle between standard file operations and Silo equivalents.\n"
"When ON: Ctrl+O/S/N use Silo Open/Save/New.\n"
"When OFF: Standard FreeCAD file operations."
),
"Pixmap": _icon("silo"),
"Checkable": True,
}
def Activated(self, checked):
param = FreeCAD.ParamGet(_PREF_GROUP)
if checked:
_swap_shortcuts(_SHORTCUT_MAP, enable_silo=True)
param.SetBool("SiloMode", True)
FreeCAD.Console.PrintMessage("Silo mode enabled\n")
else:
_swap_shortcuts(_SHORTCUT_MAP, enable_silo=False)
param.SetBool("SiloMode", False)
FreeCAD.Console.PrintMessage("Silo mode disabled\n")
def IsActive(self):
return True
# ---------------------------------------------------------------------------
# SSE live-update listener
# ---------------------------------------------------------------------------
@@ -2722,9 +2618,9 @@ class SiloAuthDockWidget:
self._url_label.setText(_get_api_url())
has_token = _client.is_authenticated()
username = _client.auth_username()
role = _client.auth_role()
source = _client.auth_source()
username = _get_auth_username()
role = _get_auth_role()
source = _get_auth_source()
# Check server connectivity
try:
@@ -3057,5 +2953,5 @@ FreeCADGui.addCommand("Silo_TagProjects", Silo_TagProjects())
FreeCADGui.addCommand("Silo_Rollback", Silo_Rollback())
FreeCADGui.addCommand("Silo_SetStatus", Silo_SetStatus())
FreeCADGui.addCommand("Silo_Settings", Silo_Settings())
FreeCADGui.addCommand("Silo_ToggleMode", Silo_ToggleMode())
FreeCADGui.addCommand("Silo_Auth", Silo_Auth())

View File

@@ -11,14 +11,13 @@ providing the standardized origin interface.
import FreeCAD
import FreeCADGui
from .silo_commands import (
from silo_commands import (
_client,
_sync,
collect_document_properties,
find_file_by_part_number,
get_tracked_object,
set_silo_properties,
find_file_by_part_number,
collect_document_properties,
)
@@ -126,19 +125,35 @@ class SiloOrigin:
def connect(self) -> bool:
"""Trigger authentication if needed.
Shows the Silo authentication dialog if not already authenticated.
Shows the Silo login dialog if not already authenticated.
Returns:
True if authenticated after this call
"""
if _client.is_authenticated():
return True
try:
ok, _ = _client.check_connection()
if ok:
return True
except Exception:
pass
# Show auth dialog via existing Silo_Auth command
# Show login dialog directly
try:
cmd = FreeCADGui.Command.get("Silo_Auth")
if cmd:
cmd.Activated()
from PySide import QtWidgets
mw = FreeCADGui.getMainWindow()
if mw is None:
return False
# Find or create the auth dock widget and trigger its login dialog
panel = mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseAuth")
if panel and hasattr(panel, "_auth"):
panel._auth._show_login_dialog()
else:
# Fallback: run the Settings command so the user can configure
FreeCADGui.runCommand("Silo_Settings")
return _client.is_authenticated()
except Exception as e:
FreeCAD.Console.PrintError(f"Silo connect failed: {e}\n")
@@ -379,7 +394,9 @@ class SiloOrigin:
# Upload to Silo
properties = collect_document_properties(doc)
_client._upload_file(obj.SiloPartNumber, str(file_path), properties, comment="")
_client._upload_file(
obj.SiloPartNumber, str(file_path), properties, comment=""
)
# Clear modified flag
doc.Modified = False