feat(workbench): fix icon loading and add settings dialog

Replace hardcoded FreeCAD addon path searches with __file__-relative
resolution for icons in both InitGui.py and silo_commands.py. Icons now
load correctly regardless of install location.

Add Silo_Settings command with URL and SSL verification fields. Settings
persist via FreeCAD preferences and take priority over env vars. Wire
SSL context into all SiloClient HTTP methods.
This commit is contained in:
forbes
2026-01-29 19:28:11 -06:00
parent 53b5edb25f
commit f08ecc14ea
2 changed files with 153 additions and 48 deletions

View File

@@ -16,24 +16,12 @@ class SiloWorkbench(FreeCADGui.Workbench):
Icon = ""
def __init__(self):
# Find icon path
try:
locations = [
os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "Silo"),
os.path.join(FreeCAD.getResourceDir(), "Mod", "Silo"),
os.path.expanduser(
"~/.var/app/org.freecad.FreeCAD/data/FreeCAD/Mod/Silo"
),
os.path.expanduser("~/.FreeCAD/Mod/Silo"),
]
for silo_dir in locations:
icon_path = os.path.join(silo_dir, "resources", "icons", "silo.svg")
if os.path.exists(icon_path):
self.__class__.Icon = icon_path
FreeCAD.Console.PrintMessage("Silo icon: " + icon_path + "\n")
break
except Exception as e:
FreeCAD.Console.PrintWarning("Silo icon error: " + str(e) + "\n")
# Resolve icon relative to this file so it works regardless of install location
icon_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "resources", "icons", "silo.svg"
)
if os.path.exists(icon_path):
self.__class__.Icon = icon_path
def Initialize(self):
"""Called when workbench is first activated."""
@@ -47,6 +35,7 @@ class SiloWorkbench(FreeCADGui.Workbench):
"Silo_Pull",
"Silo_Push",
"Silo_Info",
"Silo_Settings",
]
self.appendToolbar("Silo", self.toolbar_commands)
@@ -55,12 +44,6 @@ class SiloWorkbench(FreeCADGui.Workbench):
def Activated(self):
"""Called when workbench is activated."""
FreeCAD.Console.PrintMessage("Kindred Silo workbench activated\n")
FreeCAD.Console.PrintMessage(
" API: SILO_API_URL (default: http://localhost:8080/api)\n"
)
FreeCAD.Console.PrintMessage(
" Projects: SILO_PROJECTS_DIR (default: ~/projects)\n"
)
self._show_shortcut_recommendations()
def Deactivated(self):

View File

