From 06f2a87584fdebab20ffbdf77bdf505aa2ede58f Mon Sep 17 00:00:00 2001 From: wjyrich Date: Thu, 2 Jul 2026 19:59:14 +0800 Subject: [PATCH 1/4] feat: add FashionAlignment mode for dock panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Implement new FashionAlignment enum value (2) for dock item alignment 2. Create FashionDockController QML component to manage fashion dock layout and appearance 3. Add "Fashion Mode" menu item in dock context menu for easy mode switching 4. Integrate fashion dock logic into main dock window: adjust width, anchors, margins, window radius, and blur effects 5. Update showdesktop and taskmanager to be hidden or behave differently in fashion mode 6. Ensure width animation when entering/exiting fashion mode 7. Add appropriate DTK blur and styling for fashion dock appearance 8. Update string conversion functions to support the new "fashion" alignment 9. Hide left part and adjust spacing in fashion mode for a compact, floating design Log: Added new dock fashion alignment mode with floating UI and compact layout Influence: 1. Test switching between Center, Left, and Fashion alignment modes via context menu 2. Verify fashion mode only enables on Bottom/Top positions and not in column layout 3. Check that showdesktop widget is hidden in fashion mode 4. Verify dock width and margin adjustments in fashion mode (floating margin, vertical padding) 5. Test taskmanager behavior in fashion mode (no remaining space calculation, no text calculator) 6. Verify blur and color blending correctness in fashion mode for both light and dark themes 7. Test width animation when enabling/disabling fashion mode 8. Verify dock exclusion zone includes floating margin in fashion mode 9. Test on different screen sizes and DPI settings 10. Validate that left part and its items are hidden in fashion mode feat: 新增时尚模式对齐方式 1. 实现新的时尚模式对齐枚举值 (2),用于 dock 面板的项目对齐 2. 创建 FashionDockController QML 组件,管理时尚模式布局和外观 3. 在 dock 右键菜单中添加 "时尚模式" 菜单项,便于模式切换 4. 将时尚模式逻辑集成到主 dock 窗口中:调整宽度、锚点、边距、窗口圆角和 模糊效果 5. 更新显示桌面和任务管理器,使其在时尚模式下隐藏或改变行为 6. 在进入/退出时尚模式时启用宽度动画 7. 为时尚模式外观添加合适的 DTK 模糊和样式 8. 更新字符串转换函数以支持新的 "fashion" 对齐方式 9. 在时尚模式下隐藏左侧部分并调整间距,实现紧凑浮动设计 Log: 新增时尚模式对齐方式,浮动UI和紧凑布局 Influence: 1. 通过右键菜单测试中心、左对齐和时尚模式之间的切换 2. 验证时尚模式仅在底部/顶部位置且非列布局时启用 3. 检查显示桌面小部件在时尚模式下是否隐藏 4. 验证时尚模式下的 dock 宽度和边距调整(浮动边距、垂直内边距) 5. 测试任务管理器在时尚模式下的行为(无剩余空间计算、无文本计算器) 6. 验证时尚模式下浅色和深色主题的模糊和颜色混合正确性 7. 测试启用/禁用时尚模式时的宽度动画 8. 验证时尚模式下 dock 排除区域包含浮动边距 9. 在不同屏幕尺寸和 DPI 设置下进行测试 10. 验证左侧部分及其项目在时尚模式下是否隐藏 --- panels/dock/constants.h | 3 +- panels/dock/docksettings.cpp | 4 + panels/dock/package/FashionDockController.qml | 65 ++++++++++++++++ panels/dock/package/main.qml | 78 +++++++++++++++---- .../dock/showdesktop/package/showdesktop.qml | 10 ++- .../dock/taskmanager/package/TaskManager.qml | 10 ++- 6 files changed, 150 insertions(+), 20 deletions(-) create mode 100644 panels/dock/package/FashionDockController.qml diff --git a/panels/dock/constants.h b/panels/dock/constants.h index bf3cb9372..69fc3823b 100644 --- a/panels/dock/constants.h +++ b/panels/dock/constants.h @@ -1,6 +1,6 @@ // Copyright (C) 2011 ~ 2018 Deepin Technology Co., Ltd. // SPDX-FileCopyrightText: 2018 - 2023 UnionTech Software Technology Co., Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later @@ -36,6 +36,7 @@ enum IndicatorStyle { enum ItemAlignment { CenterAlignment = 0, LeftAlignment = 1, + FashionAlignment = 2, }; enum ColorTheme { diff --git a/panels/dock/docksettings.cpp b/panels/dock/docksettings.cpp index 75de92484..2beb841bc 100644 --- a/panels/dock/docksettings.cpp +++ b/panels/dock/docksettings.cpp @@ -86,6 +86,8 @@ static QString itemAlignment2String(const ItemAlignment& alignment) return "left"; case ItemAlignment::CenterAlignment: return "center"; + case ItemAlignment::FashionAlignment: + return "fashion"; } return "center"; @@ -112,6 +114,8 @@ static ItemAlignment string2ItenAlignment(const QString& alignmentStr) return ItemAlignment::LeftAlignment; else if (alignmentStr == "center") return ItemAlignment::CenterAlignment; + else if (alignmentStr == "fashion") + return ItemAlignment::FashionAlignment; return ItemAlignment::CenterAlignment; } diff --git a/panels/dock/package/FashionDockController.qml b/panels/dock/package/FashionDockController.qml new file mode 100644 index 000000000..d6e80a810 --- /dev/null +++ b/panels/dock/package/FashionDockController.qml @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk.style 1.0 as DStyle + +QtObject { + id: root + + property bool useColumnLayout: false + property int itemAlignment: Dock.CenterAlignment + property int position: Dock.Bottom + property int hideState: Dock.Show + property real dockSize: 0 + property real dockItemIconSize: 0 + property real devicePixelRatio: 1 + property int screenWidth: 0 + property int colorTheme: Dock.Light + property bool dragging: false + property var gridLayout: null + property var rightPart: null + + readonly property bool enabled: !useColumnLayout + && itemAlignment === Dock.FashionAlignment + && (position === Dock.Bottom || position === Dock.Top) + readonly property bool topMode: enabled && position === Dock.Top + readonly property bool bottomMode: enabled && position === Dock.Bottom + readonly property int floatingMargin: 8 + readonly property int verticalPadding: Math.max(6, Math.round(dockSize * 0.16)) + readonly property int surfaceThickness: useColumnLayout ? dockSize : dockSize + verticalPadding * 2 + readonly property int backgroundRadius: Math.round(surfaceThickness / 4) + readonly property real gridDisplayedWidth: enabled && gridLayout + ? gridLayout.implicitWidth + : 0 + readonly property real gridDisplayedHeight: enabled && gridLayout + ? gridLayout.implicitHeight + : 0 + readonly property real contentWidth: { + if (!enabled) { + return 0 + } + + let width = gridDisplayedWidth + if (rightPart && rightPart.visible) { + if (width > 0) { + width += gridLayout.columnSpacing + } + width += rightPart.implicitWidth + } + return width + } + readonly property bool widthAnimationEnabled: enabled && !dragging + + function effectiveShellWidth() { + if (!enabled) { + return 0 + } + + return Math.min(contentWidth, screenWidth) + } +} diff --git a/panels/dock/package/main.qml b/panels/dock/package/main.qml index a9a01f077..59b422a47 100644 --- a/panels/dock/package/main.qml +++ b/panels/dock/package/main.qml @@ -13,19 +13,42 @@ import Qt.labs.platform as LP import org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 import org.deepin.dtk 1.0 as D -import org.deepin.dtk.style 1.0 as DStyle Window { id: dock property int positionForAnimation: Panel.position property bool useColumnLayout: positionForAnimation % 2 + + FashionDockController { + id: fashionDockController + useColumnLayout: dock.useColumnLayout + itemAlignment: Panel.itemAlignment + position: dock.positionForAnimation + hideState: Panel.hideState + dockSize: dock.dockSize + dockItemIconSize: dock.dockItemIconSize + devicePixelRatio: Panel.devicePixelRatio > 0 ? Panel.devicePixelRatio : Screen.devicePixelRatio + screenWidth: Screen.width + colorTheme: Panel.colorTheme + dragging: dock.isDragging + gridLayout: gridLayout + rightPart: dockRightPart + } + + property alias fashionDock: fashionDockController property int dockCenterPartCount: dockCenterPartModel.count readonly property int dockRawCenterSpace: { if (useColumnLayout) { return Screen.height - dockLeftPart.implicitHeight - dockRightPart.implicitHeight; } else { - return Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth; + let space = Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth; + if (fashionDock.enabled && gridLayout) { + // 时尚模式下,dockRightPart 在 gridLayout 右侧,之间需要扣除 dockSpacing + // 这是为了确保 gridLayout 的宽度 + dockSpacing + rightPart 宽度 <= screenWidth + space -= Math.ceil(gridLayout.columnSpacing); + } + return Math.max(0, space); } } @@ -45,7 +68,13 @@ Window { property real dockItemIconSize: dockItemMaxSize * 9 / 14 // NOTE: -1 means not set its size, follow the platform size - width: positionForAnimation === Dock.Top || positionForAnimation === Dock.Bottom ? -1 : dockSize + width: { + if (fashionDock.enabled) { + return Math.max(1, fashionDock.effectiveShellWidth()) + } + + return positionForAnimation === Dock.Top || positionForAnimation === Dock.Bottom ? -1 : dockSize + } height: positionForAnimation === Dock.Left || positionForAnimation === Dock.Right ? -1 : dockSize color: "transparent" flags: Qt.WindowDoesNotAcceptFocus @@ -68,14 +97,21 @@ Window { MenuHelper.openMenu(dockMenuLoader.item) } - DLayerShellWindow.anchors: position2Anchors(positionForAnimation) + DLayerShellWindow.anchors: fashionDock.enabled + ? (fashionDock.topMode ? DLayerShellWindow.AnchorTop : DLayerShellWindow.AnchorBottom) + : position2Anchors(positionForAnimation) + DLayerShellWindow.topMargin: fashionDock.topMode ? fashionDock.floatingMargin : 0 + + DLayerShellWindow.bottomMargin: fashionDock.bottomMode ? fashionDock.floatingMargin : 0 DLayerShellWindow.layer: DLayerShellWindow.LayerTop - DLayerShellWindow.exclusionZone: Panel.hideMode === Dock.KeepShowing ? Applet.dockSize : 0 + DLayerShellWindow.exclusionZone: Panel.hideMode === Dock.KeepShowing + ? Applet.dockSize + (fashionDock.enabled ? fashionDock.floatingMargin : 0) + : 0 DLayerShellWindow.scope: "dde-shell/dock" DLayerShellWindow.keyboardInteractivity: DLayerShellWindow.KeyboardInteractivityNone D.DWindow.enabled: true - D.DWindow.windowRadius: 0 + D.DWindow.windowRadius: fashionDock.enabled ? fashionDock.backgroundRadius : 0 //TODO:由于windoweffect处理有BUG,导致动画结束后一致保持无阴影,无borderwidth状态。 无法恢复到最初的阴影和边框 //D.DWindow.windowEffect: hideShowAnimation.running ? D.PlatformHandle.EffectNoShadow | D.PlatformHandle.EffectNoBorder : 0 @@ -92,12 +128,21 @@ Window { D.ColorSelector.family: D.Palette.CrystalColor onDockSizeChanged: { - if (dock.dockSize === Dock.MIN_DOCK_SIZE) { + if (fashionDock.enabled) { + Panel.indicatorStyle = Dock.Fashion + } else if (dock.dockSize === Dock.MIN_DOCK_SIZE) { Panel.indicatorStyle = Dock.Efficient } else { Panel.indicatorStyle = Dock.Fashion } } + Behavior on width { + enabled: fashionDock.widthAnimationEnabled + NumberAnimation { + duration: 200 + easing.type: Easing.OutQuad + } + } Binding on itemIconSizeBase { when: !isDragging @@ -326,6 +371,11 @@ Window { prop: "itemAlignment" value: Dock.CenterAlignment } + EnumPropertyMenuItem { + name: qsTr("Fashion Mode") + prop: "itemAlignment" + value: Dock.FashionAlignment + } } MutuallyExclusiveMenu { title: qsTr("Position") @@ -414,7 +464,7 @@ Window { D.StyledBehindWindowBlur { control: parent anchors.fill: parent - cornerRadius: 0 + cornerRadius: fashionDock.enabled ? fashionDock.backgroundRadius : 0 blendColor: { if (valid) { return DStyle.Style.control.selectColor(undefined, @@ -500,7 +550,7 @@ Window { //此处为边距区域的点击实践特殊处理。 MouseArea { id: leftMarginArea - width: useColumnLayout ? parent.width : gridLayout.columnSpacing + width: useColumnLayout ? parent.width : (fashionDock.enabled ? 0 : gridLayout.columnSpacing) height: useColumnLayout ? gridLayout.rowSpacing : parent.height anchors.left: parent.left anchors.top: parent.top @@ -529,13 +579,14 @@ Window { Item { id: leftMargin + visible: !fashionDock.enabled implicitWidth: 0 implicitHeight: 0 } Item { id: dockLeftPart - visible: dockLeftPartModel.count > 0 + visible: !fashionDock.enabled && dockLeftPartModel.count > 0 implicitWidth: leftLoader.implicitWidth implicitHeight: leftLoader.implicitHeight Accessible.role: Accessible.Grouping @@ -562,7 +613,7 @@ Window { Layout.maximumHeight: useColumnLayout ? dockRawCenterSpace : -1 onXChanged: dockCenterPartPosChanged() onYChanged: dockCenterPartPosChanged() - Layout.leftMargin: !useColumnLayout && Panel.itemAlignment === Dock.CenterAlignment ? + Layout.leftMargin: !useColumnLayout && !fashionDock.enabled && Panel.itemAlignment === Dock.CenterAlignment ? Math.max(0, (dock.width - dockCenterPart.implicitWidth) / 2 - (dockLeftPart.implicitWidth + 20) + Math.min((dock.width - dockCenterPart.implicitWidth) / 2 - (dockRightPart.implicitWidth + 20), 0)) : 0 Layout.topMargin: useColumnLayout && Panel.itemAlignment === Dock.CenterAlignment ? Math.max(0, (dock.height - dockCenterPart.implicitHeight) / 2 - (dockLeftPart.implicitHeight + 20) + Math.min((dock.height - dockCenterPart.implicitHeight) / 2 - (dockRightPart.implicitHeight + 20), 0)) : 0 @@ -597,8 +648,9 @@ Window { } Item { - Layout.fillWidth: true - Layout.fillHeight: true + Layout.fillWidth: !fashionDock.enabled + Layout.fillHeight: !fashionDock.enabled + visible: !fashionDock.enabled } } diff --git a/panels/dock/showdesktop/package/showdesktop.qml b/panels/dock/showdesktop/package/showdesktop.qml index 7a44d4237..fb50f415b 100644 --- a/panels/dock/showdesktop/package/showdesktop.qml +++ b/panels/dock/showdesktop/package/showdesktop.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023-2026 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -13,11 +13,13 @@ AppletItem { id: showdesktop readonly property int showDesktopWidth: 10 property bool useColumnLayout: Panel.position % 2 + readonly property bool fashionMode: Panel.rootObject && Panel.rootObject.fashionDock && Panel.rootObject.fashionDock.enabled property int dockSize: Panel.rootObject.dockItemMaxSize property int dockOrder: 30 - property bool shouldVisible: Applet.visible - implicitWidth: useColumnLayout ? Panel.rootObject.dockSize : showDesktopWidth - implicitHeight: useColumnLayout ? showDesktopWidth : Panel.rootObject.dockSize + property bool shouldVisible: Applet.visible && !fashionMode + visible: shouldVisible + implicitWidth: shouldVisible ? (useColumnLayout ? Panel.rootObject.dockSize : showDesktopWidth) : 0 + implicitHeight: shouldVisible ? (useColumnLayout ? showDesktopWidth : Panel.rootObject.dockSize) : 0 PanelToolTip { id: toolTip diff --git a/panels/dock/taskmanager/package/TaskManager.qml b/panels/dock/taskmanager/package/TaskManager.qml index 1177a52d9..761a626cd 100644 --- a/panels/dock/taskmanager/package/TaskManager.qml +++ b/panels/dock/taskmanager/package/TaskManager.qml @@ -14,6 +14,8 @@ ContainmentItem { id: taskmanager property bool useColumnLayout: Panel.rootObject.useColumnLayout property int dockOrder: 16 + readonly property bool fashionMode: Panel.rootObject && Panel.rootObject.fashionDock && Panel.rootObject.fashionDock.enabled + readonly property bool horizontalFashionMode: fashionMode && !useColumnLayout Accessible.role: Accessible.Grouping Accessible.name: qsTr("Task Manager") @@ -24,7 +26,9 @@ ContainmentItem { return Panel.rootObject.dockRawCenterSpace - otherOccupied; } - readonly property real remainingSpacesForTaskManager: calcRemainingSpace(Panel.rootObject.dockItemMaxSize) + property real remainingSpacesForTaskManager: fashionMode + ? 0 + : calcRemainingSpace(Panel.rootObject.dockItemMaxSize) readonly property int appTitleSpacing: Math.max(10, Math.round(Panel.rootObject.dockItemMaxSize * 9 / 14) / 3) // Start padding for the app container so that the visual gap // (multitask icon right edge → first app icon left edge) = appTitleSpacing. @@ -40,12 +44,14 @@ ContainmentItem { readonly property real startPadding: Math.max(0, appTitleSpacing - (Panel.rootObject.dockItemMaxSize * (multitaskViewIconRatio - iconWidthToMaxSizeRatio) / 2)) implicitWidth: { + if (horizontalFashionMode) return appContainer.implicitWidth + (useColumnLayout ? 0 : startPadding) let extra = useColumnLayout ? 0 : startPadding let w = appContainer.implicitWidth + extra let maxW = Panel.itemAlignment === Dock.LeftAlignment ? Math.max(remainingSpacesForTaskManager, w) : Math.min(remainingSpacesForTaskManager, w) return useColumnLayout ? Panel.rootObject.dockSize : maxW } implicitHeight: { + if (fashionMode && useColumnLayout) return appContainer.implicitHeight + (useColumnLayout ? startPadding : 0) let extra = useColumnLayout ? startPadding : 0 let h = appContainer.implicitHeight + extra let maxH = Panel.itemAlignment === Dock.LeftAlignment ? Math.max(remainingSpacesForTaskManager, h) : Math.min(remainingSpacesForTaskManager, h) @@ -84,7 +90,7 @@ ContainmentItem { TextCalculator { id: textCalculator - enabled: taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top) + enabled: !fashionMode && taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top) dataModel: taskmanager.Applet.dataModel iconSize: Panel.rootObject.dockSize * 9 / 14 spacing: Math.max(10, Math.round(textCalculator.iconSize) / 3) From af02b08cb898c305369fc49e383eb75864e1efe2 Mon Sep 17 00:00:00 2001 From: wjyrich Date: Mon, 6 Jul 2026 14:41:54 +0800 Subject: [PATCH 2/4] feat: add card-type plugin support to dock panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add Card plugin type to TrayPluginType enum in constants.h 2. Create CardLeftDockArea QML component for displaying card-type plugins in a carousel 3. Integrate card plugin surfaces into DockCompositor lifecycle (add/ remove/find) 4. Implement card area with left-side dock placement, using current/ index navigation 5. Support hover effects, page indicators, and wheel navigation for card area 6. Replace legacy left plugin area with CardLeftDockArea when card plugins exist Log: Added card-type plugin display area with carousel navigation in dock Influence: 1. Test card plugin registration and display in dock left area 2. Verify card plugin area visibility only when card plugins exist 3. Test mouse wheel navigation through card plugin pages 4. Verify hover effects and page indicator behavior 5. Test card plugin surface geometry updates on resize 6. Verify compatibility with existing tray/quick/fixed plugin types 7. Test card area behavior in both horizontal and vertical dock modes 8. Verify area width adapts to dock size and card content feat: 为任务栏添加卡片类型插件支持 1. 在 constants.h 中添加 Card 插件类型到 TrayPluginType 枚举 2. 创建 CardLeftDockArea QML 组件,用于以轮播方式显示卡片类型插件 3. 将卡片插件表面集成到 DockCompositor 的生命周期(添加/移除/查找) 4. 实现左侧任务栏卡片区域,支持当前索引/导航 5. 支持悬停效果、页面指示器和鼠标滚轮导航 6. 当存在卡片插件时,用 CardLeftDockArea 替换传统的左侧插件区域 Log: 新增卡片类型插件显示区域,支持轮播导航 Influence: 1. 测试卡片插件的注册和在任务栏左侧区域的显示 2. 验证卡片插件区域仅在存在卡片插件时可见 3. 测试通过鼠标滚轮在卡片插件页面间导航 4. 验证悬停效果和页面指示器的行为 5. 测试卡片插件表面在窗口大小调整时的几何更新 6. 验证与现有托盘/快捷/固定插件类型的兼容性 7. 测试卡片区域在任务栏水平和垂直模式下的表现 8. 验证区域宽度根据任务栏尺寸和卡片内容自适应 PMS: TASK-392671 --- panels/dock/DockCompositor.qml | 6 + panels/dock/cardleft/CMakeLists.txt | 5 + .../cardleft/package/CardLeftDockArea.qml | 216 ++++++++++++++++++ panels/dock/cardleft/package/metadata.json | 8 + panels/dock/constants.h | 1 + panels/dock/dockpanel.cpp | 11 + panels/dock/dockpanel.h | 4 + panels/dock/package/FashionDockController.qml | 65 ------ panels/dock/package/main.qml | 81 ++++--- .../dock/showdesktop/package/showdesktop.qml | 3 +- .../dock/taskmanager/package/TaskManager.qml | 7 +- 11 files changed, 305 insertions(+), 102 deletions(-) create mode 100644 panels/dock/cardleft/CMakeLists.txt create mode 100644 panels/dock/cardleft/package/CardLeftDockArea.qml create mode 100644 panels/dock/cardleft/package/metadata.json delete mode 100644 panels/dock/package/FashionDockController.qml diff --git a/panels/dock/DockCompositor.qml b/panels/dock/DockCompositor.qml index e206ee7a4..9d58d5b18 100644 --- a/panels/dock/DockCompositor.qml +++ b/panels/dock/DockCompositor.qml @@ -30,6 +30,7 @@ Item { property ListModel trayPluginSurfaces: ListModel {} property ListModel quickPluginSurfaces: ListModel {} property ListModel fixedPluginSurfaces: ListModel {} + property ListModel cardPluginSurfaces: ListModel {} property var compositor: waylandCompositor property var panelScale: 1.0 @@ -62,6 +63,7 @@ Item { let ret = findSurfaceFromModel(trayPluginSurfaces, surfaceId) if (ret === null) ret = findSurfaceFromModel(quickPluginSurfaces, surfaceId) if (ret === null) ret = findSurfaceFromModel(fixedPluginSurfaces, surfaceId) + if (ret === null) ret = findSurfaceFromModel(cardPluginSurfaces, surfaceId) return ret } @@ -92,6 +94,8 @@ Item { quickPluginSurfaces.append({shellSurface: dockPluginSurface}) } else if (dockPluginSurface.pluginType === Dock.Fixed) { fixedPluginSurfaces.append({shellSurface: dockPluginSurface}) + } else if (dockPluginSurface.pluginType === Dock.Card) { + cardPluginSurfaces.append({shellSurface: dockPluginSurface}) } dockCompositor.pluginSurfacesUpdated() } @@ -104,6 +108,8 @@ Item { removeDockPluginSurface(quickPluginSurfaces, dockPluginSurface) } else if (dockPluginSurface.pluginType === Dock.Fixed) { removeDockPluginSurface(fixedPluginSurfaces, dockPluginSurface) + } else if (dockPluginSurface.pluginType === Dock.Card) { + removeDockPluginSurface(cardPluginSurfaces, dockPluginSurface) } dockCompositor.pluginSurfacesUpdated() } diff --git a/panels/dock/cardleft/CMakeLists.txt b/panels/dock/cardleft/CMakeLists.txt new file mode 100644 index 000000000..a049b858f --- /dev/null +++ b/panels/dock/cardleft/CMakeLists.txt @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +ds_install_package(PACKAGE org.deepin.ds.dock.cardleft) diff --git a/panels/dock/cardleft/package/CardLeftDockArea.qml b/panels/dock/cardleft/package/CardLeftDockArea.qml new file mode 100644 index 000000000..9f9fa150b --- /dev/null +++ b/panels/dock/cardleft/package/CardLeftDockArea.qml @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 +import QtWayland.Compositor + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +AppletDockItem { + id: root + + readonly property bool fashionMode: Panel.fashionMode + readonly property int dockSize: Panel.rootObject.dockSize + readonly property int hoverInset: 3 + readonly property real taskbarRadius: Panel.rootObject.fashionDock.backgroundRadius + readonly property real hoverBackgroundRadius: taskbarRadius - hoverInset + readonly property int adaptiveCardLeftWidth: 150 + Math.max(0, dockSize / 4) + readonly property int rightContentPadding: Math.max(10, Math.round(adaptiveCardLeftWidth * 0.07)) + readonly property int verticalInset: Math.max(5, Math.round(dockSize * 0.16)) + readonly property int pageContentHeight: Math.max(24, dockSize - verticalInset * 2) + + readonly property color primaryTextColor: Panel.colorTheme === Dock.Dark + ? Qt.rgba(1, 1, 1, 0.96) + : Qt.rgba(0, 0, 0, 0.92) + + dockOrder: 5 + shouldVisible: fashionMode && pageCount > 0 + readonly property int pageCount: DockCompositor.cardPluginSurfaces.count + property bool contentHovered: false + readonly property bool effectiveHovered: rootHoverHandler.hovered || contentHovered + + visible: shouldVisible + implicitWidth: adaptiveCardLeftWidth + implicitHeight: dockSize + clip: true + + function updateContentHovered() { + const item = swipeView.currentItem + contentHovered = !!item && item.surfaceHovered + } + + HoverHandler { + id: rootHoverHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + } + + AppletItemBackground { + x: root.hoverInset + y: root.hoverInset + width: parent.width - root.hoverInset + height: parent.height - root.hoverInset * 2 + radius: root.hoverBackgroundRadius + enabled: false + opacity: root.effectiveHovered ? 1 : 0 + D.ColorSelector.hovered: root.effectiveHovered + + Behavior on opacity { + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + } + + Item { + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + visible: root.effectiveHovered && root.pageCount > 1 + width: pageIndicator.implicitHeight + height: pageIndicator.implicitWidth + + PageIndicator { + id: pageIndicator + + anchors.centerIn: parent + rotation: 90 + count: swipeView.count + currentIndex: swipeView.currentIndex + padding: 0 + spacing: 4 + + delegate: Rectangle { + required property int index + + implicitWidth: 2 + implicitHeight: 2 + radius: width / 2 + color: root.primaryTextColor + opacity: index === pageIndicator.currentIndex ? 1 : 0.35 + } + } + } + + SwipeView { + id: swipeView + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 8 + anchors.rightMargin: root.rightContentPadding + height: root.pageContentHeight + orientation: Qt.Vertical + interactive: count > 1 + clip: true + + onCurrentItemChanged: { + root.updateContentHovered() + surfaceGeometryUpdateTimer.restart() + } + + Repeater { + model: DockCompositor.cardPluginSurfaces + + delegate: Item { + id: surfaceHost + + property var plugin: model.shellSurface + readonly property bool surfaceHovered: SwipeView.isCurrentItem && surfaceItem.hovered + + ShellSurfaceItemProxy { + id: surfaceItem + width: parent.width + height: parent.height + shellSurface: surfaceHost.plugin + } + + function updateSurfaceGeometry() { + if (!plugin || !SwipeView.isCurrentItem || !surfaceHost.Window.window) { + return + } + + const window = surfaceHost.Window.window + const windowPosition = surfaceHost.mapToItem(window.contentItem, 0, 0) + const globalPosition = Qt.point(windowPosition.x + window.x, + windowPosition.y + window.y) + + plugin.updatePluginGeometry(Qt.rect(Math.round(windowPosition.x), + Math.round(windowPosition.y), + Math.round(width), + Math.round(height))) + plugin.setGlobalPos(Qt.point(Math.round(globalPosition.x), + Math.round(globalPosition.y))) + surfaceItem.fixPosition() + } + + Component.onCompleted: updateSurfaceGeometry() + onWidthChanged: geometryUpdateTimer.restart() + onHeightChanged: geometryUpdateTimer.restart() + onVisibleChanged: { + geometryUpdateTimer.restart() + root.updateContentHovered() + } + onSurfaceHoveredChanged: root.updateContentHovered() + + Timer { + id: geometryUpdateTimer + interval: 50 + repeat: false + onTriggered: surfaceHost.updateSurfaceGeometry() + } + } + } + } + + Item { + anchors.fill: parent + z: 1 + + WheelHandler { + target: null + enabled: swipeView.count > 1 + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + + onWheel: function(wheel) { + const deltaY = wheel.angleDelta.y !== 0 ? wheel.angleDelta.y : wheel.pixelDelta.y + if (deltaY === 0) { + return + } + + const step = deltaY < 0 ? 1 : -1 + const nextIndex = (swipeView.currentIndex + step + swipeView.count) % swipeView.count + swipeView.setCurrentIndex(nextIndex) + wheel.accepted = true + } + } + } + + Connections { + target: swipeView.contentItem + ignoreUnknownSignals: true + + function onContentYChanged() { + surfaceGeometryUpdateTimer.restart() + } + } + + Timer { + id: surfaceGeometryUpdateTimer + + interval: 50 + repeat: false + onTriggered: { + const item = swipeView.currentItem + if (item) { + item.updateSurfaceGeometry() + } + } + } +} diff --git a/panels/dock/cardleft/package/metadata.json b/panels/dock/cardleft/package/metadata.json new file mode 100644 index 000000000..b18a9e068 --- /dev/null +++ b/panels/dock/cardleft/package/metadata.json @@ -0,0 +1,8 @@ +{ + "Plugin": { + "Version": "1.0", + "Id": "org.deepin.ds.dock.cardleft", + "Url": "CardLeftDockArea.qml", + "Parent": "org.deepin.ds.dock" + } +} diff --git a/panels/dock/constants.h b/panels/dock/constants.h index 69fc3823b..7a91c210d 100644 --- a/panels/dock/constants.h +++ b/panels/dock/constants.h @@ -80,6 +80,7 @@ enum TrayPluginType { Tray = 1, Fixed, Quick, + Card, }; enum TrayPluginSizePolicy { diff --git a/panels/dock/dockpanel.cpp b/panels/dock/dockpanel.cpp index b9488979f..3cb4d7426 100644 --- a/panels/dock/dockpanel.cpp +++ b/panels/dock/dockpanel.cpp @@ -100,6 +100,7 @@ bool DockPanel::init() }); connect(SETTINGS, &DockSettings::positionChanged, this, [this, dockDaemonAdaptor](){ Q_EMIT positionChanged(position()); + Q_EMIT fashionModeChanged(); Q_EMIT dockDaemonAdaptor->PositionChanged(position()); Q_EMIT dockDaemonAdaptor->FrontendWindowRectChanged(frontendWindowRect()); @@ -117,6 +118,9 @@ bool DockPanel::init() connect(SETTINGS, &DockSettings::dockSizeChanged, this, &DockPanel::dockSizeChanged); connect(SETTINGS, &DockSettings::hideModeChanged, this, &DockPanel::hideModeChanged); connect(SETTINGS, &DockSettings::itemAlignmentChanged, this, &DockPanel::itemAlignmentChanged); + connect(SETTINGS, &DockSettings::itemAlignmentChanged, this, [this](){ + Q_EMIT fashionModeChanged(); + }); connect(SETTINGS, &DockSettings::indicatorStyleChanged, this, &DockPanel::indicatorStyleChanged); connect(SETTINGS, &DockSettings::lockedChanged, this, &DockPanel::lockedChanged); connect(SETTINGS, &DockSettings::contextMenuEnabledChanged, this, &DockPanel::contextMenuEnabledChanged); @@ -299,6 +303,13 @@ void DockPanel::setItemAlignment(const ItemAlignment& alignment) SETTINGS->setItemAlignment(alignment); } +bool DockPanel::fashionMode() +{ + const auto dockPosition = position(); + return itemAlignment() == FashionAlignment + && (dockPosition == Top || dockPosition == Bottom); +} + IndicatorStyle DockPanel::indicatorStyle() { return SETTINGS->indicatorStyle(); diff --git a/panels/dock/dockpanel.h b/panels/dock/dockpanel.h index 1b4be8429..ae27ff1cf 100644 --- a/panels/dock/dockpanel.h +++ b/panels/dock/dockpanel.h @@ -30,6 +30,7 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext Q_PROPERTY(HideMode hideMode READ hideMode WRITE setHideMode NOTIFY hideModeChanged FINAL) Q_PROPERTY(Position position READ position WRITE setPosition NOTIFY positionChanged FINAL) Q_PROPERTY(ItemAlignment itemAlignment READ itemAlignment WRITE setItemAlignment NOTIFY itemAlignmentChanged FINAL) + Q_PROPERTY(bool fashionMode READ fashionMode NOTIFY fashionModeChanged FINAL) Q_PROPERTY(IndicatorStyle indicatorStyle READ indicatorStyle WRITE setIndicatorStyle NOTIFY indicatorStyleChanged FINAL) Q_PROPERTY(bool showInPrimary READ showInPrimary WRITE setShowInPrimary NOTIFY showInPrimaryChanged FINAL) Q_PROPERTY(QString screenName READ screenName NOTIFY screenNameChanged FINAL) @@ -73,6 +74,8 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext ItemAlignment itemAlignment(); void setItemAlignment(const ItemAlignment& alignment); + bool fashionMode(); + IndicatorStyle indicatorStyle(); void setIndicatorStyle(const IndicatorStyle& style); @@ -129,6 +132,7 @@ private Q_SLOTS: void beforePositionChanged(Position beforePosition); void positionChanged(Position position); void itemAlignmentChanged(ItemAlignment alignment); + void fashionModeChanged(); void indicatorStyleChanged(IndicatorStyle style); void showInPrimaryChanged(bool showInPrimary); void dockScreenChanged(QScreen *screen); diff --git a/panels/dock/package/FashionDockController.qml b/panels/dock/package/FashionDockController.qml deleted file mode 100644 index d6e80a810..000000000 --- a/panels/dock/package/FashionDockController.qml +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: GPL-3.0-or-later - -import QtQuick 2.15 - -import org.deepin.ds 1.0 -import org.deepin.ds.dock 1.0 -import org.deepin.dtk.style 1.0 as DStyle - -QtObject { - id: root - - property bool useColumnLayout: false - property int itemAlignment: Dock.CenterAlignment - property int position: Dock.Bottom - property int hideState: Dock.Show - property real dockSize: 0 - property real dockItemIconSize: 0 - property real devicePixelRatio: 1 - property int screenWidth: 0 - property int colorTheme: Dock.Light - property bool dragging: false - property var gridLayout: null - property var rightPart: null - - readonly property bool enabled: !useColumnLayout - && itemAlignment === Dock.FashionAlignment - && (position === Dock.Bottom || position === Dock.Top) - readonly property bool topMode: enabled && position === Dock.Top - readonly property bool bottomMode: enabled && position === Dock.Bottom - readonly property int floatingMargin: 8 - readonly property int verticalPadding: Math.max(6, Math.round(dockSize * 0.16)) - readonly property int surfaceThickness: useColumnLayout ? dockSize : dockSize + verticalPadding * 2 - readonly property int backgroundRadius: Math.round(surfaceThickness / 4) - readonly property real gridDisplayedWidth: enabled && gridLayout - ? gridLayout.implicitWidth - : 0 - readonly property real gridDisplayedHeight: enabled && gridLayout - ? gridLayout.implicitHeight - : 0 - readonly property real contentWidth: { - if (!enabled) { - return 0 - } - - let width = gridDisplayedWidth - if (rightPart && rightPart.visible) { - if (width > 0) { - width += gridLayout.columnSpacing - } - width += rightPart.implicitWidth - } - return width - } - readonly property bool widthAnimationEnabled: enabled && !dragging - - function effectiveShellWidth() { - if (!enabled) { - return 0 - } - - return Math.min(contentWidth, screenWidth) - } -} diff --git a/panels/dock/package/main.qml b/panels/dock/package/main.qml index 59b422a47..d8a1cd8ff 100644 --- a/panels/dock/package/main.qml +++ b/panels/dock/package/main.qml @@ -19,23 +19,34 @@ Window { property int positionForAnimation: Panel.position property bool useColumnLayout: positionForAnimation % 2 - FashionDockController { - id: fashionDockController - useColumnLayout: dock.useColumnLayout - itemAlignment: Panel.itemAlignment - position: dock.positionForAnimation - hideState: Panel.hideState - dockSize: dock.dockSize - dockItemIconSize: dock.dockItemIconSize - devicePixelRatio: Panel.devicePixelRatio > 0 ? Panel.devicePixelRatio : Screen.devicePixelRatio - screenWidth: Screen.width - colorTheme: Panel.colorTheme - dragging: dock.isDragging - gridLayout: gridLayout - rightPart: dockRightPart - } + property alias fashionDock: fashionDockState + + QtObject { + id: fashionDockState - property alias fashionDock: fashionDockController + readonly property bool enabled: !dock.useColumnLayout + && Panel.itemAlignment === Dock.FashionAlignment + && (dock.positionForAnimation === Dock.Bottom || dock.positionForAnimation === Dock.Top) + readonly property int floatingMargin: 8 + readonly property int backgroundRadius: { + const verticalPadding = Math.max(6, Math.round(dock.dockSize * 0.16)) + return Math.round((dock.dockSize + verticalPadding * 2) / 4) + } + readonly property real contentWidth: { + if (!enabled) { + return 0 + } + + let width = gridLayout.implicitWidth + if (dockRightPart.visible) { + if (width > 0) { + width += gridLayout.columnSpacing + } + width += dockRightPart.implicitWidth + } + return width + } + } property int dockCenterPartCount: dockCenterPartModel.count readonly property int dockRawCenterSpace: { @@ -45,8 +56,8 @@ Window { let space = Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth; if (fashionDock.enabled && gridLayout) { // 时尚模式下,dockRightPart 在 gridLayout 右侧,之间需要扣除 dockSpacing - // 这是为了确保 gridLayout 的宽度 + dockSpacing + rightPart 宽度 <= screenWidth - space -= Math.ceil(gridLayout.columnSpacing); + // 同时保留左右悬浮间距,避免窗口铺满屏幕后圆角边框被屏幕边缘裁剪。 + space -= Math.ceil(gridLayout.columnSpacing) + fashionDock.floatingMargin * 2; } return Math.max(0, space); } @@ -70,7 +81,8 @@ Window { // NOTE: -1 means not set its size, follow the platform size width: { if (fashionDock.enabled) { - return Math.max(1, fashionDock.effectiveShellWidth()) + const maximumWidth = Math.max(1, Screen.width - fashionDock.floatingMargin * 2) + return Math.max(1, Math.min(fashionDock.contentWidth, maximumWidth)) } return positionForAnimation === Dock.Top || positionForAnimation === Dock.Bottom ? -1 : dockSize @@ -98,11 +110,15 @@ Window { } DLayerShellWindow.anchors: fashionDock.enabled - ? (fashionDock.topMode ? DLayerShellWindow.AnchorTop : DLayerShellWindow.AnchorBottom) + ? (positionForAnimation === Dock.Top ? DLayerShellWindow.AnchorTop : DLayerShellWindow.AnchorBottom) : position2Anchors(positionForAnimation) - DLayerShellWindow.topMargin: fashionDock.topMode ? fashionDock.floatingMargin : 0 + DLayerShellWindow.topMargin: fashionDock.enabled && positionForAnimation === Dock.Top + ? fashionDock.floatingMargin + : 0 - DLayerShellWindow.bottomMargin: fashionDock.bottomMode ? fashionDock.floatingMargin : 0 + DLayerShellWindow.bottomMargin: fashionDock.enabled && positionForAnimation === Dock.Bottom + ? fashionDock.floatingMargin + : 0 DLayerShellWindow.layer: DLayerShellWindow.LayerTop DLayerShellWindow.exclusionZone: Panel.hideMode === Dock.KeepShowing ? Applet.dockSize + (fashionDock.enabled ? fashionDock.floatingMargin : 0) @@ -137,7 +153,7 @@ Window { } } Behavior on width { - enabled: fashionDock.widthAnimationEnabled + enabled: fashionDock.enabled && !dock.isDragging NumberAnimation { duration: 200 easing.type: Easing.OutQuad @@ -153,7 +169,7 @@ Window { PropertyAnimation { id: hideShowAnimation; // Currently, Wayland (Treeland) doesn't support StyledBehindWindowBlur inside the window, thus we keep using the window size approach on Wayland - property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" + property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" && !fashionDock.enabled property bool hiding: false target: useTransformBasedAnimation ? dockTransform : dock; property: { @@ -181,7 +197,7 @@ Window { Connections { target: dockTransform - enabled: Qt.platform.pluginName === "xcb" && hideShowAnimation.running + enabled: hideShowAnimation.useTransformBasedAnimation && hideShowAnimation.running function onXChanged() { if (dock.useColumnLayout) { @@ -198,7 +214,7 @@ Window { Connections { target: dock - enabled: Qt.platform.pluginName !== "xcb" && hideShowAnimation.running + enabled: !hideShowAnimation.useTransformBasedAnimation && hideShowAnimation.running function onWidthChanged() { if (dock.useColumnLayout) { @@ -226,7 +242,7 @@ Window { SequentialAnimation { id: dockAnimation - property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" + property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" && !fashionDock.enabled property bool isShowing: false property bool isPositionChanging: false property var target: useTransformBasedAnimation ? dockTransform : dock @@ -569,7 +585,12 @@ Window { // TODO: remove GridLayout and use delegatechosser manager all items GridLayout { id: gridLayout - anchors.fill: parent + anchors { + fill: parent + rightMargin: fashionDock.enabled && dockRightPart.visible + ? dockRightPart.implicitWidth + Math.ceil(gridLayout.columnSpacing) + : 0 + } columns: 1 rows: 1 flow: useColumnLayout ? GridLayout.LeftToRight : GridLayout.TopToBottom @@ -586,11 +607,9 @@ Window { Item { id: dockLeftPart - visible: !fashionDock.enabled && dockLeftPartModel.count > 0 + visible: dockLeftPartModel.count > 0 implicitWidth: leftLoader.implicitWidth implicitHeight: leftLoader.implicitHeight - Accessible.role: Accessible.Grouping - Accessible.name: qsTr("Dock Left Area") OverflowContainer { id: leftLoader anchors.fill: parent diff --git a/panels/dock/showdesktop/package/showdesktop.qml b/panels/dock/showdesktop/package/showdesktop.qml index fb50f415b..d3926c0e3 100644 --- a/panels/dock/showdesktop/package/showdesktop.qml +++ b/panels/dock/showdesktop/package/showdesktop.qml @@ -13,8 +13,7 @@ AppletItem { id: showdesktop readonly property int showDesktopWidth: 10 property bool useColumnLayout: Panel.position % 2 - readonly property bool fashionMode: Panel.rootObject && Panel.rootObject.fashionDock && Panel.rootObject.fashionDock.enabled - property int dockSize: Panel.rootObject.dockItemMaxSize + readonly property bool fashionMode: Panel.fashionMode property int dockOrder: 30 property bool shouldVisible: Applet.visible && !fashionMode visible: shouldVisible diff --git a/panels/dock/taskmanager/package/TaskManager.qml b/panels/dock/taskmanager/package/TaskManager.qml index 761a626cd..02de4a311 100644 --- a/panels/dock/taskmanager/package/TaskManager.qml +++ b/panels/dock/taskmanager/package/TaskManager.qml @@ -14,8 +14,7 @@ ContainmentItem { id: taskmanager property bool useColumnLayout: Panel.rootObject.useColumnLayout property int dockOrder: 16 - readonly property bool fashionMode: Panel.rootObject && Panel.rootObject.fashionDock && Panel.rootObject.fashionDock.enabled - readonly property bool horizontalFashionMode: fashionMode && !useColumnLayout + readonly property bool fashionMode: Panel.fashionMode Accessible.role: Accessible.Grouping Accessible.name: qsTr("Task Manager") @@ -44,14 +43,14 @@ ContainmentItem { readonly property real startPadding: Math.max(0, appTitleSpacing - (Panel.rootObject.dockItemMaxSize * (multitaskViewIconRatio - iconWidthToMaxSizeRatio) / 2)) implicitWidth: { - if (horizontalFashionMode) return appContainer.implicitWidth + (useColumnLayout ? 0 : startPadding) + if (fashionMode) return appContainer.implicitWidth + (useColumnLayout ? 0 : startPadding) let extra = useColumnLayout ? 0 : startPadding let w = appContainer.implicitWidth + extra let maxW = Panel.itemAlignment === Dock.LeftAlignment ? Math.max(remainingSpacesForTaskManager, w) : Math.min(remainingSpacesForTaskManager, w) return useColumnLayout ? Panel.rootObject.dockSize : maxW } implicitHeight: { - if (fashionMode && useColumnLayout) return appContainer.implicitHeight + (useColumnLayout ? startPadding : 0) + if (fashionMode && useColumnLayout) return appContainer.implicitHeight + startPadding let extra = useColumnLayout ? startPadding : 0 let h = appContainer.implicitHeight + extra let maxH = Panel.itemAlignment === Dock.LeftAlignment ? Math.max(remainingSpacesForTaskManager, h) : Math.min(remainingSpacesForTaskManager, h) From 7331383fde9ec90d768466bf59c313bdf0425512 Mon Sep 17 00:00:00 2001 From: wjyrich Date: Tue, 21 Jul 2026 20:53:36 +0800 Subject: [PATCH 3/4] fix: implement dock item overflow and panel popup improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Added `TaskOverflowController` C++ class and QML `TaskOverflowButton` for adaptive overflow when taskbar items exceed available space 2. Added `closeOnInactive` property to PanelPopup to allow popups to stay open during context menus and drag operations 3. Fixed `DockPositioner` to handle cross-window popup positioning correctly 4. Adjusted dock center space calculation to account for grid spacing and edge alignment margins 5. Made hidden delegates (moved to overflow popup) properly not contribute to layout size 6. Fixed drag-and-drop handling for overflow popup items and model reordering 7. Fixed `AppItemTitle` to only calculate text width when enabled 8. Improved text calculator to use deferred calculation and consistent state updates 9. Updated `MenuHelper` to set active menu before opening to prevent race conditions Log: Added task overflow feature that moves extra dock items into a popup when space is insufficient; fixed dock popup positioning and drag- and-drop interactions across windows Influence: 1. Test dock taskbar overflow behavior by adding many applications and resizing dock - verify overflow button appears and popup functions correctly 2. Test overflow popup item drag-and-drop: move items in/out of overflow popup 3. Test context menus on app items with overflow active - verify popup stays open 4. Test dock center alignment spacing and appearance 5. Test dock panel popup positioning with cross-window (X11) scenarios 6. Test drag-and-drop from launcher to dock with overflow active 7. Test left/top/right dock positions with overflow feature 8. Test performance with many docked applications fix: 实现任务栏溢出功能和面板弹出窗口改进 1. 添加 `TaskOverflowController` C++ 类和 `TaskOverflowButton` QML 组 件,实现任务栏项目超出可用空间时的自适应溢出 2. 为 PanelPopup 添加 `closeOnInactive` 属性,允许在右键菜单和拖拽操作期 间保持弹出窗口打开 3. 修复 DockPositioner 以正确处理跨窗口的弹出窗口定位 4. 调整 dock 中心空间计算,考虑网格间距和边缘对齐边距 5. 使隐藏的委托(移动到溢出弹出窗口)正确不贡献布局大小 6. 修复溢出弹出窗口项目的拖放处理和模型重排序 7. 修复 AppItemTitle 仅在启用时计算文本宽度 8. 改进文本计算器,使用延迟计算和一致的状态更新 9. 更新 MenuHelper,在打开之前设置活动菜单,防止竞态条件 Log: 新增任务栏溢出功能,当空间不足时将额外项目移到弹出窗口;修复 dock 弹出窗口定位和跨窗口的拖放交互 Influence: 1. 测试任务栏溢出行为:添加多个应用并调整dock大小,验证溢出按钮出现且弹 出窗口功能正常 2. 测试溢出弹出窗口项目的拖放:移入/移出溢出弹出窗口 3. 测试在溢出活跃时对应用项目操作右键菜单,验证弹出窗口保持打开 4. 测试 dock 居中对齐间距和外观 5. 测试跨窗口(X11)场景下的 dock 弹出窗口定位 6. 测试从启动器拖放到具有溢出功能的 dock 7. 测试左/上/右 dock 位置与溢出功能 8. 测试多个固定应用程序时的性能 --- frame/qml/PanelPopup.qml | 10 +- panels/dock/MenuHelper.qml | 2 +- panels/dock/OverflowContainer.qml | 10 + panels/dock/dockpositioner.cpp | 71 ++++- panels/dock/dockpositioner.h | 3 + panels/dock/package/main.qml | 18 +- panels/dock/taskmanager/CMakeLists.txt | 5 +- .../taskmanager/dockglobalelementmodel.cpp | 3 + panels/dock/taskmanager/package/AppItem.qml | 69 ++-- .../dock/taskmanager/package/AppItemTitle.qml | 2 +- .../dock/taskmanager/package/TaskManager.qml | 132 +++++++- .../package/TaskOverflowButton.qml | 230 ++++++++++++++ panels/dock/taskmanager/taskmanager.cpp | 2 + .../taskmanager/taskoverflowcontroller.cpp | 294 ++++++++++++++++++ .../dock/taskmanager/taskoverflowcontroller.h | 121 +++++++ panels/dock/taskmanager/textcalculator.cpp | 35 ++- panels/dock/taskmanager/textcalculator.h | 1 + 17 files changed, 940 insertions(+), 68 deletions(-) create mode 100644 panels/dock/taskmanager/package/TaskOverflowButton.qml create mode 100644 panels/dock/taskmanager/taskoverflowcontroller.cpp create mode 100644 panels/dock/taskmanager/taskoverflowcontroller.h diff --git a/frame/qml/PanelPopup.qml b/frame/qml/PanelPopup.qml index 625c13abe..ffe0eee23 100644 --- a/frame/qml/PanelPopup.qml +++ b/frame/qml/PanelPopup.qml @@ -17,6 +17,7 @@ Item { property bool readyBinding: false property bool openPending: false property bool grabInactivePending: false + property bool closeOnInactive: true property int grabInactiveTimeout: 200 // WM_NAME, used for kwin. property string windowTitle: "dde-shell/panelpopup" @@ -117,6 +118,9 @@ Item { if (!popupWindow || !readyBinding || popupWindow.currentItem !== control || !popup.visible) { return } + if (!control.closeOnInactive) { + return + } if (!popupWindow.active) { control.close() } @@ -139,6 +143,9 @@ Item { grabInactiveTimer.stop() return } + if (!control.closeOnInactive) { + return + } if (control.grabInactivePending || popupWindow.x11GrabFocusTransition) { return } @@ -175,7 +182,8 @@ Item { || !readyBinding || popupWindow.currentItem !== control || !popup.visible - || control.grabInactivePending) { + || control.grabInactivePending + || !control.closeOnInactive) { return } if (!popupWindow.active) { diff --git a/panels/dock/MenuHelper.qml b/panels/dock/MenuHelper.qml index 46775f090..475548f0c 100644 --- a/panels/dock/MenuHelper.qml +++ b/panels/dock/MenuHelper.qml @@ -21,8 +21,8 @@ Item { if (activeMenu) { activeMenu.close() } - menu.open() activeMenu = menu + menu.open() } function closeMenu(menu: Menu) { menu.close() diff --git a/panels/dock/OverflowContainer.qml b/panels/dock/OverflowContainer.qml index aa3066df6..6f7fae6fd 100644 --- a/panels/dock/OverflowContainer.qml +++ b/panels/dock/OverflowContainer.qml @@ -30,6 +30,11 @@ Item { } function calculateImplicitWidth(prev, current) { + // Zero-width delegates are hidden (for example, items moved into the + // overflow popup) and must not leave an extra spacing slot behind. + if (current <= 0) { + return prev + } if (useColumnLayout) { return Math.max(prev, current) } else { @@ -41,6 +46,11 @@ Item { } function calculateImplicitHeight(prev, current) { + // Keep the vertical layout consistent with the horizontal one: hidden + // delegates contribute neither size nor spacing. + if (current <= 0) { + return prev + } if (useColumnLayout) { if (prev == 0) { return current diff --git a/panels/dock/dockpositioner.cpp b/panels/dock/dockpositioner.cpp index 231a881e6..7fdd83724 100644 --- a/panels/dock/dockpositioner.cpp +++ b/panels/dock/dockpositioner.cpp @@ -65,6 +65,34 @@ void DockPositioner::setBounding(const QRect &newBounding) emit boundingChanged(); } +bool DockPositioner::isCrossWindow() const +{ + const auto item = qobject_cast(parent()); + return item && item->window() && m_panel && m_panel->window() + && item->window() != m_panel->window(); +} + +QRect DockPositioner::effectiveBounding() const +{ + if (!isCrossWindow()) + return m_bounding; + + const auto item = qobject_cast(parent()); + const auto globalTopLeft = item->window()->mapToGlobal(m_bounding.topLeft()); + return QRect(globalTopLeft - dockGeometry().topLeft(), m_bounding.size()); +} + +QRect DockPositioner::attachedItemRectInDock() const +{ + const auto item = qobject_cast(parent()); + if (!item) + return {}; + + const auto globalTopLeft = item->mapToGlobal(QPointF(0, 0)); + const auto dockTopLeft = globalTopLeft.toPoint() - dockGeometry().topLeft(); + return QRect(dockTopLeft, QSize(qRound(item->width()), qRound(item->height()))); +} + int DockPositioner::x() const { return m_x; @@ -108,27 +136,28 @@ void DockPositioner::update() void DockPositioner::updatePosition() { + const auto bounding = effectiveBounding(); int xPosition = 0; int yPosition = 0; switch (dockPosition()) { case dock::Top: { - xPosition = m_bounding.x(); - yPosition = m_bounding.y(); + xPosition = bounding.x(); + yPosition = bounding.y(); break; } case dock::Right: { - xPosition = m_bounding.x() - m_bounding.width(); - yPosition = m_bounding.y(); + xPosition = bounding.x() - bounding.width(); + yPosition = bounding.y(); break; } case dock::Bottom: { - xPosition = m_bounding.x(); - yPosition = m_bounding.y() - m_bounding.height(); + xPosition = bounding.x(); + yPosition = bounding.y() - bounding.height(); break; } case dock::Left: { - xPosition = m_bounding.x(); - yPosition = m_bounding.y(); + xPosition = bounding.x(); + yPosition = bounding.y(); break; } default: @@ -197,29 +226,39 @@ void DockPanelPositioner::resetVertialOffset() void DockPanelPositioner::updatePosition() { const auto dockWindowRect = dockGeometry(); + const auto crossWindow = isCrossWindow(); + const auto anchorRect = crossWindow ? attachedItemRectInDock() : QRect(); int xPosition = 0; int yPosition = 0; int horizontalOffset = m_horizontalOffset == -1 ? m_bounding.width() / 2 : m_horizontalOffset; int vertialOffset = m_vertialOffset == -1 ? m_bounding.height() / 2 : m_vertialOffset; switch (dockPosition()) { case dock::Top: { - xPosition = m_bounding.x() - horizontalOffset; - yPosition = dockWindowRect.height() + 10; + xPosition = crossWindow ? anchorRect.center().x() - horizontalOffset + : m_bounding.x() - horizontalOffset; + yPosition = crossWindow ? anchorRect.y() + anchorRect.height() + 10 + : dockWindowRect.height() + 10; break; } case dock::Right: { - xPosition = -m_bounding.width() - 10; - yPosition = m_bounding.y() - vertialOffset; + xPosition = crossWindow ? anchorRect.left() - m_bounding.width() - 10 + : -m_bounding.width() - 10; + yPosition = crossWindow ? anchorRect.center().y() - vertialOffset + : m_bounding.y() - vertialOffset; break; } case dock::Bottom: { - xPosition = m_bounding.x() - horizontalOffset; - yPosition = -m_bounding.height() - 10; + xPosition = crossWindow ? anchorRect.center().x() - horizontalOffset + : m_bounding.x() - horizontalOffset; + yPosition = crossWindow ? anchorRect.top() - m_bounding.height() - 10 + : -m_bounding.height() - 10; break; } case dock::Left: { - xPosition = dockWindowRect.width() + 10; - yPosition = m_bounding.y() - vertialOffset; + xPosition = crossWindow ? anchorRect.x() + anchorRect.width() + 10 + : dockWindowRect.width() + 10; + yPosition = crossWindow ? anchorRect.center().y() - vertialOffset + : m_bounding.y() - vertialOffset; break; } default: diff --git a/panels/dock/dockpositioner.h b/panels/dock/dockpositioner.h index b60ed1521..ce29ce30e 100644 --- a/panels/dock/dockpositioner.h +++ b/panels/dock/dockpositioner.h @@ -51,6 +51,9 @@ public slots: void yChanged(); protected: + bool isCrossWindow() const; + QRect effectiveBounding() const; + QRect attachedItemRectInDock() const; QPointer m_panel; QRect m_bounding {}; int m_x = 0; diff --git a/panels/dock/package/main.qml b/panels/dock/package/main.qml index d8a1cd8ff..26c69f99b 100644 --- a/panels/dock/package/main.qml +++ b/panels/dock/package/main.qml @@ -48,12 +48,13 @@ Window { } } property int dockCenterPartCount: dockCenterPartModel.count + readonly property int centerAlignmentEdgeSpacing: 14 readonly property int dockRawCenterSpace: { if (useColumnLayout) { return Screen.height - dockLeftPart.implicitHeight - dockRightPart.implicitHeight; } else { - let space = Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth; + let space = Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth - Math.ceil(gridLayout.columnSpacing); if (fashionDock.enabled && gridLayout) { // 时尚模式下,dockRightPart 在 gridLayout 右侧,之间需要扣除 dockSpacing // 同时保留左右悬浮间距,避免窗口铺满屏幕后圆角边框被屏幕边缘裁剪。 @@ -62,6 +63,17 @@ Window { return Math.max(0, space); } } + readonly property int dockEffectiveCenterSpace: { + if (Panel.itemAlignment !== Dock.CenterAlignment || fashionDock.enabled) { + return dockRawCenterSpace + } + + const screenExtent = useColumnLayout ? Screen.height : Screen.width + const leftExtent = useColumnLayout ? dockLeftPart.implicitHeight : dockLeftPart.implicitWidth + const rightExtent = useColumnLayout ? dockRightPart.implicitHeight : dockRightPart.implicitWidth + return Math.max(0, screenExtent - leftExtent - rightExtent + - centerAlignmentEdgeSpacing * 2) + } property int dockPartSpacing: gridLayout.columnSpacing // TODO @@ -633,9 +645,9 @@ Window { onXChanged: dockCenterPartPosChanged() onYChanged: dockCenterPartPosChanged() Layout.leftMargin: !useColumnLayout && !fashionDock.enabled && Panel.itemAlignment === Dock.CenterAlignment ? - Math.max(0, (dock.width - dockCenterPart.implicitWidth) / 2 - (dockLeftPart.implicitWidth + 20) + Math.min((dock.width - dockCenterPart.implicitWidth) / 2 - (dockRightPart.implicitWidth + 20), 0)) : 0 + Math.max(0, (dock.width - dockCenterPart.implicitWidth) / 2 - (dockLeftPart.implicitWidth + centerAlignmentEdgeSpacing) + Math.min((dock.width - dockCenterPart.implicitWidth) / 2 - (dockRightPart.implicitWidth + centerAlignmentEdgeSpacing), 0)) : 0 Layout.topMargin: useColumnLayout && Panel.itemAlignment === Dock.CenterAlignment ? - Math.max(0, (dock.height - dockCenterPart.implicitHeight) / 2 - (dockLeftPart.implicitHeight + 20) + Math.min((dock.height - dockCenterPart.implicitHeight) / 2 - (dockRightPart.implicitHeight + 20), 0)) : 0 + Math.max(0, (dock.height - dockCenterPart.implicitHeight) / 2 - (dockLeftPart.implicitHeight + centerAlignmentEdgeSpacing) + Math.min((dock.height - dockCenterPart.implicitHeight) / 2 - (dockRightPart.implicitHeight + centerAlignmentEdgeSpacing), 0)) : 0 Behavior on Layout.leftMargin { enabled: !dock.isDragging && !Applet.isResizing diff --git a/panels/dock/taskmanager/CMakeLists.txt b/panels/dock/taskmanager/CMakeLists.txt index 3854b7e27..7f530dc4e 100644 --- a/panels/dock/taskmanager/CMakeLists.txt +++ b/panels/dock/taskmanager/CMakeLists.txt @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS DBus WaylandClient) +find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS DBus Quick WaylandClient) find_package(yaml-cpp REQUIRED) find_package(TreelandProtocols REQUIRED) @@ -84,6 +84,8 @@ add_library(dock-taskmanager SHARED ${DBUS_INTERFACES} treelandwindowmonitor.h taskmanagersettings.cpp taskmanagersettings.h + taskoverflowcontroller.cpp + taskoverflowcontroller.h textcalculator.h textcalculator.cpp ) @@ -98,6 +100,7 @@ target_link_libraries(dock-taskmanager PRIVATE dde-shell-frame Qt${QT_VERSION_MAJOR}::WaylandClientPrivate Qt${QT_VERSION_MAJOR}::Concurrent + Qt${QT_VERSION_MAJOR}::Quick yaml-cpp PkgConfig::WaylandClient ) diff --git a/panels/dock/taskmanager/dockglobalelementmodel.cpp b/panels/dock/taskmanager/dockglobalelementmodel.cpp index 65a7125d7..917733816 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.cpp +++ b/panels/dock/taskmanager/dockglobalelementmodel.cpp @@ -398,6 +398,9 @@ QVariant DockGlobalElementModel::data(const QModelIndex &index, int role) const return false; } + case TaskManager::DockedRole: + return model == nullptr || m_dockedElements.contains(std::make_tuple("desktop", id)); + case TaskManager::MenusRole: { return getMenus(index); } diff --git a/panels/dock/taskmanager/package/AppItem.qml b/panels/dock/taskmanager/package/AppItem.qml index f4233a7fb..9527e9c46 100644 --- a/panels/dock/taskmanager/package/AppItem.qml +++ b/panels/dock/taskmanager/package/AppItem.qml @@ -26,11 +26,16 @@ Item { required property string title property real blendOpacity: 1.0 + property bool dragEnabled: true + property string dragSource: "taskbar" + property bool restorePositionAfterDrag: false + property real dragStartX: 0 + property real dragStartY: 0 signal dropFilesOnItem(itemId: string, files: list) - signal dragFinished() + signal dragFinished(int dropAction) - Drag.active: mouseArea.drag.active + Drag.active: root.dragEnabled && mouseArea.drag.active Drag.source: root Drag.hotSpot.x: icon.width / 2 Drag.hotSpot.y: icon.height / 2 @@ -41,10 +46,21 @@ Item { Accessible.name: root.name Accessible.description: root.attention ? qsTr("Demands attention") : (root.active ? qsTr("Active") : "") + Drag.supportedActions: Qt.CopyAction | Qt.MoveAction + Drag.mimeData: { "text/x-dde-dock-dnd-appid": itemId, "text/x-dde-dock-dnd-source": dragSource, "text/x-dde-dock-dnd-winid": windows.length > 0 ? windows[0] : ""} + Drag.onDragFinished: function(dropAction) { + if (root.restorePositionAfterDrag) { + root.x = root.dragStartX + root.y = root.dragStartY + } + root.dragFinished(dropAction) + } + property bool useColumnLayout: Panel.rootObject.useColumnLayout property real iconSize: Panel.rootObject.dockItemMaxSize * 9 / 14 property bool enableTitle: false property bool titleActive: enableTitle && titleLoader.active + property bool preservePanelPopupOnContextMenu: false property var iconGlobalPoint: { var a = icon var x = 0, y = 0 @@ -414,7 +430,10 @@ Item { running: false repeat: false onTriggered: { - var pos = icon.mapToItem(null, 0, 0) + const point = icon.mapToItem(null, 0, 0) + icon.DockPositioner.bounding = Qt.rect(point.x, point.y, 0, 0) + icon.DockPositioner.updatePosition() + const pos = Qt.point(icon.DockPositioner.x, icon.DockPositioner.y) taskmanager.Applet.requestUpdateWindowIconGeometry(root.modelIndex, Qt.rect(pos.x, pos.y, icon.width, icon.height), Panel.rootObject) } @@ -428,6 +447,8 @@ Item { property int xOffset: 0 property int yOffset: 0 onTriggered: { + xOffset = icon.DockPanelPositioner.x + yOffset = icon.DockPanelPositioner.y if (root.windows.length != 0 || Qt.platform.pluginName === "wayland") { // 使用基于 modelIndex 的预览API,确保精确匹配 taskmanager.Applet.requestPreview(root.modelIndex, Panel.rootObject, xOffset, yOffset, Panel.position); @@ -442,17 +463,8 @@ Item { return } - var itemPos = root.mapToItem(null, 0, 0) - let xOffset, yOffset, interval = 10 - if (Panel.position % 2 === 0) { - xOffset = itemPos.x + (root.width / 2) - yOffset = (Panel.position == 2 ? -interval : interval + Panel.dockSize) - } else { - xOffset = (Panel.position == 1 ? -interval : interval + Panel.dockSize) - yOffset = itemPos.y + (root.height / 2) - } - previewTimer.xOffset = xOffset - previewTimer.yOffset = yOffset + const point = icon.mapToItem(null, icon.width / 2, icon.height / 2) + icon.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, 0, 0) previewTimer.start() } @@ -481,7 +493,9 @@ Item { } function requestAppItemMenu() { - Panel.requestClosePopup() + if (!root.preservePanelPopupOnContextMenu) { + Panel.requestClosePopup() + } contextMenuLoader.trashEmpty = TaskManager.isTrashEmpty() contextMenuLoader.active = true MenuHelper.openMenu(contextMenuLoader.item) @@ -492,13 +506,14 @@ Item { anchors.fill: parent hoverEnabled: false acceptedButtons: Qt.LeftButton | Qt.RightButton - drag.target: root + drag.target: root.dragEnabled ? root : null drag.onActiveChanged: { if (!drag.active) { Panel.contextDragging = false - root.dragFinished() return } + root.dragStartX = root.x + root.dragStartY = root.y Panel.contextDragging = true } @@ -515,7 +530,7 @@ Item { } onPressed: function (mouse) { isTouchLongPressed = false - if (mouse.button === Qt.LeftButton) { + if (root.dragEnabled && mouse.button === Qt.LeftButton) { appItem.grabToImage(function(result) { root.Drag.imageSource = result.url; }) @@ -545,15 +560,15 @@ Item { PanelToolTip { id: toolTip - toolTipX: DockPanelPositioner.x - toolTipY: DockPanelPositioner.y + toolTipX: icon.DockPanelPositioner.x + toolTipY: icon.DockPanelPositioner.y } PanelToolTip { id: dragToolTip text: qsTr("Move to Trash") - toolTipX: DockPanelPositioner.x - toolTipY: DockPanelPositioner.y + toolTipX: icon.DockPanelPositioner.x + toolTipY: icon.DockPanelPositioner.y visible: false } @@ -561,9 +576,10 @@ Item { id: toolTipShowTimer interval: 50 onTriggered: { - var point = root.mapToItem(null, root.width / 2, root.height / 2) toolTip.text = root.itemId === "dde-trash" ? root.name + "-" + taskmanager.Applet.getTrashTipText() : root.name - toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) + const point = icon.mapToItem(null, icon.width / 2, icon.height / 2) + icon.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, + toolTip.width, toolTip.height) toolTip.open() } } @@ -585,8 +601,9 @@ Item { if (root.itemId === "dde-trash") { dragToolTipCloseTimer.stop() if (!dragToolTip.toolTipVisible) { - var point = root.mapToItem(null, root.width / 2, root.height / 2) - dragToolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, dragToolTip.width, dragToolTip.height) + const point = icon.mapToItem(null, icon.width / 2, icon.height / 2) + icon.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, + dragToolTip.width, dragToolTip.height) dragToolTip.open() } } diff --git a/panels/dock/taskmanager/package/AppItemTitle.qml b/panels/dock/taskmanager/package/AppItemTitle.qml index 06c6806e9..e71146727 100644 --- a/panels/dock/taskmanager/package/AppItemTitle.qml +++ b/panels/dock/taskmanager/package/AppItemTitle.qml @@ -19,7 +19,7 @@ Item { implicitWidth: titleLoader.width implicitHeight: titleLoader.height - TextCalculator.text: root.text + TextCalculator.text: root.enabled ? root.text : "" Loader { id: titleLoader diff --git a/panels/dock/taskmanager/package/TaskManager.qml b/panels/dock/taskmanager/package/TaskManager.qml index 02de4a311..ccbc75299 100644 --- a/panels/dock/taskmanager/package/TaskManager.qml +++ b/panels/dock/taskmanager/package/TaskManager.qml @@ -22,7 +22,7 @@ ContainmentItem { function calcRemainingSpace(baseSize) { const otherCount = Panel.rootObject.dockCenterPartCount - 1; const otherOccupied = otherCount > 0 ? otherCount * baseSize * multitaskViewIconRatio : 0; - return Panel.rootObject.dockRawCenterSpace - otherOccupied; + return Panel.rootObject.dockEffectiveCenterSpace - otherOccupied; } property real remainingSpacesForTaskManager: fashionMode @@ -40,6 +40,9 @@ ContainmentItem { // Defined in main.qml as dockItemIconSize = dockItemMaxSize * 9 / 14. // At default dock size (56), icon = 36px; at min (37), icon ≈ 24px; at max (100), icon ≈ 64px. readonly property real iconWidthToMaxSizeRatio: 9 / 14 + readonly property int minimumDockItemSize: Math.max( + Dock.MIN_DOCK_SIZE, + Math.round(Panel.rootObject.dockSize * Dock.MIN_DOCK_SIZE / Dock.DEFAULT_DOCK_SIZE)) readonly property real startPadding: Math.max(0, appTitleSpacing - (Panel.rootObject.dockItemMaxSize * (multitaskViewIconRatio - iconWidthToMaxSizeRatio) / 2)) implicitWidth: { @@ -87,6 +90,19 @@ ContainmentItem { } property real blendOpacity: blendColorAlpha(D.DTK.themeType === D.ApplicationHelper.DarkType ? 0.25 : 1.0) + TaskOverflowController { + id: taskOverflow + // Window-split title layout has priority. Only fall back to adaptive icon + // shrinking and overflow when no title layout can fit the available space. + enabled: !textCalculator.enabled || textCalculator.totalWidth <= 0 + availableExtent: Math.max(0, taskmanager.calcRemainingSpace(Panel.rootObject.dockItemMaxSize) - taskmanager.startPadding) + minimumReached: Panel.rootObject.dockItemMaxSize <= taskmanager.minimumDockItemSize + 0.5 + fallbackItemExtent: Math.max(1, Panel.rootObject.dockItemMaxSize * taskmanager.iconWidthToMaxSizeRatio) + spacing: taskmanager.appTitleSpacing + visualModel: visualModel + dataModel: taskmanager.Applet.dataModel + } + TextCalculator { id: textCalculator enabled: !fashionMode && taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top) @@ -145,6 +161,20 @@ ContainmentItem { return windows.length > 0 && launcherDndDropArea.launcherDndWinId !== windows[0] } + readonly property bool overflowActive: taskOverflow.popupItems.length > 0 + readonly property bool isOverflowButton: overflowActive + && DelegateModel.itemsIndex === taskOverflow.visibleItemCount + readonly property bool hiddenInOverflow: overflowActive + && DelegateModel.itemsIndex > taskOverflow.visibleItemCount + visible: !hiddenInOverflow + + readonly property real normalDelegateWidth: useColumnLayout + ? taskmanager.implicitWidth + : appItem.implicitWidth + readonly property real normalDelegateHeight: useColumnLayout + ? Panel.rootObject.dockItemMaxSize * 9 / 14 + : taskmanager.implicitHeight + ListView.onAdd: NumberAnimation { target: delegateRoot properties: "scale,opacity" @@ -169,8 +199,8 @@ ContainmentItem { Behavior on opacity { NumberAnimation { duration: 200 } } Behavior on scale { NumberAnimation { duration: 200 } } - implicitWidth: useColumnLayout ? taskmanager.implicitWidth : appItem.implicitWidth - implicitHeight: useColumnLayout ? Panel.rootObject.dockItemMaxSize * 9 / 14 : taskmanager.implicitHeight + implicitWidth: hiddenInOverflow? 0 : normalDelegateWidth + implicitHeight: hiddenInOverflow? 0 : normalDelegateHeight property int visualIndex: DelegateModel.itemsIndex property var modelIndex: visualModel.modelIndex(index) @@ -186,8 +216,29 @@ ContainmentItem { y: delegateRoot.y width: delegateRoot.width height: delegateRoot.height - scale: delegateRoot.scale + readonly property bool hiddenByOverflow: delegateRoot.isOverflowButton + || delegateRoot.hiddenInOverflow + property real overflowTransitionScale: hiddenByOverflow ? 0.8 : 1.0 + scale: delegateRoot.scale * overflowTransitionScale + opacity: hiddenByOverflow ? 0 : 1 + visible: opacity > 0.001 property bool positionAnimationEnabled: false + + Behavior on opacity { + enabled: appItemRect.positionAnimationEnabled + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + Behavior on overflowTransitionScale { + enabled: appItemRect.positionAnimationEnabled + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + Behavior on x { enabled: appItemRect.positionAnimationEnabled NumberAnimation { @@ -239,6 +290,12 @@ ContainmentItem { } } + TaskOverflowButton { + id: overflowButton + taskManagerItem: taskmanager + overflow: taskOverflow + } + DropArea { id: launcherDndDropArea anchors.fill: parent @@ -247,11 +304,54 @@ ContainmentItem { property string launcherDndDesktopId: "" property string launcherDndDragSource: "" property string launcherDndWinId: "" + property bool launcherDndDocked: false function resetDndState() { launcherDndDesktopId = "" launcherDndDragSource = "" launcherDndWinId = "" + launcherDndDocked = false + } + + function isOverflowButtonIndex(targetIndex) { + return taskOverflow.popupItems.length > 0 + && targetIndex === taskOverflow.visibleItemCount + } + + function targetIndexAt(x, y) { + if (launcherDndDragSource !== "overflow-popup") { + return appContainer.indexAt(x, y) + } + + const visibleCount = taskOverflow.visibleItemCount + if (visibleCount <= 0) { + return -1 + } + + const itemExtent = taskOverflow.fallbackItemExtent + const pitch = itemExtent + taskOverflow.spacing + const position = taskmanager.useColumnLayout ? y : x + const nearestSlot = Math.floor((Math.max(0, position) + taskOverflow.spacing / 2) / pitch) + return Math.max(0, Math.min(visibleCount - 1, nearestSlot)) + } + + function isInternalDrag() { + return launcherDndDragSource === "taskbar" + || launcherDndDragSource === "overflow-popup" + } + + function requestDockIfNeeded() { + if (isInternalDrag() || launcherDndDocked) { + return true + } + + if (taskmanager.Applet.requestDockByDesktopId(launcherDndDesktopId) === false) { + resetDndState() + return false + } + + launcherDndDocked = true + return true } onEntered: function(drag) { @@ -262,12 +362,17 @@ ContainmentItem { if (launcherDndDragSource !== "taskbar" && taskmanager.Applet.requestDockByDesktopId(desktopId) === false) { drag.accepted = false resetDndState() + let targetIndex = targetIndexAt(drag.x, drag.y) + if (!isOverflowButtonIndex(targetIndex) && !requestDockIfNeeded()) { + return } } onPositionChanged: function(drag) { if (launcherDndDesktopId === "") return - let targetIndex = appContainer.indexAt(drag.x, drag.y) + if (launcherDndDragSource === "overflow-popup") return + let targetIndex = targetIndexAt(drag.x, drag.y) + if (isOverflowButtonIndex(targetIndex) || !requestDockIfNeeded()) return let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(appId, launcherDndWinId) : taskmanager.findAppIndex(appId) if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { @@ -275,6 +380,7 @@ ContainmentItem { taskmanager.Applet.moveItem(currentIndex, targetIndex) } else { visualModel.items.move(currentIndex, targetIndex) + taskOverflow.scheduleSync() } } } @@ -282,7 +388,9 @@ ContainmentItem { onDropped: function(drop) { Panel.contextDragging = false if (launcherDndDesktopId === "") return - let targetIndex = appContainer.indexAt(drop.x, drop.y) + const overflowPopupDrag = launcherDndDragSource === "overflow-popup" + let targetIndex = targetIndexAt(drop.x, drop.y) + if (!requestDockIfNeeded()) return let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(appId, launcherDndWinId) : taskmanager.findAppIndex(appId) if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { @@ -290,6 +398,7 @@ ContainmentItem { taskmanager.Applet.moveItem(currentIndex, targetIndex) } else { visualModel.items.move(currentIndex, targetIndex) + taskOverflow.scheduleSync() } } let appIds = [] @@ -297,11 +406,14 @@ ContainmentItem { appIds.push(visualModel.items.get(i).model.itemId) } taskmanager.Applet.saveDockElementsOrder(appIds) + if (overflowPopupDrag) { + drop.accept(Qt.MoveAction) + } resetDndState() } onExited: function() { - if (launcherDndDesktopId !== "" && launcherDndDragSource !== "taskbar") { + if (launcherDndDesktopId !== "" && !isInternalDrag() && launcherDndDocked) { taskmanager.Applet.requestUndockByDesktopId(launcherDndDesktopId) } resetDndState() @@ -354,8 +466,10 @@ ContainmentItem { return dockSize; const otherItems = Math.max(0, Panel.rootObject.dockCenterPartCount - 1); - const optimal = solveMaxSizeFull(Panel.rootObject.dockRawCenterSpace, appCount, otherItems); - return optimal >= dockSize ? dockSize : optimal; + const optimal = solveMaxSizeFull(Panel.rootObject.dockEffectiveCenterSpace, appCount, otherItems); + const fitted = optimal >= dockSize ? dockSize : optimal; + return Math.max(taskmanager.minimumDockItemSize, fitted); }) + taskOverflow.scheduleSync() } } diff --git a/panels/dock/taskmanager/package/TaskOverflowButton.qml b/panels/dock/taskmanager/package/TaskOverflowButton.qml new file mode 100644 index 000000000..8669cc64e --- /dev/null +++ b/panels/dock/taskmanager/package/TaskOverflowButton.qml @@ -0,0 +1,230 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +Item { + id: root + + required property var taskManagerItem + required property var overflow + + readonly property int overflowCount: overflow.popupItems.length + readonly property var primaryOverflowItem: overflow.popupItems.length > 0 ? overflow.popupItems[0] : null + readonly property string overflowIconName: primaryOverflowItem && primaryOverflowItem.iconName ? primaryOverflowItem.iconName : "" + readonly property real popupItemWidth: Math.max(1, overflow.fallbackItemExtent + overflow.spacing) + readonly property real popupItemHeight: Math.max(1, Panel.rootObject ? Panel.rootObject.dockItemMaxSize : overflow.fallbackItemExtent) + readonly property real buttonOffset: overflow.visibleItemCount * (overflow.fallbackItemExtent + overflow.spacing) + readonly property real overflowIconSize: Math.max(12, Math.round(Panel.rootObject.dockItemIconSize)) + readonly property int hoverVerticalSpacing: Math.round(overflowIconSize / 8) + 1 + readonly property int hoverHorizontalSpacing: Math.round(overflowIconSize / 8) + readonly property int hoverBackgroundWidth: Math.round(overflowIconSize + hoverHorizontalSpacing * 2) + readonly property int hoverBackgroundHeight: Math.round(overflowIconSize + hoverVerticalSpacing * 2) + readonly property real dotSize: Math.max(2, Math.round(overflowIconSize * 0.11)) + readonly property real dotSpacing: Math.max(1, Math.round(dotSize * 0.75)) + readonly property color dotColor: D.DTK.themeType === D.ApplicationHelper.DarkType ? "#FFFFFF" : "#000000" + readonly property int popupColumns: Math.min(6, Math.max(1, overflowCount)) + property point popupAnchorPoint: Qt.point(0, 0) + + visible: overflowCount > 0 + x: root.taskManagerItem.useColumnLayout ? 0 : root.buttonOffset + y: root.taskManagerItem.useColumnLayout ? root.buttonOffset : 0 + implicitWidth: root.taskManagerItem.useColumnLayout ? root.taskManagerItem.implicitWidth : overflow.fallbackItemExtent + implicitHeight: root.taskManagerItem.useColumnLayout ? overflow.fallbackItemExtent : root.taskManagerItem.implicitHeight + width: implicitWidth + height: implicitHeight + + function togglePopup() { + toolTipShowTimer.stop() + toolTip.close() + + if (root.overflowCount <= 0) { + return + } + + if (overflowPopup.popupVisible) { + overflowPopup.close() + return + } + + Panel.requestClosePopup() + popupAnchorPoint = root.mapToItem(null, root.width / 2, root.height / 2) + overflowPopup.open() + } + + AppItemBackground { + width: root.hoverBackgroundWidth + height: root.hoverBackgroundHeight + radius: height / 5 + anchors.centerIn: parent + enabled: false + displayMode: Panel.indicatorStyle + windowCount: 0 + isActive: false + D.ColorSelector.hovered: overflowHoverHandler.hovered || overflowPopup.popupVisible + D.ColorSelector.pressed: overflowMouseArea.pressed + } + + Item { + id: overflowIndicator + anchors.centerIn: parent + visible: root.primaryOverflowItem !== null + width: root.overflowIconSize + height: root.overflowIconSize + + D.DciIcon { + anchors.fill: parent + name: root.overflowIconName + sourceSize: Qt.size(width, height) + retainWhileLoading: true + smooth: false + opacity: 0.4 + } + Row { + spacing: root.dotSpacing + anchors.centerIn: parent + + Repeater { + model: 3 + + Rectangle { + width: root.dotSize + height: width + radius: width / 2 + color: root.dotColor + } + } + } + } + + MouseArea { + id: overflowMouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: false + onClicked: root.togglePopup() + } + + HoverHandler { + id: overflowHoverHandler + onHoveredChanged: { + if (hovered && !overflowPopup.popupVisible) { + toolTipShowTimer.start() + } else { + toolTipShowTimer.stop() + toolTip.close() + } + } + } + + PanelToolTip { + id: toolTip + text: qsTr("Show hidden icons") + toolTipX: DockPanelPositioner.x + toolTipY: DockPanelPositioner.y + } + + Timer { + id: toolTipShowTimer + interval: 50 + onTriggered: { + var point = root.mapToItem(null, root.width / 2, root.height / 2) + toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) + toolTip.open() + } + } + + PanelPopup { + id: overflowPopup + width: overflowPopupContent.width + height: overflowPopupContent.height + popupX: DockPanelPositioner.x + popupY: DockPanelPositioner.y + closeOnInactive: MenuHelper.activeMenu === null && !Panel.contextDragging + + Component.onCompleted: { + DockPanelPositioner.bounding = Qt.binding(function () { + return Qt.rect(root.popupAnchorPoint.x, + root.popupAnchorPoint.y, + overflowPopup.width, + overflowPopup.height) + }) + } + + Control { + id: overflowPopupContent + padding: 10 + implicitWidth: overflowGrid.implicitWidth + leftPadding + rightPadding + implicitHeight: overflowGrid.implicitHeight + topPadding + bottomPadding + width: implicitWidth + height: implicitHeight + background: Item {} + + DropArea { + anchors.fill: parent + keys: ["text/x-dde-dock-dnd-appid"] + onDropped: function(drop) { + if (drop.getDataAsString("text/x-dde-dock-dnd-source") === "overflow-popup") { + drop.accept(Qt.MoveAction) + } + } + } + + contentItem: Grid { + id: overflowGrid + columns: root.popupColumns + rowSpacing: Math.max(6, Math.round(root.popupItemWidth * 0.12)) + columnSpacing: rowSpacing + + Repeater { + model: root.overflow.popupItems + + delegate: Item { + required property var modelData + + width: root.popupItemWidth + height: root.popupItemHeight + + AppItem { + width: parent.width + height: parent.height + displayMode: Panel.indicatorStyle + colorTheme: Panel.colorTheme + active: modelData.active + attention: modelData.attention + itemId: modelData.itemId + name: modelData.name + iconName: modelData.iconName + menus: modelData.menus + windows: modelData.windows + visualIndex: modelData.visualIndex + modelIndex: modelData.modelIndex + blendOpacity: root.taskManagerItem.blendOpacity + title: modelData.title + enableTitle: false + preservePanelPopupOnContextMenu: true + dragEnabled: modelData.docked + dragSource: "overflow-popup" + restorePositionAfterDrag: true + + Component.onCompleted: { + dropFilesOnItem.connect(root.taskManagerItem.Applet.dropFilesOnItem) + } + onDragFinished: function(dropAction) { + if (dropAction === Qt.IgnoreAction && modelData.docked) { + root.taskManagerItem.Applet.requestUndockByDesktopId(modelData.itemId) + } + } + } + } + } + } + } + } +} diff --git a/panels/dock/taskmanager/taskmanager.cpp b/panels/dock/taskmanager/taskmanager.cpp index bae8101ea..914d668a3 100644 --- a/panels/dock/taskmanager/taskmanager.cpp +++ b/panels/dock/taskmanager/taskmanager.cpp @@ -18,6 +18,7 @@ #include "pluginfactory.h" #include "taskmanager.h" #include "taskmanageradaptor.h" +#include "taskoverflowcontroller.h" #include "taskmanagersettings.h" #include "textcalculator.h" #include "treelandwindowmonitor.h" @@ -147,6 +148,7 @@ TaskManager::TaskManager(QObject *parent) , AbstractTaskManagerInterface(nullptr) , m_windowFullscreen(false) { + qmlRegisterType("org.deepin.ds.dock.taskmanager", 1, 0, "TaskOverflowController"); qmlRegisterType("org.deepin.ds.dock.taskmanager", 1, 0, "TextCalculator"); qmlRegisterUncreatableType("org.deepin.ds.dock.taskmanager", 1, 0, "TextCalculatorAttached", "TextCalculator Attached"); diff --git a/panels/dock/taskmanager/taskoverflowcontroller.cpp b/panels/dock/taskmanager/taskoverflowcontroller.cpp new file mode 100644 index 000000000..0d7037c12 --- /dev/null +++ b/panels/dock/taskmanager/taskoverflowcontroller.cpp @@ -0,0 +1,294 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "taskoverflowcontroller.h" + +#include "globals.h" + +#include +#include + +#include + +namespace dock { +namespace { +constexpr qreal OverflowReleaseHysteresis = 8; +} + +TaskOverflowController::TaskOverflowController(QObject *parent) + : QObject(parent) +{ +} + +void TaskOverflowController::setEnabled(bool enabled) +{ + if (m_enabled == enabled) + return; + + m_enabled = enabled; + if (!m_enabled) + m_latched = false; + emit enabledChanged(); + scheduleSync(); +} + +void TaskOverflowController::setAvailableExtent(qreal availableExtent) +{ + if (!qFuzzyCompare(m_availableExtent + 1, availableExtent + 1)) { + m_availableExtent = availableExtent; + scheduleSync(); + } +} + +void TaskOverflowController::setMinimumReached(bool minimumReached) +{ + if (m_minimumReached != minimumReached) { + m_minimumReached = minimumReached; + scheduleSync(); + } +} + +void TaskOverflowController::setFallbackItemExtent(qreal fallbackItemExtent) +{ + if (!qFuzzyCompare(m_fallbackItemExtent + 1, fallbackItemExtent + 1)) { + m_fallbackItemExtent = fallbackItemExtent; + emit fallbackItemExtentChanged(); + scheduleSync(); + } +} + +void TaskOverflowController::setSpacing(qreal spacing) +{ + if (!qFuzzyCompare(m_spacing + 1, spacing + 1)) { + m_spacing = spacing; + emit spacingChanged(); + scheduleSync(); + } +} + +void TaskOverflowController::setVisualModel(QObject *visualModel) +{ + if (m_visualModel == visualModel) + return; + + if (m_visualModel) + disconnect(m_visualModel, nullptr, this, nullptr); + + m_visualModel = visualModel; + if (m_visualModel) + connect(m_visualModel, &QObject::destroyed, this, [this] { + m_visualModel = nullptr; + scheduleSync(); + }); + + scheduleSync(); +} + +void TaskOverflowController::setDataModel(QAbstractItemModel *dataModel) +{ + if (m_dataModel == dataModel) + return; + + disconnectDataModelSignals(); + m_dataModel = dataModel; + connectDataModelSignals(); + scheduleSync(); +} + +void TaskOverflowController::scheduleSync() +{ + if (m_syncPending) + return; + + m_syncPending = true; + QTimer::singleShot(0, this, [this] { + m_syncPending = false; + sync(); + }); +} + +void TaskOverflowController::sync() +{ + const int totalCount = modelCount(); + const qreal fullExtent = totalCount > 0 + ? totalCount * m_fallbackItemExtent + (totalCount - 1) * m_spacing + : 0; + + const bool overflowEnabled = syncEnabledState(totalCount, fullExtent); + if (!overflowEnabled || totalCount <= 0 || !std::isfinite(m_availableExtent) || m_availableExtent <= 0) { + setVisibleItemCount(totalCount); + setPopupItems({}); + return; + } + + qreal usedExtent = 0; + int visibleCount = 0; + for (int index = 0; index < totalCount; ++index) { + const qreal itemExtent = m_fallbackItemExtent; + const qreal leadingSpacing = visibleCount > 0 ? m_spacing : 0; + const int remainingCount = totalCount - (visibleCount + 1); + const qreal reservedButtonExtent = remainingCount > 0 ? m_spacing + m_fallbackItemExtent : 0; + if (usedExtent + leadingSpacing + itemExtent + reservedButtonExtent > m_availableExtent) + break; + + usedExtent += leadingSpacing + itemExtent; + ++visibleCount; + } + + if (visibleCount >= totalCount) { + setVisibleItemCount(totalCount); + setPopupItems({}); + return; + } + + QVariantList overflowItems; + overflowItems.reserve(totalCount - visibleCount); + for (int index = visibleCount; index < totalCount; ++index) { + const auto itemData = entryAt(index); + if (!itemData.isEmpty()) + overflowItems.append(itemData); + } + + setVisibleItemCount(visibleCount); + setPopupItems(overflowItems); +} + +void TaskOverflowController::onDataModelDestroyed() +{ + m_dataModel = nullptr; + scheduleSync(); +} + +int TaskOverflowController::modelCount() const +{ + if (m_visualModel) { + const auto countValue = m_visualModel->property("count"); + if (countValue.isValid()) + return countValue.toInt(); + } + + return m_dataModel ? m_dataModel->rowCount() : 0; +} + +QModelIndex TaskOverflowController::modelIndexAt(int visualIndex) const +{ + if (visualIndex < 0) + return {}; + + if (m_visualModel) { + QVariant modelIndexValue; + const bool ok = QMetaObject::invokeMethod(m_visualModel, + "modelIndex", + Q_RETURN_ARG(QVariant, modelIndexValue), + Q_ARG(int, visualIndex)); + if (ok) { + const auto index = modelIndexValue.value(); + if (index.isValid()) + return index; + } + } + + if (m_dataModel && visualIndex < m_dataModel->rowCount()) + return m_dataModel->index(visualIndex, 0); + + return {}; +} + +QVariant TaskOverflowController::dataForRole(const QModelIndex &index, const QByteArray &roleName) const +{ + if (!m_dataModel || !index.isValid()) + return {}; + + const auto role = m_dataModel->roleNames().key(roleName, -1); + if (role < 0) + return {}; + + return m_dataModel->data(index, role); +} + +QVariantMap TaskOverflowController::entryAt(int visualIndex) const +{ + const auto index = modelIndexAt(visualIndex); + if (!index.isValid()) + return {}; + + auto iconName = dataForRole(index, MODEL_ICONNAME).toString(); + if (iconName.isEmpty()) + iconName = dataForRole(index, MODEL_WINICON).toString(); + if (iconName.isEmpty()) + iconName = DEFAULT_APP_ICONNAME; + + return { + { QStringLiteral("active"), dataForRole(index, MODEL_ACTIVE).toBool() }, + { QStringLiteral("attention"), dataForRole(index, MODEL_ATTENTION).toBool() }, + { QStringLiteral("docked"), dataForRole(index, MODEL_DOCKED).toBool() }, + { QStringLiteral("itemId"), dataForRole(index, MODEL_ITEMID).toString() }, + { QStringLiteral("name"), dataForRole(index, MODEL_NAME).toString() }, + { QStringLiteral("title"), dataForRole(index, MODEL_TITLE).toString() }, + { QStringLiteral("iconName"), iconName }, + { QStringLiteral("menus"), dataForRole(index, MODEL_MENUS).toString() }, + { QStringLiteral("windows"), dataForRole(index, MODEL_WINDOWS).toStringList() }, + { QStringLiteral("visualIndex"), visualIndex }, + { QStringLiteral("modelIndex"), QVariant::fromValue(index) }, + }; +} + +bool TaskOverflowController::syncEnabledState(int totalCount, qreal fullExtent) +{ + if (!m_enabled || totalCount <= 0 || !std::isfinite(m_availableExtent) || m_availableExtent <= 0) { + m_latched = false; + return false; + } + + // Enter overflow as soon as the content exceeds the available boundary. + // Keep hysteresis only when leaving overflow to avoid rapid toggling near + // the threshold without allowing visible items to overlap first. + const qreal startThreshold = m_availableExtent; + const qreal stopThreshold = std::max(0, m_availableExtent - OverflowReleaseHysteresis); + const bool nextLatched = m_latched + ? fullExtent > stopThreshold + : (m_minimumReached && fullExtent > startThreshold); + m_latched = nextLatched; + return m_latched; +} + +void TaskOverflowController::connectDataModelSignals() +{ + if (!m_dataModel) + return; + + connect(m_dataModel, &QAbstractItemModel::dataChanged, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::rowsInserted, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::rowsMoved, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::rowsRemoved, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::modelReset, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::layoutChanged, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QObject::destroyed, this, &TaskOverflowController::onDataModelDestroyed); +} + +void TaskOverflowController::disconnectDataModelSignals() +{ + if (m_dataModel) + disconnect(m_dataModel, nullptr, this, nullptr); +} + +void TaskOverflowController::setVisibleItemCount(int visibleItemCount) +{ + if (m_visibleItemCount == visibleItemCount) + return; + + m_visibleItemCount = visibleItemCount; + emit visibleItemCountChanged(); +} + +void TaskOverflowController::setPopupItems(const QVariantList &items) +{ + if (m_popupItems == items) + return; + + m_popupItems = items; + emit popupItemsChanged(); +} + +} diff --git a/panels/dock/taskmanager/taskoverflowcontroller.h b/panels/dock/taskmanager/taskoverflowcontroller.h new file mode 100644 index 000000000..d4c5838a5 --- /dev/null +++ b/panels/dock/taskmanager/taskoverflowcontroller.h @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +namespace dock { + +class TaskOverflowController : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(qreal availableExtent READ availableExtent WRITE setAvailableExtent) + Q_PROPERTY(bool minimumReached READ minimumReached WRITE setMinimumReached) + Q_PROPERTY(qreal fallbackItemExtent READ fallbackItemExtent WRITE setFallbackItemExtent NOTIFY fallbackItemExtentChanged) + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + Q_PROPERTY(QObject *visualModel READ visualModel WRITE setVisualModel) + Q_PROPERTY(QAbstractItemModel *dataModel READ dataModel WRITE setDataModel) + Q_PROPERTY(int visibleItemCount READ visibleItemCount NOTIFY visibleItemCountChanged) + Q_PROPERTY(QVariantList popupItems READ popupItems NOTIFY popupItemsChanged) + +public: + explicit TaskOverflowController(QObject *parent = nullptr); + + bool isEnabled() const + { + return m_enabled; + } + void setEnabled(bool enabled); + + qreal availableExtent() const + { + return m_availableExtent; + } + void setAvailableExtent(qreal availableExtent); + + bool minimumReached() const + { + return m_minimumReached; + } + void setMinimumReached(bool minimumReached); + + qreal fallbackItemExtent() const + { + return m_fallbackItemExtent; + } + void setFallbackItemExtent(qreal fallbackItemExtent); + + qreal spacing() const + { + return m_spacing; + } + void setSpacing(qreal spacing); + + QObject *visualModel() const + { + return m_visualModel; + } + void setVisualModel(QObject *visualModel); + + QAbstractItemModel *dataModel() const + { + return m_dataModel; + } + void setDataModel(QAbstractItemModel *dataModel); + + int visibleItemCount() const + { + return m_visibleItemCount; + } + QVariantList popupItems() const + { + return m_popupItems; + } + + Q_INVOKABLE void scheduleSync(); + +Q_SIGNALS: + void enabledChanged(); + void fallbackItemExtentChanged(); + void spacingChanged(); + void visibleItemCountChanged(); + void popupItemsChanged(); + +private Q_SLOTS: + void onDataModelDestroyed(); + +private: + void sync(); + int modelCount() const; + QModelIndex modelIndexAt(int visualIndex) const; + QVariant dataForRole(const QModelIndex &index, const QByteArray &roleName) const; + QVariantMap entryAt(int visualIndex) const; + bool syncEnabledState(int totalCount, qreal fullExtent); + + void connectDataModelSignals(); + void disconnectDataModelSignals(); + + void setVisibleItemCount(int visibleItemCount); + void setPopupItems(const QVariantList &items); + + bool m_enabled = true; + qreal m_availableExtent = 0; + bool m_minimumReached = false; + qreal m_fallbackItemExtent = 1; + qreal m_spacing = 0; + QPointer m_visualModel; + QPointer m_dataModel; + bool m_latched = false; + int m_visibleItemCount = 0; + QVariantList m_popupItems; + bool m_syncPending = false; +}; + +} diff --git a/panels/dock/taskmanager/textcalculator.cpp b/panels/dock/taskmanager/textcalculator.cpp index b3f5c6e4a..0516141b9 100644 --- a/panels/dock/taskmanager/textcalculator.cpp +++ b/panels/dock/taskmanager/textcalculator.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace dock { @@ -169,7 +170,19 @@ void TextCalculator::scheduleCalculation() resetCalculatedWidths(); return; } - calculateOptimalTextWidth(); + + if (m_calculationPending) + return; + + m_calculationPending = true; + QTimer::singleShot(0, this, [this] { + m_calculationPending = false; + if (!m_enabled || !m_dataModel) { + resetCalculatedWidths(); + return; + } + calculateOptimalTextWidth(); + }); } void TextCalculator::resetCalculatedWidths() @@ -267,12 +280,9 @@ void TextCalculator::calculateOptimalTextWidth() const int appCount = titles.size(); if (appCount <= 0 || m_remainingSpace <= 0) { - if (m_optimalSingleTextWidth != 0.0) { + if (!qFuzzyIsNull(m_optimalSingleTextWidth) || !qFuzzyIsNull(m_totalWidth)) { qCDebug(textCalculatorLog) << "Setting optimal width to 0 (no apps or no space)"; - m_optimalSingleTextWidth = 0.0; - m_totalWidth = 0; - emit optimalSingleTextWidthChanged(); - emit totalWidthChanged(); + resetCalculatedWidths(); } return; } @@ -317,15 +327,20 @@ void TextCalculator::calculateOptimalTextWidth() } } - // Update results - if (!qFuzzyCompare(m_optimalSingleTextWidth, newOptimalWidth)) { + // Publish a single, internally consistent layout result. totalWidth can change + // even when the selected character count (and therefore optimal width) does not. + const bool optimalWidthChanged = !qFuzzyCompare(m_optimalSingleTextWidth, newOptimalWidth); + const bool totalWidthDidChange = !qFuzzyCompare(m_totalWidth, newTotalWidth); + if (optimalWidthChanged || totalWidthDidChange) { qCDebug(textCalculatorLog) << "Optimal text width changed from" << m_optimalSingleTextWidth << "to" << newOptimalWidth << "App count:" << appCount << "Remaining space:" << m_remainingSpace << "Total required:" << newTotalWidth << "Char count:" << charCount << "spacing:" << m_spacing; m_optimalSingleTextWidth = newOptimalWidth; - emit optimalSingleTextWidthChanged(); m_totalWidth = newTotalWidth; - emit totalWidthChanged(); + if (optimalWidthChanged) + emit optimalSingleTextWidthChanged(); + if (totalWidthDidChange) + emit totalWidthChanged(); } } diff --git a/panels/dock/taskmanager/textcalculator.h b/panels/dock/taskmanager/textcalculator.h index 6152b3b1d..665e44941 100644 --- a/panels/dock/taskmanager/textcalculator.h +++ b/panels/dock/taskmanager/textcalculator.h @@ -184,6 +184,7 @@ private slots: QPointer m_dataModel; qreal m_remainingSpace; bool m_enabled; + bool m_calculationPending = false; QHash m_baselineWidthCache; // Cache for baseline widths of different character counts }; From 1f80b6b6befb6cc05f331fb51d31715585632863 Mon Sep 17 00:00:00 2001 From: wjyrich Date: Fri, 24 Jul 2026 17:19:36 +0800 Subject: [PATCH 4/4] feat: add folder group support to dock taskbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add DockGlobalElementModel support for group elements (launcher folders) 2. Update AppGroup to use new folder ID format ("internal/folders/" instead of "internal/folder/") 3. Implement group item details and launch functionality in AppsApplet 4. Display composite preview icons for folder groups in dock items 5. Add FolderPopup QML component with grid layout for folder contents 6. Implement drag-and-drop support for folder elements from launcher 7. Update the dock settings to handle folder dock/undock operations 8. Improve group ID parsing with input validation Log: Added folder group display in dock taskbar with composite preview and popup Influence: 1. Test folder group display in dock: verify composite preview icons show correctly 2. Test folder popup: verify grid layout, scrolling, and application launch 3. Test drag-and-drop folders from launcher to dock 4. Test folder dock/undock via context menu 5. Test folder group ID format migration ("internal/folders/" prefix) 6. Test group ID parsing with invalid inputs (empty, non-numeric, negative) 7. Test group display name and item details retrieval 8. Test folder persistence after dock restart 9. Test launcher folder operations with multiple items and special characters feat: 在任务栏中添加文件夹组支持 1. 为 DockGlobalElementModel 添加组元素支持(启动器文件夹) 2. 更新 AppGroup 使用新的文件夹 ID 格式("internal/folders/" 替代 "internal/folder/") 3. 在 AppsApplet 中实现组项目详情和启动功能 4. 在任务栏项目中显示文件夹组的复合预览图标 5. 添加带有网格布局的 FolderPopup QML 组件用于显示文件夹内容 6. 实现从启动器拖放文件夹元素到任务栏的功能 7. 更新任务栏设置以支持文件夹的停靠/取消停靠操作 8. 改进组 ID 解析验证 Log: 在任务栏中添加文件夹组显示,支持复合预览和弹出窗口 Influence: 1. 测试任务栏中的文件夹组显示:验证复合预览图标是否正确显示 2. 测试文件夹弹窗:验证网格布局、滚动和应用启动功能 3. 测试从启动器拖放文件夹到任务栏 4. 测试通过右键菜单停靠/取消停靠文件夹 5. 测试文件夹组 ID 格式迁移("internal/folders/" 前缀) 6. 测试组 ID 解析对无效输入的处理(空值、非数字、负数) 7. 测试组显示名称和项目详情的获取 8. 测试重启任务栏后文件夹状态的持久化 9. 测试启动器文件夹操作在包含多个项目和特殊字符时的表现 PMS: TASK-392671 --- applets/dde-apps/appgroup.cpp | 30 ++- applets/dde-apps/appgroup.h | 2 +- applets/dde-apps/appgroupmanager.cpp | 21 +- applets/dde-apps/appsapplet.cpp | 50 +++- applets/dde-apps/appsapplet.h | 8 +- panels/dock/CMakeLists.txt | 2 +- panels/dock/MenuHelper.qml | 2 +- panels/dock/dockpositioner.cpp | 2 +- panels/dock/dockpositioner.h | 2 +- panels/dock/package/main.qml | 14 +- .../taskmanager/dockglobalelementmodel.cpp | 249 +++++++++++++++++- .../dock/taskmanager/dockglobalelementmodel.h | 12 +- panels/dock/taskmanager/dockgroupmodel.cpp | 8 + panels/dock/taskmanager/globals.h | 1 + panels/dock/taskmanager/package/AppItem.qml | 66 ++++- .../dock/taskmanager/package/FolderPopup.qml | 209 +++++++++++++++ .../dock/taskmanager/package/TaskManager.qml | 47 +++- .../package/TaskOverflowButton.qml | 1 + panels/dock/taskmanager/taskmanager.cpp | 74 +++++- panels/dock/taskmanager/taskmanager.h | 4 + .../dock/taskmanager/taskmanagersettings.cpp | 2 +- .../taskmanager/taskoverflowcontroller.cpp | 1 + 22 files changed, 736 insertions(+), 71 deletions(-) create mode 100644 panels/dock/taskmanager/package/FolderPopup.qml diff --git a/applets/dde-apps/appgroup.cpp b/applets/dde-apps/appgroup.cpp index c37a6125f..69eb66683 100644 --- a/applets/dde-apps/appgroup.cpp +++ b/applets/dde-apps/appgroup.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -15,11 +15,11 @@ Q_LOGGING_CATEGORY(appGroupLog, "org.deepin.dde.shell.dde-apps.appgroup") namespace apps { AppGroup::AppGroup(const QString &groupId, const QString &name, const QList &appIDs) : AppItem(groupId, AppItemModel::FolderItemType) - , m_itemsPage(new ItemsPage(name, groupId == QStringLiteral("internal/folder/0") ? (4 * 8) : (3 * 4))) + , m_itemsPage(new ItemsPage(name, parseGroupId(groupId) == 0 ? (4 * 8) : (3 * 4))) { setItemsPerPage(m_itemsPage->maxItemCountPerPage()); setAppName(m_itemsPage->name()); - // folder id is a part of its groupId: "internal/folder/{folderId}" + // folder id is a part of its groupId: "internal/folders/{folderId}" setFolderId(parseGroupId(groupId)); for (const QStringList &items : appIDs) { @@ -49,19 +49,33 @@ ItemsPage *AppGroup::itemsPage() bool AppGroup::idIsFolder(const QString & id) { - return id.startsWith(QStringLiteral("internal/folder/")); + return parseGroupId(id) >= 0; } QString AppGroup::groupIdFromNumber(int groupId) { - return QStringLiteral("internal/folder/%1").arg(groupId); + return QStringLiteral("internal/folders/%1").arg(groupId); } int AppGroup::parseGroupId(const QString & id) { - using namespace std::string_view_literals; - constexpr size_t len = "internal/folder/"sv.size(); - return QStringView{id}.mid(len + 1).toInt(); + constexpr QLatin1StringView prefix("internal/folders/"); + if (!id.startsWith(prefix)) + return -1; + + const QStringView number = QStringView(id).sliced(prefix.size()); + + if (number.isEmpty()) + return -1; + + for (const QChar ch : number) { + if (!ch.isDigit()) + return -1; + } + + bool ok = false; + const int groupId = number.toInt(&ok); + return ok && groupId >= 0 ? groupId : -1; } void AppGroup::setItemsPerPage(int number) diff --git a/applets/dde-apps/appgroup.h b/applets/dde-apps/appgroup.h index 97eb4805c..e0d68684a 100644 --- a/applets/dde-apps/appgroup.h +++ b/applets/dde-apps/appgroup.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later diff --git a/applets/dde-apps/appgroupmanager.cpp b/applets/dde-apps/appgroupmanager.cpp index 157fc1771..390c48eb6 100644 --- a/applets/dde-apps/appgroupmanager.cpp +++ b/applets/dde-apps/appgroupmanager.cpp @@ -87,8 +87,8 @@ QModelIndex AppGroupManager::groupIndexById(int groupId) { for (int i = 0; i < rowCount(); i++) { auto groupIndex = index(i, 0); - auto data = groupIndex.data(GroupIdRole); - if (data.toInt() == groupId) { + auto appGroup = static_cast(itemFromIndex(groupIndex)); + if (appGroup && appGroup->folderId() == groupId) { return groupIndex; } } @@ -352,8 +352,8 @@ void AppGroupManager::loadAppGroupInfo() } // always ensure top-level group exists - if (rowCount() == 0) { - auto p = appendGroup(assignGroupId(), "Top Level", {}); + if (!group(TOPLEVEL_FOLDERID)) { + auto p = appendGroup(AppGroup::groupIdFromNumber(TOPLEVEL_FOLDERID), "Top Level", {}); Q_ASSERT(p->folderId() == TOPLEVEL_FOLDERID); } } @@ -375,18 +375,19 @@ void AppGroupManager::saveAppGroupInfo() QString AppGroupManager::assignGroupId() const { - QStringList knownGroupIds; + QSet knownGroupIds; for (int i = 0; i < rowCount(); i++) { - auto group = index(i, 0); - knownGroupIds.append(group.data(AppItemModel::DesktopIdRole).toString()); + auto appGroup = static_cast(itemFromIndex(index(i, 0))); + if (appGroup && appGroup->folderId() >= 0) + knownGroupIds.insert(appGroup->folderId()); } - int idNumber = 0; - while (knownGroupIds.contains(QString("internal/group/%1").arg(idNumber))) { + int idNumber = 1; + while (knownGroupIds.contains(idNumber)) { idNumber++; } - return QString("internal/group/%1").arg(idNumber); + return AppGroup::groupIdFromNumber(idNumber); } AppGroup * AppGroupManager::appendGroup(int groupId, QString groupName, const QList &appItemIDs) diff --git a/applets/dde-apps/appsapplet.cpp b/applets/dde-apps/appsapplet.cpp index aeaf6bfcf..fa6ea70ce 100644 --- a/applets/dde-apps/appsapplet.cpp +++ b/applets/dde-apps/appsapplet.cpp @@ -1,10 +1,14 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #include "appsapplet.h" +#include "amappitem.h" #include "amappitemmodel.h" +#include "appgroup.h" #include "appgroupmanager.h" +#include "appitemmodel.h" +#include "itemspage.h" #include "pluginfactory.h" #include @@ -12,6 +16,17 @@ namespace apps { +static AMAppItem *findApplication(AMAppItemModel *model, const QString &desktopId) +{ + if (auto item = model->appItem(desktopId)) + return item; + + const auto alternateId = desktopId.endsWith(QStringLiteral(".desktop")) + ? desktopId.chopped(8) + : desktopId + QStringLiteral(".desktop"); + return model->appItem(alternateId); +} + AppsApplet::AppsApplet(QObject *parent) : DApplet(parent) , m_appModel(new AMAppItemModel(this)) @@ -40,6 +55,39 @@ QAbstractItemModel *AppsApplet::appModel() const return m_appModel; } +QVariantList AppsApplet::groupItemDetails(const QString &groupId) const +{ + const int id = AppGroup::parseGroupId(groupId); + auto group = id > 0 ? m_groupModel->group(id) : nullptr; + if (!group) + return {}; + + const auto items = group->itemsPage()->allArrangedItems(); + QVariantList result; + result.reserve(items.size()); + for (const auto &desktopId : items) { + const auto item = findApplication(m_appModel, desktopId); + if (!item) + continue; + result.append(QVariantMap{ + {QStringLiteral("desktopId"), desktopId}, + {QStringLiteral("name"), item->data(AppItemModel::NameRole)}, + {QStringLiteral("iconName"), item->data(AppItemModel::IconNameRole)}, + }); + } + return result; +} + +bool AppsApplet::launchApplication(const QString &desktopId) +{ + if (auto item = findApplication(m_appModel, desktopId)) { + item->launch({}, {}, {{QStringLiteral("_launch_type"), QStringLiteral("dde-shell")}}); + return true; + } + + return false; +} + bool AppsApplet::appModelReady() const { return m_appModel->ready(); diff --git a/applets/dde-apps/appsapplet.h b/applets/dde-apps/appsapplet.h index 6c81d73ee..823ec113c 100644 --- a/applets/dde-apps/appsapplet.h +++ b/applets/dde-apps/appsapplet.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -13,6 +13,7 @@ DS_USE_NAMESPACE namespace apps { class AMAppItemModel; +class AppGroupManager; class AppItem; class AppsApplet : public DApplet { @@ -30,6 +31,9 @@ class AppsApplet : public DApplet QAbstractItemModel *appModel() const; QAbstractItemModel *groupModel() const; + Q_INVOKABLE QVariantList groupItemDetails(const QString &groupId) const; + Q_INVOKABLE bool launchApplication(const QString &desktopId); + bool appModelReady() const; signals: @@ -38,6 +42,6 @@ class AppsApplet : public DApplet private: bool m_appModelReady; AMAppItemModel *m_appModel; - QAbstractItemModel *m_groupModel; + AppGroupManager *m_groupModel; }; } diff --git a/panels/dock/CMakeLists.txt b/panels/dock/CMakeLists.txt index 7c084ec29..e1711e171 100644 --- a/panels/dock/CMakeLists.txt +++ b/panels/dock/CMakeLists.txt @@ -97,7 +97,7 @@ ds_install_package(PACKAGE org.deepin.ds.dock TARGET dockpanel) ds_handle_package_translation(PACKAGE org.deepin.ds.dock) # sub plugins -#add_subdirectory(cardleft) +add_subdirectory(cardleft) add_subdirectory(showdesktop) add_subdirectory(taskmanager) add_subdirectory(tray) diff --git a/panels/dock/MenuHelper.qml b/panels/dock/MenuHelper.qml index 475548f0c..160506d15 100644 --- a/panels/dock/MenuHelper.qml +++ b/panels/dock/MenuHelper.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later diff --git a/panels/dock/dockpositioner.cpp b/panels/dock/dockpositioner.cpp index 7fdd83724..9275c44e4 100644 --- a/panels/dock/dockpositioner.cpp +++ b/panels/dock/dockpositioner.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 -2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later diff --git a/panels/dock/dockpositioner.h b/panels/dock/dockpositioner.h index ce29ce30e..fc05d840f 100644 --- a/panels/dock/dockpositioner.h +++ b/panels/dock/dockpositioner.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later diff --git a/panels/dock/package/main.qml b/panels/dock/package/main.qml index 26c69f99b..12584983c 100644 --- a/panels/dock/package/main.qml +++ b/panels/dock/package/main.qml @@ -495,13 +495,13 @@ Window { cornerRadius: fashionDock.enabled ? fashionDock.backgroundRadius : 0 blendColor: { if (valid) { - return DStyle.Style.control.selectColor(undefined, + return D.Style.control.selectColor(undefined, Qt.rgba(235 / 255.0, 235 / 255.0, 235 / 255.0, dock.blendColorAlpha(0.6)), Qt.rgba(10 / 255, 10 / 255, 10 /255, dock.blendColorAlpha(85 / 255))) } - return DStyle.Style.control.selectColor(undefined, - DStyle.Style.behindWindowBlur.lightNoBlurColor, - DStyle.Style.behindWindowBlur.darkNoBlurColor) + return D.Style.control.selectColor(undefined, + D.Style.behindWindowBlur.lightNoBlurColor, + D.Style.behindWindowBlur.darkNoBlurColor) } } @@ -622,6 +622,8 @@ Window { visible: dockLeftPartModel.count > 0 implicitWidth: leftLoader.implicitWidth implicitHeight: leftLoader.implicitHeight + Accessible.role: Accessible.Grouping + Accessible.name: qsTr("Dock Left Area") OverflowContainer { id: leftLoader anchors.fill: parent @@ -640,8 +642,8 @@ Window { implicitHeight: centerLoader.implicitHeight Accessible.role: Accessible.Grouping Accessible.name: qsTr("Dock Center Area") - Layout.maximumWidth: useColumnLayout ? -1 : dockRawCenterSpace - Layout.maximumHeight: useColumnLayout ? dockRawCenterSpace : -1 + Layout.maximumWidth: useColumnLayout ? -1 : dockEffectiveCenterSpace + Layout.maximumHeight: useColumnLayout ? dockEffectiveCenterSpace : -1 onXChanged: dockCenterPartPosChanged() onYChanged: dockCenterPartPosChanged() Layout.leftMargin: !useColumnLayout && !fashionDock.enabled && Panel.itemAlignment === Dock.CenterAlignment ? diff --git a/panels/dock/taskmanager/dockglobalelementmodel.cpp b/panels/dock/taskmanager/dockglobalelementmodel.cpp index 917733816..8fc6589c6 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.cpp +++ b/panels/dock/taskmanager/dockglobalelementmodel.cpp @@ -22,16 +22,40 @@ Q_LOGGING_CATEGORY(dockGlobalElementModelLog, "org.deepin.dde.shell.dock.taskman namespace dock { -DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, DockCombineModel *activeAppModel, QObject *parent) +namespace { + +bool isGroupElement(const QString &id, const QAbstractItemModel *model) +{ + return model == nullptr && id.startsWith(QStringLiteral("internal/folders/")); +} + +int modelRole(const QAbstractItemModel *model, const QByteArray &name, int fallback = -1) +{ + return model ? model->roleNames().key(name, fallback) : fallback; +} + +} + +DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, QAbstractItemModel *groupModel, QObject *groupProvider, + DockCombineModel *activeAppModel, QObject *parent) : QAbstractListModel(parent) , AbstractTaskManagerInterface(nullptr) , m_appsModel(appsModel) + , m_groupModel(groupModel) + , m_groupProvider(groupProvider) , m_activeAppModel(activeAppModel) { connect(TaskManagerSettings::instance(), &TaskManagerSettings::dockedElementsChanged, this, &DockGlobalElementModel::loadDockedElements); connect(TaskManagerSettings::instance(), &TaskManagerSettings::windowSplitChanged, this, &DockGlobalElementModel::groupItemsByApp); connect(TaskManagerSettings::instance(), &TaskManagerSettings::dockedApplicationsEnabledChanged, this, &DockGlobalElementModel::loadDockedElements); + if (m_groupModel) { + connect(m_groupModel, &QAbstractItemModel::modelReset, this, &DockGlobalElementModel::refreshGroupElements, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::rowsInserted, this, &DockGlobalElementModel::refreshGroupElements, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::rowsRemoved, this, &DockGlobalElementModel::handleGroupRowsRemoved, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::dataChanged, this, &DockGlobalElementModel::notifyGroupElementsChanged, Qt::QueuedConnection); + } + connect( m_appsModel, &QAbstractItemModel::rowsRemoved, @@ -229,6 +253,7 @@ QHash DockGlobalElementModel::roleNames() const {TaskManager::WinTitleRole, MODEL_TITLE}, {TaskManager::WinIconRole, MODEL_WINICON}, {TaskManager::WinIdRole, MODEL_WINID}, + {TaskManager::GroupItemsRole, MODEL_GROUPITEMS}, }; } @@ -261,32 +286,45 @@ void DockGlobalElementModel::loadDockedElements() QList> newDocked; if (TaskManagerSettings::instance()->dockedApplicationsEnabled()) { for (auto elementInfo : TaskManagerSettings::instance()->dockedElements()) { - auto pair = elementInfo.split('/'); - if (pair.size() != 2) - continue; + QString type; + QString id; + if (elementInfo.startsWith(QStringLiteral("internal/folders/"))) { + type = QStringLiteral("group"); + id = elementInfo; + } else { + const auto separator = elementInfo.indexOf('/'); + if (separator <= 0 || separator == elementInfo.size() - 1) + continue; - auto type = pair[0]; - auto id = pair[1]; + type = elementInfo.left(separator); + id = elementInfo.mid(separator + 1); + if (type != QStringLiteral("desktop")) + continue; + } auto tmp = std::make_tuple(type, id); // check desktop is installed QAbstractItemModel *model = nullptr; int row = 0; - if (type == "desktop") { + if (type == QStringLiteral("desktop")) { model = m_appsModel; auto res = m_appsModel->match(m_appsModel->index(0, 0), TaskManager::DesktopIdRole, id, 1, Qt::MatchExactly).value(0); if (!res.isValid()) continue; row = res.row(); + } else if (!groupIndex(id).isValid()) { + continue; } newDocked.append(tmp); if (m_dockedElements.contains(tmp)) continue; - auto isRunning = std::any_of(m_data.constBegin(), m_data.constEnd(), [&id](const auto &data) { - return std::get<0>(data) == id; + auto isRunning = std::any_of(m_data.constBegin(), m_data.constEnd(), [&type, &id](const auto &data) { + if (std::get<0>(data) != id) + return false; + return isGroupElement(std::get<0>(data), std::get<1>(data)) == (type == QStringLiteral("group")); }); if (!isRunning) { @@ -301,8 +339,12 @@ void DockGlobalElementModel::loadDockedElements() if (newDocked.contains(*it)) continue; auto type = std::get<0>(*it), id = std::get<1>(*it); - auto dataIt = std::find_if(m_data.begin(), m_data.end(), [this, &id](const auto &data) { - return std::get<0>(data) == id && std::get<1>(data) == m_appsModel; + auto dataIt = std::find_if(m_data.begin(), m_data.end(), [this, &type, &id](const auto &item) { + if (std::get<0>(item) != id) + return false; + if (type == QStringLiteral("group")) + return isGroupElement(std::get<0>(item), std::get<1>(item)); + return std::get<1>(item) == m_appsModel; }); if (dataIt != m_data.end()) { auto pos = (dataIt - m_data.begin()); @@ -329,6 +371,15 @@ QString DockGlobalElementModel::getMenus(const QModelIndex &index) const auto model = std::get<1>(data); auto row = std::get<2>(data); + if (isGroupElement(id, model)) { + QJsonArray menusArray; + if (TaskManagerSettings::instance()->dockedApplicationsEnabled()) { + const bool isDocked = m_dockedElements.contains(std::make_tuple(QStringLiteral("group"), id)); + menusArray.append(QJsonObject{{"id", DOCK_ACTION_DOCK}, {"name", isDocked ? tr("Undock") : tr("Dock")}}); + } + return QJsonDocument(menusArray).toJson(); + } + QJsonArray menusArray; QString appNameInMenu = tr("Open"); if (model == m_activeAppModel) { @@ -379,9 +430,36 @@ QVariant DockGlobalElementModel::data(const QModelIndex &index, int role) const auto model = std::get<1>(data); auto row = std::get<2>(data); + if (isGroupElement(id, model)) { + switch (role) { + case TaskManager::ItemIdRole: + case TaskManager::DesktopIdRole: + return id; + case TaskManager::GroupItemsRole: + return groupApplications(id); + case TaskManager::NameRole: + return groupDisplayName(id); + case TaskManager::IconNameRole: + return QStringLiteral("folder"); + case TaskManager::WindowsRole: + return QStringList{}; + case TaskManager::ActiveRole: + case TaskManager::AttentionRole: + return false; + case TaskManager::DockedRole: + return m_dockedElements.contains(std::make_tuple(QStringLiteral("group"), id)); + case TaskManager::MenusRole: + return getMenus(index); + default: + return {}; + } + } + switch (role) { case TaskManager::ItemIdRole: return id; + case TaskManager::GroupItemsRole: + return QVariantList{}; case TaskManager::WindowsRole: { if (model == m_activeAppModel) { return QStringList{model->index(row, 0).data(TaskManager::WinIdRole).toString()}; @@ -421,6 +499,9 @@ void DockGlobalElementModel::requestActivate(const QModelIndex &index) const auto sourceModel = std::get<1>(data); auto sourceRow = std::get<2>(data); + if (isGroupElement(id, sourceModel)) + return; + if (sourceModel == m_activeAppModel) { auto sourceIndex = sourceModel->index(sourceRow, 0); m_activeAppModel->requestActivate(sourceIndex); @@ -436,8 +517,15 @@ void DockGlobalElementModel::requestNewInstance(const QModelIndex &index, const { auto data = m_data.value(index.row()); auto id = std::get<0>(data); + auto model = std::get<1>(data); qDebug(dockGlobalElementModelLog) << "Requesting new instance for index:" << index << "with action:" << action << "id:" << id; + if (isGroupElement(id, model)) { + if (action == DOCK_ACTION_DOCK) + TaskManagerSettings::instance()->toggleDockedElement(id); + return; + } + // Handle special actions first (for both active and docked apps) if (action == DOCK_ACTION_DOCK) { TaskManagerSettings::instance()->toggleDockedElement(QStringLiteral("desktop/%1").arg(id)); @@ -464,6 +552,9 @@ void DockGlobalElementModel::requestOpenUrls(const QModelIndex &index, const QLi { auto data = m_data.value(index.row()); auto id = std::get<0>(data); + auto model = std::get<1>(data); + if (isGroupElement(id, model)) + return; QStringList urlStrings; for (const QUrl &url : urls) { @@ -527,6 +618,134 @@ void DockGlobalElementModel::moveItem(int from, int to) endMoveRows(); } +QModelIndex DockGlobalElementModel::groupIndex(const QString &launcherFolderId) const +{ + if (!m_groupModel || m_groupModel->rowCount() == 0 || launcherFolderId == QStringLiteral("internal/folders/0")) + return {}; + + const int desktopIdRole = modelRole(m_groupModel, QByteArray(MODEL_DESKTOPID)); + if (desktopIdRole < 0) + return {}; + + return m_groupModel->match(m_groupModel->index(0, 0), desktopIdRole, + launcherFolderId, 1, Qt::MatchExactly).value(0); +} + +QString DockGlobalElementModel::groupDisplayName(const QString &launcherFolderId) const +{ + QString displayName; + if (m_groupProvider) { + QMetaObject::invokeMethod(m_groupProvider, "groupDisplayName", Qt::DirectConnection, + Q_RETURN_ARG(QString, displayName), Q_ARG(QString, launcherFolderId)); + } + + if (!displayName.isEmpty()) + return displayName; + + const auto sourceIndex = groupIndex(launcherFolderId); + const int displayRole = modelRole(m_groupModel, QByteArrayLiteral("display"), Qt::DisplayRole); + const int nameRole = modelRole(m_groupModel, QByteArray(MODEL_NAME), displayRole); + return sourceIndex.data(nameRole).toString(); +} + +QVariantList DockGlobalElementModel::groupApplications(const QString &launcherFolderId) const +{ + QVariantList applications; + if (m_groupProvider) { + QMetaObject::invokeMethod(m_groupProvider, "groupItemDetails", Qt::DirectConnection, + Q_RETURN_ARG(QVariantList, applications), Q_ARG(QString, launcherFolderId)); + } + + const int desktopIdRole = modelRole(m_appsModel, QByteArray(MODEL_DESKTOPID)); + const int nameRole = modelRole(m_appsModel, QByteArray(MODEL_NAME)); + if (desktopIdRole < 0 || nameRole < 0) + return applications; + + for (QVariant &application : applications) { + QVariantMap details = application.toMap(); + const QString desktopId = details.value(QStringLiteral("desktopId")).toString(); + if (desktopId.isEmpty()) + continue; + + QModelIndex appIndex = m_appsModel->match(m_appsModel->index(0, 0), desktopIdRole, + desktopId, 1, Qt::MatchExactly).value(0); + if (!appIndex.isValid()) { + const QString alternateId = desktopId.endsWith(QStringLiteral(".desktop")) + ? desktopId.chopped(8) + : desktopId + QStringLiteral(".desktop"); + appIndex = m_appsModel->match(m_appsModel->index(0, 0), desktopIdRole, + alternateId, 1, Qt::MatchExactly).value(0); + } + + if (!appIndex.isValid()) + continue; + + const QString localizedName = appIndex.data(nameRole).toString(); + if (!localizedName.isEmpty()) { + details.insert(QStringLiteral("name"), localizedName); + application = details; + } + } + + return applications; +} + +void DockGlobalElementModel::refreshGroupElements() +{ + loadDockedElements(); + notifyGroupElementsChanged(); +} + +void DockGlobalElementModel::handleGroupRowsRemoved() +{ + auto *settings = TaskManagerSettings::instance(); + QStringList dockedElements = settings->dockedElements(); + bool dockedElementsChanged = false; + + for (auto it = dockedElements.begin(); it != dockedElements.end();) { + if (!it->startsWith(QStringLiteral("internal/folders/"))) { + ++it; + continue; + } + + if (groupIndex(*it).isValid()) { + ++it; + continue; + } + + it = dockedElements.erase(it); + dockedElementsChanged = true; + } + + if (dockedElementsChanged) { + settings->setDockedElements(dockedElements); + } else { + loadDockedElements(); + } + notifyGroupElementsChanged(); +} + +void DockGlobalElementModel::notifyGroupElementsChanged() +{ + if (!m_data.isEmpty()) { + Q_EMIT dataChanged(index(0, 0), index(m_data.size() - 1, 0), + {TaskManager::NameRole, TaskManager::GroupItemsRole}); + } +} + +bool DockGlobalElementModel::requestDockGroup(const QString &launcherFolderId) +{ + if (!groupIndex(launcherFolderId).isValid()) + return false; + + auto *settings = TaskManagerSettings::instance(); + if (settings->isDocked(launcherFolderId)) + return false; + + settings->appendDockedElement(launcherFolderId); + return true; +} + void DockGlobalElementModel::groupItemsByApp() { if (m_data.isEmpty()) @@ -536,16 +755,20 @@ void DockGlobalElementModel::groupItemsByApp() return; for (int i = 0; i < m_data.size(); ++i) { + if (std::get<1>(m_data.at(i)) == nullptr) + continue; const QString currentId = std::get<0>(m_data.at(i)); int insertPos = i + 1; - while (insertPos < m_data.size() && std::get<0>(m_data.at(insertPos)) == currentId) { + while (insertPos < m_data.size() + && std::get<1>(m_data.at(insertPos)) != nullptr + && std::get<0>(m_data.at(insertPos)) == currentId) { ++insertPos; } for (int j = insertPos; j < m_data.size(); ++j) { - if (std::get<0>(m_data.at(j)) != currentId) + if (std::get<1>(m_data.at(j)) == nullptr || std::get<0>(m_data.at(j)) != currentId) continue; int destRow = insertPos < j ? insertPos : insertPos + 1; diff --git a/panels/dock/taskmanager/dockglobalelementmodel.h b/panels/dock/taskmanager/dockglobalelementmodel.h index 64a991bcf..97b223570 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.h +++ b/panels/dock/taskmanager/dockglobalelementmodel.h @@ -15,7 +15,8 @@ class DockGlobalElementModel : public QAbstractListModel, public AbstractTaskMan { Q_OBJECT public: - explicit DockGlobalElementModel(QAbstractItemModel *appsModel, DockCombineModel *activeAppModel, QObject *parent = nullptr); + explicit DockGlobalElementModel(QAbstractItemModel *appsModel, QAbstractItemModel *groupModel, QObject *groupProvider, + DockCombineModel *activeAppModel, QObject *parent = nullptr); QHash roleNames() const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; @@ -37,14 +38,21 @@ class DockGlobalElementModel : public QAbstractListModel, public AbstractTaskMan void requestWindowsView(const QModelIndexList &indexes) const override; void moveItem(int from, int to); + bool requestDockGroup(const QString &launcherFolderId); public slots: void initDockedElements(bool unused); private: void loadDockedElements(); + void refreshGroupElements(); + void handleGroupRowsRemoved(); + void notifyGroupElementsChanged(); QString getMenus(const QModelIndex &index) const; void groupItemsByApp(); + QModelIndex groupIndex(const QString &launcherFolderId) const; + QString groupDisplayName(const QString &launcherFolderId) const; + QVariantList groupApplications(const QString &launcherFolderId) const; private: // id, model, and pos @@ -53,6 +61,8 @@ public slots: // type, id QList> m_dockedElements; QAbstractItemModel *m_appsModel; + QAbstractItemModel *m_groupModel; + QPointer m_groupProvider; DockCombineModel *m_activeAppModel; }; } diff --git a/panels/dock/taskmanager/dockgroupmodel.cpp b/panels/dock/taskmanager/dockgroupmodel.cpp index 432453357..46fb15d39 100644 --- a/panels/dock/taskmanager/dockgroupmodel.cpp +++ b/panels/dock/taskmanager/dockgroupmodel.cpp @@ -195,6 +195,14 @@ void DockGroupModel::requestWindowsView(const QModelIndexList &indexes) const void DockGroupModel::requestNewInstance(const QModelIndex &index, const QString &action) const { + const auto itemId = index.data(TaskManager::ItemIdRole).toString(); + const bool isGroup = itemId.startsWith(QStringLiteral("internal/folders/")); + if (isGroup) { + if (action == DOCK_ACTION_DOCK) + TaskManagerSettings::instance()->toggleDockedElement(itemId); + return; + } + if (action == DOCK_ACTION_DOCK) { auto desktopId = index.data(TaskManager::DesktopIdRole).toString(); TaskManagerSettings::instance()->toggleDockedElement(QStringLiteral("desktop/%1").arg(desktopId)); diff --git a/panels/dock/taskmanager/globals.h b/panels/dock/taskmanager/globals.h index 47ec6a9b2..17cc377f5 100644 --- a/panels/dock/taskmanager/globals.h +++ b/panels/dock/taskmanager/globals.h @@ -48,6 +48,7 @@ constexpr auto MODEL_ACTIONS = "actions"; constexpr auto MODEL_MENUS = "menus"; constexpr auto MODEL_DOCKED = "docked"; constexpr auto MODEL_WINDOWS = "windows"; +constexpr auto MODEL_GROUPITEMS = "groupItems"; // default item icon constexpr auto DEFAULT_APP_ICONNAME = "application-default-icon"; diff --git a/panels/dock/taskmanager/package/AppItem.qml b/panels/dock/taskmanager/package/AppItem.qml index 9527e9c46..00694d842 100644 --- a/panels/dock/taskmanager/package/AppItem.qml +++ b/panels/dock/taskmanager/package/AppItem.qml @@ -24,6 +24,9 @@ Item { required property int visualIndex required property var modelIndex required property string title + required property var groupItems + readonly property bool isGroup: itemId.indexOf("internal/folders/") === 0 + readonly property bool useCompositePreview: root.isGroup property real blendOpacity: 1.0 property bool dragEnabled: true @@ -40,7 +43,6 @@ Item { Drag.hotSpot.x: icon.width / 2 Drag.hotSpot.y: icon.height / 2 Drag.dragType: Drag.Automatic - Drag.mimeData: { "text/x-dde-dock-dnd-appid": itemId, "text/x-dde-dock-dnd-source": "taskbar", "text/x-dde-dock-dnd-winid": windows.length > 0 ? windows[0] : ""} Accessible.role: Accessible.Button Accessible.name: root.name @@ -166,6 +168,7 @@ Item { D.DciIcon { id: icon name: root.iconName + visible: !root.useCompositePreview height: iconSize width: iconSize sourceSize: Qt.size(iconSize, iconSize) @@ -246,6 +249,44 @@ Item { running: false } } + + Item { + anchors.centerIn: parent + width: root.iconSize + height: root.iconSize + + Rectangle { + anchors.fill: parent + radius: Math.max(6, Math.round(root.iconSize / 4)) + color: root.colorTheme === Dock.Dark ? + Qt.rgba(1, 1, 1, 0.10) : + Qt.rgba(0, 0, 0, 0.10) + border.width: 1 + border.color: root.colorTheme === Dock.Dark ? + Qt.rgba(1, 1, 1, 0.40) : + Qt.rgba(0, 0, 0, 0.20) + visible: root.useCompositePreview + } + + Grid { + anchors.centerIn: parent + visible: root.useCompositePreview + columns: 2 + spacing: Math.max(1, Math.round(root.iconSize * 0.04)) + + Repeater { + model: Math.min(4, root.groupItems.length) + D.DciIcon { + required property int index + width: Math.round(root.iconSize * 0.34) + height: width + sourceSize: Qt.size(width, height) + name: root.groupItems[index].iconName + retainWhileLoading: true + } + } + } + } } WindowIndicator { @@ -458,7 +499,7 @@ Item { function onEntered() { - if (windows.length === 0) { + if (root.isGroup || windows.length === 0) { toolTipShowTimer.start() return } @@ -484,6 +525,23 @@ Item { closeItemPreview() } + function toggleGroupPopup() { + toolTip.close() + Panel.requestClosePopup() + const point = icon.mapToItem(null, icon.width / 2, icon.height / 2) + groupPopup.openAt(point) + } + + FolderPopup { + id: groupPopup + groupName: root.name + groupItems: root.groupItems + displayMode: root.displayMode + onLaunchApplicationRequested: function(desktopId) { + taskmanager.Applet.launchGroupApplication(desktopId) + } + } + function closeItemPreview() { if (previewTimer.running) { previewTimer.stop() @@ -549,6 +607,10 @@ Item { if (mouse.button === Qt.RightButton) { requestAppItemMenu() } else { + if (root.isGroup) { + toggleGroupPopup() + return + } if (root.windows.length === 0) { launchAnimation.start(); TaskManager.requestNewInstance(index, ""); diff --git a/panels/dock/taskmanager/package/FolderPopup.qml b/panels/dock/taskmanager/package/FolderPopup.qml new file mode 100644 index 000000000..3a8c090ba --- /dev/null +++ b/panels/dock/taskmanager/package/FolderPopup.qml @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +PanelPopup { + id: root + + required property string groupName + required property var groupItems + required property int displayMode + + readonly property int itemCount: root.groupItems ? root.groupItems.length : 0 + readonly property int columnCount: root.itemCount > 12 ? 5 : 4 + readonly property int rowCount: Math.max(1, Math.ceil(root.itemCount / root.columnCount)) + readonly property int visibleRowCount: Math.min(3, root.rowCount) + + readonly property int itemWidth: 96 + readonly property int itemHeight: 104 + readonly property int itemIconSize: 48 + readonly property int itemSpacing: 8 + readonly property int horizontalPadding: 5 + readonly property int titleTopPadding: 12 + readonly property int titleHeight: 24 + readonly property int titleBottomSpacing: 8 + readonly property int bottomPadding: 22 + readonly property int gridWidth: root.columnCount * root.itemWidth + + (root.columnCount - 1) * root.itemSpacing + readonly property int gridHeight: root.rowCount * root.itemHeight + + (root.rowCount - 1) * root.itemSpacing + readonly property int visibleGridHeight: root.visibleRowCount * root.itemHeight + + (root.visibleRowCount - 1) * root.itemSpacing + + property point anchorPoint: Qt.point(0, 0) + + signal launchApplicationRequested(string desktopId) + + width: root.gridWidth + root.horizontalPadding * 2 + height: root.titleTopPadding + root.titleHeight + root.titleBottomSpacing + + root.visibleGridHeight + root.bottomPadding + popupX: DockPanelPositioner.x + popupY: DockPanelPositioner.y + closeOnInactive: MenuHelper.activeMenu === null && !Panel.contextDragging + + function openAt(point) { + root.anchorPoint = point + popupFlickable.contentY = 0 + root.open() + } + + Component.onCompleted: { + DockPanelPositioner.bounding = Qt.binding(function() { + return Qt.rect(root.anchorPoint.x, root.anchorPoint.y, + root.width, root.height) + }) + } + + Control { + id: popupContent + + anchors.fill: parent + leftPadding: root.horizontalPadding + rightPadding: root.horizontalPadding + topPadding: root.titleTopPadding + root.titleHeight + root.titleBottomSpacing + bottomPadding: root.bottomPadding + background: Item {} + + Text { + id: folderTitle + + anchors.top: parent.top + anchors.topMargin: root.titleTopPadding + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: root.horizontalPadding + 8 + anchors.rightMargin: root.horizontalPadding + 8 + height: root.titleHeight + text: root.groupName + textFormat: Text.PlainText + elide: Text.ElideRight + maximumLineCount: 1 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: D.DTK.palette.windowText + font: D.DTK.fontManager.t6 + } + + contentItem: Flickable { + id: popupFlickable + + contentWidth: width + contentHeight: root.gridHeight + clip: true + interactive: root.rowCount > root.visibleRowCount + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + pixelAligned: true + + Grid { + id: popupGrid + + width: popupFlickable.width + height: root.gridHeight + columns: root.columnCount + rowSpacing: root.itemSpacing + columnSpacing: root.itemSpacing + + Repeater { + id: groupRepeater + model: root.groupItems + + delegate: Item { + id: groupAppItem + + required property var modelData + + width: root.itemWidth + height: root.itemHeight + clip: true + + Item { + id: groupAppContent + + anchors.fill: parent + + AppItemBackground { + id: groupAppHoverBackground + + anchors.fill: parent + radius: 8 + enabled: false + isActive: false + windowCount: 0 + displayMode: root.displayMode + D.ColorSelector.hovered: groupAppHoverHandler.hovered + D.ColorSelector.pressed: groupAppMouseArea.pressed + } + + D.DciIcon { + id: groupAppIcon + + width: root.itemIconSize + height: width + anchors.top: parent.top + anchors.topMargin: 7 + anchors.horizontalCenter: parent.horizontalCenter + name: modelData.iconName + sourceSize: Qt.size(width, height) + } + + Text { + id: groupAppTitle + + anchors.top: groupAppIcon.bottom + anchors.topMargin: 4 + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 2 + anchors.rightMargin: 2 + textFormat: Text.PlainText + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + text: modelData.name + color: D.DTK.palette.windowText + font: D.DTK.fontManager.t9 + } + + HoverHandler { + id: groupAppHoverHandler + } + + MouseArea { + id: groupAppMouseArea + + anchors.fill: parent + onClicked: { + root.launchApplicationRequested(modelData.desktopId) + root.close() + } + } + } + } + } + } + + ScrollBar.vertical: ScrollBar { + parent: popupContent + anchors.top: popupFlickable.top + anchors.right: popupContent.right + anchors.bottom: popupFlickable.bottom + anchors.rightMargin: 1 + width: 4 + policy: root.rowCount > root.visibleRowCount + ? ScrollBar.AsNeeded + : ScrollBar.AlwaysOff + } + } + } +} diff --git a/panels/dock/taskmanager/package/TaskManager.qml b/panels/dock/taskmanager/package/TaskManager.qml index ccbc75299..536bce88f 100644 --- a/panels/dock/taskmanager/package/TaskManager.qml +++ b/panels/dock/taskmanager/package/TaskManager.qml @@ -152,10 +152,11 @@ ContainmentItem { required property string icon // winIconName required property string menus required property list windows + required property var groupItems z: attention ? -1 : 0 property bool visibility: { - let draggedAppId = taskmanager.Applet.desktopIdToAppId(launcherDndDropArea.launcherDndDesktopId) - if (itemId !== draggedAppId) { + const draggedItemId = launcherDndDropArea.dockItemId(launcherDndDropArea.launcherDndDesktopId) + if (itemId !== draggedItemId) { return true } return windows.length > 0 && launcherDndDropArea.launcherDndWinId !== windows[0] @@ -273,6 +274,7 @@ ContainmentItem { iconName: delegateRoot.iconName menus: delegateRoot.menus windows: delegateRoot.windows + groupItems: delegateRoot.groupItems visualIndex: delegateRoot.visualIndex modelIndex: delegateRoot.modelIndex blendOpacity: taskmanager.blendOpacity @@ -300,7 +302,7 @@ ContainmentItem { id: launcherDndDropArea anchors.fill: parent z: 3 - keys: ["text/x-dde-dock-dnd-appid"] + keys: ["text/x-dde-dock-dnd-appid", "text/x-dde-launcher-dnd-desktopId"] property string launcherDndDesktopId: "" property string launcherDndDragSource: "" property string launcherDndWinId: "" @@ -340,6 +342,15 @@ ContainmentItem { || launcherDndDragSource === "overflow-popup" } + function isLauncherFolderId(id) { + return typeof id === "string" + && id.indexOf("internal/folders/") === 0 + } + + function dockItemId(id) { + return taskmanager.Applet.desktopIdToAppId(id) + } + function requestDockIfNeeded() { if (isInternalDrag() || launcherDndDocked) { return true @@ -356,16 +367,26 @@ ContainmentItem { onEntered: function(drag) { let desktopId = drag.getDataAsString("text/x-dde-dock-dnd-appid") + if (!desktopId) { + const launcherId = drag.getDataAsString("text/x-dde-launcher-dnd-desktopId") + if (isLauncherFolderId(launcherId)) { + desktopId = launcherId + } + } + if (!desktopId) { + resetDndState() + return + } launcherDndDragSource = drag.getDataAsString("text/x-dde-dock-dnd-source") launcherDndWinId = drag.getDataAsString("text/x-dde-dock-dnd-winid") launcherDndDesktopId = desktopId - if (launcherDndDragSource !== "taskbar" && taskmanager.Applet.requestDockByDesktopId(desktopId) === false) { - drag.accepted = false - resetDndState() let targetIndex = targetIndexAt(drag.x, drag.y) if (!isOverflowButtonIndex(targetIndex) && !requestDockIfNeeded()) { return } + if (!isOverflowButtonIndex(targetIndex)) { + drag.acceptProposedAction() + } } onPositionChanged: function(drag) { @@ -373,8 +394,9 @@ ContainmentItem { if (launcherDndDragSource === "overflow-popup") return let targetIndex = targetIndexAt(drag.x, drag.y) if (isOverflowButtonIndex(targetIndex) || !requestDockIfNeeded()) return - let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) - let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(appId, launcherDndWinId) : taskmanager.findAppIndex(appId) + drag.acceptProposedAction() + const itemId = dockItemId(launcherDndDesktopId) + let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(itemId, launcherDndWinId) : taskmanager.findAppIndex(itemId) if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { if (taskmanager.Applet.windowSplit) { taskmanager.Applet.moveItem(currentIndex, targetIndex) @@ -388,11 +410,10 @@ ContainmentItem { onDropped: function(drop) { Panel.contextDragging = false if (launcherDndDesktopId === "") return - const overflowPopupDrag = launcherDndDragSource === "overflow-popup" let targetIndex = targetIndexAt(drop.x, drop.y) if (!requestDockIfNeeded()) return - let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) - let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(appId, launcherDndWinId) : taskmanager.findAppIndex(appId) + const itemId = dockItemId(launcherDndDesktopId) + let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(itemId, launcherDndWinId) : taskmanager.findAppIndex(itemId) if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { if (taskmanager.Applet.windowSplit) { taskmanager.Applet.moveItem(currentIndex, targetIndex) @@ -406,8 +427,10 @@ ContainmentItem { appIds.push(visualModel.items.get(i).model.itemId) } taskmanager.Applet.saveDockElementsOrder(appIds) - if (overflowPopupDrag) { + if (isInternalDrag()) { drop.accept(Qt.MoveAction) + } else { + drop.acceptProposedAction() } resetDndState() } diff --git a/panels/dock/taskmanager/package/TaskOverflowButton.qml b/panels/dock/taskmanager/package/TaskOverflowButton.qml index 8669cc64e..aabc77d3d 100644 --- a/panels/dock/taskmanager/package/TaskOverflowButton.qml +++ b/panels/dock/taskmanager/package/TaskOverflowButton.qml @@ -203,6 +203,7 @@ Item { iconName: modelData.iconName menus: modelData.menus windows: modelData.windows + groupItems: modelData.groupItems visualIndex: modelData.visualIndex modelIndex: modelData.modelIndex blendOpacity: root.taskManagerItem.blendOpacity diff --git a/panels/dock/taskmanager/taskmanager.cpp b/panels/dock/taskmanager/taskmanager.cpp index 914d668a3..a4741e608 100644 --- a/panels/dock/taskmanager/taskmanager.cpp +++ b/panels/dock/taskmanager/taskmanager.cpp @@ -53,6 +53,15 @@ Q_LOGGING_CATEGORY(taskManagerLog, "org.deepin.dde.shell.dock.taskmanager", QtDe namespace dock { +static QString canonicalLauncherFolderId(const QString &id) +{ + static const QRegularExpression expression(QStringLiteral("^internal/folders/(\\d+)$")); + const auto match = expression.match(id.trimmed()); + if (!match.hasMatch() || match.captured(1).toInt() <= 0) + return {}; + return QStringLiteral("internal/folders/%1").arg(match.captured(1).toInt()); +} + // 通过AM(Application Manager)匹配应用程序的辅助函数 static QString getDesktopIdByPid(const QStringList &identifies) { @@ -185,10 +194,22 @@ bool TaskManager::init() DApplet::init(); - DS_NAMESPACE::DAppletBridge bridge("org.deepin.ds.dde-apps"); + DS_NAMESPACE::DAppletBridge appsBridge("org.deepin.ds.dde-apps"); BoolFilterModel *leftModel = new BoolFilterModel(m_windowMonitor.data(), m_windowMonitor->roleNames().key("shouldSkip"), this); - if (auto applet = bridge.applet()) { + if (auto applet = appsBridge.applet()) { + m_appsApplet = applet; auto model = applet->property("appModel").value(); + + DS_NAMESPACE::DAppletBridge launcherBridge("org.deepin.ds.dock.launcherapplet"); + auto launcherApplet = launcherBridge.applet(); + auto groupModel = launcherApplet + ? launcherApplet->property("itemArrangementModel").value() + : nullptr; + if (groupModel) + qCInfo(taskManagerLog) << "Using live launchpad item arrangement model"; + else + qCWarning(taskManagerLog) << "Launchpad item arrangement model is unavailable"; + Q_ASSERT(model); m_activeAppModel = new DockCombineModel(leftModel, model, TaskManager::IdentityRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { auto roleNames = model->roleNames(); @@ -220,7 +241,7 @@ bool TaskManager::init() return res.value(0); }); - m_dockGlobalElementModel = new DockGlobalElementModel(model, m_activeAppModel, this); + m_dockGlobalElementModel = new DockGlobalElementModel(model, groupModel, launcherApplet, m_activeAppModel, this); m_itemModel = new DockItemModel(m_dockGlobalElementModel, this); // 初始化预览代理模型,基于合并后的数据 @@ -403,8 +424,13 @@ QString TaskManager::desktopIdToAppId(const QString& desktopId) bool TaskManager::requestDockByDesktopId(const QString& desktopID) { - if (!Settings->dockedApplicationsEnabled()) return false; - if (desktopID.startsWith("internal/")) return false; + if (!Settings->dockedApplicationsEnabled()) + return false; + + const auto folderId = canonicalLauncherFolderId(desktopID); + if (!folderId.isEmpty()) + return m_dockGlobalElementModel && m_dockGlobalElementModel->requestDockGroup(folderId); + QString appId = desktopIdToAppId(desktopID); // 检查应用是否已经在任务栏中,如果是则返回 false if (IsDocked(appId)) @@ -415,11 +441,34 @@ bool TaskManager::requestDockByDesktopId(const QString& desktopID) bool TaskManager::requestUndockByDesktopId(const QString& desktopID) { - if (!Settings->dockedApplicationsEnabled()) return false; - if (desktopID.startsWith("internal/")) return false; + if (!Settings->dockedApplicationsEnabled()) + return false; + + const auto folderId = canonicalLauncherFolderId(desktopID); + if (!folderId.isEmpty()) + return requestUndockElement(folderId); + return RequestUndock(desktopIdToAppId(desktopID)); } +bool TaskManager::launchGroupApplication(const QString &desktopId) +{ + if (!m_appsApplet) + return false; + bool launched = false; + QMetaObject::invokeMethod(m_appsApplet, "launchApplication", Qt::DirectConnection, + Q_RETURN_ARG(bool, launched), Q_ARG(QString, desktopId)); + return launched; +} + +bool TaskManager::requestUndockElement(const QString &elementKey) +{ + if (!TaskManagerSettings::instance()->isDocked(elementKey)) + return false; + TaskManagerSettings::instance()->removeDockedElement(elementKey); + return true; +} + bool TaskManager::RequestDock(QString appID) { if (!Settings->dockedApplicationsEnabled()) @@ -504,9 +553,14 @@ void TaskManager::saveDockElementsOrder(const QStringList &appIds) const QStringList &dockedElements = TaskManagerSettings::instance()->dockedElements(); QStringList newDockedElements; for (const auto &appId : appIds) { - auto desktopElement = QString("desktop/%1").arg(appId); - if (dockedElements.contains(desktopElement) && !newDockedElements.contains(desktopElement)) { - newDockedElements.append(desktopElement); + if (appId == QStringLiteral("internal/folders/0")) + continue; + const auto folderId = canonicalLauncherFolderId(appId); + const auto element = folderId.isEmpty() + ? QStringLiteral("desktop/%1").arg(appId) + : folderId; + if (dockedElements.contains(element) && !newDockedElements.contains(element)) { + newDockedElements.append(element); } } TaskManagerSettings::instance()->setDockedElements(newDockedElements); diff --git a/panels/dock/taskmanager/taskmanager.h b/panels/dock/taskmanager/taskmanager.h index 517bd0da3..53f409dfe 100644 --- a/panels/dock/taskmanager/taskmanager.h +++ b/panels/dock/taskmanager/taskmanager.h @@ -65,6 +65,7 @@ class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManage ExecsRole, CategoriesRole, DesktopSourcePathRole, + GroupItemsRole, }; Q_ENUM(Roles) @@ -94,6 +95,8 @@ class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManage Q_INVOKABLE QString desktopIdToAppId(const QString& desktopId); Q_INVOKABLE bool requestDockByDesktopId(const QString& desktopID); Q_INVOKABLE bool requestUndockByDesktopId(const QString& desktopID); + Q_INVOKABLE bool launchGroupApplication(const QString &desktopId); + Q_INVOKABLE bool requestUndockElement(const QString &elementKey); Q_INVOKABLE bool RequestDock(QString appID); Q_INVOKABLE bool IsDocked(QString appID); Q_INVOKABLE bool RequestUndock(QString appID); @@ -126,6 +129,7 @@ private Q_SLOTS: DockItemModel *m_itemModel = nullptr; HoverPreviewProxyModel *m_hoverPreviewModel = nullptr; int queryTrashCount() const; + QPointer m_appsApplet; }; } diff --git a/panels/dock/taskmanager/taskmanagersettings.cpp b/panels/dock/taskmanager/taskmanagersettings.cpp index 4f660ddec..0303480f7 100644 --- a/panels/dock/taskmanager/taskmanagersettings.cpp +++ b/panels/dock/taskmanager/taskmanagersettings.cpp @@ -132,7 +132,7 @@ QStringList TaskManagerSettings::dockedElements() const return m_dockedElements; } -// elementId is like "desktop/sample.app.id" +// elementId is either "desktop/sample.app.id" or "internal/folders/1" bool TaskManagerSettings::isDocked(const QString &elementId) const { return m_dockedElements.contains(elementId); diff --git a/panels/dock/taskmanager/taskoverflowcontroller.cpp b/panels/dock/taskmanager/taskoverflowcontroller.cpp index 0d7037c12..1b260b834 100644 --- a/panels/dock/taskmanager/taskoverflowcontroller.cpp +++ b/panels/dock/taskmanager/taskoverflowcontroller.cpp @@ -229,6 +229,7 @@ QVariantMap TaskOverflowController::entryAt(int visualIndex) const { QStringLiteral("iconName"), iconName }, { QStringLiteral("menus"), dataForRole(index, MODEL_MENUS).toString() }, { QStringLiteral("windows"), dataForRole(index, MODEL_WINDOWS).toStringList() }, + { QStringLiteral("groupItems"), dataForRole(index, MODEL_GROUPITEMS).toList() }, { QStringLiteral("visualIndex"), visualIndex }, { QStringLiteral("modelIndex"), QVariant::fromValue(index) }, };