From 4ae8ef927ce0cc3a6a8da7f228bed0db2527fb8c Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 2 Jul 2026 11:18:33 -0400 Subject: [PATCH] refactor(launcher): invert framemode emerge from top position --- .../Modals/DankLauncherV2/Controller.qml | 4 + .../Modals/DankLauncherV2/LauncherContent.qml | 194 ++++++++++-------- .../Modals/DankLauncherV2/ResultsList.qml | 66 +++++- .../Modals/DankLauncherV2/SectionHeader.qml | 11 +- 4 files changed, 174 insertions(+), 101 deletions(-) diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index f5b948970..e347c2a99 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -43,6 +43,7 @@ Item { signal queryChanged(string query) signal viewModeChanged(string sectionId, string mode) signal searchQueryRequested(string query) + signal sectionExpanded(string sectionId) Ref { service: AppSearchService @@ -1874,6 +1875,9 @@ Item { selectedFlatIndex = getFirstItemIndex(); } updateSelectedItem(); + + if (!newCollapsed[sectionId]) + sectionExpanded(sectionId); } function executeSelected() { diff --git a/quickshell/Modals/DankLauncherV2/LauncherContent.qml b/quickshell/Modals/DankLauncherV2/LauncherContent.qml index f9822ee6b..1292f60de 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContent.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContent.qml @@ -314,9 +314,13 @@ FocusScope { } Item { + id: contentHolder anchors.fill: parent visible: !editMode + readonly property bool inverted: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "top") + readonly property bool _connectedArcAtHeader: inverted && !(root.parentModal?.launcherArcExtenderActive ?? false) + Item { id: footerBar readonly property bool _connectedBottomEmerge: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "bottom") @@ -325,11 +329,10 @@ FocusScope { anchors.left: parent.left anchors.right: parent.right - anchors.bottom: parent.bottom anchors.leftMargin: root.parentModal?.borderWidth ?? 1 anchors.rightMargin: root.parentModal?.borderWidth ?? 1 - anchors.bottomMargin: _connectedBottomEmerge ? 0 : (root.parentModal?.borderWidth ?? 1) - height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0 + y: contentHolder.inverted ? 0 : (parent.height - height - (_connectedBottomEmerge ? 0 : (root.parentModal?.borderWidth ?? 1))) + height: showFooter ? ((_connectedArcAtFooter || contentHolder._connectedArcAtHeader) ? 76 : 36) : 0 visible: showFooter clip: true @@ -446,105 +449,112 @@ FocusScope { } } - Column { + Row { + id: searchRow + spacing: Theme.spacingS anchors.left: parent.left anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: footerBar.top anchors.leftMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM - anchors.topMargin: Theme.spacingM - spacing: Theme.spacingXS - clip: false + y: contentHolder.inverted ? (parent.height - height - Theme.spacingM) : Theme.spacingM - Row { - width: parent.width - spacing: Theme.spacingS + Rectangle { + id: pluginBadge + visible: controller.activePluginName.length > 0 + width: visible ? pluginBadgeContent.implicitWidth + Theme.spacingM : 0 + height: searchField.height + radius: 16 + color: Theme.primary - Rectangle { - id: pluginBadge - visible: controller.activePluginName.length > 0 - width: visible ? pluginBadgeContent.implicitWidth + Theme.spacingM : 0 - height: searchField.height - radius: 16 - color: Theme.primary + Row { + id: pluginBadgeContent + anchors.centerIn: parent + spacing: Theme.spacingXS - Row { - id: pluginBadgeContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - anchors.verticalCenter: parent.verticalCenter - name: "extension" - size: 14 - color: Theme.primaryText - } - - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: controller.activePluginName - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.primaryText - } + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "extension" + size: 14 + color: Theme.primaryText } - Behavior on width { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: controller.activePluginName + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primaryText } } - DankTextField { - id: searchField - width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0) - cornerRadius: Theme.cornerRadius - backgroundColor: root._launcherSearchFieldColor - normalBorderColor: root._launcherSearchBorderColor - focusedBorderColor: root._launcherSearchFocusedBorderColor - borderWidth: 1 - focusedBorderWidth: 2 - leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search" - leftIconSize: Theme.iconSize - leftIconColor: Theme.surfaceVariantText - leftIconFocusedColor: Theme.primary - showClearButton: true - textColor: Theme.surfaceText - font.pixelSize: Theme.fontSizeLarge - enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true - placeholderText: "" - ignoreUpDownKeys: true - ignoreTabKeys: true - keyForwardTargets: [root] - - onTextChanged: { - controller.setSearchQuery(text); - if (actionPanel.expanded) { - actionPanel.hide(); - } - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - if (root.parentModal) { - root.parentModal.hide(); - } - event.accepted = true; - } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter)) { - if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) { - actionPanel.executeSelectedAction(); - } else { - controller.executeSelected(); - } - event.accepted = true; - } + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing } } } + DankTextField { + id: searchField + width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0) + cornerRadius: Theme.cornerRadius + backgroundColor: root._launcherSearchFieldColor + normalBorderColor: root._launcherSearchBorderColor + focusedBorderColor: root._launcherSearchFocusedBorderColor + borderWidth: 1 + focusedBorderWidth: 2 + leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search" + leftIconSize: Theme.iconSize + leftIconColor: Theme.surfaceVariantText + leftIconFocusedColor: Theme.primary + showClearButton: true + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge + enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true + placeholderText: "" + ignoreUpDownKeys: true + ignoreTabKeys: true + keyForwardTargets: [root] + + onTextChanged: { + controller.setSearchQuery(text); + if (actionPanel.expanded) { + actionPanel.hide(); + } + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + if (root.parentModal) { + root.parentModal.hide(); + } + event.accepted = true; + } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter)) { + if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) { + actionPanel.executeSelectedAction(); + } else { + controller.executeSelected(); + } + event.accepted = true; + } + } + } + } + + Item { + id: contentStack + anchors.left: parent.left + anchors.right: parent.right + anchors.top: contentHolder.inverted ? footerBar.bottom : searchRow.bottom + anchors.bottom: contentHolder.inverted ? searchRow.top : footerBar.top + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: contentHolder.inverted && !footerBar.showFooter ? Theme.spacingM : contentStack.gap + anchors.bottomMargin: contentHolder.inverted ? contentStack.gap : 0 + readonly property real gap: Theme.spacingXS + clip: false + Row { id: categoryRow width: parent.width @@ -552,6 +562,8 @@ FocusScope { height: showPluginCategories ? 36 : 0 visible: showPluginCategories spacing: Theme.spacingS + anchors.top: parent.top + anchors.topMargin: 0 clip: true @@ -607,6 +619,8 @@ FocusScope { width: parent.width height: showFileFilters ? fileFilterContent.height : 0 visible: showFileFilters + anchors.top: parent.top + anchors.topMargin: 0 readonly property bool showFileFilters: controller.searchMode === "files" @@ -746,8 +760,12 @@ FocusScope { } Item { + id: resultsSlot width: parent.width - height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) + anchors.top: fileFilterRow.visible ? fileFilterRow.bottom : (categoryRow.visible ? categoryRow.bottom : parent.top) + anchors.topMargin: (fileFilterRow.visible || categoryRow.visible) ? contentStack.gap : 0 + anchors.bottom: actionPanel.top + anchors.bottomMargin: actionPanel.height > 0 || !contentHolder.inverted ? contentStack.gap : 0 opacity: { if (!root.parentModal) return 1; @@ -760,6 +778,7 @@ FocusScope { id: resultsList anchors.fill: parent controller: root.controller + leadingSectionHeaderAtBottom: contentHolder.inverted onItemRightClicked: (index, item, sceneX, sceneY) => { if (item && contextMenu.hasContextMenuActions(item)) { @@ -773,6 +792,7 @@ FocusScope { ActionPanel { id: actionPanel width: parent.width + anchors.bottom: parent.bottom selectedItem: controller.selectedItem controller: controller } diff --git a/quickshell/Modals/DankLauncherV2/ResultsList.qml b/quickshell/Modals/DankLauncherV2/ResultsList.qml index aa360f133..71dd4dfea 100644 --- a/quickshell/Modals/DankLauncherV2/ResultsList.qml +++ b/quickshell/Modals/DankLauncherV2/ResultsList.qml @@ -11,9 +11,11 @@ Item { property var controller: null property int gridColumns: controller?.gridColumns ?? 4 + property bool leadingSectionHeaderAtBottom: false property var _visualRows: [] property var _flatIndexToRowMap: ({}) property var _cumulativeHeights: [] + readonly property bool _bottomSectionHeaderActive: leadingSectionHeaderAtBottom && (controller?.sections?.length ?? 0) > 0 signal itemRightClicked(int index, var item, real mouseX, real mouseY) @@ -28,15 +30,17 @@ Item { var section = sections[s]; var sectionId = section.id; - cumHeights.push(cumY); - rows.push({ - _rowId: "h_" + sectionId, - type: "header", - section: section, - sectionId: sectionId, - height: 32 - }); - cumY += 32; + if (!root._bottomSectionHeaderActive || s > 0) { + cumHeights.push(cumY); + rows.push({ + _rowId: "h_" + sectionId, + type: "header", + section: section, + sectionId: sectionId, + height: 32 + }); + cumY += 32; + } if (section.collapsed) continue; @@ -103,6 +107,7 @@ Item { onGridColumnsChanged: Qt.callLater(_rebuildVisualModel) onWidthChanged: Qt.callLater(_rebuildVisualModel) + onLeadingSectionHeaderAtBottomChanged: Qt.callLater(_rebuildVisualModel) Connections { target: root.controller @@ -117,12 +122,27 @@ Item { root._cumulativeHeights = []; root._flatIndexToRowMap = {}; } + function onSectionExpanded(sectionId) { + Qt.callLater(() => { + root._rebuildVisualModel(); + Qt.callLater(() => root.revealExpandedSection(sectionId)); + }); + } } function resetScroll() { mainListView.contentY = mainListView.originY; } + function revealExpandedSection(sectionId) { + for (var i = 0; i < _visualRows.length; i++) { + if (_visualRows[i].sectionId === sectionId) { + mainListView.positionViewAtIndex(i, ListView.Beginning); + return; + } + } + } + function ensureVisible(index) { if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length) return; @@ -194,6 +214,7 @@ Item { id: listClip anchors.fill: parent anchors.topMargin: BlurService.enabled && stickyHeader.visible ? 32 : 0 + anchors.bottomMargin: bottomSectionHeader.visible ? bottomSectionHeader.height : 0 clip: true DankListView { @@ -332,6 +353,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom + anchors.bottomMargin: bottomSectionHeader.visible ? bottomSectionHeader.height : 0 height: 24 z: 100 visible: { @@ -377,7 +399,7 @@ Item { height: 32 z: 101 color: Theme.floatingSurface - visible: stickyHeaderSection !== null + visible: !root._bottomSectionHeaderActive && stickyHeaderSection !== null readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0 @@ -428,6 +450,30 @@ Item { } } + SectionHeader { + id: bottomSectionHeader + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + z: 101 + visible: root._bottomSectionHeaderActive + section: visible ? root.controller.sections[0] : null + controller: root.controller + viewMode: { + var vt = root.controller?.viewModeVersion ?? 0; + void (vt); + return root.controller?.getSectionViewMode(section?.id ?? "") ?? "list"; + } + canChangeViewMode: { + var vt = root.controller?.viewModeVersion ?? 0; + void (vt); + return root.controller?.canChangeSectionViewMode(section?.id ?? "") ?? false; + } + canCollapse: root.controller?.canCollapseSection(section?.id ?? "") ?? false + isSticky: true + popupAbove: true + } + Item { anchors.centerIn: parent visible: (!root.controller?.sections || root.controller.sections.length === 0) && !root.controller?.isFileSearching diff --git a/quickshell/Modals/DankLauncherV2/SectionHeader.qml b/quickshell/Modals/DankLauncherV2/SectionHeader.qml index b82560c54..e039f8957 100644 --- a/quickshell/Modals/DankLauncherV2/SectionHeader.qml +++ b/quickshell/Modals/DankLauncherV2/SectionHeader.qml @@ -15,6 +15,8 @@ Rectangle { property bool canChangeViewMode: true property bool canCollapse: true property bool isSticky: false + property bool popupAbove: false + property Item popupAboveItem: null signal viewModeToggled @@ -122,9 +124,10 @@ Rectangle { if (categoryPopup.visible) { categoryPopup.close(); } else { - const pos = categoryChip.mapToItem(Overlay.overlay, 0, 0); - categoryPopup.x = pos.x; - categoryPopup.y = pos.y + categoryChip.height + 4; + const chipPos = categoryChip.mapToItem(Overlay.overlay, 0, 0); + const abovePos = (root.popupAboveItem ?? categoryChip).mapToItem(Overlay.overlay, 0, 0); + categoryPopup.x = chipPos.x; + categoryPopup.y = root.popupAbove ? abovePos.y - categoryPopup.height - 4 : chipPos.y + categoryChip.height + 4; categoryPopup.open(); } } @@ -325,7 +328,7 @@ Rectangle { anchors.fill: parent anchors.rightMargin: rightContent.width + Theme.spacingS cursorShape: root.canCollapse ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: root.canCollapse + enabled: root.canCollapse && !leftContent.hasAppCategories onClicked: { if (root.canCollapse && root.controller && root.section) { root.controller.toggleSection(root.section.id);