From e7707fb5e4d8a8a777e4eb21bdbf8f14783be6e4 Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 29 Oct 2026 23:23:20 -0400 Subject: [PATCH] island: optional dash tab bar on the island faces - bump qml-common Port 2.0 (cherry picked from commit e777288904e6f46fbf241fb8c00b674c6bff4c4b) --- .../Common/settings/BarWidgetDefaults.js | 1 + quickshell/Modules/DDash/DashMetrics.qml | 5 +- .../DIsland/Activities/DashTabFace.qml | 61 ++++++++++++-- .../DIsland/Activities/HomeExpanded.qml | 18 +--- quickshell/Modules/DIsland/DIslandSurface.qml | 6 ++ quickshell/Modules/DIsland/IslandBarHost.qml | 1 + .../Modules/DIsland/IslandController.qml | 27 ++++-- .../Settings/Widgets/IslandBehaviorCard.qml | 9 ++ .../translations/settings_search_index.json | 84 ++++++++++++++++--- 9 files changed, 168 insertions(+), 44 deletions(-) diff --git a/quickshell/Common/settings/BarWidgetDefaults.js b/quickshell/Common/settings/BarWidgetDefaults.js index b7e420576..faf97357f 100644 --- a/quickshell/Common/settings/BarWidgetDefaults.js +++ b/quickshell/Common/settings/BarWidgetDefaults.js @@ -36,6 +36,7 @@ var ISLAND_DEFAULTS = { islandHoverCloseDelay: 150, islandPalette: "default", islandHighContrast: false, + islandDashTabs: false, islandMediaClockVisible: true, islandMediaLyrics: false, islandMediaAutoSwitch: false, diff --git a/quickshell/Modules/DDash/DashMetrics.qml b/quickshell/Modules/DDash/DashMetrics.qml index 89b9f1db5..789a6cfdb 100644 --- a/quickshell/Modules/DDash/DashMetrics.qml +++ b/quickshell/Modules/DDash/DashMetrics.qml @@ -26,7 +26,6 @@ Singleton { // Card pills overhang their card by half their height and must stay inside the pages clip. readonly property real islandPillOverhang: PopoutMetrics.chromeButtonSize / 2 readonly property real islandEditChromeHeight: islandEditHeaderInset + editHeaderHeight + contentPadding + islandPillOverhang + islandEditBottomInset - readonly property real islandEditRoom: islandEditChromeHeight - islandHandleChromeHeight readonly property real spinnerSize: Theme.iconButtonSize readonly property real triggerWidth: CcMetrics.triggerWidth readonly property int transitionDuration: CcMetrics.transitionDuration @@ -36,6 +35,10 @@ Singleton { readonly property int overlayZ: CcMetrics.overlayZ readonly property bool animationsEnabled: CcMetrics.animationsEnabled + function islandChromeHeight(tabBarHeight) { + return tabBarHeight > 0 ? contentPadding * 2 + tabBarHeight + contentGap : islandHandleChromeHeight; + } + function widthFor(weekNumbers, screenWidth, columns = gridColumns) { // The floor is sized at comfortable spacing, so it carries the density offsets. const floorWidth = minimumPopoutWidth + (gridGap - Theme.spacingS) * (columns - 1) + (contentPadding - Theme.spacingM) * 2; diff --git a/quickshell/Modules/DIsland/Activities/DashTabFace.qml b/quickshell/Modules/DIsland/Activities/DashTabFace.qml index 61beb18fe..0deb5c0f1 100644 --- a/quickshell/Modules/DIsland/Activities/DashTabFace.qml +++ b/quickshell/Modules/DIsland/Activities/DashTabFace.qml @@ -22,12 +22,16 @@ FocusScope { readonly property var tab: tabLoader.item readonly property real tabHeight: tab?.implicitHeight ?? 0 readonly property real contentHeight: DashMetrics.panelHeightFor(entryId, tabHeight) - readonly property bool handleHeader: !root.editMode + readonly property bool tabsShown: root.controller.dashTabsShownFor(root.activityId) + readonly property real chromeHeight: DashMetrics.islandChromeHeight(tabsShown ? (tabBarLoader.item?.implicitHeight ?? Theme.navigationHeight) : 0) + readonly property bool handleHeader: !root.editMode && !root.tabsShown readonly property real pillOverhang: editMode ? DashMetrics.islandPillOverhang : 0 readonly property int panelColumns: DashMetrics.panelColumnsFor(entryId) readonly property int contentRows: DashMetrics.rowsForHeight(tabHeight) readonly property int panelRows: Math.max(DashMetrics.panelFloorRowsFor(entryId), contentRows) + signal windowRequested(string windowName) + readonly property QtObject resizeHost: QtObject { readonly property real renderedAlignedX: root.resizeGeometry?.renderedX ?? 0 readonly property real renderedAlignedY: root.resizeGeometry?.renderedY ?? 0 @@ -72,7 +76,13 @@ FocusScope { function focusFace() { pageActions.clearFocus(); sheetHandle.focusTarget.focus = false; + if (tabBarLoader.item) + tabBarLoader.item.focus = false; tabLoader.focus = false; + if (root.controller.dashNavigationFocused && tabBarLoader.item && !root.editMode) { + tabBarLoader.item.forceActiveFocus(Qt.OtherFocusReason); + return true; + } root.forceActiveFocus(Qt.OtherFocusReason); if (!root.editMode) root.tab?.restoreFocus?.(); @@ -80,10 +90,11 @@ FocusScope { } function reportHeight() { - root.controller.setDashboardContentHeight(root.activityId, root.contentHeight + DashMetrics.islandHandleChromeHeight); + root.controller.setDashboardContentHeight(root.activityId, root.contentHeight + root.chromeHeight); } onContentHeightChanged: reportHeight() + onChromeHeightChanged: reportHeight() onLiveChanged: { if (live) return; @@ -97,7 +108,7 @@ FocusScope { focusFace(); } onEditModeChanged: { - root.controller.setEditing(root.activityId, editMode, DashMetrics.islandEditRoom); + root.controller.setEditing(root.activityId, editMode, Math.max(0, DashMetrics.islandEditChromeHeight - root.chromeHeight)); if (!editMode) panelResizer.cancel(); Qt.callLater(() => { @@ -119,8 +130,8 @@ FocusScope { tabItem: root.tab contentScope: tabLoader contentTarget: root.tab?.focusTarget ?? null - headerTargets: root.editMode ? pageActions.focusTargets : [sheetHandle.focusTarget] - onTabCycleRequested: direction => root.controller.cycleDashTab(direction) + headerTargets: root.editMode ? pageActions.focusTargets : [tabBarLoader.item, sheetHandle.focusTarget] + onTabCycleRequested: direction => root.controller.cycleDashTab(direction, tabBarLoader.item?.activeFocus ?? false) onEditModeRequested: on => root.editMode = on onOptionsDismissRequested: tabOptions.dismiss() onCloseRequested: root.controller.requestCollapse() @@ -133,11 +144,41 @@ FocusScope { top: parent.top left: parent.left right: parent.right - topMargin: root.editMode ? DashMetrics.islandEditHeaderInset : 0 + topMargin: root.editMode ? DashMetrics.islandEditHeaderInset : root.tabsShown ? DashMetrics.contentPadding : 0 leftMargin: DashMetrics.contentPadding rightMargin: DashMetrics.contentPadding } - height: root.handleHeader ? DashMetrics.islandHandleHeight : DashMetrics.editHeaderHeight + height: root.editMode ? DashMetrics.editHeaderHeight : root.tabsShown ? (tabBarLoader.item?.implicitHeight ?? Theme.navigationHeight) : DashMetrics.islandHandleHeight + + Loader { + id: tabBarLoader + anchors.fill: parent + active: root.tabsShown + visible: !root.editMode + + sourceComponent: DNavigationBar { + id: tabBar + + editable: true + tabHighlights: true + evenlySpaced: SettingsData.dashTabsEvenlySpaced + currentIndex: root.controller.dashTabIds.indexOf(root.entryId) + nextFocusTarget: root.tab?.focusTarget ?? null + previousFocusTarget: root.tab?.focusTarget ?? null + model: root.controller.dashTabBarModel + onEditRequested: root.editMode = true + onActivated: index => { + const id = root.controller.dashTabIds[index]; + if (id === undefined || id === root.entryId) + return; + if (DashRegistry.isAction(id)) { + root.windowRequested(id); + return; + } + root.controller.requestDashTab(id, tabBar.activeFocus); + } + } + } DashSheetHandle { id: sheetHandle @@ -170,7 +211,7 @@ FocusScope { showScrollBar: false anchors { top: header.bottom - topMargin: root.editMode ? DashMetrics.contentPadding : 0 + topMargin: root.editMode ? DashMetrics.contentPadding : root.tabsShown ? DashMetrics.contentGap : 0 left: parent.left right: parent.right bottom: parent.bottom @@ -190,6 +231,10 @@ FocusScope { asynchronous: true visible: status === Loader.Ready sourceComponent: root.tabComponent + onActiveFocusChanged: { + if (activeFocus) + root.controller.dashNavigationFocused = false; + } onLoaded: { root.reportHeight(); if (root.live) diff --git a/quickshell/Modules/DIsland/Activities/HomeExpanded.qml b/quickshell/Modules/DIsland/Activities/HomeExpanded.qml index 052ca0e80..73e96aca8 100644 --- a/quickshell/Modules/DIsland/Activities/HomeExpanded.qml +++ b/quickshell/Modules/DIsland/Activities/HomeExpanded.qml @@ -15,23 +15,7 @@ DashTabFace { editMode: root.editMode rowBudget: root.controller.dashboardRowBudget transientSurfaceTracker: root.controller.transientSurfaces - onTabRequested: id => { - switch (id) { - case "media": - root.controller.requestActivity("media", true, true); - break; - case "weather": - if (SettingsData.weatherEnabled) - root.controller.requestWeather(false); - break; - case "wellbeing": - root.controller.requestActivity("wellbeing", true, true); - break; - case "notifications": - root.controller.requestNotificationCenter(false); - break; - } - } + onTabRequested: id => root.controller.requestDashTab(id) } } } diff --git a/quickshell/Modules/DIsland/DIslandSurface.qml b/quickshell/Modules/DIsland/DIslandSurface.qml index cd3ac0a81..a0b9f50d5 100644 --- a/quickshell/Modules/DIsland/DIslandSurface.qml +++ b/quickshell/Modules/DIsland/DIslandSurface.qml @@ -627,6 +627,7 @@ Item { HomeExpanded { controller: root.controller + onWindowRequested: windowName => root.openAfterCollapse(windowName) resizeGeometry: root.resizeGeometry } } @@ -645,6 +646,7 @@ Item { MediaExpanded { controller: root.controller + onWindowRequested: windowName => root.openAfterCollapse(windowName) resizeGeometry: root.resizeGeometry } } @@ -734,6 +736,7 @@ Item { WallpaperExpanded { controller: root.controller + onWindowRequested: windowName => root.openAfterCollapse(windowName) resizeGeometry: root.resizeGeometry effectiveScreen: root.effectiveScreen } @@ -755,6 +758,7 @@ Item { WeatherExpanded { controller: root.controller + onWindowRequested: windowName => root.openAfterCollapse(windowName) resizeGeometry: root.resizeGeometry } } @@ -775,6 +779,7 @@ Item { WellbeingExpanded { controller: root.controller + onWindowRequested: windowName => root.openAfterCollapse(windowName) resizeGeometry: root.resizeGeometry } } @@ -845,6 +850,7 @@ Item { NotificationCenterExpanded { controller: root.controller + onWindowRequested: windowName => root.openAfterCollapse(windowName) resizeGeometry: root.resizeGeometry } } diff --git a/quickshell/Modules/DIsland/IslandBarHost.qml b/quickshell/Modules/DIsland/IslandBarHost.qml index 6732be861..adddf2313 100644 --- a/quickshell/Modules/DIsland/IslandBarHost.qml +++ b/quickshell/Modules/DIsland/IslandBarHost.qml @@ -436,6 +436,7 @@ Item { batteryStyle: root.setting("islandBatteryStyle") mediaClockVisible: root.setting("islandMediaClockVisible") mediaLyrics: root.setting("islandMediaLyrics") + dashTabs: root.setting("islandDashTabs") // Home and media share the dot face; see mediaFaceToggleAvailable. mediaAutoSwitch: !root.dotMode && root.setting("islandMediaAutoSwitch") launcherCycleEnabled: SettingsData.launcherStyle === "island" diff --git a/quickshell/Modules/DIsland/IslandController.qml b/quickshell/Modules/DIsland/IslandController.qml index 17b674892..b73cd3e02 100644 --- a/quickshell/Modules/DIsland/IslandController.qml +++ b/quickshell/Modules/DIsland/IslandController.qml @@ -68,6 +68,14 @@ QtObject { property string batteryStyle: "duo" property bool mediaClockVisible: true property bool mediaLyrics: false + property bool dashTabs: false + // Carries tab-bar focus across a face switch. + property bool dashNavigationFocused: false + + onExpandedChanged: { + if (!expanded) + dashNavigationFocused = false; + } property int transientTimeout: 2200 property int notificationTimeout: 5000 property string transientReturnActivity: "" @@ -106,7 +114,10 @@ QtObject { readonly property int dashboardColumnCap: DashMetrics.columnCapFor(dashboardAvailableWidth - PopoutMetrics.editOverflow * 2, SettingsData.showWeekNumber) readonly property real dashboardMaxWidth: Math.min(dashboardAvailableWidth, DashMetrics.widthFor(SettingsData.showWeekNumber, undefined, editingActivity !== "" ? dashboardColumnCap : DashRegistry.widestPanelColumns)) property var dashboardContentHeights: ({}) - readonly property real dashChromeHeight: DashMetrics.islandHandleChromeHeight + // Plugin tabs have no island face, and the media face needs a player. + readonly property var dashTabIds: dashTabs ? DashRegistry.navigationIds.filter(id => DashRegistry.isAction(id) || dashActivityFor(id) !== "") : [] + readonly property var dashTabBarModel: dashTabIds.length === 0 ? [] : DashRegistry.tabBarModel.filter((_, index) => dashTabIds.indexOf(DashRegistry.navigationIds[index]) >= 0) + readonly property real dashChromeHeight: DashMetrics.islandChromeHeight(dashTabIds.length > 1 ? Theme.navigationHeight : 0) readonly property real editSurfaceHeight: editRoom <= 0 ? 0 : editingActivity === "controlcenter" ? controlCenterHeightFor(true) : dashboardHeightFor(editingActivity, true) readonly property real dashboardHeight: Math.min(dashboardAvailableHeight, Math.max(DashMetrics.tabDefaultHeight + root.dashChromeHeight, ...Object.values(dashboardContentHeights))) readonly property int dashboardRowBudget: Math.max(DashMetrics.minimumTabRows, Math.floor((dashboardAvailableHeight - DashMetrics.islandEditChromeHeight + DashMetrics.gridGap) / (DashMetrics.gridRowUnit + DashMetrics.gridGap))) @@ -166,20 +177,25 @@ QtObject { return ""; } - function requestDashTab(entryId) { + function dashTabsShownFor(activityId) { + return dashTabIds.length > 1 && dashTabIds.indexOf(dashEntryIdFor(activityId)) >= 0; + } + + function requestDashTab(entryId, fromNavigation) { + dashNavigationFocused = fromNavigation === true; if (entryId === "notifications") return requestNotificationCenter(false); const activityId = dashActivityFor(entryId); return activityId !== "" && requestActivity(activityId, true, true); } - function cycleDashTab(direction) { + function cycleDashTab(direction, fromNavigation) { const ids = DashRegistry.visibleTabIds.filter(id => dashActivityFor(id) !== ""); if (ids.length === 0) return false; const pos = ids.indexOf(dashEntryIdFor(activeActivity)); const next = ids[pos < 0 ? (direction > 0 ? 0 : ids.length - 1) : (pos + direction + ids.length) % ids.length]; - return next === dashEntryIdFor(activeActivity) || requestDashTab(next); + return next === dashEntryIdFor(activeActivity) || requestDashTab(next, fromNavigation); } function dashboardWidthFor(activityId) { @@ -188,7 +204,8 @@ QtObject { function dashboardHeightFor(activityId, withEditRoom) { const minimum = DashMetrics.panelHeightFor(dashEntryIdFor(activityId)); - const height = Math.max(minimum + root.dashChromeHeight, dashboardContentHeights[activityId] ?? 0); + const chrome = DashMetrics.islandChromeHeight(dashTabsShownFor(activityId) ? Theme.navigationHeight : 0); + const height = Math.max(minimum + chrome, dashboardContentHeights[activityId] ?? 0); return Math.min(dashboardAvailableHeight, height + (withEditRoom ? root.editRoom : 0)); } diff --git a/quickshell/Modules/Settings/Widgets/IslandBehaviorCard.qml b/quickshell/Modules/Settings/Widgets/IslandBehaviorCard.qml index 1b60165ac..bec656056 100644 --- a/quickshell/Modules/Settings/Widgets/IslandBehaviorCard.qml +++ b/quickshell/Modules/Settings/Widgets/IslandBehaviorCard.qml @@ -174,6 +174,15 @@ SettingsCard { onSliderDragFinished: value => root.store.apply("islandHoverCloseDelay", value) } + SettingsToggleRow { + settingKey: root.keyPrefix + "DashTabs" + tags: ["island", "dashboard", "dash", "tabs", "navigation"] + text: I18n.tr("Dashboard tabs", "island settings: dashboard tab bar toggle") + description: I18n.tr("Shows the dashboard tab bar instead of the sheet handle", "island settings: dashboard tab bar toggle description") + checked: root.store.setting("islandDashTabs") + onToggled: checked => root.store.apply("islandDashTabs", checked) + } + SettingsToggleRow { settingKey: root.keyPrefix + "MediaClockVisible" tags: ["island", "media", "clock", "compact", "time"] diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index a7a807f6e..abba72ef6 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -3572,6 +3572,31 @@ "parentLabel": "Bar", "conditionKey": "islandEnabled" }, + { + "section": "islandDashTabs", + "label": "Dashboard tabs", + "tabIndex": 3, + "category": "General", + "keywords": [ + "bar", + "dash", + "dashboard", + "general", + "handle", + "island", + "navigation", + "panel", + "sheet", + "shows", + "statusbar", + "tabs", + "taskbar", + "topbar" + ], + "parentLabel": "Bar", + "description": "Shows the dashboard tab bar instead of the sheet handle", + "conditionKey": "islandEnabled" + }, { "section": "barDisplay", "label": "Displays", @@ -10108,7 +10133,7 @@ "optionFile": "PrivacyOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:28", + "section": "barWidgetOption:IslandOptions.qml:29", "label": "Charging", "tabIndex": 22, "keywords": [], @@ -10116,7 +10141,7 @@ "optionFile": "IslandOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:30", + "section": "barWidgetOption:IslandOptions.qml:31", "label": "Clear badge on open", "tabIndex": 22, "keywords": [], @@ -10307,6 +10332,14 @@ "runtimeType": "barWidgetOption", "optionFile": "IslandOptions.qml" }, + { + "section": "barWidgetOption:IslandOptions.qml:22", + "label": "Dashboard tabs", + "tabIndex": 22, + "keywords": [], + "runtimeType": "barWidgetOption", + "optionFile": "IslandOptions.qml" + }, { "section": "barWidgetOption:SystemMonitorOptions.qml:7", "label": "Disk usage format", @@ -10332,7 +10365,7 @@ "optionFile": "WorkspaceSwitcherOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:29", + "section": "barWidgetOption:IslandOptions.qml:30", "label": "Expand by default", "tabIndex": 22, "keywords": [], @@ -10524,7 +10557,7 @@ "optionFile": "ControlCenterOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:22", + "section": "barWidgetOption:IslandOptions.qml:23", "label": "Keep clock with media", "tabIndex": 22, "keywords": [], @@ -10556,7 +10589,7 @@ "optionFile": "AppsDockOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:23", + "section": "barWidgetOption:IslandOptions.qml:24", "label": "Lyrics", "tabIndex": 22, "keywords": [], @@ -10644,7 +10677,7 @@ "optionFile": "WorkspaceSwitcherOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:31", + "section": "barWidgetOption:IslandOptions.qml:32", "label": "Motion", "tabIndex": 22, "keywords": [], @@ -10692,7 +10725,7 @@ "optionFile": "ClockOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:25", + "section": "barWidgetOption:IslandOptions.qml:26", "label": "Notifications", "tabIndex": 22, "keywords": [], @@ -10972,7 +11005,7 @@ "optionFile": "AppsDockOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:33", + "section": "barWidgetOption:IslandOptions.qml:34", "label": "Spring damping", "tabIndex": 22, "keywords": [], @@ -10980,7 +11013,7 @@ "optionFile": "IslandOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:34", + "section": "barWidgetOption:IslandOptions.qml:35", "label": "Spring mass", "tabIndex": 22, "keywords": [], @@ -10988,7 +11021,7 @@ "optionFile": "IslandOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:32", + "section": "barWidgetOption:IslandOptions.qml:33", "label": "Spring stiffness", "tabIndex": 22, "keywords": [], @@ -11020,7 +11053,7 @@ "optionFile": "WorkspaceSwitcherOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:24", + "section": "barWidgetOption:IslandOptions.qml:25", "label": "Switch to media when playback starts", "tabIndex": 22, "keywords": [], @@ -11076,7 +11109,7 @@ "optionFile": "SystemTrayOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:27", + "section": "barWidgetOption:IslandOptions.qml:28", "label": "Use standard OSDs", "tabIndex": 22, "keywords": [], @@ -11084,7 +11117,7 @@ "optionFile": "IslandOptions.qml" }, { - "section": "barWidgetOption:IslandOptions.qml:26", + "section": "barWidgetOption:IslandOptions.qml:27", "label": "Use standard popups", "tabIndex": 22, "keywords": [], @@ -17960,6 +17993,31 @@ "parentLabel": "Bar", "conditionKey": "dotEnabled" }, + { + "section": "dotDashTabs", + "label": "Dashboard tabs", + "tabIndex": 65, + "category": "Dot", + "keywords": [ + "bar", + "dash", + "dashboard", + "dot", + "handle", + "island", + "navigation", + "panel", + "sheet", + "shows", + "statusbar", + "tabs", + "taskbar", + "topbar" + ], + "parentLabel": "Bar", + "description": "Shows the dashboard tab bar instead of the sheet handle", + "conditionKey": "dotEnabled" + }, { "section": "dotDisplays", "label": "Displays",