diff --git a/panels/dock/MenuHelper.qml b/panels/dock/MenuHelper.qml index 46775f090..4bacac408 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 @@ -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 eb3604fcd..c13558ad3 100644 --- a/panels/dock/OverflowContainer.qml +++ b/panels/dock/OverflowContainer.qml @@ -8,6 +8,7 @@ import QtQuick.Layouts 2.15 Item { id: root required property bool useColumnLayout + property real trailingExtent: 0 property alias model: listView.model property alias delegate: listView.delegate property alias spacing: listView.spacing @@ -31,6 +32,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 { @@ -42,6 +48,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 @@ -61,6 +72,8 @@ Item { for (let child of listView.contentItem.visibleChildren) { width = calculateImplicitWidth(width, child.implicitWidth) } + if (!useColumnLayout && trailingExtent > 0) + width = calculateImplicitWidth(width, trailingExtent) // TODO: abvoe qt6.8 implicitSize to 0 will make size to 0 default. // so make minimum implicitSize to 1, find why and remove below return Math.max(width, 1) @@ -70,6 +83,8 @@ Item { for (let child of listView.contentItem.visibleChildren) { height = calculateImplicitHeight(height, child.implicitHeight) } + if (useColumnLayout && trailingExtent > 0) + height = calculateImplicitHeight(height, trailingExtent) return Math.max(height, 1) } } diff --git a/panels/dock/dockpositioner.cpp b/panels/dock/dockpositioner.cpp index fccc5a02e..df7d2a6ab 100644 --- a/panels/dock/dockpositioner.cpp +++ b/panels/dock/dockpositioner.cpp @@ -73,6 +73,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; @@ -133,27 +161,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: @@ -222,29 +251,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 07b0693b8..f9ae154e7 100644 --- a/panels/dock/dockpositioner.h +++ b/panels/dock/dockpositioner.h @@ -55,6 +55,9 @@ public slots: protected: void setScreen(QScreen *screen); + bool isCrossWindow() const; + QRect effectiveBounding() const; + QRect attachedItemRectInDock() const; QPointer m_screen; QPointer m_panel; QRect m_bounding {}; diff --git a/panels/dock/package/main.qml b/panels/dock/package/main.qml index e34e36a52..7174df502 100644 --- a/panels/dock/package/main.qml +++ b/panels/dock/package/main.qml @@ -49,11 +49,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 +64,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 @@ -653,8 +666,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: { @@ -662,10 +675,10 @@ Window { return dockLeftPart.visible ? 0 : Math.max(10, Math.round(dockItemIconSize) / 3) } return !useColumnLayout && 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 808ac6da6..5eadc561e 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 0.9.0 REQUIRED) @@ -86,6 +86,8 @@ add_library(dock-taskmanager SHARED ${DBUS_INTERFACES} treelandwindowmonitor.h taskmanagersettings.cpp taskmanagersettings.h + taskoverflowcontroller.cpp + taskoverflowcontroller.h textcalculator.h textcalculator.cpp ) @@ -100,6 +102,7 @@ target_link_libraries(dock-taskmanager PRIVATE dde-shell-dock 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 e24461e1b..e49e50795 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.cpp +++ b/panels/dock/taskmanager/dockglobalelementmodel.cpp @@ -616,6 +616,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 4cc7c3d3c..0386509ac 100644 --- a/panels/dock/taskmanager/package/AppItem.qml +++ b/panels/dock/taskmanager/package/AppItem.qml @@ -30,9 +30,15 @@ Item { property var folderPresentation: null readonly property bool isFolder: folderType !== "" 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 + property bool dragFinishedForPress: false signal dropFilesOnItem(itemId: string, files: list) - signal dragFinished() + signal dragFinished(int dropAction) // Binding Drag.active to mouseArea.drag.active would make Drag.active read // back the very property it drives (see visible/fixPosition below), @@ -42,18 +48,38 @@ Item { Drag.hotSpot.x: icon.width / 2 Drag.hotSpot.y: icon.height / 2 Drag.dragType: Drag.Automatic - Drag.mimeData: root.folderPresentation - ? { "text/x-dde-dock-dnd-folder": itemId, "text/x-dde-dock-dnd-source": "taskbar" } - : { "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 Accessible.description: root.attention ? qsTr("Demands attention") : (root.active ? qsTr("Active") : "") + Drag.supportedActions: Qt.CopyAction | Qt.MoveAction + Drag.mimeData: root.folderPresentation + ? { "text/x-dde-dock-dnd-folder": itemId, "text/x-dde-dock-dnd-source": dragSource } + : { "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) { + // MouseArea is still processing the initiating move while the native + // drag returns. Keep its target alive until that event has finished. + Qt.callLater(function() { + if (!root) + return + root.dragFinishedForPress = true + if (root.restorePositionAfterDrag) { + root.x = root.dragStartX + root.y = root.dragStartY + } + }) + Panel.contextDragging = false + 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 bool updateIconGeometry: true + property Item previewAnchor: icon property var iconGlobalPoint: { var a = icon var x = 0, y = 0 @@ -441,6 +467,9 @@ Item { } FolderPopup { id: folderPopup + // An overflow item lives in the shared popup window. Position + // from the item before that window switches to folder content. + positionerAnchor: root folderName: root.name contents: root.applicationFolderContents presentation: root.folderPresentation @@ -493,7 +522,12 @@ Item { running: false repeat: false onTriggered: { - var pos = icon.mapToItem(null, 0, 0) + if (!root.updateIconGeometry) + return + 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) } @@ -507,6 +541,8 @@ Item { property int xOffset: 0 property int yOffset: 0 onTriggered: { + xOffset = root.previewAnchor.DockPanelPositioner.x + yOffset = root.previewAnchor.DockPanelPositioner.y if (root.windows.length != 0 || Qt.platform.pluginName === "wayland") { // 使用基于 modelIndex 的预览API,确保精确匹配 taskmanager.Applet.requestPreview(root.modelIndex, Panel.rootObject, xOffset, yOffset, Panel.position); @@ -521,17 +557,9 @@ 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 = root.previewAnchor.mapToItem(null, root.previewAnchor.width / 2, root.previewAnchor.height / 2) + root.previewAnchor.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, 0, 0) + root.previewAnchor.DockPanelPositioner.updatePosition() previewTimer.start() } @@ -560,7 +588,9 @@ Item { } function requestAppItemMenu() { - Panel.requestClosePopup() + if (!root.preservePanelPopupOnContextMenu) { + Panel.requestClosePopup() + } if (root.isFolder) { if (folderUi.item) folderUi.item.contextMenu.openFor(root.folderType, root.itemId, root.name) return @@ -575,16 +605,14 @@ Item { anchors.fill: parent hoverEnabled: false acceptedButtons: Qt.LeftButton | Qt.RightButton - drag.target: root + preventStealing: root.dragEnabled + drag.target: root.dragEnabled && !root.dragFinishedForPress ? root : null drag.onActiveChanged: { + // Automatic drag enters the native event loop synchronously. Protect + // the popup from focus loss before starting it, not after it returns. + if (drag.active && folderUi.item) folderUi.item.popup.close() + Panel.contextDragging = drag.active root.Drag.active = drag.active - if (!drag.active) { - Panel.contextDragging = false - root.dragFinished() - return - } - if (folderUi.item) folderUi.item.popup.close() - Panel.contextDragging = true } // 记录是否是触摸长按导致的,防止在 onClicked 中重复处理 @@ -600,7 +628,10 @@ Item { } onPressed: function (mouse) { isTouchLongPressed = false - if (mouse.button === Qt.LeftButton) { + root.dragFinishedForPress = false + if (root.dragEnabled && mouse.button === Qt.LeftButton) { + root.dragStartX = root.x + root.dragStartY = root.y appItem.grabToImage(function(result) { root.Drag.imageSource = result.url; }) @@ -636,8 +667,8 @@ Item { Accessible.role: Accessible.ToolTip Accessible.id: "ToolTip" id: toolTip - toolTipX: DockPanelPositioner.x - toolTipY: DockPanelPositioner.y + toolTipX: icon.DockPanelPositioner.x + toolTipY: icon.DockPanelPositioner.y } PanelToolTip { @@ -645,8 +676,8 @@ Item { Accessible.id: "MoveToTrash" id: dragToolTip text: qsTr("Move to Trash") - toolTipX: DockPanelPositioner.x - toolTipY: DockPanelPositioner.y + toolTipX: icon.DockPanelPositioner.x + toolTipY: icon.DockPanelPositioner.y visible: false } @@ -654,9 +685,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() } } @@ -678,8 +710,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() } } @@ -709,4 +742,23 @@ Item { onIconGlobalPointChanged: { updateWindowIconGeometryTimer.start() } + + onUpdateIconGeometryChanged: updateWindowIconGeometryTimer.start() + + // The recent shortcut can change apps while the pointer stays over it. + onItemIdChanged: { + if (hoverHandler.hovered) { + onExited() + onEntered() + } + } + + onVisibleChanged: { + if (!visible) { + toolTipShowTimer.stop() + toolTip.close() + if (hoverHandler.hovered || previewTimer.running) + closeItemPreview() + } + } } 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/FolderPopup.qml b/panels/dock/taskmanager/package/FolderPopup.qml index 83103efc1..c3da5930e 100644 --- a/panels/dock/taskmanager/package/FolderPopup.qml +++ b/panels/dock/taskmanager/package/FolderPopup.qml @@ -14,6 +14,7 @@ PanelPopup { property var contents: [] property var presentation: null property point anchorPoint: Qt.point(0, 0) + property Item positionerAnchor: root property bool interactionActive: false signal activated(var entry) signal contextMenuRequested(var entry) @@ -46,11 +47,10 @@ PanelPopup { : Math.min(Math.min(3, rows) * cellHeight + 56, maximumHeight) contentReady: hasProviderContent ? contentLoader.status !== Loader.Loading : true popupX: Math.max(screenX - Panel.rootObject.x + 10, - Math.min(DockPanelPositioner.x, screenX + screenWidth - Panel.rootObject.x - width - 10)) + Math.min(positionerAnchor.DockPanelPositioner.x, screenX + screenWidth - Panel.rootObject.x - width - 10)) popupY: Math.max(screenY - Panel.rootObject.y + 10, - Math.min(DockPanelPositioner.y, screenY + screenHeight - Panel.rootObject.y - height - 10)) + Math.min(positionerAnchor.DockPanelPositioner.y, screenY + screenHeight - Panel.rootObject.y - height - 10)) closeOnInactive: !interactionActive && !Panel.contextDragging - DockPanelPositioner.bounding: Qt.rect(anchorPoint.x, anchorPoint.y, width, height) function openAt(point) { anchorPoint = point @@ -65,6 +65,11 @@ PanelPopup { Item { anchors.fill: parent + Binding { + target: root.positionerAnchor.DockPanelPositioner + property: "bounding" + value: Qt.rect(root.anchorPoint.x, root.anchorPoint.y, root.width, root.height) + } Loader { id: contentLoader objectName: "providerContentLoader" diff --git a/panels/dock/taskmanager/package/TaskManager.qml b/panels/dock/taskmanager/package/TaskManager.qml index c0e1bdaa9..2680c0dc8 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: { @@ -100,6 +103,20 @@ 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 + overflowSidePadding: taskmanager.appTitleSpacing + 1 + visualModel: visualModel + dataModel: taskmanager.Applet.dataModel + } + TextCalculator { id: textCalculator enabled: !fashionMode && taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top) @@ -126,6 +143,7 @@ ContainmentItem { anchors.topMargin: useColumnLayout ? taskmanager.startPadding : 0 useColumnLayout: taskmanager.useColumnLayout spacing: taskmanager.appTitleSpacing + trailingExtent: taskOverflow.trailingExtent remove: Transition { NumberAnimation { properties: "scale,opacity" @@ -137,6 +155,10 @@ ContainmentItem { model: DelegateModel { id: visualModel model: taskmanager.Applet.dataModel + function sourceIndexAt(visualIndex) { + // modelIndex() takes a source row, whereas items can be reordered. + return modelIndex(items.get(visualIndex).model.index) + } // 1:4 the distance between app : dock height; get width/height≈0.8 property real cellWidth: Panel.rootObject.dockItemMaxSize * 0.8 delegate: Item { @@ -163,6 +185,18 @@ ContainmentItem { return windows.length > 0 && launcherDndDropArea.launcherDndWinId !== windows[0] } + readonly property bool overflowActive: taskOverflow.popupCount > 0 + 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" @@ -187,8 +221,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) @@ -204,8 +238,28 @@ ContainmentItem { y: delegateRoot.y width: delegateRoot.width height: delegateRoot.height - scale: delegateRoot.scale + readonly property bool hiddenByOverflow: 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 { @@ -250,6 +304,7 @@ ContainmentItem { blendOpacity: taskmanager.blendOpacity title: delegateRoot.title enableTitle: textCalculator.enabled + updateIconGeometry: !delegateRoot.hiddenInOverflow ListView.delayRemove: Drag.active Component.onCompleted: { dropFilesOnItem.connect(taskmanager.Applet.dropFilesOnItem) @@ -262,6 +317,15 @@ ContainmentItem { } } + TaskOverflowButton { + id: overflowButton + taskManagerItem: taskmanager + overflow: taskOverflow + onDragFinished: function(dropAction, itemId, canUndock) { + launcherDndDropArea.finishDrag(dropAction, itemId, canUndock) + } + } + DropArea { id: launcherDndDropArea anchors.fill: parent @@ -270,27 +334,125 @@ ContainmentItem { property string launcherDndDesktopId: "" property string launcherDndDragSource: "" property string launcherDndWinId: "" + property bool launcherDndDocked: false + property var pendingOverflowDrop: null + + function overflowTargetIndexAt(x, y) { + // Include gaps and the trailing shortcuts in the drop target. + // A full taskbar makes room by pushing its last app into overflow. + const axisPosition = taskmanager.useColumnLayout ? y : x + const index = Math.floor((axisPosition + taskOverflow.spacing / 2) + / (taskOverflow.fallbackItemExtent + taskOverflow.spacing)) + return Math.max(0, Math.min(index, taskOverflow.visibleItemCount - 1)) + } + + function saveOrder() { + let appIds = [] + for (let i = 0; i < visualModel.items.count; i++) { + appIds.push(visualModel.items.get(i).model.itemId) + } + taskmanager.Applet.saveDockElementsOrder(appIds) + } + + function moveApp(appId, winId, targetIndex) { + const currentIndex = taskmanager.Applet.windowSplit + ? taskmanager.findAppIndexByWindow(appId, winId) + : taskmanager.findAppIndex(appId) + if (currentIndex < 0 || targetIndex < 0) return false + if (currentIndex !== targetIndex) { + if (taskmanager.Applet.windowSplit) + taskmanager.Applet.moveItem(currentIndex, targetIndex) + else + visualModel.items.move(currentIndex, targetIndex) + taskOverflow.scheduleSync() + } + return true + } + + function finishDrag(dropAction, itemId, canUndock) { + const pending = pendingOverflowDrop + pendingOverflowDrop = null + const shouldUndock = taskOverflow.finishDrag(dropAction) + resetDndState() + // Both moves and unpinning can destroy the source. Commit them + // only after QDrag and the initiating MouseArea event return. + Qt.callLater(function() { + if (pending && dropAction === Qt.MoveAction) { + const targetIndex = Math.min(pending.targetIndex, taskOverflow.visibleItemCount - 1) + if (moveApp(pending.appId, pending.winId, targetIndex)) + saveOrder() + } else if (canUndock && shouldUndock) { + taskmanager.Applet.requestUndockByDesktopId(itemId) + } + }) + } function resetDndState() { launcherDndDesktopId = "" launcherDndDragSource = "" launcherDndWinId = "" + launcherDndDocked = false + } + + function isOverflowButtonIndex(targetIndex) { + return taskOverflow.popupCount > 0 + && (targetIndex < 0 || targetIndex >= taskOverflow.visibleItemCount) + } + + function targetIndexAt(x, y) { + return appContainer.indexAt(x, y) + } + + 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) { let desktopId = drag.getDataAsString("text/x-dde-dock-dnd-folder") || drag.getDataAsString("text/x-dde-dock-dnd-appid") + if (!desktopId) { + drag.accepted = false + 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) { + if (launcherDndDragSource === "overflow-popup") { + pendingOverflowDrop = null + drag.acceptProposedAction() + return + } + let targetIndex = targetIndexAt(drag.x, drag.y) + if (!isOverflowButtonIndex(targetIndex) && !requestDockIfNeeded()) { drag.accepted = false - resetDndState() + return + } + if (!isOverflowButtonIndex(targetIndex)) { + drag.acceptProposedAction() } } 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 + drag.acceptProposedAction() 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) { @@ -298,14 +460,30 @@ ContainmentItem { taskmanager.Applet.moveItem(currentIndex, targetIndex) } else { visualModel.items.move(currentIndex, targetIndex) + taskOverflow.scheduleSync() } } } onDropped: function(drop) { - Panel.contextDragging = false if (launcherDndDesktopId === "") return - let targetIndex = appContainer.indexAt(drop.x, drop.y) + if (launcherDndDragSource === "overflow-popup") { + pendingOverflowDrop = { + appId: taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId), + winId: launcherDndWinId, + targetIndex: overflowTargetIndexAt(drop.x, drop.y) + } + drop.accept(Qt.MoveAction) + resetDndState() + return + } + Panel.contextDragging = false + let targetIndex = targetIndexAt(drop.x, drop.y) + if (isOverflowButtonIndex(targetIndex)) { + resetDndState() + return + } + 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) { @@ -313,22 +491,22 @@ ContainmentItem { taskmanager.Applet.moveItem(currentIndex, targetIndex) } else { visualModel.items.move(currentIndex, targetIndex) + taskOverflow.scheduleSync() } } - let appIds = [] - for (let i = 0; i < visualModel.items.count; i++) { - appIds.push(visualModel.items.get(i).model.itemId) - } - taskmanager.Applet.saveDockElementsOrder(appIds) - if (drop.formats.indexOf("text/x-dde-dock-dnd-folder") !== -1) + saveOrder() + if (drop.formats.indexOf("text/x-dde-dock-dnd-folder") !== -1) { drop.accept(Qt.CopyAction) - else + } else if (isInternalDrag()) { + drop.accept(Qt.MoveAction) + } else { drop.acceptProposedAction() + } resetDndState() } onExited: function() { - if (launcherDndDesktopId !== "" && launcherDndDragSource !== "taskbar") { + if (launcherDndDesktopId !== "" && !isInternalDrag() && launcherDndDocked) { taskmanager.Applet.requestUndockByDesktopId(launcherDndDesktopId) } resetDndState() @@ -382,8 +560,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..b0f2e79a1 --- /dev/null +++ b/panels/dock/taskmanager/package/TaskOverflowButton.qml @@ -0,0 +1,334 @@ +// 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 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 + + signal dragFinished(int dropAction, string itemId, bool canUndock) + + readonly property int overflowCount: overflow.popupCount + readonly property var primaryOverflowItem: overflow.popupCount > 0 ? overflow.primaryItem : null + readonly property string overflowIconName: primaryOverflowItem && primaryOverflowItem.iconName ? primaryOverflowItem.iconName : "" + readonly property real popupItemWidth: overflow.fallbackItemExtent + readonly property real popupItemHeight: overflow.fallbackItemExtent + readonly property real buttonOffset: overflow.buttonOffset + readonly property real overflowIconSize: Math.min(overflow.buttonExtent, 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 color separatorColor: D.DTK.themeType === D.ApplicationHelper.DarkType + ? Qt.rgba(1, 1, 1, 0.1) : Qt.rgba(0, 0, 0, 0.1) + readonly property real gridSpacing: overflow.spacing + readonly property int popupCellWidth: Math.ceil(popupItemWidth + gridSpacing) + readonly property int popupCellHeight: Math.ceil(popupItemHeight + + Math.max(10, 2 * (Math.round(popupItemHeight / 8) + 2))) + readonly property real popupAvailableWidth: Math.max(popupCellWidth + 20, + Screen.width - 40 - (root.taskManagerItem.useColumnLayout ? Panel.rootObject.dockSize : 0)) + readonly property real popupAvailableHeight: Math.max(popupCellHeight + 20, + Screen.height - 40 - (root.taskManagerItem.useColumnLayout ? 0 : Panel.rootObject.dockSize)) + readonly property int popupColumns: Math.min(6, Math.max(1, overflowCount), + Math.max(1, Math.floor((popupAvailableWidth - 20) / popupCellWidth))) + property point popupAnchorPoint: Qt.point(0, 0) + + visible: overflowCount > 0 && overflow.buttonExtent > 0 + x: root.taskManagerItem.useColumnLayout ? 0 : root.buttonOffset + y: root.taskManagerItem.useColumnLayout ? root.buttonOffset : 0 + implicitWidth: root.taskManagerItem.useColumnLayout ? root.taskManagerItem.implicitWidth : overflow.buttonExtent + implicitHeight: root.taskManagerItem.useColumnLayout ? overflow.buttonExtent : root.taskManagerItem.implicitHeight + width: implicitWidth + height: implicitHeight + + Accessible.role: Accessible.Button + Accessible.name: qsTr("Overflow area") + + // Leave a full application gap between each outer line and its shortcut. + // A vertical dock uses horizontal lines before and after the group. + Repeater { + model: 2 + + Rectangle { + required property int index + visible: overflow.separatorPadding >= 1 + readonly property real axisPosition: index === 0 + ? -(overflow.showRecent ? overflow.fallbackItemExtent + overflow.spacing : 0) - overflow.separatorPadding + : overflow.buttonExtent + overflow.separatorPadding - 1 + + width: root.taskManagerItem.useColumnLayout ? Panel.rootObject.dockItemIconSize : 1 + height: root.taskManagerItem.useColumnLayout ? 1 : Panel.rootObject.dockItemIconSize + x: root.taskManagerItem.useColumnLayout ? (root.width - width) / 2 : Math.round(axisPosition) + y: root.taskManagerItem.useColumnLayout ? Math.round(axisPosition) : (root.height - height) / 2 + color: root.separatorColor + Accessible.ignored: true + } + } + + // The recent shortcut precedes the overflow entry in both dock orientations. + Loader { + active: root.visible && root.overflow.showRecent && !!root.overflow.recentItem.itemId + x: root.taskManagerItem.useColumnLayout ? 0 : -(overflow.fallbackItemExtent + overflow.spacing) + y: root.taskManagerItem.useColumnLayout ? -(overflow.fallbackItemExtent + overflow.spacing) : 0 + width: root.width + height: root.height + sourceComponent: AppItem { + readonly property var entry: root.overflow.recentItem + displayMode: Panel.indicatorStyle + colorTheme: Panel.colorTheme + active: entry.active + attention: entry.attention + itemId: entry.itemId + name: entry.name + iconName: entry.iconName + folderType: entry.folderType + applicationFolderContents: entry.applicationFolderContents + folderPresentation: entry.folderPresentation + menus: entry.menus + windows: entry.windows + visualIndex: entry.visualIndex + modelIndex: entry.modelIndex + title: entry.title + blendOpacity: root.taskManagerItem.blendOpacity + dragEnabled: false + Component.onCompleted: dropFilesOnItem.connect(root.taskManagerItem.Applet.dropFilesOnItem) + } + } + + onOverflowCountChanged: { + if (overflowCount === 0) + overflowPopup.close() + } + + Connections { + target: Panel + function onPositionChanged() { overflowPopup.close() } + } + + 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) + popupContentLoader.active = true + 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 + // Consume right clicks without opening the popup or the dock's menu. + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: false + onClicked: function(mouse) { + if (mouse.button === Qt.LeftButton) + root.togglePopup() + } + } + + HoverHandler { + id: overflowHoverHandler + onHoveredChanged: { + if (hovered && !overflowPopup.popupVisible) { + toolTipShowTimer.start() + } else { + toolTipShowTimer.stop() + toolTip.close() + } + } + } + + PanelToolTip { + id: toolTip + text: qsTr("Overflow area") + 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: popupContentLoader.item ? popupContentLoader.item.implicitWidth : 0 + height: popupContentLoader.item ? popupContentLoader.item.implicitHeight : 0 + contentReady: popupContentLoader.status === Loader.Ready + 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) + }) + } + + // Create the grid on first use and retain it across close/open and native + // drags. GridView only instantiates the visible rows (plus its cache). + Loader { + id: popupContentLoader + active: false + width: overflowPopup.width + height: overflowPopup.height + sourceComponent: Control { + id: overflowPopupContent + padding: 10 + implicitWidth: root.popupColumns * overflowGrid.cellWidth + leftPadding + rightPadding + implicitHeight: Math.min(root.popupAvailableHeight, + Math.ceil(root.overflowCount / root.popupColumns) * overflowGrid.cellHeight + + topPadding + bottomPadding) + background: Item {} + + DropArea { + anchors.fill: parent + keys: ["text/x-dde-dock-dnd-appid", "text/x-dde-dock-dnd-folder"] + onDropped: function(drop) { + if (drop.getDataAsString("text/x-dde-dock-dnd-source") === "overflow-popup") + drop.accept(Qt.MoveAction) + } + } + + contentItem: GridView { + id: overflowGrid + cellWidth: root.popupCellWidth + cellHeight: root.popupCellHeight + interactive: contentHeight > height && !Panel.contextDragging + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + model: root.overflow.popupModel + + delegate: Item { + id: popupDelegate + required property int index + required property var entry + width: overflowGrid.cellWidth + height: overflowGrid.cellHeight + GridView.delayRemove: popupApp.Drag.active + + AppItem { + id: popupApp + x: (popupDelegate.width - width) / 2 + y: (popupDelegate.height - height) / 2 + width: root.popupItemWidth + height: root.popupItemHeight + displayMode: Panel.indicatorStyle + colorTheme: Panel.colorTheme + active: popupDelegate.entry.active + attention: popupDelegate.entry.attention + itemId: popupDelegate.entry.itemId + name: popupDelegate.entry.name + iconName: popupDelegate.entry.iconName + folderType: popupDelegate.entry.folderType + applicationFolderContents: popupDelegate.entry.applicationFolderContents + folderPresentation: popupDelegate.entry.folderPresentation + menus: popupDelegate.entry.menus + windows: popupDelegate.entry.windows + visualIndex: popupDelegate.entry.visualIndex + modelIndex: popupDelegate.entry.modelIndex + blendOpacity: root.taskManagerItem.blendOpacity + title: popupDelegate.entry.title + enableTitle: false + preservePanelPopupOnContextMenu: true + previewAnchor: overflowPopupContent + updateIconGeometry: false + dragEnabled: popupDelegate.entry.docked && windows.length === 0 + dragSource: "overflow-popup" + restorePositionAfterDrag: true + Drag.onDragStarted: { + // Views retain their current item outside the viewport. + overflowGrid.currentIndex = popupDelegate.index + root.overflow.beginDrag() + } + onDropFilesOnItem: function(itemId, files) { + root.taskManagerItem.Applet.dropFilesOnItem(itemId, files) + } + onDragFinished: function(dropAction) { + root.dragFinished(dropAction, itemId, dragEnabled) + } + } + } + } + } + } + } +} diff --git a/panels/dock/taskmanager/taskmanager.cpp b/panels/dock/taskmanager/taskmanager.cpp index 1f2c0504d..3c2accdc7 100644 --- a/panels/dock/taskmanager/taskmanager.cpp +++ b/panels/dock/taskmanager/taskmanager.cpp @@ -19,6 +19,7 @@ #include "pluginfactory.h" #include "taskmanager.h" #include "taskmanageradaptor.h" +#include "taskoverflowcontroller.h" #include "taskmanagersettings.h" #include "textcalculator.h" #include "treelandwindowmonitor.h" @@ -150,6 +151,7 @@ TaskManager::TaskManager(QObject *parent) , m_folderMenuRegistry(this) , 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..6bc982b95 --- /dev/null +++ b/panels/dock/taskmanager/taskoverflowcontroller.cpp @@ -0,0 +1,585 @@ +// 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 +#include + +#include +#include +#include + +namespace dock { +namespace { +constexpr qreal OverflowReleaseHysteresis = 8; +} + +int TaskOverflowModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_entries.size(); +} + +QVariant TaskOverflowModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size() || role != Qt::UserRole) + return {}; + return m_entries.at(index.row()).values; +} + +QHash TaskOverflowModel::roleNames() const +{ + return {{Qt::UserRole, "entry"}}; +} + +void TaskOverflowModel::setItems(const QVariantList &items) +{ + for (int row = 0; row < items.size(); ++row) { + const auto values = items.at(row).toMap(); + const QPersistentModelIndex source = values.value(QStringLiteral("modelIndex")).value(); + int existing = row; + while (existing < m_entries.size() && m_entries.at(existing).source != source) + ++existing; + if (existing == m_entries.size()) { + beginInsertRows({}, row, row); + m_entries.insert(row, {source, values}); + endInsertRows(); + } else { + if (existing != row) { + beginMoveRows({}, existing, existing, {}, row); + m_entries.move(existing, row); + endMoveRows(); + } + if (m_entries.at(row).values != values) { + m_entries[row].values = values; + emit dataChanged(index(row), index(row), {Qt::UserRole}); + } + } + } + if (m_entries.size() > items.size()) { + beginRemoveRows({}, items.size(), m_entries.size() - 1); + m_entries.erase(m_entries.begin() + items.size(), m_entries.end()); + endRemoveRows(); + } +} + +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) +{ + availableExtent = std::isfinite(availableExtent) ? std::max(0, availableExtent) : 0; + 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) +{ + fallbackItemExtent = std::isfinite(fallbackItemExtent) ? std::max(1, fallbackItemExtent) : 1; + if (!qFuzzyCompare(m_fallbackItemExtent + 1, fallbackItemExtent + 1)) { + m_fallbackItemExtent = fallbackItemExtent; + emit fallbackItemExtentChanged(); + scheduleSync(); + } +} + +void TaskOverflowController::setSpacing(qreal spacing) +{ + spacing = std::isfinite(spacing) ? std::max(0, spacing) : 0; + if (!qFuzzyCompare(m_spacing + 1, spacing + 1)) { + m_spacing = spacing; + emit spacingChanged(); + scheduleSync(); + } +} + +void TaskOverflowController::setOverflowSidePadding(qreal padding) +{ + padding = std::isfinite(padding) ? std::max(0, padding) : 0; + if (qFuzzyCompare(m_overflowSidePadding + 1, padding + 1)) + return; + + m_overflowSidePadding = padding; + emit overflowSidePaddingChanged(); + 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; + cacheRoles(); + m_entryCache.clear(); + m_recentHistory.clear(); + m_pendingActivity.clear(); + captureActivity(true); + connectDataModelSignals(); + scheduleSync(); +} + +void TaskOverflowController::scheduleSync() +{ + if (m_syncPending) + return; + + m_syncPending = true; + QTimer::singleShot(0, this, [this] { + m_syncPending = false; + sync(); + }); +} + +void TaskOverflowController::beginDrag() +{ + if (m_dragInProgress) + return; + m_dragInProgress = true; + emit dragInProgressChanged(); + m_dragCancelled = false; + // Install after QDrag starts its event loop so that Escape is observed + // before the native drag filter consumes it and clears the mouse state. + QTimer::singleShot(0, this, [this] { + if (m_dragInProgress) + QCoreApplication::instance()->installEventFilter(this); + }); +} + +bool TaskOverflowController::eventFilter(QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::KeyPress && static_cast(event)->key() == Qt::Key_Escape) + m_dragCancelled = true; + return QObject::eventFilter(watched, event); +} + +bool TaskOverflowController::finishDrag(int dropAction) +{ + if (!m_dragInProgress) + return false; + m_dragInProgress = false; + QCoreApplication::instance()->removeEventFilter(this); + emit dragInProgressChanged(); + if (m_syncDeferred) { + m_syncDeferred = false; + scheduleSync(); + } + // IgnoreAction represents both dropping outside and cancelling with Escape. + return dropAction == Qt::IgnoreAction && !m_dragCancelled + && !(QGuiApplication::mouseButtons() & Qt::LeftButton); +} + +void TaskOverflowController::setLayout(bool showRecent, qreal trailingExtent, qreal buttonOffset, + qreal buttonExtent, qreal separatorPadding) +{ + if (m_showRecent == showRecent && m_trailingExtent == trailingExtent + && m_buttonOffset == buttonOffset && m_buttonExtent == buttonExtent + && m_separatorPadding == separatorPadding) + return; + m_showRecent = showRecent; + m_trailingExtent = trailingExtent; + m_buttonOffset = buttonOffset; + m_buttonExtent = buttonExtent; + m_separatorPadding = separatorPadding; + emit layoutChanged(); +} + +void TaskOverflowController::sync() +{ + // A native drag runs a nested event loop. Keep its source delegate alive + // until it returns, even if a window opens/closes or the dock is resized. + if (m_dragInProgress) { + m_syncDeferred = true; + return; + } + const int totalCount = modelCount(); + const qreal fullExtent = totalCount > 0 + ? totalCount * m_fallbackItemExtent + (totalCount - 1) * m_spacing : 0; + if (!syncEnabledState(totalCount, fullExtent)) { + setLayout(false, 0, 0, 0, 0); + setVisibleItemCount(totalCount); + setPopupItems({}); + return; + } + + const qreal available = std::max(0, m_availableExtent); + const qreal pairExtent = 2 * (m_fallbackItemExtent + m_overflowSidePadding) + m_spacing; + const bool showRecent = pairExtent <= available; + const qreal buttonExtent = std::min(m_fallbackItemExtent, available); + const qreal padding = std::min(m_overflowSidePadding, (available - buttonExtent) / 2); + const qreal groupExtent = showRecent ? pairExtent : buttonExtent + 2 * padding; + const qreal stride = m_fallbackItemExtent + m_spacing; + const int visibleCount = std::clamp(int(std::floor((available - groupExtent) / stride)), 0, totalCount - 1); + const qreal buttonOffset = visibleCount * stride + padding + (showRecent ? stride : 0); + + QVariantList overflowItems; + overflowItems.reserve(totalCount - visibleCount); + for (int index = visibleCount; index < totalCount; ++index) { + const auto itemData = entryAt(index); + if (!itemData.isEmpty()) + overflowItems.append(itemData); + } + setLayout(showRecent, groupExtent, buttonOffset, buttonExtent, padding); + setVisibleItemCount(visibleCount); + setPopupItems(overflowItems); +} + +void TaskOverflowController::onDataModelDestroyed() +{ + m_dataModel = nullptr; + m_entryCache.clear(); + m_activity.clear(); + m_activeItems.clear(); + m_runningItems.clear(); + 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, + "sourceIndexAt", + Q_RETURN_ARG(QVariant, modelIndexValue), + Q_ARG(QVariant, 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_roles.value(roleName, -1); + if (role < 0) + return {}; + + return m_dataModel->data(index, role); +} + +QVariantMap TaskOverflowController::entryAt(int visualIndex) +{ + const auto index = modelIndexAt(visualIndex); + if (!index.isValid()) + return {}; + + auto cached = m_entryCache.constFind(index); + if (cached == m_entryCache.cend()) { + auto iconName = dataForRole(index, MODEL_ICONNAME).toString(); + if (iconName.isEmpty()) + iconName = dataForRole(index, MODEL_WINICON).toString(); + if (iconName.isEmpty()) + iconName = DEFAULT_APP_ICONNAME; + + QVariantMap values { + { 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("folderType"), dataForRole(index, MODEL_FOLDERTYPE).toString() }, + { QStringLiteral("applicationFolderContents"), dataForRole(index, MODEL_APPLICATIONFOLDERCONTENTS).toList() }, + { QStringLiteral("folderPresentation"), dataForRole(index, MODEL_FOLDERPRESENTATION) }, + }; + cached = m_entryCache.insert(index, values); + } + auto result = *cached; + result.insert(QStringLiteral("visualIndex"), visualIndex); + result.insert(QStringLiteral("modelIndex"), QVariant::fromValue(index)); + return result; +} + +bool TaskOverflowController::syncEnabledState(int totalCount, qreal fullExtent) +{ + if (!m_enabled || totalCount <= 0 || !std::isfinite(m_availableExtent)) { + 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; + + const auto structureChanged = [this] { + m_entryCache.clear(); + captureActivity(); + scheduleSync(); + }; + connect(m_dataModel, &QAbstractItemModel::dataChanged, this, + [this](const QModelIndex &first, const QModelIndex &last, const QList &roles) { + const auto changed = [this, &roles](const QByteArray &name) { + return roles.isEmpty() || roles.contains(m_roles.value(name, -1)); + }; + if (!roles.isEmpty() && std::none_of(roles.cbegin(), roles.cend(), [this](int role) { + return m_entryRoles.contains(role); + })) + return; + for (int row = first.row(); row <= last.row(); ++row) + m_entryCache.remove(m_dataModel->index(row, 0)); + if (changed(MODEL_ACTIVE) || changed(MODEL_WINDOWS) || changed(MODEL_ITEMID)) + captureActivity(false, first.row(), last.row()); + scheduleSync(); + }); + connect(m_dataModel, &QAbstractItemModel::rowsInserted, this, structureChanged); + connect(m_dataModel, &QAbstractItemModel::rowsMoved, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::rowsRemoved, this, structureChanged); + connect(m_dataModel, &QAbstractItemModel::modelReset, this, [this, structureChanged] { + cacheRoles(); + structureChanged(); + }); + 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) +{ + syncRecentItem(items); + if (m_popupItems == items) + return; + + m_popupItems = items; + m_popupModel.setItems(items); + emit popupItemsChanged(); +} + +void TaskOverflowController::cacheRoles() +{ + m_roles.clear(); + m_entryRoles.clear(); + if (!m_dataModel) + return; + const auto roles = m_dataModel->roleNames(); + for (const QByteArray name : {MODEL_ICONNAME, MODEL_WINICON, MODEL_ACTIVE, MODEL_ATTENTION, + MODEL_DOCKED, MODEL_ITEMID, MODEL_NAME, MODEL_TITLE, MODEL_MENUS, MODEL_WINDOWS, + MODEL_FOLDERTYPE, MODEL_APPLICATIONFOLDERCONTENTS, MODEL_FOLDERPRESENTATION}) { + const int role = roles.key(name, -1); + if (role >= 0) { + m_roles.insert(name, role); + m_entryRoles.insert(role); + } + } +} + +void TaskOverflowController::captureActivity(bool baseline, int first, int last) +{ + if (last < 0) { + m_activity.clear(); + last = m_dataModel ? m_dataModel->rowCount() - 1 : -1; + } + QStringList changedIds; + for (int row = first; m_dataModel && row <= last; ++row) { + const auto index = m_dataModel->index(row, 0); + Activity state {dataForRole(index, MODEL_ITEMID).toString(), + dataForRole(index, MODEL_ACTIVE).toBool(), + !dataForRole(index, MODEL_WINDOWS).toStringList().isEmpty()}; + m_activity.insert(index, state); + if (!state.id.isEmpty() && !changedIds.contains(state.id)) + changedIds.append(state.id); + } + // Aggregate cached rows because split windows can share an application ID. + QSet activeItems; + QSet runningItems; + for (const auto &state : std::as_const(m_activity)) { + if (state.active) + activeItems.insert(state.id); + if (state.running) + runningItems.insert(state.id); + } + if (!baseline) { + for (const auto &id : changedIds) { + if ((activeItems.contains(id) && !m_activeItems.contains(id)) + || (runningItems.contains(id) && !m_runningItems.contains(id))) { + m_pendingActivity.removeAll(id); + m_pendingActivity.append(id); + } + } + } + m_activeItems = activeItems; + m_runningItems = runningItems; +} + +void TaskOverflowController::syncRecentItem(const QVariantList &items) +{ + const bool recentClosed = !m_recentItem.value(QStringLiteral("windows")).toStringList().isEmpty() + && !m_runningItems.contains(m_recentItem.value(QStringLiteral("itemId")).toString()); + QHash applications; + for (const auto &item : items) { + const auto entry = item.toMap(); + const auto id = entry.value(QStringLiteral("itemId")).toString(); + if (id.isEmpty()) + continue; + // In window-split mode prefer the active, then a running window of an app. + const auto previous = applications.value(id); + if (previous.isEmpty() || entry.value(QStringLiteral("active")).toBool() + || (!previous.value(QStringLiteral("active")).toBool() + && previous.value(QStringLiteral("windows")).toStringList().isEmpty())) + applications.insert(id, entry); + } + const auto eligible = [&applications](const QString &id) { + const auto entry = applications.value(id); + return !entry.isEmpty() && (entry.value(QStringLiteral("docked")).toBool() + || !entry.value(QStringLiteral("windows")).toStringList().isEmpty()); + }; + for (auto it = m_recentHistory.begin(); it != m_recentHistory.end();) { + if (!eligible(*it)) + it = m_recentHistory.erase(it); + else + ++it; + } + // A background app can close while another overflow app remains active, + // without producing a new active-role transition for that fallback app. + if (recentClosed) { + for (const auto &item : items) { + const auto entry = item.toMap(); + if (entry.value(QStringLiteral("active")).toBool()) + m_pendingActivity.append(entry.value(QStringLiteral("itemId")).toString()); + } + } + for (const auto &id : std::as_const(m_pendingActivity)) { + if (!eligible(id)) + continue; + m_recentHistory.removeAll(id); + m_recentHistory.append(id); + if (m_recentHistory.size() > 30) + m_recentHistory.erase(m_recentHistory.begin(), m_recentHistory.begin() + 10); + } + m_pendingActivity.clear(); + + QVariantMap recent; + // Closing the current app falls back to the next running record, even if + // the closed app is pinned and therefore still present in the history. + for (auto it = m_recentHistory.crbegin(); it != m_recentHistory.crend(); ++it) { + const auto entry = applications.value(*it); + if (!entry.value(QStringLiteral("windows")).toStringList().isEmpty()) { + recent = entry; + break; + } + } + if (recent.isEmpty() && !m_recentHistory.isEmpty()) { + const auto previousId = m_recentItem.value(QStringLiteral("itemId")).toString(); + if (!recentClosed && m_recentHistory.contains(previousId)) { + recent = applications.value(previousId); + } else { + for (auto it = m_recentHistory.crbegin(); it != m_recentHistory.crend(); ++it) { + if (*it != previousId) { + recent = applications.value(*it); + break; + } + } + } + if (recent.isEmpty()) + recent = applications.value(m_recentHistory.last()); + } + if (recent.isEmpty() && !items.isEmpty()) + recent = items.last().toMap(); + if (m_recentItem != recent) { + m_recentItem = recent; + emit recentItemChanged(); + } +} + +} diff --git a/panels/dock/taskmanager/taskoverflowcontroller.h b/panels/dock/taskmanager/taskoverflowcontroller.h new file mode 100644 index 000000000..53b28d564 --- /dev/null +++ b/panels/dock/taskmanager/taskoverflowcontroller.h @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace dock { + +class TaskOverflowModel : public QAbstractListModel +{ + Q_OBJECT +public: + using QAbstractListModel::QAbstractListModel; + int rowCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + void setItems(const QVariantList &items); + +private: + struct Entry { + QPersistentModelIndex source; + QVariantMap values; + }; + QList m_entries; +}; + +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(qreal overflowSidePadding READ overflowSidePadding WRITE setOverflowSidePadding NOTIFY overflowSidePaddingChanged) + 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(QAbstractItemModel *popupModel READ popupModel CONSTANT) + Q_PROPERTY(int popupCount READ popupCount NOTIFY popupItemsChanged) + Q_PROPERTY(QVariantMap primaryItem READ primaryItem NOTIFY popupItemsChanged) + Q_PROPERTY(QVariantMap recentItem READ recentItem NOTIFY recentItemChanged) + Q_PROPERTY(bool showRecent READ showRecent NOTIFY layoutChanged) + Q_PROPERTY(qreal trailingExtent READ trailingExtent NOTIFY layoutChanged) + Q_PROPERTY(qreal buttonOffset READ buttonOffset NOTIFY layoutChanged) + Q_PROPERTY(qreal buttonExtent READ buttonExtent NOTIFY layoutChanged) + Q_PROPERTY(qreal separatorPadding READ separatorPadding NOTIFY layoutChanged) + Q_PROPERTY(bool dragInProgress READ dragInProgress NOTIFY dragInProgressChanged) + +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); + + qreal overflowSidePadding() const { return m_overflowSidePadding; } + void setOverflowSidePadding(qreal padding); + + 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; + } + QVariantMap recentItem() const { return m_recentItem; } + QAbstractItemModel *popupModel() { return &m_popupModel; } + int popupCount() const { return m_popupItems.size(); } + QVariantMap primaryItem() const { return m_popupItems.value(0).toMap(); } + bool showRecent() const { return m_showRecent; } + qreal trailingExtent() const { return m_trailingExtent; } + qreal buttonOffset() const { return m_buttonOffset; } + qreal buttonExtent() const { return m_buttonExtent; } + qreal separatorPadding() const { return m_separatorPadding; } + bool dragInProgress() const { return m_dragInProgress; } + + Q_INVOKABLE void scheduleSync(); + Q_INVOKABLE void beginDrag(); + Q_INVOKABLE bool finishDrag(int dropAction); + +Q_SIGNALS: + void enabledChanged(); + void fallbackItemExtentChanged(); + void spacingChanged(); + void overflowSidePaddingChanged(); + void visibleItemCountChanged(); + void popupItemsChanged(); + void recentItemChanged(); + void layoutChanged(); + void dragInProgressChanged(); + +private Q_SLOTS: + void onDataModelDestroyed(); + +private: + bool eventFilter(QObject *watched, QEvent *event) override; + void sync(); + int modelCount() const; + QModelIndex modelIndexAt(int visualIndex) const; + QVariant dataForRole(const QModelIndex &index, const QByteArray &roleName) const; + QVariantMap entryAt(int visualIndex); + bool syncEnabledState(int totalCount, qreal fullExtent); + + void connectDataModelSignals(); + void disconnectDataModelSignals(); + + void setVisibleItemCount(int visibleItemCount); + void setPopupItems(const QVariantList &items); + void cacheRoles(); + void captureActivity(bool baseline = false, int first = 0, int last = -1); + void syncRecentItem(const QVariantList &items); + void setLayout(bool showRecent, qreal trailingExtent, qreal buttonOffset, + qreal buttonExtent, qreal separatorPadding); + + bool m_enabled = true; + qreal m_availableExtent = 0; + bool m_minimumReached = false; + qreal m_fallbackItemExtent = 1; + qreal m_spacing = 0; + qreal m_overflowSidePadding = 0; + QPointer m_visualModel; + QPointer m_dataModel; + bool m_latched = false; + int m_visibleItemCount = 0; + QVariantList m_popupItems; + TaskOverflowModel m_popupModel; + QHash m_roles; + QSet m_entryRoles; + QHash m_entryCache; + struct Activity { + QString id; + bool active; + bool running; + }; + QHash m_activity; + QVariantMap m_recentItem; + QStringList m_recentHistory; // Oldest first, one record per application. + QStringList m_pendingActivity; + QSet m_activeItems; + QSet m_runningItems; + bool m_syncPending = false; + bool m_dragInProgress = false; + bool m_dragCancelled = false; + bool m_syncDeferred = false; + bool m_showRecent = false; + qreal m_trailingExtent = 0; + qreal m_buttonOffset = 0; + qreal m_buttonExtent = 0; + qreal m_separatorPadding = 0; +}; + +} 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 }; diff --git a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager.ts b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager.ts index e089821e3..8a88e4694 100644 --- a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager.ts +++ b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager.ts @@ -111,4 +111,11 @@ + + TaskOverflowButton + + Overflow area + + + diff --git a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_CN.ts b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_CN.ts index ba2603284..7c6fec9ce 100644 --- a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_CN.ts +++ b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_CN.ts @@ -109,4 +109,11 @@ 移除驻留 + + TaskOverflowButton + + Overflow area + 溢出区域 + + \ No newline at end of file diff --git a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_HK.ts b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_HK.ts index c6225d7df..935696265 100644 --- a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_HK.ts +++ b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_HK.ts @@ -109,4 +109,11 @@ 移除駐留 + + TaskOverflowButton + + Overflow area + 溢出區域 + + diff --git a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_TW.ts b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_TW.ts index 7f02bfa8d..a02126878 100644 --- a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_TW.ts +++ b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_TW.ts @@ -109,4 +109,11 @@ 移除駐留 + + TaskOverflowButton + + Overflow area + 溢出區域 + +