diff --git a/panels/dock/taskmanager/dockglobalelementmodel.cpp b/panels/dock/taskmanager/dockglobalelementmodel.cpp index 9ef221be8..03cfbc5a8 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.cpp +++ b/panels/dock/taskmanager/dockglobalelementmodel.cpp @@ -123,7 +123,7 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do *it = std::make_tuple(id, m_appsModel, row); Q_EMIT dataChanged(pIndex, pIndex, - {TaskManager::ActiveRole, TaskManager::AttentionRole, TaskManager::WindowsRole, TaskManager::MenusRole}); + {TaskManager::ActiveRole, TaskManager::AttentionRole, TaskManager::WindowsRole, TaskManager::MenusRole, TaskManager::WinTitleRole}); } } else { beginRemoveRows(QModelIndex(), pos, pos); @@ -303,7 +303,11 @@ QString DockGlobalElementModel::getMenus(const QModelIndex &index) const if (TaskManagerSettings::instance()->isAllowedForceQuit()) { menusArray.append(QJsonObject{{"id", DOCK_ACTION_FORCEQUIT}, {"name", tr("Force Quit")}}); } - menusArray.append(QJsonObject{{"id", DOCK_ACTION_CLOSEALL}, {"name", tr("Close All")}}); + if (TaskManagerSettings::instance()->isWindowSplit()) { + menusArray.append(QJsonObject{{"id", DOCK_ACTION_CLOSEWINDOW}, {"name", tr("Close")}}); + } else { + menusArray.append(QJsonObject{{"id", DOCK_ACTION_CLOSEALL}, {"name", tr("Close All")}}); + } } return QJsonDocument(menusArray).toJson(); @@ -366,6 +370,49 @@ void DockGlobalElementModel::requestActivate(const QModelIndex &index) const } } +void DockGlobalElementModel::requestNewInstance(const QModelIndex &index, const QString &action) const +{ + auto data = m_data.value(index.row()); + auto id = std::get<0>(data); + auto sourceModel = std::get<1>(data); + auto sourceRow = std::get<2>(data); + + // Handle special actions first (for both active and docked apps) + if (action == DOCK_ACTION_DOCK) { + TaskManagerSettings::instance()->toggleDockedElement(QStringLiteral("desktop/%1").arg(id)); + return; + } else if (action == DOCK_ACTION_FORCEQUIT) { + requestClose(index, true); + return; + } else if (action == DOCK_ACTION_CLOSEWINDOW || action == DOCK_ACTION_CLOSEALL) { + requestClose(index, false); + return; + } + + //应用自身处理的action + if (!action.isEmpty()) { + QProcess process; + process.setProcessChannelMode(QProcess::MergedChannels); + process.start("dde-am", {"--by-user", id, action}); + process.waitForFinished(); + return; + } + + // Handle launch/activate (empty action) + if (sourceModel == m_activeAppModel) { + auto sourceIndex = sourceModel->index(sourceRow, 0); + m_activeAppModel->requestNewInstance(sourceIndex, action); + } else { + QString dbusPath = QStringLiteral("/org/desktopspec/ApplicationManager1/") + escapeToObjectPath(id); + using Application = org::desktopspec::ApplicationManager1::Application; + Application appInterface(QStringLiteral("org.desktopspec.ApplicationManager1"), dbusPath, QDBusConnection::sessionBus()); + + if (appInterface.isValid()) { + appInterface.Launch(QString(), QStringList(), QVariantMap()); + } + } +} + void DockGlobalElementModel::requestOpenUrls(const QModelIndex &index, const QList &urls) const { auto data = m_data.value(index.row()); diff --git a/panels/dock/taskmanager/dockglobalelementmodel.h b/panels/dock/taskmanager/dockglobalelementmodel.h index 8b49225e2..15db77425 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.h +++ b/panels/dock/taskmanager/dockglobalelementmodel.h @@ -28,6 +28,7 @@ class DockGlobalElementModel : public QAbstractListModel, public AbstractTaskMan inline int mapToSourceModelRole(QAbstractItemModel *model, int role) const; void requestActivate(const QModelIndex &index) const override; + void requestNewInstance(const QModelIndex &index, const QString &action) const override; void requestOpenUrls(const QModelIndex &index, const QList &urls) const override; void requestClose(const QModelIndex &index, bool force = false) const override; diff --git a/panels/dock/taskmanager/globals.h b/panels/dock/taskmanager/globals.h index d47f6d4f7..32c57697d 100644 --- a/panels/dock/taskmanager/globals.h +++ b/panels/dock/taskmanager/globals.h @@ -13,6 +13,7 @@ namespace dock { static inline const QString DOCK_ACTION_ALLWINDOW = "dock-action-allWindow"; static inline const QString DOCK_ACTION_FORCEQUIT = "dock-action-forceQuit"; static inline const QString DOCK_ACTION_CLOSEALL = "dock-action-closeAll"; +static inline const QString DOCK_ACTION_CLOSEWINDOW = "dock-action-closeWindow"; static inline const QString DOCK_ACTIN_LAUNCH = "dock-action-launch"; static inline const QString DOCK_ACTION_DOCK = "dock-action-dock"; diff --git a/panels/dock/taskmanager/package/AppItemWithTitle.qml b/panels/dock/taskmanager/package/AppItemWithTitle.qml new file mode 100644 index 000000000..b32e32875 --- /dev/null +++ b/panels/dock/taskmanager/package/AppItemWithTitle.qml @@ -0,0 +1,538 @@ +// 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: 2 + + 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 ce0300c3e..215c4db2f 100644 --- a/panels/dock/taskmanager/package/TaskManager.qml +++ b/panels/dock/taskmanager/package/TaskManager.qml @@ -15,10 +15,14 @@ ContainmentItem { property int dockOrder: 16 property int remainingSpacesForTaskManager: Panel.itemAlignment === Dock.LeftAlignment ? Panel.rootObject.dockLeftSpaceForCenter : Panel.rootObject.dockRemainingSpaceForCenter property int forceRelayoutWorkaround: 0 - + + property int remainingSpacesForSplitWindow: Panel.rootObject.dockLeftSpaceForCenter - (Panel.rootObject.dockCenterPartCount - 1) * Panel.rootObject.dockItemMaxSize * 9 / 14 // 用于居中计算的实际应用区域尺寸 property int appContainerWidth: useColumnLayout ? Panel.rootObject.dockSize : (appContainer.implicitWidth + forceRelayoutWorkaround) property int appContainerHeight: useColumnLayout ? (appContainer.implicitHeight + forceRelayoutWorkaround) : Panel.rootObject.dockSize + + // 动态字符限制数组,存储每个应用的最大显示字符数 + property var dynamicCharLimits: [] Timer { // FIXME: dockItemMaxSize(visualModel.cellWidth,actually its implicitWidth/Height) change will cause all delegate item's position change, but @@ -47,6 +51,187 @@ ContainmentItem { } return -1 } + + // Helper function to find the current index by window ID (for windowSplit mode) + function findAppIndexByWindow(appId, winId) { + if (!winId) return findAppIndex(appId) + for (let i = 0; i < visualModel.items.count; i++) { + const item = visualModel.items.get(i); + if (item.model.itemId === appId && item.model.windows.length > 0 && item.model.windows[0] === winId) { + return item.itemsIndex + } + } + 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 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) + } OverflowContainer { id: appContainer @@ -74,6 +259,16 @@ ContainmentItem { properties: "x,y" easing.type: Easing.OutQuad } + NumberAnimation { + properties: "scale" + to: 1 + duration: 200 + } + NumberAnimation { + properties: "opacity" + to: 1 + duration: 200 + } } move: displaced model: DelegateModel { @@ -83,6 +278,7 @@ ContainmentItem { property real cellWidth: Panel.rootObject.dockItemMaxSize * 0.8 onCountChanged: function() { relayoutWorkaroundTimer.start() + DS.singleShot(300, updateDynamicCharLimits) } delegate: Item { id: delegateRoot @@ -91,12 +287,30 @@ ContainmentItem { required property bool attention required property string itemId required property string name + required property string title // winTitle required property string iconName required property string icon // winIconName required property string menus required property list windows z: attention ? -1 : 0 - property bool visibility: itemId !== taskmanager.Applet.desktopIdToAppId(launcherDndDropArea.launcherDndDesktopId) + property bool visibility: { + let draggedAppId = taskmanager.Applet.desktopIdToAppId(launcherDndDropArea.launcherDndDesktopId) + 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 + } states: [ State { @@ -114,34 +328,79 @@ ContainmentItem { Behavior on opacity { NumberAnimation { duration: 200 } } Behavior on scale { NumberAnimation { duration: 200 } } - // TODO: 临时溢出逻辑,待后面修改 - implicitWidth: useColumnLayout ? taskmanager.implicitWidth : visualModel.cellWidth + 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) implicitHeight: useColumnLayout ? visualModel.cellWidth : taskmanager.implicitHeight property int visualIndex: DelegateModel.itemsIndex property var modelIndex: visualModel.modelIndex(index) - AppItem { - id: app - 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) + 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() + } + } } - onDragFinished: function() { - launcherDndDropArea.resetDndState() + + 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() + } + } } - anchors.fill: parent // This is mandatory for draggable item center in drop area } } } @@ -152,15 +411,18 @@ ContainmentItem { keys: ["text/x-dde-dock-dnd-appid"] property string launcherDndDesktopId: "" property string launcherDndDragSource: "" + property string launcherDndWinId: "" function resetDndState() { launcherDndDesktopId = "" launcherDndDragSource = "" + launcherDndWinId = "" } onEntered: function(drag) { let desktopId = drag.getDataAsString("text/x-dde-dock-dnd-appid") launcherDndDragSource = drag.getDataAsString("text/x-dde-dock-dnd-source") + launcherDndWinId = drag.getDataAsString("text/x-dde-dock-dnd-winid") launcherDndDesktopId = desktopId if (launcherDndDragSource !== "taskbar" && taskmanager.Applet.requestDockByDesktopId(desktopId) === false) { resetDndState() @@ -171,7 +433,7 @@ ContainmentItem { if (launcherDndDesktopId === "") return let targetIndex = appContainer.indexAt(drag.x, drag.y) let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) - let currentIndex = taskmanager.findAppIndex(appId) + let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(appId, launcherDndWinId) : taskmanager.findAppIndex(appId) if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { visualModel.items.move(currentIndex, targetIndex) } @@ -182,7 +444,7 @@ ContainmentItem { if (launcherDndDesktopId === "") return let targetIndex = appContainer.indexAt(drop.x, drop.y) let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) - let currentIndex = taskmanager.findAppIndex(appId) + let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(appId, launcherDndWinId) : taskmanager.findAppIndex(appId) if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { visualModel.items.move(currentIndex, targetIndex) } @@ -203,9 +465,45 @@ ContainmentItem { } } + //windowSplit下:计算标签长度过程中会导致图标卡顿,挤在一起,计算完成后刷新布局 + Timer { + id: windowSplitRelayoutTimer + interval: 500 + repeat: false + onTriggered: { + updateDynamicCharLimits() + taskmanager.forceRelayoutWorkaround = (visualModel.count + 1) % 2 + 1 + } + } + + 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() } }