feat: QuickNav Phase 1 — core infrastructure (#320)
Keyboard-driven command navigation addon for Kindred Create. - Event filter with key routing (0=toggle, 1-9=commands, Shift+1-9=groupings, Ctrl+1-9=workbenches) - Navigation bar (QToolBar) with workbench/grouping/command display - QuickNavManager singleton with workbench switching, grouping selection, command execution - Hardcoded workbench slots (Sketcher, PartDesign, Assembly, Spreadsheet, TechDraw) - Input widget safety (QLineEdit, QTextEdit, QAbstractSpinBox, TaskView) - Numpad support via KeypadModifier stripping - Conditional Catppuccin theming via SDK (Qt defaults on standalone FreeCAD) - QuickNavWorkbench with transparent overlay pattern - Preference persistence (BaseApp/Preferences/Mod/QuickNav)
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
||||||
3
Init.py
Normal file
3
Init.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import FreeCAD
|
||||||
|
|
||||||
|
FreeCAD.Console.PrintLog("quicknav addon loaded\n")
|
||||||
37
InitGui.py
Normal file
37
InitGui.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
|
||||||
|
|
||||||
|
class QuickNavWorkbench(Gui.Workbench):
|
||||||
|
"""Invisible workbench that installs QuickNav on load.
|
||||||
|
|
||||||
|
QuickNav does not replace the active workbench -- it layers on top.
|
||||||
|
Loading QuickNav installs the event filter and nav bar, then
|
||||||
|
immediately re-activates the previously active workbench.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MenuText = "QuickNav"
|
||||||
|
ToolTip = "Keyboard-driven command navigation"
|
||||||
|
|
||||||
|
def Initialize(self):
|
||||||
|
from quicknav.core import QuickNavManager
|
||||||
|
|
||||||
|
QuickNavManager.instance().install()
|
||||||
|
App.Console.PrintMessage("QuickNav workbench initialized\n")
|
||||||
|
|
||||||
|
def Activated(self):
|
||||||
|
from quicknav.core import QuickNavManager
|
||||||
|
|
||||||
|
QuickNavManager.instance().handle_workbench_activated()
|
||||||
|
|
||||||
|
def Deactivated(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def GetClassName(self):
|
||||||
|
return "Gui::PythonWorkbench"
|
||||||
|
|
||||||
|
|
||||||
|
Gui.addWorkbench(QuickNavWorkbench())
|
||||||
|
|
||||||
|
# Eager command registration
|
||||||
|
from quicknav.commands import QuickNav_Toggle # noqa: F401
|
||||||
25
package.xml
Normal file
25
package.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||||
|
<name>QuickNav</name>
|
||||||
|
<description>Keyboard-driven command navigation</description>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
<maintainer email="dev@kindred-systems.com">Kindred Systems</maintainer>
|
||||||
|
<license>LGPL-2.1</license>
|
||||||
|
<url type="repository">https://git.kindred-systems.com/kindred/quicknav</url>
|
||||||
|
|
||||||
|
<content>
|
||||||
|
<workbench>
|
||||||
|
<classname>QuickNavWorkbench</classname>
|
||||||
|
<subdirectory>./</subdirectory>
|
||||||
|
</workbench>
|
||||||
|
</content>
|
||||||
|
|
||||||
|
<kindred>
|
||||||
|
<min_create_version>0.1.0</min_create_version>
|
||||||
|
<load_priority>20</load_priority>
|
||||||
|
<pure_python>true</pure_python>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>sdk</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</kindred>
|
||||||
|
</package>
|
||||||
1
quicknav/__init__.py
Normal file
1
quicknav/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""QuickNav -- keyboard-driven command navigation for FreeCAD and Kindred Create."""
|
||||||
24
quicknav/commands.py
Normal file
24
quicknav/commands.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""FreeCAD command registrations for QuickNav."""
|
||||||
|
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
|
||||||
|
|
||||||
|
class QuickNav_Toggle:
|
||||||
|
"""Toggle QuickNav keyboard navigation on/off."""
|
||||||
|
|
||||||
|
def GetResources(self):
|
||||||
|
return {
|
||||||
|
"MenuText": "Toggle QuickNav",
|
||||||
|
"ToolTip": "Toggle keyboard-driven command navigation on/off",
|
||||||
|
}
|
||||||
|
|
||||||
|
def IsActive(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def Activated(self):
|
||||||
|
from quicknav.core import QuickNavManager
|
||||||
|
|
||||||
|
QuickNavManager.instance().toggle_active()
|
||||||
|
|
||||||
|
|
||||||
|
Gui.addCommand("QuickNav_Toggle", QuickNav_Toggle())
|
||||||
208
quicknav/core.py
Normal file
208
quicknav/core.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""QuickNav manager singleton.
|
||||||
|
|
||||||
|
Orchestrates the event filter, navigation bar, and workbench/grouping
|
||||||
|
state. Created once when the QuickNavWorkbench is first activated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
|
||||||
|
from quicknav.workbench_map import (
|
||||||
|
get_command,
|
||||||
|
get_grouping,
|
||||||
|
get_groupings,
|
||||||
|
get_workbench_slot,
|
||||||
|
)
|
||||||
|
|
||||||
|
_PREF_PATH = "User parameter:BaseApp/Preferences/Mod/QuickNav"
|
||||||
|
|
||||||
|
|
||||||
|
class QuickNavManager:
|
||||||
|
"""Singleton managing QuickNav lifecycle and state."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def instance(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = cls()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._active = False
|
||||||
|
self._installed = False
|
||||||
|
self._event_filter = None
|
||||||
|
self._nav_bar = None
|
||||||
|
self._current_workbench_slot = 2 # PartDesign default
|
||||||
|
self._current_grouping_idx = 0
|
||||||
|
self.previous_workbench = None
|
||||||
|
self._reactivating = False
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Lifecycle
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
"""Install the event filter and navigation bar."""
|
||||||
|
if self._installed:
|
||||||
|
return
|
||||||
|
|
||||||
|
mw = Gui.getMainWindow()
|
||||||
|
if mw is None:
|
||||||
|
App.Console.PrintWarning("QuickNav: main window not available\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Capture which workbench was active before QuickNav loaded.
|
||||||
|
try:
|
||||||
|
wb = Gui.activeWorkbench()
|
||||||
|
if wb:
|
||||||
|
self.previous_workbench = wb.__class__.__name__
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Event filter
|
||||||
|
from quicknav.event_filter import QuickNavEventFilter
|
||||||
|
|
||||||
|
self._event_filter = QuickNavEventFilter(manager=self, parent=mw)
|
||||||
|
mw.installEventFilter(self._event_filter)
|
||||||
|
|
||||||
|
# Navigation bar
|
||||||
|
from quicknav.nav_bar import QuickNavBar
|
||||||
|
|
||||||
|
self._nav_bar = QuickNavBar(manager=self, parent=mw)
|
||||||
|
mw.addToolBar(self._nav_bar)
|
||||||
|
mw.insertToolBarBreak(self._nav_bar)
|
||||||
|
|
||||||
|
# Read saved preference
|
||||||
|
self._active = self._load_preference()
|
||||||
|
if not self._active:
|
||||||
|
self._nav_bar.hide()
|
||||||
|
|
||||||
|
self._installed = True
|
||||||
|
self._update_nav_bar()
|
||||||
|
App.Console.PrintLog("QuickNav: installed\n")
|
||||||
|
|
||||||
|
def uninstall(self):
|
||||||
|
"""Remove the event filter and navigation bar."""
|
||||||
|
if not self._installed:
|
||||||
|
return
|
||||||
|
|
||||||
|
mw = Gui.getMainWindow()
|
||||||
|
if mw and self._event_filter:
|
||||||
|
mw.removeEventFilter(self._event_filter)
|
||||||
|
self._event_filter = None
|
||||||
|
|
||||||
|
if self._nav_bar:
|
||||||
|
self._nav_bar.hide()
|
||||||
|
self._nav_bar.setParent(None)
|
||||||
|
self._nav_bar.deleteLater()
|
||||||
|
self._nav_bar = None
|
||||||
|
|
||||||
|
self._installed = False
|
||||||
|
App.Console.PrintLog("QuickNav: uninstalled\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# State queries
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return self._active
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Actions
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def toggle_active(self):
|
||||||
|
"""Toggle QuickNav on/off."""
|
||||||
|
self._active = not self._active
|
||||||
|
self._save_preference()
|
||||||
|
if self._nav_bar:
|
||||||
|
if self._active:
|
||||||
|
self._nav_bar.show()
|
||||||
|
self._update_nav_bar()
|
||||||
|
else:
|
||||||
|
self._nav_bar.hide()
|
||||||
|
state = "on" if self._active else "off"
|
||||||
|
App.Console.PrintMessage(f"QuickNav: {state}\n")
|
||||||
|
|
||||||
|
def switch_workbench(self, n):
|
||||||
|
"""Switch to the workbench assigned to Ctrl+N."""
|
||||||
|
slot = get_workbench_slot(n)
|
||||||
|
if slot is None:
|
||||||
|
return
|
||||||
|
self._current_workbench_slot = n
|
||||||
|
self._current_grouping_idx = 0
|
||||||
|
|
||||||
|
# Activate the target workbench in FreeCAD.
|
||||||
|
try:
|
||||||
|
Gui.activateWorkbench(slot["class_name"])
|
||||||
|
except Exception as e:
|
||||||
|
App.Console.PrintWarning(f"QuickNav: could not activate {slot['class_name']}: {e}\n")
|
||||||
|
|
||||||
|
self._update_nav_bar()
|
||||||
|
|
||||||
|
def switch_grouping(self, n):
|
||||||
|
"""Switch to the Nth grouping (1-based) in the current workbench."""
|
||||||
|
slot = get_workbench_slot(self._current_workbench_slot)
|
||||||
|
if slot is None:
|
||||||
|
return
|
||||||
|
groupings = get_groupings(slot["key"])
|
||||||
|
idx = n - 1
|
||||||
|
if 0 <= idx < len(groupings):
|
||||||
|
self._current_grouping_idx = idx
|
||||||
|
self._update_nav_bar()
|
||||||
|
|
||||||
|
def execute_command(self, n):
|
||||||
|
"""Execute the Nth command (1-based) in the active grouping."""
|
||||||
|
slot = get_workbench_slot(self._current_workbench_slot)
|
||||||
|
if slot is None:
|
||||||
|
return
|
||||||
|
cmd_id = get_command(slot["key"], self._current_grouping_idx, n)
|
||||||
|
if cmd_id:
|
||||||
|
try:
|
||||||
|
Gui.runCommand(cmd_id)
|
||||||
|
except Exception as e:
|
||||||
|
App.Console.PrintWarning(f"QuickNav: command {cmd_id} failed: {e}\n")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Workbench re-activation guard
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def handle_workbench_activated(self):
|
||||||
|
"""Called from QuickNavWorkbench.Activated() to restore the
|
||||||
|
previous workbench without infinite recursion."""
|
||||||
|
if self._reactivating:
|
||||||
|
return
|
||||||
|
if self.previous_workbench:
|
||||||
|
self._reactivating = True
|
||||||
|
try:
|
||||||
|
Gui.activateWorkbench(self.previous_workbench)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self._reactivating = False
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Internal
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _update_nav_bar(self):
|
||||||
|
if not self._nav_bar:
|
||||||
|
return
|
||||||
|
slot = get_workbench_slot(self._current_workbench_slot)
|
||||||
|
if slot is None:
|
||||||
|
return
|
||||||
|
groupings = get_groupings(slot["key"])
|
||||||
|
grouping = get_grouping(slot["key"], self._current_grouping_idx)
|
||||||
|
commands = grouping["commands"] if grouping else []
|
||||||
|
self._nav_bar.update_display(
|
||||||
|
slot["display"], groupings, self._current_grouping_idx, commands
|
||||||
|
)
|
||||||
|
|
||||||
|
def _load_preference(self):
|
||||||
|
param = App.ParamGet(_PREF_PATH)
|
||||||
|
return param.GetBool("Enabled", True)
|
||||||
|
|
||||||
|
def _save_preference(self):
|
||||||
|
param = App.ParamGet(_PREF_PATH)
|
||||||
|
param.SetBool("Enabled", self._active)
|
||||||
84
quicknav/event_filter.py
Normal file
84
quicknav/event_filter.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""Key event filter for QuickNav.
|
||||||
|
|
||||||
|
Installed on FreeCAD's main window to intercept number key presses
|
||||||
|
when QuickNav is active. Passes through all events when inactive
|
||||||
|
(except Key_0 for re-enable toggle).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PySide.QtCore import QEvent, QObject, Qt
|
||||||
|
from PySide.QtWidgets import (
|
||||||
|
QAbstractSpinBox,
|
||||||
|
QApplication,
|
||||||
|
QLineEdit,
|
||||||
|
QPlainTextEdit,
|
||||||
|
QTextEdit,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Widget types that accept text input. QAbstractSpinBox covers
|
||||||
|
# QSpinBox, QDoubleSpinBox, and FreeCAD's custom QuantitySpinBox
|
||||||
|
# (used for Sketcher inline dimension entry).
|
||||||
|
_TEXT_INPUT_TYPES = (QLineEdit, QTextEdit, QPlainTextEdit, QAbstractSpinBox)
|
||||||
|
|
||||||
|
|
||||||
|
class QuickNavEventFilter(QObject):
|
||||||
|
"""Intercepts key presses for QuickNav dispatch."""
|
||||||
|
|
||||||
|
def __init__(self, manager, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._manager = manager
|
||||||
|
|
||||||
|
def eventFilter(self, obj, event):
|
||||||
|
if event.type() != QEvent.KeyPress:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key = event.key()
|
||||||
|
modifiers = event.modifiers() & ~Qt.KeypadModifier
|
||||||
|
|
||||||
|
# Key_0 with no modifiers always toggles, even when inactive.
|
||||||
|
if key == Qt.Key_0 and modifiers == Qt.NoModifier:
|
||||||
|
if not self._focus_is_text_input():
|
||||||
|
self._manager.toggle_active()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self._manager.is_active():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Don't intercept when a text input has focus.
|
||||||
|
if self._focus_is_text_input():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if Qt.Key_1 <= key <= Qt.Key_9:
|
||||||
|
n = key - Qt.Key_0
|
||||||
|
if modifiers == Qt.ControlModifier:
|
||||||
|
self._manager.switch_workbench(n)
|
||||||
|
return True
|
||||||
|
elif modifiers == Qt.ShiftModifier:
|
||||||
|
self._manager.switch_grouping(n)
|
||||||
|
return True
|
||||||
|
elif modifiers == Qt.NoModifier:
|
||||||
|
self._manager.execute_command(n)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _focus_is_text_input(self):
|
||||||
|
"""Return True if the focused widget is a text/number input."""
|
||||||
|
focused = QApplication.focusWidget()
|
||||||
|
if focused is None:
|
||||||
|
return False
|
||||||
|
if isinstance(focused, _TEXT_INPUT_TYPES):
|
||||||
|
return True
|
||||||
|
return self._is_in_task_panel(focused)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_in_task_panel(widget):
|
||||||
|
"""Return True if *widget* is inside FreeCAD's task panel."""
|
||||||
|
parent = widget.parent() if widget else None
|
||||||
|
while parent is not None:
|
||||||
|
name = parent.objectName() or ""
|
||||||
|
cls = parent.__class__.__name__
|
||||||
|
if "TaskView" in name or "TaskView" in cls:
|
||||||
|
return True
|
||||||
|
parent = parent.parent() if hasattr(parent, "parent") else None
|
||||||
|
return False
|
||||||
142
quicknav/nav_bar.py
Normal file
142
quicknav/nav_bar.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
"""Navigation bar widget for QuickNav.
|
||||||
|
|
||||||
|
A QToolBar that displays the current workbench, command groupings,
|
||||||
|
and numbered commands. Positioned at the bottom of the toolbar area.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PySide.QtCore import Signal
|
||||||
|
from PySide.QtWidgets import QHBoxLayout, QLabel, QToolBar, QWidget
|
||||||
|
|
||||||
|
|
||||||
|
class _ClickableLabel(QLabel):
|
||||||
|
"""QLabel that emits *clicked* on mouse press."""
|
||||||
|
|
||||||
|
clicked = Signal()
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
self.clicked.emit()
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
class QuickNavBar(QToolBar):
|
||||||
|
"""Bottom toolbar showing QuickNav state."""
|
||||||
|
|
||||||
|
_MAX_SLOTS = 9
|
||||||
|
|
||||||
|
def __init__(self, manager, parent=None):
|
||||||
|
super().__init__("QuickNav", parent)
|
||||||
|
self._manager = manager
|
||||||
|
self.setObjectName("QuickNavBar")
|
||||||
|
self.setMovable(False)
|
||||||
|
self.setFloatable(False)
|
||||||
|
|
||||||
|
container = QWidget()
|
||||||
|
layout = QHBoxLayout(container)
|
||||||
|
layout.setContentsMargins(4, 2, 4, 2)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Workbench label
|
||||||
|
self._wb_label = QLabel()
|
||||||
|
self._wb_label.setContentsMargins(4, 0, 8, 0)
|
||||||
|
layout.addWidget(self._wb_label)
|
||||||
|
|
||||||
|
# Vertical separator
|
||||||
|
sep1 = QLabel("|")
|
||||||
|
sep1.setContentsMargins(4, 0, 4, 0)
|
||||||
|
layout.addWidget(sep1)
|
||||||
|
|
||||||
|
# Grouping labels
|
||||||
|
self._grouping_labels = []
|
||||||
|
for i in range(self._MAX_SLOTS):
|
||||||
|
label = _ClickableLabel()
|
||||||
|
label.setContentsMargins(4, 0, 4, 0)
|
||||||
|
label.clicked.connect(lambda n=i + 1: self._manager.switch_grouping(n))
|
||||||
|
self._grouping_labels.append(label)
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
# Vertical separator
|
||||||
|
self._cmd_sep = QLabel("|")
|
||||||
|
self._cmd_sep.setContentsMargins(4, 0, 4, 0)
|
||||||
|
layout.addWidget(self._cmd_sep)
|
||||||
|
|
||||||
|
# Command labels
|
||||||
|
self._command_labels = []
|
||||||
|
for i in range(self._MAX_SLOTS):
|
||||||
|
label = _ClickableLabel()
|
||||||
|
label.setContentsMargins(4, 0, 4, 0)
|
||||||
|
label.clicked.connect(lambda n=i + 1: self._manager.execute_command(n))
|
||||||
|
self._command_labels.append(label)
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.addWidget(container)
|
||||||
|
|
||||||
|
self._apply_theme()
|
||||||
|
|
||||||
|
def update_display(self, wb_name, groupings, active_grouping_idx, commands):
|
||||||
|
"""Refresh the bar with current state.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
wb_name : str
|
||||||
|
Workbench display name.
|
||||||
|
groupings : list[dict]
|
||||||
|
Grouping dicts with ``"name"`` keys.
|
||||||
|
active_grouping_idx : int
|
||||||
|
0-based index of the active grouping.
|
||||||
|
commands : list[tuple[str, str]]
|
||||||
|
``(command_id, display_name)`` for the active grouping.
|
||||||
|
"""
|
||||||
|
self._wb_label.setText(f"WB: {wb_name}")
|
||||||
|
|
||||||
|
for i, label in enumerate(self._grouping_labels):
|
||||||
|
if i < len(groupings):
|
||||||
|
prefix = f"\u276{i + 1}" if i == active_grouping_idx else f"{i + 1}"
|
||||||
|
label.setText(f" {prefix}: {groupings[i]['name']} ")
|
||||||
|
if i == active_grouping_idx:
|
||||||
|
label.setStyleSheet(self._active_grouping_style)
|
||||||
|
else:
|
||||||
|
label.setStyleSheet(self._inactive_grouping_style)
|
||||||
|
label.show()
|
||||||
|
else:
|
||||||
|
label.hide()
|
||||||
|
|
||||||
|
for i, label in enumerate(self._command_labels):
|
||||||
|
if i < len(commands):
|
||||||
|
label.setText(f" {i + 1}: {commands[i][1]} ")
|
||||||
|
label.setStyleSheet(self._command_style)
|
||||||
|
label.show()
|
||||||
|
else:
|
||||||
|
label.hide()
|
||||||
|
|
||||||
|
def _apply_theme(self):
|
||||||
|
"""Apply Catppuccin Mocha theme if SDK is available, else Qt defaults."""
|
||||||
|
palette = None
|
||||||
|
try:
|
||||||
|
from kindred_sdk import load_palette
|
||||||
|
|
||||||
|
palette = load_palette()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if palette:
|
||||||
|
bg = palette.get("background.toolbar") or "#181825"
|
||||||
|
fg = palette.get("foreground") or "#cdd6f4"
|
||||||
|
accent = palette.get("accent.info") or "#89b4fa"
|
||||||
|
border = palette.get("border") or "#45475a"
|
||||||
|
muted = palette.get("foreground.muted") or "#a6adc8"
|
||||||
|
else:
|
||||||
|
# No theme — leave styles empty so Qt defaults apply.
|
||||||
|
self._active_grouping_style = "font-weight: bold;"
|
||||||
|
self._inactive_grouping_style = ""
|
||||||
|
self._command_style = ""
|
||||||
|
self._wb_label.setStyleSheet("font-weight: bold;")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.setStyleSheet(
|
||||||
|
f"QToolBar#QuickNavBar {{ background: {bg}; border-top: 1px solid {border}; }}"
|
||||||
|
)
|
||||||
|
self._wb_label.setStyleSheet(f"color: {accent}; font-weight: bold;")
|
||||||
|
self._active_grouping_style = f"color: {accent}; font-weight: bold;"
|
||||||
|
self._inactive_grouping_style = f"color: {muted};"
|
||||||
|
self._command_style = f"color: {fg};"
|
||||||
285
quicknav/workbench_map.py
Normal file
285
quicknav/workbench_map.py
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
"""Static workbench slot assignments and command groupings.
|
||||||
|
|
||||||
|
Phase 1 uses hardcoded data from the QuickNav spec (SPEC.md section 5).
|
||||||
|
Phase 2 replaces this with dynamic toolbar introspection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Fixed Ctrl+N workbench assignments
|
||||||
|
WORKBENCH_SLOTS = {
|
||||||
|
1: {
|
||||||
|
"key": "sketcher",
|
||||||
|
"class_name": "SketcherWorkbench",
|
||||||
|
"display": "Sketcher",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
"key": "partdesign",
|
||||||
|
"class_name": "PartDesignWorkbench",
|
||||||
|
"display": "Part Design",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
"key": "assembly",
|
||||||
|
"class_name": "AssemblyWorkbench",
|
||||||
|
"display": "Assembly",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
"key": "spreadsheet",
|
||||||
|
"class_name": "SpreadsheetWorkbench",
|
||||||
|
"display": "Spreadsheet",
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
"key": "techdraw",
|
||||||
|
"class_name": "TechDrawWorkbench",
|
||||||
|
"display": "TechDraw",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Command groupings per workbench. Each grouping has a name and up to
|
||||||
|
# 9 commands as (FreeCAD_command_id, display_name) tuples.
|
||||||
|
WORKBENCH_GROUPINGS = {
|
||||||
|
"sketcher": [
|
||||||
|
{
|
||||||
|
"name": "Primitives",
|
||||||
|
"commands": [
|
||||||
|
("Sketcher_CreateLine", "Line"),
|
||||||
|
("Sketcher_CreateRectangle", "Rectangle"),
|
||||||
|
("Sketcher_CreateCircle", "Circle"),
|
||||||
|
("Sketcher_CreateArc", "Arc"),
|
||||||
|
("Sketcher_CreatePoint", "Point"),
|
||||||
|
("Sketcher_CreateSlot", "Slot"),
|
||||||
|
("Sketcher_CreateBSpline", "B-Spline"),
|
||||||
|
("Sketcher_CreatePolyline", "Polyline"),
|
||||||
|
("Sketcher_CreateEllipseByCenter", "Ellipse"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Constraints",
|
||||||
|
"commands": [
|
||||||
|
("Sketcher_ConstrainCoincidentUnified", "Coincident"),
|
||||||
|
("Sketcher_ConstrainHorizontal", "Horizontal"),
|
||||||
|
("Sketcher_ConstrainVertical", "Vertical"),
|
||||||
|
("Sketcher_ConstrainParallel", "Parallel"),
|
||||||
|
("Sketcher_ConstrainPerpendicular", "Perpendicular"),
|
||||||
|
("Sketcher_ConstrainTangent", "Tangent"),
|
||||||
|
("Sketcher_ConstrainEqual", "Equal"),
|
||||||
|
("Sketcher_ConstrainSymmetric", "Symmetric"),
|
||||||
|
("Sketcher_ConstrainBlock", "Block"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dimensions",
|
||||||
|
"commands": [
|
||||||
|
("Sketcher_ConstrainDistance", "Distance"),
|
||||||
|
("Sketcher_ConstrainDistanceX", "Horiz. Distance"),
|
||||||
|
("Sketcher_ConstrainDistanceY", "Vert. Distance"),
|
||||||
|
("Sketcher_ConstrainRadius", "Radius"),
|
||||||
|
("Sketcher_ConstrainDiameter", "Diameter"),
|
||||||
|
("Sketcher_ConstrainAngle", "Angle"),
|
||||||
|
("Sketcher_ConstrainLock", "Lock"),
|
||||||
|
("Sketcher_ConstrainSnellsLaw", "Refraction"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Construction",
|
||||||
|
"commands": [
|
||||||
|
("Sketcher_ToggleConstruction", "Toggle Constr."),
|
||||||
|
("Sketcher_External", "External Geom."),
|
||||||
|
("Sketcher_CarbonCopy", "Carbon Copy"),
|
||||||
|
("Sketcher_Offset", "Offset"),
|
||||||
|
("Sketcher_Trimming", "Trim"),
|
||||||
|
("Sketcher_Extend", "Extend"),
|
||||||
|
("Sketcher_Split", "Split"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tools",
|
||||||
|
"commands": [
|
||||||
|
("Sketcher_Symmetry", "Mirror"),
|
||||||
|
("Sketcher_RectangularArray", "Linear Array"),
|
||||||
|
("Sketcher_Move", "Move"),
|
||||||
|
("Sketcher_Rotate", "Rotate"),
|
||||||
|
("Sketcher_Scale", "Scale"),
|
||||||
|
("Sketcher_CloseShape", "Close Shape"),
|
||||||
|
("Sketcher_ConnectLines", "Connect Edges"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"partdesign": [
|
||||||
|
{
|
||||||
|
"name": "Additive",
|
||||||
|
"commands": [
|
||||||
|
("PartDesign_Pad", "Pad"),
|
||||||
|
("PartDesign_Revolution", "Revolution"),
|
||||||
|
("PartDesign_AdditiveLoft", "Add. Loft"),
|
||||||
|
("PartDesign_AdditivePipe", "Add. Pipe"),
|
||||||
|
("PartDesign_AdditiveHelix", "Add. Helix"),
|
||||||
|
("PartDesign_CompPrimitiveAdditive", "Add. Primitive"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Subtractive",
|
||||||
|
"commands": [
|
||||||
|
("PartDesign_Pocket", "Pocket"),
|
||||||
|
("PartDesign_Hole", "Hole"),
|
||||||
|
("PartDesign_Groove", "Groove"),
|
||||||
|
("PartDesign_SubtractiveLoft", "Sub. Loft"),
|
||||||
|
("PartDesign_SubtractivePipe", "Sub. Pipe"),
|
||||||
|
("PartDesign_SubtractiveHelix", "Sub. Helix"),
|
||||||
|
("PartDesign_CompPrimitiveSubtractive", "Sub. Primitive"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Datums",
|
||||||
|
"commands": [
|
||||||
|
("PartDesign_NewSketch", "New Sketch"),
|
||||||
|
("PartDesign_Plane", "Datum Plane"),
|
||||||
|
("PartDesign_Line", "Datum Line"),
|
||||||
|
("PartDesign_Point", "Datum Point"),
|
||||||
|
("PartDesign_ShapeBinder", "Shape Binder"),
|
||||||
|
("PartDesign_SubShapeBinder", "Sub-Shape Binder"),
|
||||||
|
("ZTools_DatumCreator", "ZT Datum Creator"),
|
||||||
|
("ZTools_DatumManager", "ZT Datum Manager"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Transformations",
|
||||||
|
"commands": [
|
||||||
|
("PartDesign_Mirrored", "Mirrored"),
|
||||||
|
("PartDesign_LinearPattern", "Linear Pattern"),
|
||||||
|
("PartDesign_PolarPattern", "Polar Pattern"),
|
||||||
|
("PartDesign_MultiTransform", "MultiTransform"),
|
||||||
|
("ZTools_RotatedLinearPattern", "ZT Rot. Linear"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Modeling",
|
||||||
|
"commands": [
|
||||||
|
("PartDesign_Fillet", "Fillet"),
|
||||||
|
("PartDesign_Chamfer", "Chamfer"),
|
||||||
|
("PartDesign_Draft", "Draft"),
|
||||||
|
("PartDesign_Thickness", "Thickness"),
|
||||||
|
("PartDesign_Boolean", "Boolean"),
|
||||||
|
("ZTools_EnhancedPocket", "ZT Enh. Pocket"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"assembly": [
|
||||||
|
{
|
||||||
|
"name": "Components",
|
||||||
|
"commands": [
|
||||||
|
("Assembly_InsertLink", "Insert Component"),
|
||||||
|
("Assembly_InsertNewPart", "Create Part"),
|
||||||
|
("Assembly_CreateAssembly", "Create Assembly"),
|
||||||
|
("Assembly_ToggleGrounded", "Ground"),
|
||||||
|
("Assembly_CreateBom", "BOM"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Joints",
|
||||||
|
"commands": [
|
||||||
|
("Assembly_CreateJointFixed", "Fixed"),
|
||||||
|
("Assembly_CreateJointRevolute", "Revolute"),
|
||||||
|
("Assembly_CreateJointCylindrical", "Cylindrical"),
|
||||||
|
("Assembly_CreateJointSlider", "Slider"),
|
||||||
|
("Assembly_CreateJointBall", "Ball"),
|
||||||
|
("Assembly_CreateJointDistance", "Distance"),
|
||||||
|
("Assembly_CreateJointAngle", "Angle"),
|
||||||
|
("Assembly_CreateJointParallel", "Parallel"),
|
||||||
|
("Assembly_CreateJointPerpendicular", "Perpendicular"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Patterns",
|
||||||
|
"commands": [
|
||||||
|
("ZTools_AssemblyLinearPattern", "Linear Pattern"),
|
||||||
|
("ZTools_AssemblyPolarPattern", "Polar Pattern"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"spreadsheet": [
|
||||||
|
{
|
||||||
|
"name": "Editing",
|
||||||
|
"commands": [
|
||||||
|
("Spreadsheet_MergeCells", "Merge Cells"),
|
||||||
|
("Spreadsheet_SplitCell", "Split Cell"),
|
||||||
|
("Spreadsheet_SetAlias", "Alias"),
|
||||||
|
("Spreadsheet_Import", "Import CSV"),
|
||||||
|
("Spreadsheet_Export", "Export CSV"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Formatting",
|
||||||
|
"commands": [
|
||||||
|
("ZTools_SpreadsheetStyleBold", "Bold"),
|
||||||
|
("ZTools_SpreadsheetStyleItalic", "Italic"),
|
||||||
|
("ZTools_SpreadsheetStyleUnderline", "Underline"),
|
||||||
|
("ZTools_SpreadsheetAlignLeft", "Align Left"),
|
||||||
|
("ZTools_SpreadsheetAlignCenter", "Align Center"),
|
||||||
|
("ZTools_SpreadsheetAlignRight", "Align Right"),
|
||||||
|
("ZTools_SpreadsheetBgColor", "BG Color"),
|
||||||
|
("ZTools_SpreadsheetTextColor", "Text Color"),
|
||||||
|
("ZTools_SpreadsheetQuickAlias", "Quick Alias"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"techdraw": [
|
||||||
|
{
|
||||||
|
"name": "Views",
|
||||||
|
"commands": [
|
||||||
|
("TechDraw_PageDefault", "New Page"),
|
||||||
|
("TechDraw_View", "Insert View"),
|
||||||
|
("TechDraw_ProjectionGroup", "Projection Group"),
|
||||||
|
("TechDraw_SectionView", "Section View"),
|
||||||
|
("TechDraw_DetailView", "Detail View"),
|
||||||
|
("TechDraw_ActiveView", "Active View"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dimensions",
|
||||||
|
"commands": [
|
||||||
|
("TechDraw_LengthDimension", "Length"),
|
||||||
|
("TechDraw_HorizontalDimension", "Horizontal"),
|
||||||
|
("TechDraw_VerticalDimension", "Vertical"),
|
||||||
|
("TechDraw_RadiusDimension", "Radius"),
|
||||||
|
("TechDraw_DiameterDimension", "Diameter"),
|
||||||
|
("TechDraw_AngleDimension", "Angle"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Annotations",
|
||||||
|
"commands": [
|
||||||
|
("TechDraw_Annotation", "Annotation"),
|
||||||
|
("TechDraw_Balloon", "Balloon"),
|
||||||
|
("TechDraw_LeaderLine", "Leader Line"),
|
||||||
|
("TechDraw_CosmeticVertex", "Cosmetic Vertex"),
|
||||||
|
("TechDraw_Midpoints", "Midpoints"),
|
||||||
|
("TechDraw_CenterLine", "Center Line"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_workbench_slot(n):
|
||||||
|
"""Return workbench info dict for Ctrl+N, or None if unassigned."""
|
||||||
|
return WORKBENCH_SLOTS.get(n)
|
||||||
|
|
||||||
|
|
||||||
|
def get_groupings(workbench_key):
|
||||||
|
"""Return the grouping list for a workbench key."""
|
||||||
|
return WORKBENCH_GROUPINGS.get(workbench_key, [])
|
||||||
|
|
||||||
|
|
||||||
|
def get_grouping(workbench_key, grouping_idx):
|
||||||
|
"""Return a specific grouping dict by 0-based index, or None."""
|
||||||
|
groupings = get_groupings(workbench_key)
|
||||||
|
if 0 <= grouping_idx < len(groupings):
|
||||||
|
return groupings[grouping_idx]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_command(workbench_key, grouping_idx, n):
|
||||||
|
"""Return the FreeCAD command ID for the Nth command (1-based), or None."""
|
||||||
|
grouping = get_grouping(workbench_key, grouping_idx)
|
||||||
|
if grouping and 1 <= n <= len(grouping["commands"]):
|
||||||
|
return grouping["commands"][n - 1][0]
|
||||||
|
return None
|
||||||
Reference in New Issue
Block a user