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:
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user