@@ -3,6 +3,7 @@
import json
import os
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
@@ -12,12 +13,41 @@ from typing import Any, Dict, List, Optional, Tuple
import FreeCAD
import FreeCADGui
# Configuration
SILO_API_URL = os.environ.get("SILO_API_URL", "http://localhost:8080/api")
# Preference group for Kindred Silo settings
_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo"
# Configuration - preferences take priority over env vars
SILO_PROJECTS_DIR = os.environ.get(
"SILO_PROJECTS_DIR", os.path.expanduser("~/projects")
)
def _get_api_url() -> str:
"""Get Silo API URL from preferences, falling back to env var then default."""
param = FreeCAD.ParamGet(_PREF_GROUP)
url = param.GetString("ApiUrl", "")
if url:
return url
return os.environ.get("SILO_API_URL", "http://localhost:8080/api")
def _get_ssl_verify() -> bool:
"""Get SSL verification setting from preferences."""
param = FreeCAD.ParamGet(_PREF_GROUP)
return param.GetBool("SslVerify", True)
def _get_ssl_context() -> ssl.SSLContext:
"""Build an SSL context based on the current SSL verification preference."""
if _get_ssl_verify():
return ssl.create_default_context()
else:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
# Category name mapping for folder structure
# Format: CCC -> "descriptive_name"
CATEGORY_NAMES = {
@@ -203,23 +233,10 @@ CATEGORY_NAMES = {
}
# Icon directory
def _get_icon_dir():
"""Get the icons directory path."""
locations = [
os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "Silo", "resources", "icons"),
os.path.expanduser(
"~/.var/app/org.freecad.FreeCAD/data/FreeCAD/Mod/Silo/resources/icons"
),
os.path.expanduser("~/.FreeCAD/Mod/Silo/resources/icons"),
]
for loc in locations:
if os.path.isdir(loc):
return loc
return ""
_ICON_DIR = _get_icon_dir()
# Icon directory - resolve relative to this file so it works regardless of install location
_ICON_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "resources", "icons"
)
def _icon(name):
@@ -241,8 +258,14 @@ def get_projects_dir() -> Path:
class SiloClient:
"""HTTP client for Silo API."""
def __init__(self, base_url: str = SILO_API_URL):
self.base_url = base_url.rstrip("/")
def __init__(self, base_url: str = None):
self._explicit_url = base_url
@property
def base_url(self) -> str:
if self._explicit_url:
return self._explicit_url.rstrip("/")
return _get_api_url().rstrip("/")
def _request(
self, method: str, path: str, data: Optional[Dict] = None
@@ -254,7 +277,7 @@ class SiloClient:
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as resp:
with urllib.request.urlopen(req, context=_get_ssl_context()) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
error_body = e.read().decode()
@@ -268,7 +291,7 @@ class SiloClient:
req = urllib.request.Request(url, method="GET")
try:
with urllib.request.urlopen(req) as resp:
with urllib.request.urlopen(req, context=_get_ssl_context()) as resp:
with open(dest_path, "wb") as f:
while True:
chunk = resp.read(8192)
@@ -330,7 +353,7 @@ class SiloClient:
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req) as resp:
with urllib.request.urlopen(req, context=_get_ssl_context()) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
raise RuntimeError(f"Upload error {e.code}: {e.read().decode()}")
@@ -1785,6 +1808,104 @@ class Silo_SetStatus:
return FreeCAD.ActiveDocument is not None
class Silo_Settings:
"""Configure Silo connection settings."""
def GetResources(self):
return {
"MenuText": "Settings",
"ToolTip": "Configure Silo API URL and SSL settings",
"Pixmap": _icon("info"),
}
def Activated(self):
from PySide import QtCore, QtGui
param = FreeCAD.ParamGet(_PREF_GROUP)
dialog = QtGui.QDialog()
dialog.setWindowTitle("Silo Settings")
dialog.setMinimumWidth(450)
layout = QtGui.QVBoxLayout(dialog)
# URL
url_label = QtGui.QLabel("Silo API URL:")
layout.addWidget(url_label)
url_input = QtGui.QLineEdit()
url_input.setPlaceholderText("http://localhost:8080/api")
current_url = param.GetString("ApiUrl", "")
if current_url:
url_input.setText(current_url)
else:
env_url = os.environ.get("SILO_API_URL", "")
if env_url:
url_input.setText(env_url)
layout.addWidget(url_input)
url_hint = QtGui.QLabel(
"Leave empty to use SILO_API_URL environment variable "
"or default (http://localhost:8080/api)"
)
url_hint.setWordWrap(True)
url_hint.setStyleSheet("color: #888; font-size: 11px;")
layout.addWidget(url_hint)
layout.addSpacing(10)
# SSL
ssl_checkbox = QtGui.QCheckBox("Verify SSL certificates")
ssl_checkbox.setChecked(param.GetBool("SslVerify", True))
layout.addWidget(ssl_checkbox)
ssl_hint = QtGui.QLabel(
"Disable only for internal servers with self-signed certificates."
)
ssl_hint.setWordWrap(True)
ssl_hint.setStyleSheet("color: #888; font-size: 11px;")
layout.addWidget(ssl_hint)
layout.addSpacing(10)
# Current effective values (read-only)
status_label = QtGui.QLabel(
f"<b>Active URL:</b> {_get_api_url()}<br>"
f"<b>SSL verification:</b> {'enabled' if _get_ssl_verify() else 'disabled'}"
)
status_label.setTextFormat(QtCore.Qt.RichText)
layout.addWidget(status_label)
layout.addStretch()
# Buttons
btn_layout = QtGui.QHBoxLayout()
save_btn = QtGui.QPushButton("Save")
cancel_btn = QtGui.QPushButton("Cancel")
btn_layout.addStretch()
btn_layout.addWidget(save_btn)
btn_layout.addWidget(cancel_btn)
layout.addLayout(btn_layout)
def on_save():
url = url_input.text().strip()
param.SetString("ApiUrl", url)
param.SetBool("SslVerify", ssl_checkbox.isChecked())
FreeCAD.Console.PrintMessage(
f"Silo settings saved. URL: {_get_api_url()}, "
f"SSL verify: {_get_ssl_verify()}\n"
)
dialog.accept()
save_btn.clicked.connect(on_save)
cancel_btn.clicked.connect(dialog.reject)
dialog.exec_()
def IsActive(self):
return True
# Register commands
FreeCADGui.addCommand("Silo_Open", Silo_Open())
FreeCADGui.addCommand("Silo_New", Silo_New())
@@ -1796,3 +1917,4 @@ FreeCADGui.addCommand("Silo_Info", Silo_Info())
FreeCADGui.addCommand("Silo_TagProjects", Silo_TagProjects())
FreeCADGui.addCommand("Silo_Rollback", Silo_Rollback())
FreeCADGui.addCommand("Silo_SetStatus", Silo_SetStatus())
FreeCADGui.addCommand("Silo_Settings", Silo_Settings())