* fix: lots of fixes and name refactoring * feat: Add workbench preferences * fix: MCP bridge status widget and just command fixes * fix(tests): Use the correct mesa-glx package * fix(ci): Add fontconfig to GUI test dependencies FreeCAD GUI was failing to start with: "Fontconfig error: Cannot load default config file: No such file" Added fontconfig and fonts-dejavu-core packages to the GUI test job dependencies to resolve the font configuration issue. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(addon): Extract path utilities into shared module Create path_utils.py module that consolidates duplicated path-finding logic from commands.py and InitGui.py: - get_addon_path(): Find addon directory with caching and fallbacks - get_icon_path(): Get full path to an icon file - get_icons_dir(): Get path to icons directory - get_workbench_icon(): Get path to workbench main icon This removes ~100 lines of duplicated code while preserving the same behavior including _addon_path_cache and all fallback methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(addon): Prevent stale plugin state on startup failure The StartMCPBridgeCommand.Activated method could leave _mcp_plugin in a partially initialized state if FreecadMCPPlugin.start() failed after the plugin was instantiated. Changes: - Create plugin in a local variable first - Only assign to _mcp_plugin after start() succeeds - Explicitly clear _mcp_plugin and _running_config in exception handlers to ensure clean state for subsequent retry attempts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Lot of broad improvements * fix(ci): Use blocking headless_server.py for GUI tests The GUI test was using startup_bridge.py which is non-blocking (designed for interactive use). For CI, even in GUI mode, we need the blocking headless_server.py that calls run_forever() to keep FreeCAD running. GUI features are still available since we use the 'freecad' executable instead of 'freecadcmd'. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(addon): Rename headless_server.py to blocking_bridge.py The old name was misleading because: - It works with both GUI (freecad) and headless (freecadcmd) modes - The key characteristic is that it BLOCKS with run_forever() New naming convention clarifies the difference: - blocking_bridge.py: Starts bridge and blocks (for CI, servers) - startup_bridge.py: Starts bridge and returns (for interactive GUI) Updated all references across: - GitHub workflow (macro-test.yaml) - Just commands (freecad.just) - Unit tests (test_addon_structure.py) - Documentation (5 files) - CLAUDE.md Also improved the script to detect GUI mode dynamically using FreeCAD.GuiUp and display the appropriate status message. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(just): Remove erroneous rm of startup_bridge.py on error The startup script is now a permanent source file in the repository, not a generated temporary file. The rm -f would have deleted source code if FreeCAD wasn't found. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: General improvements * fix: Lots of general fixes and only stable to PyPi * fix: small cleanup * fix: Small fixes and hopefully fixes the GUI tests * fix: Add proper library paths for FreeCAD GUI in CI - Create wrapper scripts instead of symlinks for AppImage binaries - Set LD_LIBRARY_PATH, QT_PLUGIN_PATH for GUI mode - Add diagnostic output to identify startup failures * fix: Use apprun for GUI tests in CI * fix: Improving Xvfb tests * fix: GUI tests worlk * chore: remove invalid --no-splash comments * fix: ARM64 architecture support and other fixes * fix: cleanup * test: just commands test suite * test: improve just command tests * fix: more general improvements * fix: more cleanup * fix: more updates * fix: small tweaks --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
210 lines
7.4 KiB
Python
210 lines
7.4 KiB
Python
"""Preferences page for FreeCAD Preferences dialog integration.
|
|
|
|
SPDX-License-Identifier: MIT
|
|
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
|
|
|
|
This module provides a QWidget-based preferences page that integrates
|
|
with FreeCAD's main Preferences dialog (Edit → Preferences).
|
|
|
|
NOTE: This is separate from the preferences.py module which handles
|
|
the actual preference storage. This module only handles the UI.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from PySide import QtCore, QtWidgets # type: ignore[import-not-found]
|
|
|
|
|
|
class MCPBridgePreferencesPage(QtWidgets.QWidget):
|
|
"""Preferences page for FreeCAD's Preferences dialog.
|
|
|
|
This widget appears in the FreeCAD Preferences dialog sidebar
|
|
when registered via FreeCADGui.addPreferencePage().
|
|
|
|
Required methods:
|
|
- loadSettings(): Load preferences into widgets
|
|
- saveSettings(): Save widget values to preferences
|
|
"""
|
|
|
|
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
|
|
"""Initialize the preferences page widget."""
|
|
super().__init__(parent)
|
|
# Set window title - this appears in the preferences tree under the category
|
|
self.setWindowTitle("General")
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self) -> None:
|
|
"""Set up the user interface."""
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
layout.setContentsMargins(10, 10, 10, 10)
|
|
|
|
# Title
|
|
title = QtWidgets.QLabel("<h2>Robust MCP Bridge</h2>")
|
|
layout.addWidget(title)
|
|
|
|
description = QtWidgets.QLabel(
|
|
"Configure the MCP Bridge for AI assistant integration with FreeCAD."
|
|
)
|
|
description.setWordWrap(True)
|
|
layout.addWidget(description)
|
|
|
|
layout.addSpacing(10)
|
|
|
|
# Startup group
|
|
startup_group = QtWidgets.QGroupBox("Startup")
|
|
startup_layout = QtWidgets.QVBoxLayout(startup_group)
|
|
|
|
self.auto_start_cb = QtWidgets.QCheckBox(
|
|
"Auto-start bridge when FreeCAD launches"
|
|
)
|
|
self.auto_start_cb.setToolTip(
|
|
"Automatically start the MCP bridge server when FreeCAD starts.\n"
|
|
"The bridge allows AI assistants like Claude to control FreeCAD."
|
|
)
|
|
startup_layout.addWidget(self.auto_start_cb)
|
|
|
|
layout.addWidget(startup_group)
|
|
|
|
# Display group
|
|
display_group = QtWidgets.QGroupBox("Display")
|
|
display_layout = QtWidgets.QVBoxLayout(display_group)
|
|
|
|
self.status_bar_cb = QtWidgets.QCheckBox("Show status indicator in status bar")
|
|
self.status_bar_cb.setToolTip(
|
|
"Display MCP bridge connection status in FreeCAD's status bar."
|
|
)
|
|
display_layout.addWidget(self.status_bar_cb)
|
|
|
|
layout.addWidget(display_group)
|
|
|
|
# Network Ports group
|
|
ports_group = QtWidgets.QGroupBox("Network Ports")
|
|
ports_layout = QtWidgets.QFormLayout(ports_group)
|
|
|
|
self.xmlrpc_spin = QtWidgets.QSpinBox()
|
|
self.xmlrpc_spin.setRange(1024, 65535)
|
|
self.xmlrpc_spin.setToolTip(
|
|
"Port for XML-RPC connections.\n"
|
|
"Default: 9875\n\n"
|
|
"The MCP server connects to this port to communicate with FreeCAD."
|
|
)
|
|
ports_layout.addRow("XML-RPC Port:", self.xmlrpc_spin)
|
|
|
|
self.socket_spin = QtWidgets.QSpinBox()
|
|
self.socket_spin.setRange(1024, 65535)
|
|
self.socket_spin.setToolTip(
|
|
"Port for JSON-RPC socket connections.\n"
|
|
"Default: 9876\n\n"
|
|
"Alternative connection method using raw sockets."
|
|
)
|
|
ports_layout.addRow("Socket Port:", self.socket_spin)
|
|
|
|
# Warning about restart
|
|
port_warning = QtWidgets.QLabel(
|
|
"<i>Note: Changing ports requires restarting the bridge.</i>"
|
|
)
|
|
port_warning.setWordWrap(True)
|
|
ports_layout.addRow(port_warning)
|
|
|
|
layout.addWidget(ports_group)
|
|
|
|
# Server Configuration info
|
|
server_group = QtWidgets.QGroupBox("MCP Server Configuration")
|
|
server_layout = QtWidgets.QVBoxLayout(server_group)
|
|
|
|
server_intro = QtWidgets.QLabel(
|
|
"The external MCP server (used by Claude Code, etc.) is configured "
|
|
"separately using environment variables:"
|
|
)
|
|
server_intro.setWordWrap(True)
|
|
server_layout.addWidget(server_intro)
|
|
|
|
server_layout.addSpacing(5)
|
|
|
|
# Environment variables as a form layout for better alignment
|
|
env_layout = QtWidgets.QFormLayout()
|
|
env_layout.setLabelAlignment(QtCore.Qt.AlignRight)
|
|
|
|
env_vars = [
|
|
("FREECAD_XMLRPC_PORT", "XML-RPC port (default: 9875)"),
|
|
("FREECAD_SOCKET_PORT", "JSON-RPC socket port (default: 9876)"),
|
|
("FREECAD_MODE", "Connection mode: xmlrpc, socket, or embedded"),
|
|
("FREECAD_SOCKET_HOST", "Server hostname (default: localhost)"),
|
|
]
|
|
|
|
for var_name, description in env_vars:
|
|
var_label = QtWidgets.QLabel(f"<code>{var_name}</code>")
|
|
var_label.setTextFormat(QtCore.Qt.RichText)
|
|
desc_label = QtWidgets.QLabel(description)
|
|
env_layout.addRow(var_label, desc_label)
|
|
|
|
server_layout.addLayout(env_layout)
|
|
|
|
server_layout.addSpacing(5)
|
|
|
|
server_note = QtWidgets.QLabel(
|
|
"<i>Ensure these match the ports configured above.</i>"
|
|
)
|
|
server_note.setTextFormat(QtCore.Qt.RichText)
|
|
server_layout.addWidget(server_note)
|
|
|
|
layout.addWidget(server_group)
|
|
|
|
# Add stretch to push everything to the top
|
|
layout.addStretch()
|
|
|
|
def loadSettings(self) -> None:
|
|
"""Load settings from FreeCAD preferences into widgets.
|
|
|
|
This method is called by FreeCAD when the Preferences dialog opens.
|
|
"""
|
|
# Import here to avoid circular imports and ensure module is available
|
|
from preferences import (
|
|
get_auto_start,
|
|
get_socket_port,
|
|
get_status_bar_enabled,
|
|
get_xmlrpc_port,
|
|
)
|
|
|
|
self.auto_start_cb.setChecked(get_auto_start())
|
|
self.status_bar_cb.setChecked(get_status_bar_enabled())
|
|
self.xmlrpc_spin.setValue(get_xmlrpc_port())
|
|
self.socket_spin.setValue(get_socket_port())
|
|
|
|
def saveSettings(self) -> None:
|
|
"""Save settings from widgets to FreeCAD preferences.
|
|
|
|
This method is called by FreeCAD when OK or Apply is clicked.
|
|
"""
|
|
from preferences import (
|
|
get_socket_port,
|
|
get_xmlrpc_port,
|
|
set_auto_start,
|
|
set_socket_port,
|
|
set_status_bar_enabled,
|
|
set_xmlrpc_port,
|
|
)
|
|
|
|
# Track if ports changed for potential restart
|
|
old_xmlrpc = get_xmlrpc_port()
|
|
old_socket = get_socket_port()
|
|
|
|
# Save all preferences
|
|
set_auto_start(self.auto_start_cb.isChecked())
|
|
set_status_bar_enabled(self.status_bar_cb.isChecked())
|
|
set_xmlrpc_port(self.xmlrpc_spin.value())
|
|
set_socket_port(self.socket_spin.value())
|
|
|
|
# Check if ports changed and notify about restart if needed
|
|
new_xmlrpc = self.xmlrpc_spin.value()
|
|
new_socket = self.socket_spin.value()
|
|
|
|
if old_xmlrpc != new_xmlrpc or old_socket != new_socket:
|
|
# Import FreeCAD here to avoid issues at module load time
|
|
import FreeCAD
|
|
|
|
FreeCAD.Console.PrintMessage(
|
|
"MCP Bridge ports changed. "
|
|
"If the bridge is running, restart it for changes to take effect.\n"
|
|
)
|