From f2343240971b180be9ee957f88491c41c4c13ed1 Mon Sep 17 00:00:00 2001 From: yeshanshan Date: Mon, 22 Dec 2025 11:06:06 +0800 Subject: [PATCH] refactor: unify app split/merge display and optimize text calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Replaced separate AppItemWithTitle.qml with unified AppItem.qml that handles both split and merged window modes 2. Introduced TextCalculator C++ class to handle dynamic text width calculation for better performance 3. Created AppItemTitle.qml as a reusable component for displaying application titles with proper elision 4. Removed complex QML-based text width calculations and dynamic character limit arrays 5. Simplified TaskManager.qml by moving text calculation logic to C+ + backend 6. Improved window split mode handling with more accurate space allocation calculations Log: Unified application split and merge display into single item view Influence: 1. Test application display in both split and merged window modes 2. Verify text elision works correctly with long application titles 3. Check performance when multiple applications are open with window split enabled 4. Test drag and drop functionality between applications 5. Verify application title visibility in different dock positions 6. Test window preview and context menu functionality 7. Check memory usage with many open applications refactor: 统一应用拆分/合并显示并优化文本计算 1. 将单独的 AppItemWithTitle.qml 替换为统一的 AppItem.qml,同时处理拆分 和合并窗口模式 2. 引入 TextCalculator C++ 类来处理动态文本宽度计算,提高性能 3. 创建 AppItemTitle.qml 作为可重用组件,用于显示带正确省略的应用标题 4. 移除复杂的基于 QML 的文本宽度计算和动态字符限制数组 5. 通过将文本计算逻辑移至 C++ 后端简化 TaskManager.qml 6. 改进窗口拆分模式处理,提供更准确的空间分配计算 Log: 将应用拆分和合并显示统一为单一项视图 Influence: 1. 测试应用在拆分和合并窗口模式下的显示 2. 验证长应用标题的文本省略功能是否正常工作 3. 检查启用窗口拆分时多个应用打开的性能 4. 测试应用之间的拖放功能 5. 验证不同任务栏位置下应用标题的可见性 6. 测试窗口预览和上下文菜单功能 7. 检查打开多个应用时的内存使用情况 PMS: TASK-384101 --- panels/dock/package/main.qml | 1 + panels/dock/taskmanager/CMakeLists.txt | 2 + panels/dock/taskmanager/package/AppItem.qml | 320 ++++++----- .../dock/taskmanager/package/AppItemTitle.qml | 41 ++ .../taskmanager/package/AppItemWithTitle.qml | 538 ------------------ .../dock/taskmanager/package/TaskManager.qml | 334 ++--------- panels/dock/taskmanager/taskmanager.cpp | 9 +- panels/dock/taskmanager/textcalculator.cpp | 420 ++++++++++++++ panels/dock/taskmanager/textcalculator.h | 181 ++++++ 9 files changed, 873 insertions(+), 973 deletions(-) create mode 100644 panels/dock/taskmanager/package/AppItemTitle.qml delete mode 100644 panels/dock/taskmanager/package/AppItemWithTitle.qml create mode 100644 panels/dock/taskmanager/textcalculator.cpp create mode 100644 panels/dock/taskmanager/textcalculator.h diff --git a/panels/dock/package/main.qml b/panels/dock/package/main.qml index 708e85dec..49a935a6d 100644 --- a/panels/dock/package/main.qml +++ b/panels/dock/package/main.qml @@ -25,6 +25,7 @@ Window { property int dockRemainingSpaceForCenter: useColumnLayout ? (Screen.height / 1.8 - dockRightPart.implicitHeight) : (Screen.width / 1.8 - dockRightPart.implicitWidth) + property int dockPartSpacing: gridLayout.columnSpacing // TODO signal dockCenterPartPosChanged() signal pressedAndDragging(bool isDragging) diff --git a/panels/dock/taskmanager/CMakeLists.txt b/panels/dock/taskmanager/CMakeLists.txt index 3b13ebc8e..9fcade49d 100644 --- a/panels/dock/taskmanager/CMakeLists.txt +++ b/panels/dock/taskmanager/CMakeLists.txt @@ -84,6 +84,8 @@ add_library(dock-taskmanager SHARED ${DBUS_INTERFACES} treelandwindowmonitor.h taskmanagersettings.cpp taskmanagersettings.h + textcalculator.h + textcalculator.cpp ) qt_generate_wayland_protocol_client_sources(dock-taskmanager diff --git a/panels/dock/taskmanager/package/AppItem.qml b/panels/dock/taskmanager/package/AppItem.qml index de984c645..d12b75faf 100644 --- a/panels/dock/taskmanager/package/AppItem.qml +++ b/panels/dock/taskmanager/package/AppItem.qml @@ -23,6 +23,9 @@ Item { required property list windows required property int visualIndex required property var modelIndex + required property string title + + property real blendOpacity: 1.0 signal dropFilesOnItem(itemId: string, files: list) signal dragFinished() @@ -32,11 +35,13 @@ 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" } - + 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] : ""} + property bool useColumnLayout: Panel.position % 2 property int statusIndicatorSize: useColumnLayout ? root.width * 0.72 : root.height * 0.72 property int iconSize: Panel.rootObject.dockItemMaxSize * 9 / 14 + property bool enableTitle: false + property bool titleActive: enableTitle && titleLoader.active property var iconGlobalPoint: { var a = icon @@ -50,25 +55,84 @@ Item { return Qt.point(x, y) } - Item { + implicitWidth: appItem.implicitWidth + + AppItemPalette { + id: itemPalette + displayMode: root.displayMode + colorTheme: root.colorTheme + active: root.active + backgroundColor: D.DTK.palette.highlight + } + + Control { anchors.fill: parent id: appItem + implicitWidth: root.titleActive ? (iconContainer.width + 4 + titleLoader.width) : iconContainer.width visible: !root.Drag.active // When in dragging, hide app item - AppItemPalette { - id: itemPalette - displayMode: root.displayMode - colorTheme: root.colorTheme - active: root.active - backgroundColor: D.DTK.palette.highlight - } - StatusIndicator { - id: statusIndicator - palette: itemPalette - width: root.statusIndicatorSize - height: root.statusIndicatorSize - anchors.centerIn: icon - visible: root.displayMode === Dock.Efficient && root.windows.length > 0 + Item { + id: iconContainer + anchors.verticalCenter: root.useColumnLayout ? undefined : parent.verticalCenter + anchors.horizontalCenter: root.useColumnLayout ? parent.horizontalCenter : undefined + width: root.titleActive ? root.iconSize : Panel.rootObject.dockItemMaxSize * 0.8 + height: parent.height + StatusIndicator { + id: statusIndicator + palette: itemPalette + width: root.statusIndicatorSize + height: root.statusIndicatorSize + anchors.centerIn: iconContainer + visible: root.displayMode === Dock.Efficient && root.windows.length > 0 + } + + Connections { + function onPositionChanged() { + windowIndicator.updateIndicatorAnchors() + updateWindowIconGeometryTimer.start() + } + target: Panel + } + + D.DciIcon { + id: icon + name: root.iconName + height: iconSize + width: iconSize + sourceSize: Qt.size(iconSize, iconSize) + anchors.centerIn: parent + retainWhileLoading: true + + LaunchAnimation { + id: launchAnimation + launchSpace: { + switch (Panel.position) { + case Dock.Top: + case Dock.Bottom: + return (root.height - icon.height) / 2 + case Dock.Left: + case Dock.Right: + return (root.width - icon.width) / 2 + } + } + + direction: { + switch (Panel.position) { + case Dock.Top: + return LaunchAnimation.Direction.Down + case Dock.Bottom: + return LaunchAnimation.Direction.Up + case Dock.Left: + return LaunchAnimation.Direction.Right + case Dock.Right: + return LaunchAnimation.Direction.Left + } + } + target: icon + loops: 1 + running: false + } + } } WindowIndicator { @@ -95,13 +159,13 @@ Item { switch(Panel.position) { case Dock.Top: { - windowIndicator.anchors.horizontalCenter = parent.horizontalCenter + windowIndicator.anchors.horizontalCenter = iconContainer.horizontalCenter windowIndicator.anchors.top = parent.top windowIndicator.anchors.topMargin = Qt.binding(() => {return (root.height - iconSize) / 2 / 3}) return } case Dock.Bottom: { - windowIndicator.anchors.horizontalCenter = parent.horizontalCenter + windowIndicator.anchors.horizontalCenter = iconContainer.horizontalCenter windowIndicator.anchors.bottom = parent.bottom windowIndicator.anchors.bottomMargin = Qt.binding(() => {return (root.height - iconSize) / 2 / 3}) return @@ -126,90 +190,19 @@ Item { } } - Connections { - function onPositionChanged() { - windowIndicator.updateIndicatorAnchors() - updateWindowIconGeometryTimer.start() - } - target: Panel - } - - Loader { - id: contextMenuLoader - active: false - property bool trashEmpty: true - sourceComponent: LP.Menu { - id: contextMenu - Instantiator { - id: menuItemInstantiator - model: JSON.parse(menus) - delegate: LP.MenuItem { - text: modelData.name - enabled: (root.itemId === "dde-trash" && modelData.id === "clean-trash") - ? !contextMenuLoader.trashEmpty - : true - onTriggered: { - TaskManager.requestNewInstance(root.modelIndex, modelData.id); - } - } - onObjectAdded: (index, object) => contextMenu.insertItem(index, object) - onObjectRemoved: (index, object) => contextMenu.removeItem(object) - } - } - } - - D.DciIcon { - id: icon - name: root.iconName - height: iconSize - width: iconSize - sourceSize: Qt.size(iconSize, iconSize) - anchors.centerIn: parent - retainWhileLoading: true - scale: Panel.rootObject.isDragging ? 1.0 : 1.0 - - LaunchAnimation { - id: launchAnimation - launchSpace: { - switch (Panel.position) { - case Dock.Top: - case Dock.Bottom: - return (root.height - icon.height) / 2 - case Dock.Left: - case Dock.Right: - return (root.width - icon.width) / 2 - } - } - - direction: { - switch (Panel.position) { - case Dock.Top: - return LaunchAnimation.Direction.Down - case Dock.Bottom: - return LaunchAnimation.Direction.Up - case Dock.Left: - return LaunchAnimation.Direction.Right - case Dock.Right: - return LaunchAnimation.Direction.Left - } - } - target: icon - loops: 1 - running: false - } + AppItemTitle { + id: titleLoader + anchors.left: iconContainer.right + anchors.leftMargin: 4 + anchors.verticalCenter: parent.verticalCenter + enabled: root.enableTitle && root.windows.length > 0 + text: root.title } // TODO: value can set during debugPanel Loader { - id: aniamtionRoot - function blendColorAlpha(fallback) { - var appearance = DS.applet("org.deepin.ds.dde-appearance") - if (!appearance || appearance.opacity < 0) - return fallback - return appearance.opacity - } - property real blendOpacity: blendColorAlpha(D.DTK.themeType === D.ApplicationHelper.DarkType ? 0.25 : 1.0) - anchors.fill: icon + id: animationRoot + anchors.fill: parent z: -1 active: root.attention && !Panel.rootObject.isDragging sourceComponent: Repeater { @@ -225,7 +218,7 @@ Item { color: Qt.rgba(1, 1, 1, 0.1) anchors.centerIn: parent - opacity: Math.min(3 - width / originSize, aniamtionRoot.blendOpacity) + opacity: Math.min(3 - width / originSize, root.blendOpacity) SequentialAnimation { running: true @@ -267,6 +260,40 @@ Item { } } } + + HoverHandler { + onHoveredChanged: function () { + if (hovered) { + root.onEntered() + } else { + root.onExited() + } + } + } + } + + Loader { + id: contextMenuLoader + active: false + property bool trashEmpty: true + sourceComponent: LP.Menu { + id: contextMenu + Instantiator { + id: menuItemInstantiator + model: JSON.parse(menus) + delegate: LP.MenuItem { + text: modelData.name + enabled: (root.itemId === "dde-trash" && modelData.id === "clean-trash") + ? !contextMenuLoader.trashEmpty + : true + onTriggered: { + TaskManager.requestNewInstance(root.modelIndex, modelData.id); + } + } + onObjectAdded: (index, object) => contextMenu.insertItem(index, object) + onObjectRemoved: (index, object) => contextMenu.removeItem(object) + } + } } Timer { @@ -296,10 +323,55 @@ Item { } } + + function onEntered() { + if (Qt.platform.pluginName === "xcb" && windows.length === 0) { + toolTipShowTimer.start() + 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 + previewTimer.start() + } + + function onExited() { + if (toolTipShowTimer.running) { + toolTipShowTimer.stop() + } + + if (previewTimer.running) { + previewTimer.stop() + } + + if (Qt.platform.pluginName === "xcb" && windows.length === 0) { + toolTip.close() + return + } + closeItemPreview() + } + + function closeItemPreview() { + if (previewTimer.running) { + previewTimer.stop() + } else { + taskmanager.Applet.hideItemPreview() + } + } + MouseArea { id: mouseArea anchors.fill: parent - hoverEnabled: true + hoverEnabled: false acceptedButtons: Qt.LeftButton | Qt.RightButton drag.target: root drag.onActiveChanged: { @@ -313,7 +385,7 @@ Item { onPressed: function (mouse) { if (mouse.button === Qt.LeftButton) { - icon.grabToImage(function(result) { + appItem.grabToImage(function(result) { root.Drag.imageSource = result.url; }) } @@ -336,42 +408,6 @@ Item { } } - onEntered: { - if (Qt.platform.pluginName === "xcb" && windows.length === 0) { - toolTipShowTimer.start() - 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 - previewTimer.start() - } - - onExited: { - if (toolTipShowTimer.running) { - toolTipShowTimer.stop() - } - - if (previewTimer.running) { - previewTimer.stop() - } - - if (Qt.platform.pluginName === "xcb" && windows.length === 0) { - toolTip.close() - return - } - closeItemPreview() - } - PanelToolTip { id: toolTip toolTipX: DockPanelPositioner.x @@ -396,14 +432,6 @@ Item { toolTip.open() } } - - function closeItemPreview() { - if (previewTimer.running) { - previewTimer.stop() - } else { - taskmanager.Applet.hideItemPreview() - } - } } DropArea { diff --git a/panels/dock/taskmanager/package/AppItemTitle.qml b/panels/dock/taskmanager/package/AppItemTitle.qml new file mode 100644 index 000000000..ac4a296b4 --- /dev/null +++ b/panels/dock/taskmanager/package/AppItemTitle.qml @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls + +import org.deepin.ds.dock.taskmanager 1.0 +import org.deepin.dtk 1.0 as D + +Item { + id: root + + property bool active: titleLoader.active + property string text: "" + + implicitWidth: titleLoader.width + implicitHeight: titleLoader.height + + TextCalculator.text: root.text + + Loader { + id: titleLoader + active: root.enabled && root.TextCalculator.elidedText.length > 0 + sourceComponent: Text { + id: titleText + + text: root.TextCalculator.elidedText + + color: D.DTK.themeType === D.ApplicationHelper.DarkType ? "#FFFFFF" : "#000000" + font: root.TextCalculator.calculator.font + verticalAlignment: Text.AlignVCenter + + opacity: visible ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { duration: 150 } + } + } + } +} diff --git a/panels/dock/taskmanager/package/AppItemWithTitle.qml b/panels/dock/taskmanager/package/AppItemWithTitle.qml deleted file mode 100644 index 7bf6b8534..000000000 --- a/panels/dock/taskmanager/package/AppItemWithTitle.qml +++ /dev/null @@ -1,538 +0,0 @@ -// SPDX-FileCopyrightText: 2025 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 -import Qt.labs.platform 1.1 as LP - -Item { - id: root - required property int displayMode - required property int colorTheme - required property bool active - required property bool attention - required property string itemId - required property string name - required property string windowTitle - required property string iconName - required property string menus - required property list windows - required property int visualIndex - required property var modelIndex - property int maxCharLimit: 7 - - signal dropFilesOnItem(itemId: string, files: list) - signal dragFinished() - - Drag.active: mouseArea.drag.active - Drag.source: root - Drag.hotSpot.x: iconContainer.width / 2 - Drag.hotSpot.y: iconContainer.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] : "" - } - - property bool useColumnLayout: Panel.position % 2 - property int statusIndicatorSize: useColumnLayout ? root.width * 0.72 : root.height * 0.72 - property int iconSize: Panel.rootObject.dockItemMaxSize * 9 / 14 - - // 根据图标尺寸计算文字大小,最大20最小10 - property int textSize: Math.max(10, Math.min(20, Math.round(iconSize * 0.35))) - - property string displayText: { - if (root.windows.length === 0) - return "" - - if (!root.windowTitle || root.windowTitle.length === 0) - return "" - - var source = root.windowTitle - var maxChars = root.maxCharLimit - - // maxCharLimit 为 0 时不显示文字 - if (maxChars <= 0) - return "" - - var len = source.length - var displayLen = 0 - - if (len <= maxChars) { - displayLen = len - } else { - // 文本超过最大字符数时,最多显示 6 个字符,再加省略号 - displayLen = maxChars - } - - if (displayLen <= 0) - return "" - - if (len > maxChars) { - return source.substring(0, displayLen) + "…" - } else { - return source.substring(0, displayLen) - } - } - - property int actualWidth: { - if (displayText.length === 0) { - // 文字完全隐藏时,只占用图标宽度 - return iconSize + 4 - } - // 有文字时,计算实际文字宽度 - var textWidth = titleText.implicitWidth - return iconSize + textWidth + 8 - } - - property var iconGlobalPoint: { - var a = iconContainer - var x = 0, y = 0 - while(a.parent) { - x += a.x - y += a.y - a = a.parent - } - return Qt.point(x, y) - } - - Item { - anchors.fill: parent - id: appItem - visible: !root.Drag.active - - AppItemPalette { - id: itemPalette - displayMode: root.displayMode - colorTheme: root.colorTheme - active: root.active - backgroundColor: D.DTK.palette.highlight - } - Item { - id: hoverBackground - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: -2 - anchors.rightMargin: 2 - width: root.actualWidth - height: parent.height - 4 - opacity: mouseArea.containsMouse ? 1.0 : 0.0 - visible: opacity > 0 - z: -1 - Rectangle { - anchors.fill: parent - anchors.margins: -1 - radius: 9 - color: "transparent" - antialiasing: true - border.color: Qt.rgba(0, 0, 0, 0.1) - border.width: 1 - - Rectangle { - anchors.fill: parent - anchors.margins: 1 - radius: 8 - color: Qt.rgba(1, 1, 1, 0.15) - antialiasing: true - border.color: Qt.rgba(1, 1, 1, 0.1) - border.width: 1 - } - } - Behavior on opacity { - NumberAnimation { duration: 150 } - } - } - Item { - id: iconContainer - width: iconSize - height: parent.height - anchors.left: parent.left - - StatusIndicator { - id: statusIndicator - palette: itemPalette - width: root.statusIndicatorSize - height: root.statusIndicatorSize - anchors.centerIn: icon - visible: root.displayMode === Dock.Efficient && root.windows.length > 0 - } - - D.DciIcon { - id: icon - name: root.iconName - height: iconSize - width: iconSize - sourceSize: Qt.size(iconSize, iconSize) - anchors.centerIn: parent - retainWhileLoading: true - scale: Panel.rootObject.isDragging ? 1.0 : 1.0 - - LaunchAnimation { - id: launchAnimation - launchSpace: { - switch (Panel.position) { - case Dock.Top: - case Dock.Bottom: - return (root.height - icon.height) / 2 - case Dock.Left: - case Dock.Right: - return (root.width - icon.width) / 2 - } - } - - direction: { - switch (Panel.position) { - case Dock.Top: - return LaunchAnimation.Direction.Down - case Dock.Bottom: - return LaunchAnimation.Direction.Up - case Dock.Left: - return LaunchAnimation.Direction.Right - case Dock.Right: - return LaunchAnimation.Direction.Left - } - } - target: icon - loops: 1 - running: false - } - } - - Loader { - id: aniamtionRoot - function blendColorAlpha(fallback) { - var appearance = DS.applet("org.deepin.ds.dde-appearance") - if (!appearance || appearance.opacity < 0) - return fallback - return appearance.opacity - } - property real blendOpacity: blendColorAlpha(D.DTK.themeType === D.ApplicationHelper.DarkType ? 0.25 : 1.0) - anchors.fill: icon - z: -1 - active: root.attention && !Panel.rootObject.isDragging - sourceComponent: Repeater { - model: 5 - Rectangle { - id: rect - required property int index - property var originSize: iconSize - - width: originSize * (index - 1) - height: width - radius: width / 2 - color: Qt.rgba(1, 1, 1, 0.1) - - anchors.centerIn: parent - opacity: Math.min(3 - width / originSize, aniamtionRoot.blendOpacity) - - SequentialAnimation { - running: true - loops: Animation.Infinite - - ParallelAnimation { - NumberAnimation { target: rect; property: "width"; from: Math.max(originSize * (index - 1), 0); to: originSize * (index); duration: 1200 } - ColorAnimation { target: rect; property: "color"; from: Qt.rgba(1, 1, 1, 0.4); to: Qt.rgba(1, 1, 1, 0.1); duration: 1200 } - NumberAnimation { target: icon; property: "scale"; from: 1.0; to: 1.15; duration: 1200; easing.type: Easing.OutElastic; easing.amplitude: 1; easing.period: 0.2 } - } - - ParallelAnimation { - NumberAnimation { target: rect; property: "width"; from: originSize * (index); to: originSize * (index + 1); duration: 1200 } - ColorAnimation { target: rect; property: "color"; from: Qt.rgba(1, 1, 1, 0.4); to: Qt.rgba(1, 1, 1, 0.1); duration: 1200 } - NumberAnimation { target: icon; property: "scale"; from: 1.15; to: 1.0; duration: 1200; easing.type: Easing.OutElastic; easing.amplitude: 1; easing.period: 0.2 } - } - - ParallelAnimation { - NumberAnimation { target: rect; property: "width"; from: originSize * (index + 1); to: originSize * (index + 2); duration: 1200 } - ColorAnimation { target: rect; property: "color"; from: Qt.rgba(1, 1, 1, 0.4); to: Qt.rgba(1, 1, 1, 0.1); duration: 1200 } - } - } - } - } - } - } - - // 标题文本,位于图标右侧 - Text { - id: titleText - anchors.left: iconContainer.right - anchors.leftMargin: 4 - anchors.verticalCenter: parent.verticalCenter - - text: displayText - - color: D.DTK.themeType === D.ApplicationHelper.DarkType ? "#FFFFFF" : "#000000" - font.pixelSize: textSize - font.family: D.DTK.fontManager.t5.family - elide: Text.ElideNone // 我们已经在displayText中处理了截断 - verticalAlignment: Text.AlignVCenter - - visible: displayText.length > 0 - opacity: visible ? 1.0 : 0.0 - - - Behavior on opacity { - NumberAnimation { duration: 150 } - } - } - - WindowIndicator { - id: windowIndicator - dotWidth: root.useColumnLayout ? Math.max(iconSize / 16, 2) : Math.max(iconSize / 3, 2) - dotHeight: root.useColumnLayout ? Math.max(iconSize / 3, 2) : Math.max(iconSize / 16, 2) - windows: root.windows - displayMode: root.displayMode - useColumnLayout: root.useColumnLayout - palette: itemPalette - visible: (root.displayMode === Dock.Efficient && root.windows.length > 1) || (root.displayMode === Dock.Fashion && root.windows.length > 0) - - function updateIndicatorAnchors() { - windowIndicator.anchors.top = undefined - windowIndicator.anchors.topMargin = 0 - windowIndicator.anchors.bottom = undefined - windowIndicator.anchors.bottomMargin = 0 - windowIndicator.anchors.left = undefined - windowIndicator.anchors.leftMargin = 0 - windowIndicator.anchors.right = undefined - windowIndicator.anchors.rightMargin = 0 - windowIndicator.anchors.horizontalCenter = undefined - windowIndicator.anchors.verticalCenter = undefined - - switch(Panel.position) { - case Dock.Top: { - windowIndicator.anchors.horizontalCenter = iconContainer.horizontalCenter - windowIndicator.anchors.top = parent.top - windowIndicator.anchors.topMargin = Qt.binding(() => {return (root.height - iconSize) / 2 / 3}) - return - } - case Dock.Bottom: { - windowIndicator.anchors.horizontalCenter = iconContainer.horizontalCenter - windowIndicator.anchors.bottom = parent.bottom - windowIndicator.anchors.bottomMargin = Qt.binding(() => {return (root.height - iconSize) / 2 / 3}) - return - } - case Dock.Left: { - windowIndicator.anchors.verticalCenter = iconContainer.verticalCenter - windowIndicator.anchors.left = parent.left - windowIndicator.anchors.leftMargin = Qt.binding(() => {return (root.width - iconSize) / 2 / 3}) - return - } - case Dock.Right:{ - windowIndicator.anchors.verticalCenter = iconContainer.verticalCenter - windowIndicator.anchors.right = parent.right - windowIndicator.anchors.rightMargin = Qt.binding(() => {return (root.width - iconSize) / 2 / 3}) - return - } - } - } - - Component.onCompleted: { - windowIndicator.updateIndicatorAnchors() - } - } - - Connections { - function onPositionChanged() { - windowIndicator.updateIndicatorAnchors() - updateWindowIconGeometryTimer.start() - } - target: Panel - } - - Loader { - id: contextMenuLoader - active: false - property bool trashEmpty: true - sourceComponent: LP.Menu { - id: contextMenu - Instantiator { - id: menuItemInstantiator - model: JSON.parse(menus) - delegate: LP.MenuItem { - text: modelData.name - enabled: (root.itemId === "dde-trash" && modelData.id === "clean-trash") - ? !contextMenuLoader.trashEmpty - : true - onTriggered: { - TaskManager.requestNewInstance(root.modelIndex, modelData.id); - } - } - onObjectAdded: (index, object) => contextMenu.insertItem(index, object) - onObjectRemoved: (index, object) => contextMenu.removeItem(object) - } - } - } - } - - Timer { - id: updateWindowIconGeometryTimer - interval: 500 - running: false - repeat: false - onTriggered: { - var pos = icon.mapToItem(null, 0, 0) - taskmanager.Applet.requestUpdateWindowIconGeometry(root.modelIndex, Qt.rect(pos.x, pos.y, - icon.width, icon.height), Panel.rootObject) - } - } - - Timer { - id: previewTimer - interval: 500 - running: false - repeat: false - property int xOffset: 0 - property int yOffset: 0 - onTriggered: { - if (root.windows.length != 0 || Qt.platform.pluginName === "wayland") { - taskmanager.Applet.requestPreview(root.modelIndex, Panel.rootObject, xOffset, yOffset, Panel.position); - } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - drag.target: root - drag.onActiveChanged: { - if (!drag.active) { - Panel.contextDragging = false - root.dragFinished() - return - } - Panel.contextDragging = true - } - - onPressed: function (mouse) { - if (mouse.button === Qt.LeftButton) { - iconContainer.grabToImage(function(result) { - root.Drag.imageSource = result.url; - }) - } - toolTip.close() - closeItemPreview() - } - onClicked: function (mouse) { - let index = root.modelIndex; - if (mouse.button === Qt.RightButton) { - contextMenuLoader.trashEmpty = TaskManager.isTrashEmpty() - contextMenuLoader.active = true - MenuHelper.openMenu(contextMenuLoader.item) - } else { - if (root.windows.length === 0) { - launchAnimation.start(); - TaskManager.requestNewInstance(index, ""); - return; - } - TaskManager.requestActivate(index); - } - } - - onEntered: { - if (Qt.platform.pluginName === "xcb" && windows.length === 0) { - toolTipShowTimer.start() - 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 - previewTimer.start() - } - - onExited: { - if (toolTipShowTimer.running) { - toolTipShowTimer.stop() - } - - if (previewTimer.running) { - previewTimer.stop() - } - - if (Qt.platform.pluginName === "xcb" && windows.length === 0) { - toolTip.close() - return - } - closeItemPreview() - } - - PanelToolTip { - id: toolTip - toolTipX: DockPanelPositioner.x - toolTipY: DockPanelPositioner.y - } - - PanelToolTip { - id: dragToolTip - text: qsTr("Move to Trash") - toolTipX: DockPanelPositioner.x - toolTipY: DockPanelPositioner.y - visible: false - } - - 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.text = root.itemId === "dde-trash" ? root.name + "-" + taskmanager.Applet.getTrashTipText() : root.name - toolTip.open() - } - } - - function closeItemPreview() { - if (previewTimer.running) { - previewTimer.stop() - } else { - taskmanager.Applet.hideItemPreview() - } - } - } - - DropArea { - anchors.fill: parent - keys: ["dfm_app_type_for_drag"] - - onEntered: function (drag) { - if (root.itemId === "dde-trash") { - var point = root.mapToItem(null, root.width / 2, root.height / 2) - dragToolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, dragToolTip.width, dragToolTip.height) - dragToolTip.open() - } - } - - onExited: function (drag) { - if (root.itemId === "dde-trash") { - dragToolTip.close() - } - } - - onDropped: function (drop){ - root.dropFilesOnItem(root.itemId, drop.urls) - } - } - - onWindowsChanged: { - updateWindowIconGeometryTimer.start() - } - - onIconGlobalPointChanged: { - updateWindowIconGeometryTimer.start() - } -} \ No newline at end of file diff --git a/panels/dock/taskmanager/package/TaskManager.qml b/panels/dock/taskmanager/package/TaskManager.qml index a15719962..3fff93655 100644 --- a/panels/dock/taskmanager/package/TaskManager.qml +++ b/panels/dock/taskmanager/package/TaskManager.qml @@ -7,22 +7,21 @@ import QtQuick.Controls 2.15 import org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 +import org.deepin.ds.dock.taskmanager 1.0 import org.deepin.dtk 1.0 as D ContainmentItem { id: taskmanager property bool useColumnLayout: Panel.position % 2 property int dockOrder: 16 - property int remainingSpacesForTaskManager: Panel.itemAlignment === Dock.LeftAlignment ? Panel.rootObject.dockLeftSpaceForCenter : Panel.rootObject.dockRemainingSpaceForCenter - - property int remainingSpacesForSplitWindow: Panel.rootObject.dockLeftSpaceForCenter - (Panel.rootObject.dockCenterPartCount - 1) * Panel.rootObject.dockItemMaxSize * 9 / 14 + property real remainingSpacesForTaskManager: Panel.itemAlignment === Dock.LeftAlignment ? Panel.rootObject.dockLeftSpaceForCenter : Panel.rootObject.dockRemainingSpaceForCenter + + property real remainingSpacesForSplitWindow: Panel.rootObject.dockLeftSpaceForCenter - ( + (Panel.rootObject.dockCenterPartCount - 1) * (visualModel.cellWidth + appContainer.spacing) + (Panel.rootObject.dockCenterPartCount) * Panel.rootObject.dockPartSpacing) // 用于居中计算的实际应用区域尺寸 property int appContainerWidth: useColumnLayout ? Panel.rootObject.dockSize : appContainer.implicitWidth property int appContainerHeight: useColumnLayout ? appContainer.implicitHeight : Panel.rootObject.dockSize - // 动态字符限制数组,存储每个应用的最大显示字符数 - property var dynamicCharLimits: [] - implicitWidth: useColumnLayout ? Panel.rootObject.dockSize : Math.max(remainingSpacesForTaskManager, appContainer.implicitWidth) implicitHeight: useColumnLayout ? Math.max(remainingSpacesForTaskManager, appContainer.implicitHeight) : Panel.rootObject.dockSize @@ -48,174 +47,26 @@ ContainmentItem { } return -1 } - TextMetrics { - id: textMetrics - font.family: D.DTK.fontManager.t5.family - } - - // 使用 TextMetrics 计算文本宽度 - function calculateTextWidth(text, textSize) { - if (!text || text.length === 0) return 0 - textMetrics.font.pixelSize = textSize - textMetrics.text = text - //+4 for padding 保持跟appitemwithtitle显示的大小一致 否则UI上显示会溢出 - return textMetrics.advanceWidth + 4 - } - - // 计算文本在给定宽度下的最大字符数 - function calculateMaxCharsWithinWidth(title, maxWidth, textSize) { - if (!title || title.length === 0) return 0 - let low = 1 - let high = title.length - let result = 0 - while (low <= high) { - let mid = Math.floor((low + high) / 2) - let sub = title.substring(0, mid) - let width = calculateTextWidth(sub, textSize) - if (width <= maxWidth) { - result = mid - low = mid + 1 - } else { - high = mid - 1 - } - } - return result - } - // 计算单个应用的显示宽度 iconsize + titlewidth - function calculateItemWidth(title, maxChars, iconSize, textSize) { - if (!title || title.length === 0) { - return iconSize + 4 - } - - // maxCharLimit 为 0 时不显示文字 - if (maxChars <= 0) { - return iconSize + 4 - } - - let titleLength = title.length - let displayLen = 0 - - if (titleLength <= maxChars) { - displayLen = titleLength - } else { - displayLen = maxChars - } - - if (displayLen <= 0) { - return iconSize + 4 - } - - let text = "" - if (titleLength > maxChars) { - text = title.substring(0, displayLen) + "…" - } else { - text = title.substring(0, displayLen) - } - - let textWidth = calculateTextWidth(text, textSize) - return iconSize + textWidth + 8 + function blendColorAlpha(fallback) { + var appearance = DS.applet("org.deepin.ds.dde-appearance") + if (!appearance || appearance.opacity < 0) + return fallback + return appearance.opacity } + property real blendOpacity: blendColorAlpha(D.DTK.themeType === D.ApplicationHelper.DarkType ? 0.25 : 1.0) - // 计算所有应用的总宽度 - function calculateTotalWidth(charLimits, iconSize, textSize) { - let count = visualModel.items.count - if (count === 0) return 0 - - let totalAppWidth = 0 - for (let i = 0; i < count; i++) { - const item = visualModel.items.get(i) - let maxChars = charLimits[i] !== undefined ? charLimits[i] : 7 - totalAppWidth += calculateItemWidth(item.model.title, maxChars, iconSize, textSize) - } - - // 加上应用之间的间距 - let spacing = Panel.rootObject.itemSpacing + (count % 2) - let totalSpacing = Math.max(0, count - 1) * spacing - - return totalAppWidth + totalSpacing - } - - // 找出当前显示字符数最多的应用索引 - function findLongestTitleIndex(charLimits) { - let maxIdx = -1 - let maxChars = -1 - for (let i = 0; i < visualModel.items.count; i++) { - let currentLimit = charLimits[i] !== undefined ? charLimits[i] : 7 - if (currentLimit > maxChars) { - maxChars = currentLimit - maxIdx = i - } - } - return maxIdx - } - - // 动态计算每个应用的字符限制数组 - function calculateDynamicCharLimits(remainingSpace, iconSize, textSize) { - if (visualModel.items.count === 0) { - return [] - } - - // 初始化:所有应用都按7个汉字宽度计算 - let charLimits = [] - let maxTitleWidth = calculateTextWidth("计算七个字长度", textSize) - for (let i = 0; i < visualModel.items.count; i++) { - const item = visualModel.items.get(i) - let title = item.model.title || "" - if (title.length === 0) { - charLimits[i] = 0 - } else { - charLimits[i] = calculateMaxCharsWithinWidth(title, maxTitleWidth, textSize) - } - } - - // 计算总宽度 - let totalWidth = calculateTotalWidth(charLimits, iconSize, textSize) - - // 如果总宽度超过剩余空间,逐步缩减最长标题 - while (totalWidth > remainingSpace) { - let longestIdx = findLongestTitleIndex(charLimits) - - if (longestIdx === -1) { - // 所有标题都已缩减到0,无法再缩减 - break - } - - // 缩减该标题的字符数 - charLimits[longestIdx] = charLimits[longestIdx] - 1 - if (charLimits[longestIdx] < 0) { - charLimits[longestIdx] = 0 - } - - // 重新计算总宽度 - totalWidth = calculateTotalWidth(charLimits, iconSize, textSize) - } - //过滤掉字符数为1的,因为一个字符+省略号,不美观 直接全部显示相应的图标 - for (let i = 0; i < visualModel.items.count; i++) { - const item = visualModel.items.get(i) - let title = item.model.title || "" - let maxChars = charLimits[i] !== undefined ? charLimits[i] : 7 - if (maxChars === 1 && title.length > 1) { - charLimits[i] = 0 - } - } - - return charLimits - } - function updateDynamicCharLimits() { - if (!taskmanager.Applet.windowSplit || taskmanager.useColumnLayout) { - taskmanager.dynamicCharLimits = [] - return - } - - if (!(Panel.position === Dock.Bottom || Panel.position === Dock.Top)) { - taskmanager.dynamicCharLimits = [] - return - } - - let iconSize = Panel.rootObject.dockItemMaxSize * 9 / 14 - let textSize = Math.max(10, Math.min(20, Math.round(iconSize * 0.35))) - taskmanager.dynamicCharLimits = calculateDynamicCharLimits(taskmanager.remainingSpacesForSplitWindow, iconSize, textSize) + TextCalculator { + id: textCalculator + enabled: taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top) + dataModel: taskmanager.Applet.dataModel + iconSize: Panel.rootObject.dockItemMaxSize * 9 / 14 + spacing: appContainer.spacing + cellSize: visualModel.cellWidth + itemPadding: 4 + remainingSpace: taskmanager.remainingSpacesForSplitWindow + font.family: D.DTK.fontManager.t6.family + font.pixelSize: Math.max(10, Math.min(20, Math.round(textCalculator.iconSize * 0.35))) } OverflowContainer { @@ -253,9 +104,6 @@ ContainmentItem { model: taskmanager.Applet.dataModel // 1:4 the distance between app : dock height; get width/height≈0.8 property real cellWidth: Panel.rootObject.dockItemMaxSize * 0.8 - onCountChanged: function() { - DS.singleShot(300, updateDynamicCharLimits) - } delegate: Item { id: delegateRoot required property int index @@ -274,18 +122,7 @@ ContainmentItem { if (itemId !== draggedAppId) { return true } - // 同一个应用,在 windowSplit 模式下需要检查窗口ID - if (taskmanager.Applet.windowSplit) { - if (launcherDndDropArea.launcherDndWinId) { - // 拖拽的是具体窗口:只隐藏该窗口,显示其他窗口和驻留图标 - return windows.length === 0 || windows[0] !== launcherDndDropArea.launcherDndWinId - } else { - // 拖拽的是驻留图标(无窗口ID):只隐藏驻留图标,显示运行中的窗口 - return windows.length > 0 - } - } - // 非 windowSplit 模式,隐藏整个应用 - return false + return windows.length > 0 && launcherDndDropArea.launcherDndWinId !== windows[0] } ListView.onAdd: NumberAnimation { @@ -312,78 +149,36 @@ ContainmentItem { Behavior on opacity { NumberAnimation { duration: 200 } } Behavior on scale { NumberAnimation { duration: 200 } } - property int dynamicCharLimit: { - if (!taskmanager.Applet.windowSplit || useColumnLayout) { - return 7 - } - if (taskmanager.dynamicCharLimits && DelegateModel.itemsIndex < taskmanager.dynamicCharLimits.length) { - return taskmanager.dynamicCharLimits[DelegateModel.itemsIndex] - } - return 7 - } - - implicitWidth: useColumnLayout ? taskmanager.implicitWidth : - (taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top) - ? (appLoader.item && appLoader.item.actualWidth ? appLoader.item.actualWidth : visualModel.cellWidth) - : visualModel.cellWidth) + implicitWidth: useColumnLayout ? taskmanager.implicitWidth : appItem.implicitWidth implicitHeight: useColumnLayout ? visualModel.cellWidth : taskmanager.implicitHeight property int visualIndex: DelegateModel.itemsIndex property var modelIndex: visualModel.modelIndex(index) - Loader { - id: appLoader - anchors.fill: parent - sourceComponent: (taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top)) ? appItemWithTitleComponent : appItemComponent - - Component { - id: appItemComponent - AppItem { - displayMode: Panel.indicatorStyle - colorTheme: Panel.colorTheme - active: delegateRoot.active - attention: delegateRoot.attention - itemId: delegateRoot.itemId - name: delegateRoot.name - iconName: delegateRoot.iconName - menus: delegateRoot.menus - windows: delegateRoot.windows - visualIndex: delegateRoot.visualIndex - modelIndex: delegateRoot.modelIndex - ListView.delayRemove: Drag.active - Component.onCompleted: { - dropFilesOnItem.connect(taskmanager.Applet.dropFilesOnItem) - } - onDragFinished: function() { - launcherDndDropArea.resetDndState() - } - } + AppItem { + id: appItem + anchors.fill: parent // This is mandatory for draggable item center in drop area + + displayMode: Panel.indicatorStyle + colorTheme: Panel.colorTheme + active: delegateRoot.active + attention: delegateRoot.attention + itemId: delegateRoot.itemId + name: delegateRoot.name + iconName: delegateRoot.iconName + menus: delegateRoot.menus + windows: delegateRoot.windows + visualIndex: delegateRoot.visualIndex + modelIndex: delegateRoot.modelIndex + blendOpacity: taskmanager.blendOpacity + title: delegateRoot.title + enableTitle: textCalculator.enabled + ListView.delayRemove: Drag.active + Component.onCompleted: { + dropFilesOnItem.connect(taskmanager.Applet.dropFilesOnItem) } - - Component { - id: appItemWithTitleComponent - AppItemWithTitle { - displayMode: Panel.indicatorStyle - colorTheme: Panel.colorTheme - active: delegateRoot.active - attention: delegateRoot.attention - itemId: delegateRoot.itemId - name: delegateRoot.name - windowTitle: delegateRoot.title - iconName: delegateRoot.iconName - menus: delegateRoot.menus - windows: delegateRoot.windows - visualIndex: delegateRoot.visualIndex - modelIndex: delegateRoot.modelIndex - maxCharLimit: delegateRoot.dynamicCharLimit - ListView.delayRemove: Drag.active - Component.onCompleted: { - dropFilesOnItem.connect(taskmanager.Applet.dropFilesOnItem) - } - onDragFinished: function() { - launcherDndDropArea.resetDndState() - } - } + onDragFinished: function() { + launcherDndDropArea.resetDndState() } } } @@ -449,44 +244,9 @@ ContainmentItem { } } - //windowSplit下:计算标签长度过程中会导致图标卡顿,挤在一起,计算完成后刷新布局 - Timer { - id: windowSplitRelayoutTimer - interval: 500 - repeat: false - onTriggered: { - updateDynamicCharLimits() - } - } - - Connections { - target: taskmanager.Applet - function onWindowSplitChanged() { - windowSplitRelayoutTimer.start() - } - } - - Connections { - target: taskmanager.Applet.dataModel - function onDataChanged(topLeft, bottomRight, roles) { - if (!taskmanager.Applet.windowSplit || taskmanager.useColumnLayout) - return - if (!(Panel.position === Dock.Bottom || Panel.position === Dock.Top)) - return - DS.singleShot(300, updateDynamicCharLimits) - } - } - - // 监听 remainingSpacesForSplitWindow 变化 - onRemainingSpacesForSplitWindowChanged: { - DS.singleShot(300, updateDynamicCharLimits) - } - Component.onCompleted: { Panel.rootObject.dockItemMaxSize = Qt.binding(function(){ return Math.min(Panel.rootObject.dockSize, Panel.rootObject.dockLeftSpaceForCenter * 1.2 / (Panel.rootObject.dockCenterPartCount - 1 + visualModel.count) - 2) }) - if(taskmanager.Applet.windowSplit) - windowSplitRelayoutTimer.start() } } diff --git a/panels/dock/taskmanager/taskmanager.cpp b/panels/dock/taskmanager/taskmanager.cpp index f0a4298bc..88a270160 100644 --- a/panels/dock/taskmanager/taskmanager.cpp +++ b/panels/dock/taskmanager/taskmanager.cpp @@ -19,13 +19,15 @@ #include "taskmanager.h" #include "taskmanageradaptor.h" #include "taskmanagersettings.h" +#include "textcalculator.h" #include "treelandwindowmonitor.h" #include +#include +#include #include #include -#include -#include +#include #include #include @@ -97,6 +99,9 @@ TaskManager::TaskManager(QObject *parent) , AbstractTaskManagerInterface(nullptr) , m_windowFullscreen(false) { + qmlRegisterType("org.deepin.ds.dock.taskmanager", 1, 0, "TextCalculator"); + qmlRegisterUncreatableType("org.deepin.ds.dock.taskmanager", 1, 0, "TextCalculatorAttached", "TextCalculator Attached"); + connect(Settings, &TaskManagerSettings::allowedForceQuitChanged, this, &TaskManager::allowedForceQuitChanged); connect(Settings, &TaskManagerSettings::windowSplitChanged, this, &TaskManager::windowSplitChanged); } diff --git a/panels/dock/taskmanager/textcalculator.cpp b/panels/dock/taskmanager/textcalculator.cpp new file mode 100644 index 000000000..22be54132 --- /dev/null +++ b/panels/dock/taskmanager/textcalculator.cpp @@ -0,0 +1,420 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "textcalculator.h" + +#include +#include +#include +#include + +namespace dock +{ +Q_LOGGING_CATEGORY(textCalculatorLog, "ds.taskmanager.textcalculator"); + +static bool isValidElidedText(const QString &text) +{ + return !text.isEmpty() && text != "…"; +} + +TextCalculator::TextCalculator(QObject *parent) + : QObject(parent) + , m_optimalSingleTextWidth(0.0) + , m_totalWidth(0) + , m_font(QGuiApplication::font()) + , m_iconSize(48) + , m_cellSize(48) + , m_spacing(8) + , m_itemPadding(4) + , m_dataModel(nullptr) + , m_remainingSpace(0) + , m_enabled(false) +{ +} + +TextCalculator::~TextCalculator() +{ + if (m_dataModel) { + disconnectDataModelSignals(); + } +} + +void TextCalculator::setFont(const QFont &font) +{ + if (m_font != font) { + qCDebug(textCalculatorLog) << "Font changed, clearing cache and recalculating"; + m_font = font; + m_baselineWidthCache.clear(); + emit fontChanged(); + scheduleCalculation(); + } +} + +void TextCalculator::setIconSize(qreal size) +{ + if (m_iconSize != size) { + m_iconSize = size; + emit iconSizeChanged(); + scheduleCalculation(); + } +} + +void TextCalculator::setCellSize(qreal size) +{ + if (m_cellSize != size) { + m_cellSize = size; + emit cellSizeChanged(); + scheduleCalculation(); + } +} + +void TextCalculator::setSpacing(int spacing) +{ + if (m_spacing != spacing) { + m_spacing = spacing; + emit spacingChanged(); + scheduleCalculation(); + } +} + +void TextCalculator::setItemPadding(int padding) +{ + if (m_itemPadding != padding) { + m_itemPadding = padding; + emit itemPaddingChanged(); + scheduleCalculation(); + } +} + +void TextCalculator::setDataModel(QAbstractItemModel *model) +{ + if (m_dataModel != model) { + qCDebug(textCalculatorLog) << "DataModel changed, reconnecting signals"; + disconnectDataModelSignals(); + m_dataModel = model; + connectDataModelSignals(); + emit dataModelChanged(); + scheduleCalculation(); + } +} + +void TextCalculator::setRemainingSpace(qreal space) +{ + if (m_remainingSpace != space) { + m_remainingSpace = space; + emit remainingSpaceChanged(); + scheduleCalculation(); + } +} + +void TextCalculator::setEnabled(bool enabled) +{ + if (m_enabled == enabled) + return; + + qCDebug(textCalculatorLog) << "TextCalculator enabled state changed to:" << enabled; + m_enabled = enabled; + if (m_enabled) { + scheduleCalculation(); + } else { + m_optimalSingleTextWidth = 0.0; + m_totalWidth = 0; + emit optimalSingleTextWidthChanged(); + emit totalWidthChanged(); + } + emit enabledChanged(); +} + +void TextCalculator::componentComplete() +{ + complete = true; + scheduleCalculation(); +} + +void TextCalculator::connectDataModelSignals() +{ + if (m_dataModel) { + connect(m_dataModel, &QAbstractItemModel::rowsInserted, this, &TextCalculator::onDataModelChanged); + connect(m_dataModel, &QAbstractItemModel::rowsRemoved, this, &TextCalculator::onDataModelChanged); + connect(m_dataModel, + &QAbstractItemModel::dataChanged, + this, + [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles) { + const auto titleRole = m_dataModel->roleNames().key("title"); + if (roles.contains(titleRole) || roles.isEmpty()) { + scheduleCalculation(); + } + }); + connect(m_dataModel, &QAbstractItemModel::modelReset, this, &TextCalculator::onDataModelChanged); + } +} + +void TextCalculator::disconnectDataModelSignals() +{ + if (m_dataModel) { + disconnect(m_dataModel, nullptr, this, nullptr); + } +} + +void TextCalculator::onDataModelChanged() +{ + scheduleCalculation(); +} + +void TextCalculator::scheduleCalculation() +{ + if (!complete) + return; + + if (!m_enabled || !m_dataModel) { + return; + } + calculateOptimalTextWidth(); +} + +qreal TextCalculator::calculateBaselineWidth(int charCount) const +{ + if (m_baselineWidthCache.contains(charCount)) { + return m_baselineWidthCache[charCount]; + } + + // Generate baseline text: repeat "字" × character count + QString baselineText = QString("字").repeated(charCount); + + QFontMetricsF fontMetrics(m_font); + qreal width = fontMetrics.horizontalAdvance(baselineText); + + const_cast(this)->m_baselineWidthCache[charCount] = width; + return width; +} + +qreal TextCalculator::calculateElidedTextWidth(const QString &text, qreal maxWidth) const +{ + if (text.isEmpty()) { + return 0.0; + } + + QFontMetricsF fontMetrics(m_font); + QString elidedText = fontMetrics.elidedText(text, Qt::ElideRight, maxWidth); + if (!isValidElidedText(elidedText)) + return 0.0; + + return fontMetrics.horizontalAdvance(elidedText); +} + +QStringList TextCalculator::getApplicationTitles() const +{ + QStringList titles; + + if (!m_dataModel) { + return titles; + } + + const int rowCount = m_dataModel->rowCount(); + + for (int i = 0; i < rowCount; ++i) { + QModelIndex index = m_dataModel->index(i, 0); + + QString title; + + QHash roleNames = m_dataModel->roleNames(); + + // Find title-related role + for (auto it = roleNames.begin(); it != roleNames.end(); ++it) { + if (it.value() == "title") { + QVariant titleData = m_dataModel->data(index, it.key()); + if (titleData.isValid() && !titleData.toString().isEmpty()) { + title = titleData.toString(); + break; + } + } + } + + // If title is empty, keep it as empty string (indicating icon-only display) + titles.append(title); + } + + return titles; +} + +void TextCalculator::calculateOptimalTextWidth() +{ + QStringList titles = getApplicationTitles(); + const int appCount = titles.size(); + + if (appCount <= 0 || m_remainingSpace <= 0) { + if (m_optimalSingleTextWidth != 0.0) { + qCDebug(textCalculatorLog) << "Setting optimal width to 0 (no apps or no space)"; + m_optimalSingleTextWidth = 0.0; + m_totalWidth = 0; + emit optimalSingleTextWidthChanged(); + emit totalWidthChanged(); + } + return; + } + + qreal newOptimalWidth = 0.0; + qreal newTotalWidth = 0.0; + int charCount = 7; // Maximum character count limit + + // Iterate from 7 characters to 1 character, finding the optimal solution + for (; charCount >= 1; --charCount) { + // 1. Calculate baseline width (based on character count) + qreal baselineWidth = calculateBaselineWidth(charCount); + + // 2. Calculate total width for each app item: icon + padding + text + qreal totalRequiredWidth = 0.0; + + for (int i = 0; i < titles.size(); ++i) { + const QString &title = titles[i]; + // Base width for each app item = icon width + qreal itemWidth = m_iconSize; + + qreal textWidth = calculateElidedTextWidth(title, baselineWidth); + // Only add spacing between icon and text when text is present + if (textWidth > 0.0) { + itemWidth = m_iconSize + m_itemPadding + textWidth; + } else { + itemWidth = m_cellSize; + } + + totalRequiredWidth += itemWidth; + } + + // 3. Add spacing between apps + qreal spacingWidth = m_spacing * qMax(0, appCount - 1); + qreal totalSpaceRequired = totalRequiredWidth + spacingWidth; + + // 4. Check if space requirements are met + if (totalSpaceRequired <= m_remainingSpace) { + newOptimalWidth = baselineWidth; + newTotalWidth = totalSpaceRequired; + break; + } + } + + // Update results + if (!qFuzzyCompare(m_optimalSingleTextWidth, newOptimalWidth)) { + 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(); + } +} + +TextCalculatorAttached *TextCalculator::qmlAttachedProperties(QObject *object) +{ + return new TextCalculatorAttached(object); +} + +TextCalculatorAttached::TextCalculatorAttached(QObject *parent) + : QObject(parent) + , m_calculator(nullptr) + , m_initialized(false) +{ + connect(this, &TextCalculatorAttached::textChanged, this, &TextCalculatorAttached::updateElidedText); +} + +TextCalculatorAttached::~TextCalculatorAttached() +{ +} + +static TextCalculator *findCalculatorForObject(QObject *object) +{ + if (!object) { + qCDebug(textCalculatorLog) << "findCalculatorForObject: null object"; + return nullptr; + } + + QQuickItem *obj = qobject_cast(object); + + // Traverse up parent objects to find TextCalculator instance + while (obj) { + // Check if current object is a TextCalculator + if (auto *calculator = qobject_cast(obj)) { + return calculator; + } + + // Check if current object's children contain a TextCalculator + if (auto calculator = obj->findChild(Qt::FindDirectChildrenOnly)) { + return calculator; + } + + obj = obj->parentItem(); + } + + qCWarning(textCalculatorLog) << "No TextCalculator found for object"; + return nullptr; +} + +void TextCalculatorAttached::setText(const QString &text) +{ + if (m_text == text) { + return; + } + m_text = text; + ensureInitialize(); + emit textChanged(); +} + +QString TextCalculatorAttached::elidedText() const +{ + const_cast(this)->ensureInitialize(); + return m_elidedText; +} + +void TextCalculatorAttached::setCalculator(TextCalculator *calculator) +{ + if (calculator) { + m_calculator = calculator; + connect(calculator, &TextCalculator::optimalSingleTextWidthChanged, this, &TextCalculatorAttached::updateElidedText); + updateElidedText(); + } +} + +TextCalculator *TextCalculatorAttached::calculator() +{ + ensureInitialize(); + return m_calculator; +} + +void TextCalculatorAttached::ensureInitialize() +{ + if (m_initialized) { + return; + } + + m_initialized = true; + if (!m_calculator) { + auto calculator = findCalculatorForObject(parent()); + setCalculator(calculator); + } +} + +void TextCalculatorAttached::updateElidedText() +{ + if (!m_calculator) { + qCDebug(textCalculatorLog) << "No calculator available for elided text update"; + m_elidedText.clear(); + emit elidedTextChanged(); + return; + } + + QFontMetricsF fontMetrics(m_calculator->font()); + qreal maxWidth = m_calculator->optimalSingleTextWidth(); + + QString newElidedText = fontMetrics.elidedText(m_text, Qt::ElideRight, maxWidth); + if (!isValidElidedText(newElidedText)) { + newElidedText = {}; + } + if (m_elidedText != newElidedText) { + m_elidedText = newElidedText; + emit elidedTextChanged(); + } +} + +} // namespace dock diff --git a/panels/dock/taskmanager/textcalculator.h b/panels/dock/taskmanager/textcalculator.h new file mode 100644 index 000000000..78e3813dc --- /dev/null +++ b/panels/dock/taskmanager/textcalculator.h @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace dock +{ +class TextCalculator; +class TextCalculatorAttached : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + Q_PROPERTY(QString elidedText READ elidedText NOTIFY elidedTextChanged) + Q_PROPERTY(TextCalculator *calculator READ calculator NOTIFY calculatorChanged) + +public: + explicit TextCalculatorAttached(QObject *parent = nullptr); + ~TextCalculatorAttached(); + + void setCalculator(TextCalculator *calculator); + TextCalculator *calculator(); + + QString text() const + { + return m_text; + } + void setText(const QString &text); + + QString elidedText() const; + + void ensureInitialize(); + +Q_SIGNALS: + void textChanged(); + void elidedTextChanged(); + void calculatorChanged(); + +private Q_SLOTS: + void updateElidedText(); + +private: + QString m_text; + QString m_elidedText; + TextCalculator *m_calculator; + bool m_initialized = false; +}; + +class TextCalculator : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + QML_ELEMENT + QML_ATTACHED(TextCalculatorAttached) + Q_PROPERTY(qreal optimalSingleTextWidth READ optimalSingleTextWidth NOTIFY optimalSingleTextWidthChanged) + Q_PROPERTY(qreal totalWidth READ totalWidth NOTIFY totalWidthChanged) + Q_PROPERTY(QAbstractItemModel *dataModel READ dataModel WRITE setDataModel NOTIFY dataModelChanged) + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(qreal remainingSpace READ remainingSpace WRITE setRemainingSpace NOTIFY remainingSpaceChanged) + Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged) + Q_PROPERTY(qreal iconSize READ iconSize WRITE setIconSize NOTIFY iconSizeChanged) + Q_PROPERTY(qreal cellSize READ cellSize WRITE setCellSize NOTIFY cellSizeChanged) + Q_PROPERTY(int spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + Q_PROPERTY(int itemPadding READ itemPadding WRITE setItemPadding NOTIFY itemPaddingChanged) + +public: + explicit TextCalculator(QObject *parent = nullptr); + ~TextCalculator(); + + qreal optimalSingleTextWidth() const + { + return m_optimalSingleTextWidth; + } + + qreal totalWidth() const + { + return m_totalWidth; + } + + QFont font() const + { + return m_font; + } + void setFont(const QFont &font); + + qreal iconSize() const + { + return m_iconSize; + } + void setIconSize(qreal size); + + qreal cellSize() const + { + return m_cellSize; + } + void setCellSize(qreal size); + + int spacing() const + { + return m_spacing; + } + void setSpacing(int spacing); + + int itemPadding() const + { + return m_itemPadding; + } + void setItemPadding(int padding); + + QAbstractItemModel *dataModel() const + { + return m_dataModel; + } + void setDataModel(QAbstractItemModel *model); + + qreal remainingSpace() const + { + return m_remainingSpace; + } + void setRemainingSpace(qreal space); + + bool isEnabled() const + { + return m_enabled; + } + void setEnabled(bool enabled); + + static TextCalculatorAttached *qmlAttachedProperties(QObject *object); + + virtual void classBegin() override + { + } + virtual void componentComplete() override; + +Q_SIGNALS: + void optimalSingleTextWidthChanged(); + void totalWidthChanged(); + void fontChanged(); + void iconSizeChanged(); + void cellSizeChanged(); + void spacingChanged(); + void itemPaddingChanged(); + void dataModelChanged(); + void remainingSpaceChanged(); + void enabledChanged(); + +private slots: + void onDataModelChanged(); + void calculateOptimalTextWidth(); + +private: + void connectDataModelSignals(); + void disconnectDataModelSignals(); + void scheduleCalculation(); + + qreal calculateBaselineWidth(int charCount) const; + qreal calculateElidedTextWidth(const QString &text, qreal maxWidth) const; + QStringList getApplicationTitles() const; + + bool complete = false; + qreal m_optimalSingleTextWidth; + qreal m_totalWidth; + QFont m_font; + qreal m_iconSize; + qreal m_cellSize; + int m_spacing; + int m_itemPadding; + QAbstractItemModel *m_dataModel; + qreal m_remainingSpace; + bool m_enabled; + + QHash m_baselineWidthCache; // Cache for baseline widths of different character counts +}; + +} +QML_DECLARE_TYPEINFO(dock::TextCalculator, QML_HAS_ATTACHED_PROPERTIES)