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)