Files
create/reference/quicknav/quicknav/core.py
forbes c8b0706a1d
All checks were successful
Build and Test / build (pull_request) Successful in 32m17s
chore: archive QuickNav and ZTools into reference folder (#345)
Copy QuickNav and ZTools source trees into reference/ for developer
reference during the UI/UX rework. These are plain directories (not
submodules) and are not included in the build.

- reference/quicknav/ — QuickNav addon source
- reference/ztools/ — ZTools addon source

Part of the UI/UX rework preparation. See #346.
2026-02-27 12:54:40 -06:00

209 lines
6.6 KiB
Python

"""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)