- IPanelProvider: abstract interface for dock panels with PySide widget bridging - PyIPanelProvider/PyProviderHolder: pybind11 trampoline + GIL-safe holder - WidgetBridge: PySide QWidget → C++ QWidget* conversion via Shiboken - SDKRegistry: panel registration, creation, and lifecycle management - ThemeEngine: C++ singleton with minimal YAML parser, palette cache, getColor/allTokens/formatQss matching Python Palette API - kcsdk bindings: DockArea, PanelPersistence enums, panel functions, theme_color, theme_tokens, format_qss, load_palette - dock.py: kcsdk delegation with FreeCADGui fallback - theme.py: kcsdk delegation with Python YAML fallback
This commit is contained in:
@@ -1,18 +1,40 @@
|
||||
"""Deferred dock panel registration helper.
|
||||
"""Dock panel registration helper.
|
||||
|
||||
Replaces the manual ``QTimer.singleShot()`` + duplicate-check +
|
||||
try/except pattern used in ``src/Mod/Create/InitGui.py``.
|
||||
Routes through the ``kcsdk`` C++ module (IPanelProvider / DockWindowManager)
|
||||
when available, falling back to direct PySide QDockWidget creation for
|
||||
backwards compatibility.
|
||||
"""
|
||||
|
||||
import FreeCAD
|
||||
|
||||
# Try to import the C++ SDK module; None if not yet built/installed.
|
||||
try:
|
||||
import kcsdk as _kcsdk
|
||||
except ImportError:
|
||||
_kcsdk = None
|
||||
|
||||
_AREA_MAP = {
|
||||
"left": 1, # Qt.LeftDockWidgetArea
|
||||
"right": 2, # Qt.RightDockWidgetArea
|
||||
"top": 4, # Qt.TopDockWidgetArea
|
||||
"bottom": 8, # Qt.BottomDockWidgetArea
|
||||
"left": 1, # Qt.LeftDockWidgetArea / DockArea.Left
|
||||
"right": 2, # Qt.RightDockWidgetArea / DockArea.Right
|
||||
"top": 4, # Qt.TopDockWidgetArea / DockArea.Top
|
||||
"bottom": 8, # Qt.BottomDockWidgetArea / DockArea.Bottom
|
||||
}
|
||||
|
||||
_DOCK_AREA_MAP = None # lazily populated from kcsdk
|
||||
|
||||
|
||||
def _get_dock_area(area_str):
|
||||
"""Convert area string to kcsdk.DockArea enum value."""
|
||||
global _DOCK_AREA_MAP
|
||||
if _DOCK_AREA_MAP is None:
|
||||
_DOCK_AREA_MAP = {
|
||||
"left": _kcsdk.DockArea.Left,
|
||||
"right": _kcsdk.DockArea.Right,
|
||||
"top": _kcsdk.DockArea.Top,
|
||||
"bottom": _kcsdk.DockArea.Bottom,
|
||||
}
|
||||
return _DOCK_AREA_MAP.get(area_str)
|
||||
|
||||
|
||||
def register_dock_panel(object_name, title, widget_factory, area="right", delay_ms=0):
|
||||
"""Register a dock panel, optionally deferred.
|
||||
@@ -38,15 +60,62 @@ def register_dock_panel(object_name, title, widget_factory, area="right", delay_
|
||||
if not callable(widget_factory):
|
||||
raise TypeError("widget_factory must be callable")
|
||||
|
||||
qt_area = _AREA_MAP.get(area)
|
||||
if qt_area is None:
|
||||
if area not in _AREA_MAP:
|
||||
raise ValueError(f"area must be one of {list(_AREA_MAP)}, got {area!r}")
|
||||
|
||||
if _kcsdk is not None:
|
||||
_register_via_kcsdk(object_name, title, widget_factory, area, delay_ms)
|
||||
else:
|
||||
_register_via_pyside(object_name, title, widget_factory, area, delay_ms)
|
||||
|
||||
|
||||
def _register_via_kcsdk(object_name, title, widget_factory, area, delay_ms):
|
||||
"""Register using the C++ SDK panel provider system."""
|
||||
dock_area = _get_dock_area(area)
|
||||
|
||||
class _AnonymousProvider(_kcsdk.IPanelProvider):
|
||||
def id(self):
|
||||
return object_name
|
||||
|
||||
def title(self):
|
||||
return title
|
||||
|
||||
def create_widget(self):
|
||||
return widget_factory()
|
||||
|
||||
def preferred_area(self):
|
||||
return dock_area
|
||||
|
||||
try:
|
||||
_kcsdk.register_panel(_AnonymousProvider())
|
||||
|
||||
def _create():
|
||||
try:
|
||||
_kcsdk.create_panel(object_name)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintLog(
|
||||
f"kindred_sdk: Panel '{object_name}' creation failed: {e}\n"
|
||||
)
|
||||
|
||||
from PySide.QtCore import QTimer
|
||||
|
||||
QTimer.singleShot(max(0, delay_ms), _create)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintLog(
|
||||
f"kindred_sdk: kcsdk panel registration failed for '{object_name}', "
|
||||
f"falling back: {e}\n"
|
||||
)
|
||||
_register_via_pyside(object_name, title, widget_factory, area, delay_ms)
|
||||
|
||||
|
||||
def _register_via_pyside(object_name, title, widget_factory, area, delay_ms):
|
||||
"""Legacy fallback: create dock widget directly via PySide."""
|
||||
qt_area = _AREA_MAP[area]
|
||||
|
||||
def _create():
|
||||
try:
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
import FreeCADGui
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
if mw is None:
|
||||
@@ -61,7 +130,9 @@ def register_dock_panel(object_name, title, widget_factory, area="right", delay_
|
||||
panel.setWidget(widget)
|
||||
mw.addDockWidget(QtCore.Qt.DockWidgetArea(qt_area), panel)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintLog(f"kindred_sdk: Dock panel '{object_name}' skipped: {e}\n")
|
||||
FreeCAD.Console.PrintLog(
|
||||
f"kindred_sdk: Dock panel '{object_name}' skipped: {e}\n"
|
||||
)
|
||||
|
||||
try:
|
||||
from PySide.QtCore import QTimer
|
||||
|
||||
@@ -135,15 +135,51 @@ _cache = {}
|
||||
_PALETTES_DIR = os.path.join(os.path.dirname(__file__), "palettes")
|
||||
|
||||
|
||||
def _kcsdk_available():
|
||||
"""Return the kcsdk module if available, else None."""
|
||||
try:
|
||||
import kcsdk
|
||||
|
||||
return kcsdk
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def load_palette(name="catppuccin-mocha"):
|
||||
"""Load a named palette from the ``palettes/`` directory.
|
||||
|
||||
When the C++ ``kcsdk`` module is available (GUI mode), delegates to
|
||||
``kcsdk.load_palette()`` and builds a ``Palette`` from the C++ token
|
||||
map. Falls back to the Python YAML loader for console mode.
|
||||
|
||||
Results are cached; subsequent calls with the same *name* return
|
||||
the same ``Palette`` instance.
|
||||
"""
|
||||
if name in _cache:
|
||||
return _cache[name]
|
||||
|
||||
# Try C++ backend first
|
||||
kcsdk = _kcsdk_available()
|
||||
if kcsdk is not None:
|
||||
try:
|
||||
if kcsdk.load_palette(name):
|
||||
tokens = kcsdk.theme_tokens()
|
||||
# Separate colors from roles by checking if the token
|
||||
# existed in the original colors set. Since the C++ engine
|
||||
# merges them, we rebuild by loading the YAML for metadata.
|
||||
# Simpler approach: use all tokens as colors (roles are
|
||||
# already resolved to hex values in the C++ engine).
|
||||
palette = Palette(
|
||||
name=name,
|
||||
slug=name,
|
||||
colors=tokens,
|
||||
roles={},
|
||||
)
|
||||
_cache[name] = palette
|
||||
return palette
|
||||
except Exception:
|
||||
pass # Fall through to Python loader
|
||||
|
||||
path = os.path.join(_PALETTES_DIR, f"{name}.yaml")
|
||||
if not os.path.isfile(path):
|
||||
FreeCAD.Console.PrintWarning(f"kindred_sdk: Palette file not found: {path}\n")
|
||||
@@ -152,7 +188,9 @@ def load_palette(name="catppuccin-mocha"):
|
||||
try:
|
||||
raw = _load_yaml(path)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"kindred_sdk: Failed to load palette '{name}': {e}\n")
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"kindred_sdk: Failed to load palette '{name}': {e}\n"
|
||||
)
|
||||
return None
|
||||
|
||||
palette = Palette(
|
||||
@@ -172,9 +210,20 @@ def load_palette(name="catppuccin-mocha"):
|
||||
def get_theme_tokens(name="catppuccin-mocha"):
|
||||
"""Return a dict of ``{token_name: "#hex"}`` for all colors in a palette.
|
||||
|
||||
This is a convenience shorthand for ``load_palette(name).colors``.
|
||||
When the C++ ``kcsdk`` module is available, delegates directly to
|
||||
``kcsdk.theme_tokens()`` for best performance. Falls back to the
|
||||
Python palette loader otherwise.
|
||||
|
||||
Returns a copy so callers cannot mutate the cached palette.
|
||||
"""
|
||||
kcsdk = _kcsdk_available()
|
||||
if kcsdk is not None:
|
||||
try:
|
||||
kcsdk.load_palette(name)
|
||||
return dict(kcsdk.theme_tokens())
|
||||
except Exception:
|
||||
pass # Fall through to Python loader
|
||||
|
||||
palette = load_palette(name)
|
||||
if palette is None:
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user