feat: add QuickNav addon — Phase 1 core infrastructure (#320)
All checks were successful
Build and Test / build (pull_request) Successful in 29m58s

Add quicknav submodule and create-side integration for keyboard-driven
command navigation.

Submodule: mods/quicknav (https://git.kindred-systems.com/kindred/quicknav)

Create-side changes:
- CMakeLists.txt: add quicknav install rules
- test_kindred_pure.py: add 16 workbench_map validation tests
- docs/src/quicknav/SPEC.md: QuickNav specification

QuickNav provides numbered-key access to workbenches (Ctrl+1-5),
command groupings (Shift+1-9), and individual commands (1-9), with
a navigation bar toolbar and input-widget safety guards.
This commit is contained in:
2026-02-23 14:12:02 -06:00
parent f652d6ccf8
commit e73c5fc750
5 changed files with 575 additions and 0 deletions

View File

@@ -101,6 +101,7 @@ sys.path.insert(0, str(_REPO_ROOT / "src" / "Mod" / "Create"))
sys.path.insert(0, str(_REPO_ROOT / "mods" / "sdk"))
sys.path.insert(0, str(_REPO_ROOT / "mods" / "ztools" / "ztools"))
sys.path.insert(0, str(_REPO_ROOT / "mods" / "silo" / "freecad"))
sys.path.insert(0, str(_REPO_ROOT / "mods" / "quicknav"))
# ---------------------------------------------------------------------------
# Now import the modules under test
@@ -123,6 +124,15 @@ from silo_commands import _safe_float # noqa: E402
import silo_start # noqa: E402
import silo_origin # noqa: E402
from quicknav.workbench_map import ( # noqa: E402
WORKBENCH_SLOTS,
WORKBENCH_GROUPINGS,
get_workbench_slot,
get_groupings,
get_grouping,
get_command,
)
# ===================================================================
# Test: update_checker._parse_version
@@ -554,6 +564,110 @@ class TestDatumModes(unittest.TestCase):
self.assertEqual(len(points), 5)
# ===================================================================
# Test: quicknav workbench_map
# ===================================================================
class TestWorkbenchMap(unittest.TestCase):
"""Tests for quicknav.workbench_map data and helpers."""
def test_all_slots_defined(self):
for n in range(1, 6):
slot = WORKBENCH_SLOTS.get(n)
self.assertIsNotNone(slot, f"Slot {n} missing from WORKBENCH_SLOTS")
def test_slot_keys(self):
for n, slot in WORKBENCH_SLOTS.items():
self.assertIn("key", slot)
self.assertIn("class_name", slot)
self.assertIn("display", slot)
self.assertIsInstance(slot["key"], str)
self.assertIsInstance(slot["class_name"], str)
self.assertIsInstance(slot["display"], str)
def test_each_slot_has_groupings(self):
for n, slot in WORKBENCH_SLOTS.items():
groupings = WORKBENCH_GROUPINGS.get(slot["key"])
self.assertIsNotNone(groupings, f"No groupings for workbench key '{slot['key']}'")
self.assertGreater(len(groupings), 0, f"Empty groupings for slot {n}")
def test_max_nine_groupings_per_workbench(self):
for wb_key, groupings in WORKBENCH_GROUPINGS.items():
self.assertLessEqual(len(groupings), 9, f"More than 9 groupings for '{wb_key}'")
def test_max_nine_commands_per_grouping(self):
for wb_key, groupings in WORKBENCH_GROUPINGS.items():
for i, grp in enumerate(groupings):
self.assertLessEqual(
len(grp["commands"]),
9,
f"More than 9 commands in '{wb_key}' grouping {i}",
)
def test_command_tuples_are_str_str(self):
for wb_key, groupings in WORKBENCH_GROUPINGS.items():
for i, grp in enumerate(groupings):
self.assertIn("name", grp)
self.assertIn("commands", grp)
for j, cmd in enumerate(grp["commands"]):
self.assertIsInstance(cmd, tuple, f"{wb_key}[{i}][{j}] not tuple")
self.assertEqual(len(cmd), 2, f"{wb_key}[{i}][{j}] not length 2")
self.assertIsInstance(cmd[0], str, f"{wb_key}[{i}][{j}][0] not str")
self.assertIsInstance(cmd[1], str, f"{wb_key}[{i}][{j}][1] not str")
def test_get_workbench_slot_valid(self):
for n in range(1, 6):
slot = get_workbench_slot(n)
self.assertIsNotNone(slot)
self.assertEqual(slot, WORKBENCH_SLOTS[n])
def test_get_workbench_slot_invalid(self):
self.assertIsNone(get_workbench_slot(0))
self.assertIsNone(get_workbench_slot(6))
self.assertIsNone(get_workbench_slot(99))
def test_get_groupings_valid(self):
for slot in WORKBENCH_SLOTS.values():
result = get_groupings(slot["key"])
self.assertIsNotNone(result)
self.assertIsInstance(result, list)
def test_get_groupings_invalid(self):
self.assertEqual(get_groupings("nonexistent"), [])
def test_get_grouping_valid(self):
for wb_key, groupings in WORKBENCH_GROUPINGS.items():
for i in range(len(groupings)):
grp = get_grouping(wb_key, i)
self.assertIsNotNone(grp)
self.assertEqual(grp, groupings[i])
def test_get_grouping_invalid_index(self):
wb_key = WORKBENCH_SLOTS[1]["key"]
self.assertIsNone(get_grouping(wb_key, 99))
self.assertIsNone(get_grouping(wb_key, -1))
def test_get_grouping_invalid_key(self):
self.assertIsNone(get_grouping("nonexistent", 0))
def test_get_command_valid(self):
for wb_key, groupings in WORKBENCH_GROUPINGS.items():
for gi, grp in enumerate(groupings):
for ci in range(len(grp["commands"])):
cmd_id = get_command(wb_key, gi, ci + 1)
self.assertIsNotNone(cmd_id, f"None for {wb_key}[{gi}][{ci + 1}]")
self.assertEqual(cmd_id, grp["commands"][ci][0])
def test_get_command_invalid_number(self):
wb_key = WORKBENCH_SLOTS[1]["key"]
self.assertIsNone(get_command(wb_key, 0, 0))
self.assertIsNone(get_command(wb_key, 0, 99))
def test_get_command_invalid_workbench(self):
self.assertIsNone(get_command("nonexistent", 0, 1))
# ===================================================================