From 0f79d631efbb7df7f8a285173f59604297baab12 Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 8 May 2026 02:00:27 -0400 Subject: [PATCH 1/2] Filter inspector sections by editor mode --- qml/PropertiesPanel.qml | 137 ++++++++++++++++++++++++------ src/EditorModeController.cpp | 49 ++++++++++- src/EditorModeController.h | 14 +++ src/EditorModeController_test.cpp | 52 ++++++++++++ 4 files changed, 226 insertions(+), 26 deletions(-) diff --git a/qml/PropertiesPanel.qml b/qml/PropertiesPanel.qml index c9097fc89..2e4c6a0e3 100644 --- a/qml/PropertiesPanel.qml +++ b/qml/PropertiesPanel.qml @@ -9,13 +9,34 @@ Rectangle { id: root color: PropertiesPanelController.panelColor + readonly property int inspectorTab: EditorModeController.InspectorTab + readonly property int sceneTab: EditorModeController.SceneTab + readonly property int modeToolsTab: EditorModeController.ModeToolsTab + readonly property int historyTab: EditorModeController.HistoryTab property int currentTab: 0 + property bool showAllModeTools: false function showModeToolsForMode(mode) { - return mode === EditorModeController.EditMode - || mode === EditorModeController.AnimationMode - || mode === EditorModeController.MaterialMode - || mode === EditorModeController.ValidationMode + return EditorModeController.modeHasModeTools(mode) + } + + function defaultTabForMode(mode) { + return EditorModeController.defaultInspectorTabForMode(mode) + } + + function shouldKeepExplicitTab(tab) { + return EditorModeController.shouldKeepExplicitInspectorTab(tab) + } + + function modeToolMatches(mode) { + return EditorModeController.modeToolMatchesCurrentMode( + mode, root.showAllModeTools, EditorModeController.currentMode) + } + + function modeToolSectionVisible(mode, available) { + return root.currentTab === root.modeToolsTab + && available + && root.modeToolMatches(mode) } function targetAccent(kind) { @@ -36,9 +57,9 @@ Rectangle { // Don't yank the user away from Scene (1) or History (3) when // they're explicitly browsing those tabs. Only retarget the // Inspector/Mode-Tools pair, which are the mode-aware ones. - if (root.currentTab === 1 || root.currentTab === 3) + if (root.shouldKeepExplicitTab(root.currentTab)) return - root.currentTab = root.showModeToolsForMode(EditorModeController.currentMode) ? 2 : 0 + root.currentTab = root.defaultTabForMode(EditorModeController.currentMode) } } @@ -97,6 +118,66 @@ Rectangle { } } + Rectangle { + width: parent.width + height: visible ? 34 : 0 + visible: root.currentTab === root.modeToolsTab + color: Qt.darker(PropertiesPanelController.headerColor, 1.08) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + spacing: 6 + + Text { + Layout.fillWidth: true + text: EditorModeController.modeName + " Tools" + color: PropertiesPanelController.textColor + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + } + + Repeater { + model: [ + { label: "Current", all: false }, + { label: "All", all: true } + ] + + Rectangle { + width: Math.max(54, filterLabel.implicitWidth + 16) + height: 22 + radius: 4 + color: root.showAllModeTools === modelData.all + ? PropertiesPanelController.highlightColor + : filterMouse.containsMouse + ? Qt.lighter(PropertiesPanelController.panelColor, 1.2) + : PropertiesPanelController.panelColor + border.color: PropertiesPanelController.borderColor + border.width: 1 + + Text { + id: filterLabel + anchors.centerIn: parent + text: modelData.label + color: PropertiesPanelController.textColor + font.pixelSize: 10 + font.bold: root.showAllModeTools === modelData.all + } + + MouseArea { + id: filterMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.showAllModeTools = modelData.all + } + } + } + } + } + // ---- Selection Target Indicator ---- Rectangle { width: parent.width @@ -170,7 +251,9 @@ Rectangle { // ---- Edit Mode Tools ---- CollapsibleSection { title: "Edit Mode Tools" - sectionVisible: root.currentTab === 2 && EditModeController.editModeActive + sectionVisible: root.modeToolSectionVisible( + EditorModeController.EditMode, + EditModeController.editModeActive) expanded: true Component.onCompleted: content = editModeToolsComponent @@ -179,7 +262,7 @@ Rectangle { // ---- Scene Outliner ---- CollapsibleSection { title: "Scene" - sectionVisible: root.currentTab === 1 + sectionVisible: root.currentTab === root.sceneTab expanded: true Component.onCompleted: content = sceneOutlinerComponent @@ -188,7 +271,8 @@ Rectangle { // ---- Transform ---- CollapsibleSection { title: PropertiesPanelController.transformTargetLabel - sectionVisible: root.currentTab === 0 && PropertiesPanelController.hasSelection + sectionVisible: root.currentTab === root.inspectorTab + && PropertiesPanelController.hasSelection Component.onCompleted: content = transformComponent } @@ -196,7 +280,7 @@ Rectangle { // ---- Snap Settings ---- CollapsibleSection { title: "Snap Settings" - sectionVisible: root.currentTab === 0 + sectionVisible: root.currentTab === root.inspectorTab expanded: false Component.onCompleted: content = snapSettingsComponent @@ -205,7 +289,8 @@ Rectangle { // ---- Primitive Parameters ---- CollapsibleSection { title: "Primitive: " + PropertiesPanelController.primitiveType - sectionVisible: root.currentTab === 0 && PropertiesPanelController.hasPrimitive + sectionVisible: root.currentTab === root.inspectorTab + && PropertiesPanelController.hasPrimitive Component.onCompleted: content = primitiveComponent } @@ -213,9 +298,9 @@ Rectangle { // ---- Animations ---- CollapsibleSection { title: "Animations" - sectionVisible: root.currentTab === 2 - && EditorModeController.currentMode === EditorModeController.AnimationMode - && PropertiesPanelController.hasAnimations + sectionVisible: root.modeToolSectionVisible( + EditorModeController.AnimationMode, + PropertiesPanelController.hasAnimations) Component.onCompleted: content = animationComponent } @@ -223,9 +308,9 @@ Rectangle { // ---- Animation Control (keyframe editor) ---- CollapsibleSection { title: "Animation Control" - sectionVisible: root.currentTab === 2 - && EditorModeController.currentMode === EditorModeController.AnimationMode - && AnimationControlController.hasAnimation + sectionVisible: root.modeToolSectionVisible( + EditorModeController.AnimationMode, + AnimationControlController.hasAnimation) expanded: false Component.onCompleted: content = animControlComponent @@ -234,7 +319,9 @@ Rectangle { // ---- LOD Generation ---- CollapsibleSection { title: "LOD Generation" - sectionVisible: root.currentTab === 2 && MeshLodController.hasSelection + sectionVisible: root.modeToolSectionVisible( + EditorModeController.ValidationMode, + MeshLodController.hasSelection) expanded: false Component.onCompleted: content = lodComponent @@ -243,9 +330,9 @@ Rectangle { // ---- Material Presets ---- CollapsibleSection { title: "Material Presets" - sectionVisible: root.currentTab === 2 - && EditorModeController.currentMode === EditorModeController.MaterialMode - && PropertiesPanelController.hasSelection + sectionVisible: root.modeToolSectionVisible( + EditorModeController.MaterialMode, + PropertiesPanelController.hasSelection) expanded: false Component.onCompleted: content = materialPresetsComponent @@ -254,9 +341,9 @@ Rectangle { // ---- Mesh Validation ---- CollapsibleSection { title: "Mesh Validation" - sectionVisible: root.currentTab === 2 - && EditorModeController.currentMode === EditorModeController.ValidationMode - && MeshValidator.hasSelection + sectionVisible: root.modeToolSectionVisible( + EditorModeController.ValidationMode, + MeshValidator.hasSelection) expanded: false Component.onCompleted: content = validationComponent @@ -265,7 +352,7 @@ Rectangle { // ---- Undo History ---- CollapsibleSection { title: "Undo History" - sectionVisible: root.currentTab === 3 + sectionVisible: root.currentTab === root.historyTab expanded: false Component.onCompleted: content = undoHistoryComponent diff --git a/src/EditorModeController.cpp b/src/EditorModeController.cpp index ea5096aff..14d5bd1bb 100644 --- a/src/EditorModeController.cpp +++ b/src/EditorModeController.cpp @@ -8,6 +8,14 @@ EditorModeController* EditorModeController::m_pSingleton = nullptr; +namespace { +bool isValidMode(int mode) +{ + return mode >= EditorModeController::ObjectMode + && mode <= EditorModeController::ValidationMode; +} +} + EditorModeController::EditorModeController(QObject* parent) : QObject(parent) { @@ -58,7 +66,7 @@ void EditorModeController::setCurrentMode(int mode) void EditorModeController::requestMode(int mode) { - if (mode < ObjectMode || mode > ValidationMode) + if (!isValidMode(mode)) return; setModeInternal(static_cast(mode), true); @@ -124,6 +132,45 @@ QVariantList EditorModeController::availableModes() const return modes; } +bool EditorModeController::modeHasModeTools(int mode) const +{ + if (!isValidMode(mode)) + return false; + + return mode == EditMode + || mode == AnimationMode + || mode == MaterialMode + || mode == ValidationMode; +} + +int EditorModeController::defaultInspectorTabForMode(int mode) const +{ + return modeHasModeTools(mode) ? ModeToolsTab : InspectorTab; +} + +bool EditorModeController::shouldKeepExplicitInspectorTab(int tab) const +{ + return tab == SceneTab || tab == HistoryTab; +} + +bool EditorModeController::modeToolMatches(int sectionMode, bool showAllModeTools) const +{ + return modeToolMatchesCurrentMode( + sectionMode, showAllModeTools, static_cast(m_currentMode)); +} + +bool EditorModeController::modeToolMatchesCurrentMode( + int sectionMode, bool showAllModeTools, int currentMode) const +{ + if (!isValidMode(sectionMode)) + return false; + + if (!isValidMode(currentMode)) + return false; + + return showAllModeTools || currentMode == sectionMode; +} + bool EditorModeController::editModeAvailable() const { auto* edit = editController(); diff --git a/src/EditorModeController.h b/src/EditorModeController.h index 319bd6e0e..de0e7077a 100644 --- a/src/EditorModeController.h +++ b/src/EditorModeController.h @@ -36,6 +36,14 @@ class EditorModeController : public QObject }; Q_ENUM(Mode) + enum InspectorTabId { + InspectorTab = 0, + SceneTab = 1, + ModeToolsTab = 2, + HistoryTab = 3 + }; + Q_ENUM(InspectorTabId) + static EditorModeController* instance(); static EditorModeController* qmlInstance(QQmlEngine* engine, QJSEngine* scriptEngine); static void kill(); @@ -52,6 +60,12 @@ class EditorModeController : public QObject Q_INVOKABLE void toggleObjectEditMode(); Q_INVOKABLE QString modeNameFor(int mode) const; Q_INVOKABLE QString modeTooltipFor(int mode) const; + Q_INVOKABLE bool modeHasModeTools(int mode) const; + Q_INVOKABLE int defaultInspectorTabForMode(int mode) const; + Q_INVOKABLE bool shouldKeepExplicitInspectorTab(int tab) const; + Q_INVOKABLE bool modeToolMatches(int sectionMode, bool showAllModeTools) const; + Q_INVOKABLE bool modeToolMatchesCurrentMode( + int sectionMode, bool showAllModeTools, int currentMode) const; signals: void modeChanged(); diff --git a/src/EditorModeController_test.cpp b/src/EditorModeController_test.cpp index 8b11b1a22..979891661 100644 --- a/src/EditorModeController_test.cpp +++ b/src/EditorModeController_test.cpp @@ -66,6 +66,58 @@ TEST_F(EditorModeControllerTest, ModeTooltipsCoverAllModes) EXPECT_FALSE(ctrl->modeTooltipFor(EditorModeController::ValidationMode).isEmpty()); } +TEST_F(EditorModeControllerTest, InspectorTabPolicyDefaultsByMode) +{ + auto* ctrl = EditorModeController::instance(); + + EXPECT_FALSE(ctrl->modeHasModeTools(EditorModeController::ObjectMode)); + EXPECT_TRUE(ctrl->modeHasModeTools(EditorModeController::EditMode)); + EXPECT_TRUE(ctrl->modeHasModeTools(EditorModeController::AnimationMode)); + EXPECT_TRUE(ctrl->modeHasModeTools(EditorModeController::MaterialMode)); + EXPECT_TRUE(ctrl->modeHasModeTools(EditorModeController::ValidationMode)); + EXPECT_FALSE(ctrl->modeHasModeTools(99)); + + EXPECT_EQ(ctrl->defaultInspectorTabForMode(EditorModeController::ObjectMode), + EditorModeController::InspectorTab); + EXPECT_EQ(ctrl->defaultInspectorTabForMode(EditorModeController::AnimationMode), + EditorModeController::ModeToolsTab); + EXPECT_EQ(ctrl->defaultInspectorTabForMode(EditorModeController::MaterialMode), + EditorModeController::ModeToolsTab); + EXPECT_EQ(ctrl->defaultInspectorTabForMode(99), + EditorModeController::InspectorTab); +} + +TEST_F(EditorModeControllerTest, InspectorTabPolicyKeepsExplicitSceneAndHistoryTabs) +{ + auto* ctrl = EditorModeController::instance(); + + EXPECT_FALSE(ctrl->shouldKeepExplicitInspectorTab(EditorModeController::InspectorTab)); + EXPECT_TRUE(ctrl->shouldKeepExplicitInspectorTab(EditorModeController::SceneTab)); + EXPECT_FALSE(ctrl->shouldKeepExplicitInspectorTab(EditorModeController::ModeToolsTab)); + EXPECT_TRUE(ctrl->shouldKeepExplicitInspectorTab(EditorModeController::HistoryTab)); +} + +TEST_F(EditorModeControllerTest, ModeToolFilterKeepsNonCurrentSectionsReachableThroughAll) +{ + auto* ctrl = EditorModeController::instance(); + ctrl->requestMode(EditorModeController::AnimationMode); + + EXPECT_TRUE(ctrl->modeToolMatches(EditorModeController::AnimationMode, false)); + EXPECT_FALSE(ctrl->modeToolMatches(EditorModeController::MaterialMode, false)); + EXPECT_TRUE(ctrl->modeToolMatches(EditorModeController::MaterialMode, true)); + EXPECT_TRUE(ctrl->modeToolMatches(EditorModeController::ValidationMode, true)); + EXPECT_FALSE(ctrl->modeToolMatches(99, true)); + + EXPECT_TRUE(ctrl->modeToolMatchesCurrentMode( + EditorModeController::MaterialMode, false, EditorModeController::MaterialMode)); + EXPECT_FALSE(ctrl->modeToolMatchesCurrentMode( + EditorModeController::ValidationMode, false, EditorModeController::MaterialMode)); + EXPECT_TRUE(ctrl->modeToolMatchesCurrentMode( + EditorModeController::ValidationMode, true, EditorModeController::MaterialMode)); + EXPECT_FALSE(ctrl->modeToolMatchesCurrentMode( + EditorModeController::ValidationMode, true, 99)); +} + TEST_F(EditorModeControllerTest, NonEditModesUpdateModeAndStatus) { auto* ctrl = EditorModeController::instance(); From 0f4fbf3e2a1c96b647ef4fa85340cae834673ecb Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 8 May 2026 09:31:21 -0400 Subject: [PATCH 2/2] Address PR #435 reviews: reset filter, decouple tab order, extend tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PropertiesPanel.qml: reset `showAllModeTools` back to false when the editor mode changes. Otherwise toggling "All" in Animation and switching to Material would silently land on Material's ModeToolsTab with every section expanded — contradicting the "current-mode tools" default the filter advertises. - PropertiesPanel.qml: change the top-level tab Repeater model from a plain `["Inspector", "Scene", "Mode Tools", "History"]` array (indexed via the Repeater's implicit `index`) to objects that carry an explicit `id` referencing `root.inspectorTab` / `sceneTab` / `modeToolsTab` / `historyTab`. Reordering either the array or the `InspectorTabId` enum no longer silently mis-routes every `sectionVisible` binding downstream. - EditorModeController_test.cpp: extend `InspectorTabPolicyDefaultsByMode` to also assert `defaultInspectorTabForMode(EditMode)` and `defaultInspectorTabForMode(ValidationMode)` — closes the contract gap with `modeHasModeTools` already covering all four non-object modes. Co-authored-by: Cursor --- qml/PropertiesPanel.qml | 26 +++++++++++++++++++++----- src/EditorModeController_test.cpp | 4 ++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/qml/PropertiesPanel.qml b/qml/PropertiesPanel.qml index 2e4c6a0e3..955e31206 100644 --- a/qml/PropertiesPanel.qml +++ b/qml/PropertiesPanel.qml @@ -60,6 +60,11 @@ Rectangle { if (root.shouldKeepExplicitTab(root.currentTab)) return root.currentTab = root.defaultTabForMode(EditorModeController.currentMode) + // Switching modes resets the Mode-Tools filter back to "Current" + // so each mode lands on its own tools by default. A sticky "All" + // would silently survive a mode change and contradict the + // "current-mode tools" default the filter advertises. + root.showAllModeTools = false } } @@ -83,13 +88,24 @@ Rectangle { spacing: 3 Repeater { - model: [ "Inspector", "Scene", "Mode Tools", "History" ] + // Bind label order to the canonical InspectorTabId enum + // values exposed by EditorModeController. Don't rely on + // the Repeater's implicit `index` to map tabs — that + // would silently couple this array's order to the C++ + // enum order and break every `sectionVisible` binding + // if anyone reordered either. + model: [ + { label: "Inspector", id: root.inspectorTab }, + { label: "Scene", id: root.sceneTab }, + { label: "Mode Tools", id: root.modeToolsTab }, + { label: "History", id: root.historyTab } + ] Rectangle { width: Math.max(66, (parent.width - 9) / 4) height: 28 radius: 4 - color: root.currentTab === index + color: root.currentTab === modelData.id ? PropertiesPanelController.highlightColor : tabMouse.containsMouse ? Qt.lighter(PropertiesPanelController.panelColor, 1.2) @@ -99,10 +115,10 @@ Rectangle { Text { anchors.centerIn: parent - text: modelData + text: modelData.label color: PropertiesPanelController.textColor font.pixelSize: 10 - font.bold: root.currentTab === index + font.bold: root.currentTab === modelData.id elide: Text.ElideRight } @@ -111,7 +127,7 @@ Rectangle { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: root.currentTab = index + onClicked: root.currentTab = modelData.id } } } diff --git a/src/EditorModeController_test.cpp b/src/EditorModeController_test.cpp index 979891661..dac623aa5 100644 --- a/src/EditorModeController_test.cpp +++ b/src/EditorModeController_test.cpp @@ -79,10 +79,14 @@ TEST_F(EditorModeControllerTest, InspectorTabPolicyDefaultsByMode) EXPECT_EQ(ctrl->defaultInspectorTabForMode(EditorModeController::ObjectMode), EditorModeController::InspectorTab); + EXPECT_EQ(ctrl->defaultInspectorTabForMode(EditorModeController::EditMode), + EditorModeController::ModeToolsTab); EXPECT_EQ(ctrl->defaultInspectorTabForMode(EditorModeController::AnimationMode), EditorModeController::ModeToolsTab); EXPECT_EQ(ctrl->defaultInspectorTabForMode(EditorModeController::MaterialMode), EditorModeController::ModeToolsTab); + EXPECT_EQ(ctrl->defaultInspectorTabForMode(EditorModeController::ValidationMode), + EditorModeController::ModeToolsTab); EXPECT_EQ(ctrl->defaultInspectorTabForMode(99), EditorModeController::InspectorTab); }