diff --git a/applets/dde-apps/appgroup.cpp b/applets/dde-apps/appgroup.cpp index c37a6125f..69eb66683 100644 --- a/applets/dde-apps/appgroup.cpp +++ b/applets/dde-apps/appgroup.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -15,11 +15,11 @@ Q_LOGGING_CATEGORY(appGroupLog, "org.deepin.dde.shell.dde-apps.appgroup") namespace apps { AppGroup::AppGroup(const QString &groupId, const QString &name, const QList &appIDs) : AppItem(groupId, AppItemModel::FolderItemType) - , m_itemsPage(new ItemsPage(name, groupId == QStringLiteral("internal/folder/0") ? (4 * 8) : (3 * 4))) + , m_itemsPage(new ItemsPage(name, parseGroupId(groupId) == 0 ? (4 * 8) : (3 * 4))) { setItemsPerPage(m_itemsPage->maxItemCountPerPage()); setAppName(m_itemsPage->name()); - // folder id is a part of its groupId: "internal/folder/{folderId}" + // folder id is a part of its groupId: "internal/folders/{folderId}" setFolderId(parseGroupId(groupId)); for (const QStringList &items : appIDs) { @@ -49,19 +49,33 @@ ItemsPage *AppGroup::itemsPage() bool AppGroup::idIsFolder(const QString & id) { - return id.startsWith(QStringLiteral("internal/folder/")); + return parseGroupId(id) >= 0; } QString AppGroup::groupIdFromNumber(int groupId) { - return QStringLiteral("internal/folder/%1").arg(groupId); + return QStringLiteral("internal/folders/%1").arg(groupId); } int AppGroup::parseGroupId(const QString & id) { - using namespace std::string_view_literals; - constexpr size_t len = "internal/folder/"sv.size(); - return QStringView{id}.mid(len + 1).toInt(); + constexpr QLatin1StringView prefix("internal/folders/"); + if (!id.startsWith(prefix)) + return -1; + + const QStringView number = QStringView(id).sliced(prefix.size()); + + if (number.isEmpty()) + return -1; + + for (const QChar ch : number) { + if (!ch.isDigit()) + return -1; + } + + bool ok = false; + const int groupId = number.toInt(&ok); + return ok && groupId >= 0 ? groupId : -1; } void AppGroup::setItemsPerPage(int number) diff --git a/applets/dde-apps/appgroup.h b/applets/dde-apps/appgroup.h index 97eb4805c..e0d68684a 100644 --- a/applets/dde-apps/appgroup.h +++ b/applets/dde-apps/appgroup.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later diff --git a/applets/dde-apps/appgroupmanager.cpp b/applets/dde-apps/appgroupmanager.cpp index 157fc1771..390c48eb6 100644 --- a/applets/dde-apps/appgroupmanager.cpp +++ b/applets/dde-apps/appgroupmanager.cpp @@ -87,8 +87,8 @@ QModelIndex AppGroupManager::groupIndexById(int groupId) { for (int i = 0; i < rowCount(); i++) { auto groupIndex = index(i, 0); - auto data = groupIndex.data(GroupIdRole); - if (data.toInt() == groupId) { + auto appGroup = static_cast(itemFromIndex(groupIndex)); + if (appGroup && appGroup->folderId() == groupId) { return groupIndex; } } @@ -352,8 +352,8 @@ void AppGroupManager::loadAppGroupInfo() } // always ensure top-level group exists - if (rowCount() == 0) { - auto p = appendGroup(assignGroupId(), "Top Level", {}); + if (!group(TOPLEVEL_FOLDERID)) { + auto p = appendGroup(AppGroup::groupIdFromNumber(TOPLEVEL_FOLDERID), "Top Level", {}); Q_ASSERT(p->folderId() == TOPLEVEL_FOLDERID); } } @@ -375,18 +375,19 @@ void AppGroupManager::saveAppGroupInfo() QString AppGroupManager::assignGroupId() const { - QStringList knownGroupIds; + QSet knownGroupIds; for (int i = 0; i < rowCount(); i++) { - auto group = index(i, 0); - knownGroupIds.append(group.data(AppItemModel::DesktopIdRole).toString()); + auto appGroup = static_cast(itemFromIndex(index(i, 0))); + if (appGroup && appGroup->folderId() >= 0) + knownGroupIds.insert(appGroup->folderId()); } - int idNumber = 0; - while (knownGroupIds.contains(QString("internal/group/%1").arg(idNumber))) { + int idNumber = 1; + while (knownGroupIds.contains(idNumber)) { idNumber++; } - return QString("internal/group/%1").arg(idNumber); + return AppGroup::groupIdFromNumber(idNumber); } AppGroup * AppGroupManager::appendGroup(int groupId, QString groupName, const QList &appItemIDs) diff --git a/applets/dde-apps/appsapplet.cpp b/applets/dde-apps/appsapplet.cpp index aeaf6bfcf..fa6ea70ce 100644 --- a/applets/dde-apps/appsapplet.cpp +++ b/applets/dde-apps/appsapplet.cpp @@ -1,10 +1,14 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #include "appsapplet.h" +#include "amappitem.h" #include "amappitemmodel.h" +#include "appgroup.h" #include "appgroupmanager.h" +#include "appitemmodel.h" +#include "itemspage.h" #include "pluginfactory.h" #include @@ -12,6 +16,17 @@ namespace apps { +static AMAppItem *findApplication(AMAppItemModel *model, const QString &desktopId) +{ + if (auto item = model->appItem(desktopId)) + return item; + + const auto alternateId = desktopId.endsWith(QStringLiteral(".desktop")) + ? desktopId.chopped(8) + : desktopId + QStringLiteral(".desktop"); + return model->appItem(alternateId); +} + AppsApplet::AppsApplet(QObject *parent) : DApplet(parent) , m_appModel(new AMAppItemModel(this)) @@ -40,6 +55,39 @@ QAbstractItemModel *AppsApplet::appModel() const return m_appModel; } +QVariantList AppsApplet::groupItemDetails(const QString &groupId) const +{ + const int id = AppGroup::parseGroupId(groupId); + auto group = id > 0 ? m_groupModel->group(id) : nullptr; + if (!group) + return {}; + + const auto items = group->itemsPage()->allArrangedItems(); + QVariantList result; + result.reserve(items.size()); + for (const auto &desktopId : items) { + const auto item = findApplication(m_appModel, desktopId); + if (!item) + continue; + result.append(QVariantMap{ + {QStringLiteral("desktopId"), desktopId}, + {QStringLiteral("name"), item->data(AppItemModel::NameRole)}, + {QStringLiteral("iconName"), item->data(AppItemModel::IconNameRole)}, + }); + } + return result; +} + +bool AppsApplet::launchApplication(const QString &desktopId) +{ + if (auto item = findApplication(m_appModel, desktopId)) { + item->launch({}, {}, {{QStringLiteral("_launch_type"), QStringLiteral("dde-shell")}}); + return true; + } + + return false; +} + bool AppsApplet::appModelReady() const { return m_appModel->ready(); diff --git a/applets/dde-apps/appsapplet.h b/applets/dde-apps/appsapplet.h index 6c81d73ee..823ec113c 100644 --- a/applets/dde-apps/appsapplet.h +++ b/applets/dde-apps/appsapplet.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -13,6 +13,7 @@ DS_USE_NAMESPACE namespace apps { class AMAppItemModel; +class AppGroupManager; class AppItem; class AppsApplet : public DApplet { @@ -30,6 +31,9 @@ class AppsApplet : public DApplet QAbstractItemModel *appModel() const; QAbstractItemModel *groupModel() const; + Q_INVOKABLE QVariantList groupItemDetails(const QString &groupId) const; + Q_INVOKABLE bool launchApplication(const QString &desktopId); + bool appModelReady() const; signals: @@ -38,6 +42,6 @@ class AppsApplet : public DApplet private: bool m_appModelReady; AMAppItemModel *m_appModel; - QAbstractItemModel *m_groupModel; + AppGroupManager *m_groupModel; }; } diff --git a/frame/qml/PanelPopup.qml b/frame/qml/PanelPopup.qml index 625c13abe..ffe0eee23 100644 --- a/frame/qml/PanelPopup.qml +++ b/frame/qml/PanelPopup.qml @@ -17,6 +17,7 @@ Item { property bool readyBinding: false property bool openPending: false property bool grabInactivePending: false + property bool closeOnInactive: true property int grabInactiveTimeout: 200 // WM_NAME, used for kwin. property string windowTitle: "dde-shell/panelpopup" @@ -117,6 +118,9 @@ Item { if (!popupWindow || !readyBinding || popupWindow.currentItem !== control || !popup.visible) { return } + if (!control.closeOnInactive) { + return + } if (!popupWindow.active) { control.close() } @@ -139,6 +143,9 @@ Item { grabInactiveTimer.stop() return } + if (!control.closeOnInactive) { + return + } if (control.grabInactivePending || popupWindow.x11GrabFocusTransition) { return } @@ -175,7 +182,8 @@ Item { || !readyBinding || popupWindow.currentItem !== control || !popup.visible - || control.grabInactivePending) { + || control.grabInactivePending + || !control.closeOnInactive) { return } if (!popupWindow.active) { diff --git a/panels/dock/CMakeLists.txt b/panels/dock/CMakeLists.txt index 7c084ec29..e1711e171 100644 --- a/panels/dock/CMakeLists.txt +++ b/panels/dock/CMakeLists.txt @@ -97,7 +97,7 @@ ds_install_package(PACKAGE org.deepin.ds.dock TARGET dockpanel) ds_handle_package_translation(PACKAGE org.deepin.ds.dock) # sub plugins -#add_subdirectory(cardleft) +add_subdirectory(cardleft) add_subdirectory(showdesktop) add_subdirectory(taskmanager) add_subdirectory(tray) diff --git a/panels/dock/DockCompositor.qml b/panels/dock/DockCompositor.qml index e206ee7a4..9d58d5b18 100644 --- a/panels/dock/DockCompositor.qml +++ b/panels/dock/DockCompositor.qml @@ -30,6 +30,7 @@ Item { property ListModel trayPluginSurfaces: ListModel {} property ListModel quickPluginSurfaces: ListModel {} property ListModel fixedPluginSurfaces: ListModel {} + property ListModel cardPluginSurfaces: ListModel {} property var compositor: waylandCompositor property var panelScale: 1.0 @@ -62,6 +63,7 @@ Item { let ret = findSurfaceFromModel(trayPluginSurfaces, surfaceId) if (ret === null) ret = findSurfaceFromModel(quickPluginSurfaces, surfaceId) if (ret === null) ret = findSurfaceFromModel(fixedPluginSurfaces, surfaceId) + if (ret === null) ret = findSurfaceFromModel(cardPluginSurfaces, surfaceId) return ret } @@ -92,6 +94,8 @@ Item { quickPluginSurfaces.append({shellSurface: dockPluginSurface}) } else if (dockPluginSurface.pluginType === Dock.Fixed) { fixedPluginSurfaces.append({shellSurface: dockPluginSurface}) + } else if (dockPluginSurface.pluginType === Dock.Card) { + cardPluginSurfaces.append({shellSurface: dockPluginSurface}) } dockCompositor.pluginSurfacesUpdated() } @@ -104,6 +108,8 @@ Item { removeDockPluginSurface(quickPluginSurfaces, dockPluginSurface) } else if (dockPluginSurface.pluginType === Dock.Fixed) { removeDockPluginSurface(fixedPluginSurfaces, dockPluginSurface) + } else if (dockPluginSurface.pluginType === Dock.Card) { + removeDockPluginSurface(cardPluginSurfaces, dockPluginSurface) } dockCompositor.pluginSurfacesUpdated() } diff --git a/panels/dock/MenuHelper.qml b/panels/dock/MenuHelper.qml index 46775f090..160506d15 100644 --- a/panels/dock/MenuHelper.qml +++ b/panels/dock/MenuHelper.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -21,8 +21,8 @@ Item { if (activeMenu) { activeMenu.close() } - menu.open() activeMenu = menu + menu.open() } function closeMenu(menu: Menu) { menu.close() diff --git a/panels/dock/OverflowContainer.qml b/panels/dock/OverflowContainer.qml index aa3066df6..6f7fae6fd 100644 --- a/panels/dock/OverflowContainer.qml +++ b/panels/dock/OverflowContainer.qml @@ -30,6 +30,11 @@ Item { } function calculateImplicitWidth(prev, current) { + // Zero-width delegates are hidden (for example, items moved into the + // overflow popup) and must not leave an extra spacing slot behind. + if (current <= 0) { + return prev + } if (useColumnLayout) { return Math.max(prev, current) } else { @@ -41,6 +46,11 @@ Item { } function calculateImplicitHeight(prev, current) { + // Keep the vertical layout consistent with the horizontal one: hidden + // delegates contribute neither size nor spacing. + if (current <= 0) { + return prev + } if (useColumnLayout) { if (prev == 0) { return current diff --git a/panels/dock/cardleft/CMakeLists.txt b/panels/dock/cardleft/CMakeLists.txt new file mode 100644 index 000000000..a049b858f --- /dev/null +++ b/panels/dock/cardleft/CMakeLists.txt @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +ds_install_package(PACKAGE org.deepin.ds.dock.cardleft) diff --git a/panels/dock/cardleft/package/CardLeftDockArea.qml b/panels/dock/cardleft/package/CardLeftDockArea.qml new file mode 100644 index 000000000..9f9fa150b --- /dev/null +++ b/panels/dock/cardleft/package/CardLeftDockArea.qml @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 +import QtWayland.Compositor + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +AppletDockItem { + id: root + + readonly property bool fashionMode: Panel.fashionMode + readonly property int dockSize: Panel.rootObject.dockSize + readonly property int hoverInset: 3 + readonly property real taskbarRadius: Panel.rootObject.fashionDock.backgroundRadius + readonly property real hoverBackgroundRadius: taskbarRadius - hoverInset + readonly property int adaptiveCardLeftWidth: 150 + Math.max(0, dockSize / 4) + readonly property int rightContentPadding: Math.max(10, Math.round(adaptiveCardLeftWidth * 0.07)) + readonly property int verticalInset: Math.max(5, Math.round(dockSize * 0.16)) + readonly property int pageContentHeight: Math.max(24, dockSize - verticalInset * 2) + + readonly property color primaryTextColor: Panel.colorTheme === Dock.Dark + ? Qt.rgba(1, 1, 1, 0.96) + : Qt.rgba(0, 0, 0, 0.92) + + dockOrder: 5 + shouldVisible: fashionMode && pageCount > 0 + readonly property int pageCount: DockCompositor.cardPluginSurfaces.count + property bool contentHovered: false + readonly property bool effectiveHovered: rootHoverHandler.hovered || contentHovered + + visible: shouldVisible + implicitWidth: adaptiveCardLeftWidth + implicitHeight: dockSize + clip: true + + function updateContentHovered() { + const item = swipeView.currentItem + contentHovered = !!item && item.surfaceHovered + } + + HoverHandler { + id: rootHoverHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + } + + AppletItemBackground { + x: root.hoverInset + y: root.hoverInset + width: parent.width - root.hoverInset + height: parent.height - root.hoverInset * 2 + radius: root.hoverBackgroundRadius + enabled: false + opacity: root.effectiveHovered ? 1 : 0 + D.ColorSelector.hovered: root.effectiveHovered + + Behavior on opacity { + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + } + + Item { + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + visible: root.effectiveHovered && root.pageCount > 1 + width: pageIndicator.implicitHeight + height: pageIndicator.implicitWidth + + PageIndicator { + id: pageIndicator + + anchors.centerIn: parent + rotation: 90 + count: swipeView.count + currentIndex: swipeView.currentIndex + padding: 0 + spacing: 4 + + delegate: Rectangle { + required property int index + + implicitWidth: 2 + implicitHeight: 2 + radius: width / 2 + color: root.primaryTextColor + opacity: index === pageIndicator.currentIndex ? 1 : 0.35 + } + } + } + + SwipeView { + id: swipeView + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 8 + anchors.rightMargin: root.rightContentPadding + height: root.pageContentHeight + orientation: Qt.Vertical + interactive: count > 1 + clip: true + + onCurrentItemChanged: { + root.updateContentHovered() + surfaceGeometryUpdateTimer.restart() + } + + Repeater { + model: DockCompositor.cardPluginSurfaces + + delegate: Item { + id: surfaceHost + + property var plugin: model.shellSurface + readonly property bool surfaceHovered: SwipeView.isCurrentItem && surfaceItem.hovered + + ShellSurfaceItemProxy { + id: surfaceItem + width: parent.width + height: parent.height + shellSurface: surfaceHost.plugin + } + + function updateSurfaceGeometry() { + if (!plugin || !SwipeView.isCurrentItem || !surfaceHost.Window.window) { + return + } + + const window = surfaceHost.Window.window + const windowPosition = surfaceHost.mapToItem(window.contentItem, 0, 0) + const globalPosition = Qt.point(windowPosition.x + window.x, + windowPosition.y + window.y) + + plugin.updatePluginGeometry(Qt.rect(Math.round(windowPosition.x), + Math.round(windowPosition.y), + Math.round(width), + Math.round(height))) + plugin.setGlobalPos(Qt.point(Math.round(globalPosition.x), + Math.round(globalPosition.y))) + surfaceItem.fixPosition() + } + + Component.onCompleted: updateSurfaceGeometry() + onWidthChanged: geometryUpdateTimer.restart() + onHeightChanged: geometryUpdateTimer.restart() + onVisibleChanged: { + geometryUpdateTimer.restart() + root.updateContentHovered() + } + onSurfaceHoveredChanged: root.updateContentHovered() + + Timer { + id: geometryUpdateTimer + interval: 50 + repeat: false + onTriggered: surfaceHost.updateSurfaceGeometry() + } + } + } + } + + Item { + anchors.fill: parent + z: 1 + + WheelHandler { + target: null + enabled: swipeView.count > 1 + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + + onWheel: function(wheel) { + const deltaY = wheel.angleDelta.y !== 0 ? wheel.angleDelta.y : wheel.pixelDelta.y + if (deltaY === 0) { + return + } + + const step = deltaY < 0 ? 1 : -1 + const nextIndex = (swipeView.currentIndex + step + swipeView.count) % swipeView.count + swipeView.setCurrentIndex(nextIndex) + wheel.accepted = true + } + } + } + + Connections { + target: swipeView.contentItem + ignoreUnknownSignals: true + + function onContentYChanged() { + surfaceGeometryUpdateTimer.restart() + } + } + + Timer { + id: surfaceGeometryUpdateTimer + + interval: 50 + repeat: false + onTriggered: { + const item = swipeView.currentItem + if (item) { + item.updateSurfaceGeometry() + } + } + } +} diff --git a/panels/dock/cardleft/package/metadata.json b/panels/dock/cardleft/package/metadata.json new file mode 100644 index 000000000..b18a9e068 --- /dev/null +++ b/panels/dock/cardleft/package/metadata.json @@ -0,0 +1,8 @@ +{ + "Plugin": { + "Version": "1.0", + "Id": "org.deepin.ds.dock.cardleft", + "Url": "CardLeftDockArea.qml", + "Parent": "org.deepin.ds.dock" + } +} diff --git a/panels/dock/constants.h b/panels/dock/constants.h index bf3cb9372..7a91c210d 100644 --- a/panels/dock/constants.h +++ b/panels/dock/constants.h @@ -1,6 +1,6 @@ // Copyright (C) 2011 ~ 2018 Deepin Technology Co., Ltd. // SPDX-FileCopyrightText: 2018 - 2023 UnionTech Software Technology Co., Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later @@ -36,6 +36,7 @@ enum IndicatorStyle { enum ItemAlignment { CenterAlignment = 0, LeftAlignment = 1, + FashionAlignment = 2, }; enum ColorTheme { @@ -79,6 +80,7 @@ enum TrayPluginType { Tray = 1, Fixed, Quick, + Card, }; enum TrayPluginSizePolicy { diff --git a/panels/dock/dockpanel.cpp b/panels/dock/dockpanel.cpp index b9488979f..3cb4d7426 100644 --- a/panels/dock/dockpanel.cpp +++ b/panels/dock/dockpanel.cpp @@ -100,6 +100,7 @@ bool DockPanel::init() }); connect(SETTINGS, &DockSettings::positionChanged, this, [this, dockDaemonAdaptor](){ Q_EMIT positionChanged(position()); + Q_EMIT fashionModeChanged(); Q_EMIT dockDaemonAdaptor->PositionChanged(position()); Q_EMIT dockDaemonAdaptor->FrontendWindowRectChanged(frontendWindowRect()); @@ -117,6 +118,9 @@ bool DockPanel::init() connect(SETTINGS, &DockSettings::dockSizeChanged, this, &DockPanel::dockSizeChanged); connect(SETTINGS, &DockSettings::hideModeChanged, this, &DockPanel::hideModeChanged); connect(SETTINGS, &DockSettings::itemAlignmentChanged, this, &DockPanel::itemAlignmentChanged); + connect(SETTINGS, &DockSettings::itemAlignmentChanged, this, [this](){ + Q_EMIT fashionModeChanged(); + }); connect(SETTINGS, &DockSettings::indicatorStyleChanged, this, &DockPanel::indicatorStyleChanged); connect(SETTINGS, &DockSettings::lockedChanged, this, &DockPanel::lockedChanged); connect(SETTINGS, &DockSettings::contextMenuEnabledChanged, this, &DockPanel::contextMenuEnabledChanged); @@ -299,6 +303,13 @@ void DockPanel::setItemAlignment(const ItemAlignment& alignment) SETTINGS->setItemAlignment(alignment); } +bool DockPanel::fashionMode() +{ + const auto dockPosition = position(); + return itemAlignment() == FashionAlignment + && (dockPosition == Top || dockPosition == Bottom); +} + IndicatorStyle DockPanel::indicatorStyle() { return SETTINGS->indicatorStyle(); diff --git a/panels/dock/dockpanel.h b/panels/dock/dockpanel.h index 1b4be8429..ae27ff1cf 100644 --- a/panels/dock/dockpanel.h +++ b/panels/dock/dockpanel.h @@ -30,6 +30,7 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext Q_PROPERTY(HideMode hideMode READ hideMode WRITE setHideMode NOTIFY hideModeChanged FINAL) Q_PROPERTY(Position position READ position WRITE setPosition NOTIFY positionChanged FINAL) Q_PROPERTY(ItemAlignment itemAlignment READ itemAlignment WRITE setItemAlignment NOTIFY itemAlignmentChanged FINAL) + Q_PROPERTY(bool fashionMode READ fashionMode NOTIFY fashionModeChanged FINAL) Q_PROPERTY(IndicatorStyle indicatorStyle READ indicatorStyle WRITE setIndicatorStyle NOTIFY indicatorStyleChanged FINAL) Q_PROPERTY(bool showInPrimary READ showInPrimary WRITE setShowInPrimary NOTIFY showInPrimaryChanged FINAL) Q_PROPERTY(QString screenName READ screenName NOTIFY screenNameChanged FINAL) @@ -73,6 +74,8 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext ItemAlignment itemAlignment(); void setItemAlignment(const ItemAlignment& alignment); + bool fashionMode(); + IndicatorStyle indicatorStyle(); void setIndicatorStyle(const IndicatorStyle& style); @@ -129,6 +132,7 @@ private Q_SLOTS: void beforePositionChanged(Position beforePosition); void positionChanged(Position position); void itemAlignmentChanged(ItemAlignment alignment); + void fashionModeChanged(); void indicatorStyleChanged(IndicatorStyle style); void showInPrimaryChanged(bool showInPrimary); void dockScreenChanged(QScreen *screen); diff --git a/panels/dock/dockpositioner.cpp b/panels/dock/dockpositioner.cpp index 231a881e6..9275c44e4 100644 --- a/panels/dock/dockpositioner.cpp +++ b/panels/dock/dockpositioner.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 -2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -65,6 +65,34 @@ void DockPositioner::setBounding(const QRect &newBounding) emit boundingChanged(); } +bool DockPositioner::isCrossWindow() const +{ + const auto item = qobject_cast(parent()); + return item && item->window() && m_panel && m_panel->window() + && item->window() != m_panel->window(); +} + +QRect DockPositioner::effectiveBounding() const +{ + if (!isCrossWindow()) + return m_bounding; + + const auto item = qobject_cast(parent()); + const auto globalTopLeft = item->window()->mapToGlobal(m_bounding.topLeft()); + return QRect(globalTopLeft - dockGeometry().topLeft(), m_bounding.size()); +} + +QRect DockPositioner::attachedItemRectInDock() const +{ + const auto item = qobject_cast(parent()); + if (!item) + return {}; + + const auto globalTopLeft = item->mapToGlobal(QPointF(0, 0)); + const auto dockTopLeft = globalTopLeft.toPoint() - dockGeometry().topLeft(); + return QRect(dockTopLeft, QSize(qRound(item->width()), qRound(item->height()))); +} + int DockPositioner::x() const { return m_x; @@ -108,27 +136,28 @@ void DockPositioner::update() void DockPositioner::updatePosition() { + const auto bounding = effectiveBounding(); int xPosition = 0; int yPosition = 0; switch (dockPosition()) { case dock::Top: { - xPosition = m_bounding.x(); - yPosition = m_bounding.y(); + xPosition = bounding.x(); + yPosition = bounding.y(); break; } case dock::Right: { - xPosition = m_bounding.x() - m_bounding.width(); - yPosition = m_bounding.y(); + xPosition = bounding.x() - bounding.width(); + yPosition = bounding.y(); break; } case dock::Bottom: { - xPosition = m_bounding.x(); - yPosition = m_bounding.y() - m_bounding.height(); + xPosition = bounding.x(); + yPosition = bounding.y() - bounding.height(); break; } case dock::Left: { - xPosition = m_bounding.x(); - yPosition = m_bounding.y(); + xPosition = bounding.x(); + yPosition = bounding.y(); break; } default: @@ -197,29 +226,39 @@ void DockPanelPositioner::resetVertialOffset() void DockPanelPositioner::updatePosition() { const auto dockWindowRect = dockGeometry(); + const auto crossWindow = isCrossWindow(); + const auto anchorRect = crossWindow ? attachedItemRectInDock() : QRect(); int xPosition = 0; int yPosition = 0; int horizontalOffset = m_horizontalOffset == -1 ? m_bounding.width() / 2 : m_horizontalOffset; int vertialOffset = m_vertialOffset == -1 ? m_bounding.height() / 2 : m_vertialOffset; switch (dockPosition()) { case dock::Top: { - xPosition = m_bounding.x() - horizontalOffset; - yPosition = dockWindowRect.height() + 10; + xPosition = crossWindow ? anchorRect.center().x() - horizontalOffset + : m_bounding.x() - horizontalOffset; + yPosition = crossWindow ? anchorRect.y() + anchorRect.height() + 10 + : dockWindowRect.height() + 10; break; } case dock::Right: { - xPosition = -m_bounding.width() - 10; - yPosition = m_bounding.y() - vertialOffset; + xPosition = crossWindow ? anchorRect.left() - m_bounding.width() - 10 + : -m_bounding.width() - 10; + yPosition = crossWindow ? anchorRect.center().y() - vertialOffset + : m_bounding.y() - vertialOffset; break; } case dock::Bottom: { - xPosition = m_bounding.x() - horizontalOffset; - yPosition = -m_bounding.height() - 10; + xPosition = crossWindow ? anchorRect.center().x() - horizontalOffset + : m_bounding.x() - horizontalOffset; + yPosition = crossWindow ? anchorRect.top() - m_bounding.height() - 10 + : -m_bounding.height() - 10; break; } case dock::Left: { - xPosition = dockWindowRect.width() + 10; - yPosition = m_bounding.y() - vertialOffset; + xPosition = crossWindow ? anchorRect.x() + anchorRect.width() + 10 + : dockWindowRect.width() + 10; + yPosition = crossWindow ? anchorRect.center().y() - vertialOffset + : m_bounding.y() - vertialOffset; break; } default: diff --git a/panels/dock/dockpositioner.h b/panels/dock/dockpositioner.h index b60ed1521..fc05d840f 100644 --- a/panels/dock/dockpositioner.h +++ b/panels/dock/dockpositioner.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -51,6 +51,9 @@ public slots: void yChanged(); protected: + bool isCrossWindow() const; + QRect effectiveBounding() const; + QRect attachedItemRectInDock() const; QPointer m_panel; QRect m_bounding {}; int m_x = 0; diff --git a/panels/dock/docksettings.cpp b/panels/dock/docksettings.cpp index 75de92484..2beb841bc 100644 --- a/panels/dock/docksettings.cpp +++ b/panels/dock/docksettings.cpp @@ -86,6 +86,8 @@ static QString itemAlignment2String(const ItemAlignment& alignment) return "left"; case ItemAlignment::CenterAlignment: return "center"; + case ItemAlignment::FashionAlignment: + return "fashion"; } return "center"; @@ -112,6 +114,8 @@ static ItemAlignment string2ItenAlignment(const QString& alignmentStr) return ItemAlignment::LeftAlignment; else if (alignmentStr == "center") return ItemAlignment::CenterAlignment; + else if (alignmentStr == "fashion") + return ItemAlignment::FashionAlignment; return ItemAlignment::CenterAlignment; } diff --git a/panels/dock/package/main.qml b/panels/dock/package/main.qml index a9a01f077..12584983c 100644 --- a/panels/dock/package/main.qml +++ b/panels/dock/package/main.qml @@ -13,21 +13,67 @@ import Qt.labs.platform as LP import org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 import org.deepin.dtk 1.0 as D -import org.deepin.dtk.style 1.0 as DStyle Window { id: dock property int positionForAnimation: Panel.position property bool useColumnLayout: positionForAnimation % 2 + + property alias fashionDock: fashionDockState + + QtObject { + id: fashionDockState + + readonly property bool enabled: !dock.useColumnLayout + && Panel.itemAlignment === Dock.FashionAlignment + && (dock.positionForAnimation === Dock.Bottom || dock.positionForAnimation === Dock.Top) + readonly property int floatingMargin: 8 + readonly property int backgroundRadius: { + const verticalPadding = Math.max(6, Math.round(dock.dockSize * 0.16)) + return Math.round((dock.dockSize + verticalPadding * 2) / 4) + } + readonly property real contentWidth: { + if (!enabled) { + return 0 + } + + let width = gridLayout.implicitWidth + if (dockRightPart.visible) { + if (width > 0) { + width += gridLayout.columnSpacing + } + width += dockRightPart.implicitWidth + } + return width + } + } property int dockCenterPartCount: dockCenterPartModel.count + readonly property int centerAlignmentEdgeSpacing: 14 readonly property int dockRawCenterSpace: { if (useColumnLayout) { return Screen.height - dockLeftPart.implicitHeight - dockRightPart.implicitHeight; } else { - return Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth; + let space = Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth - Math.ceil(gridLayout.columnSpacing); + if (fashionDock.enabled && gridLayout) { + // 时尚模式下,dockRightPart 在 gridLayout 右侧,之间需要扣除 dockSpacing + // 同时保留左右悬浮间距,避免窗口铺满屏幕后圆角边框被屏幕边缘裁剪。 + space -= Math.ceil(gridLayout.columnSpacing) + fashionDock.floatingMargin * 2; + } + return Math.max(0, space); } } + readonly property int dockEffectiveCenterSpace: { + if (Panel.itemAlignment !== Dock.CenterAlignment || fashionDock.enabled) { + return dockRawCenterSpace + } + + const screenExtent = useColumnLayout ? Screen.height : Screen.width + const leftExtent = useColumnLayout ? dockLeftPart.implicitHeight : dockLeftPart.implicitWidth + const rightExtent = useColumnLayout ? dockRightPart.implicitHeight : dockRightPart.implicitWidth + return Math.max(0, screenExtent - leftExtent - rightExtent + - centerAlignmentEdgeSpacing * 2) + } property int dockPartSpacing: gridLayout.columnSpacing // TODO @@ -45,7 +91,14 @@ Window { property real dockItemIconSize: dockItemMaxSize * 9 / 14 // NOTE: -1 means not set its size, follow the platform size - width: positionForAnimation === Dock.Top || positionForAnimation === Dock.Bottom ? -1 : dockSize + width: { + if (fashionDock.enabled) { + const maximumWidth = Math.max(1, Screen.width - fashionDock.floatingMargin * 2) + return Math.max(1, Math.min(fashionDock.contentWidth, maximumWidth)) + } + + return positionForAnimation === Dock.Top || positionForAnimation === Dock.Bottom ? -1 : dockSize + } height: positionForAnimation === Dock.Left || positionForAnimation === Dock.Right ? -1 : dockSize color: "transparent" flags: Qt.WindowDoesNotAcceptFocus @@ -68,14 +121,25 @@ Window { MenuHelper.openMenu(dockMenuLoader.item) } - DLayerShellWindow.anchors: position2Anchors(positionForAnimation) + DLayerShellWindow.anchors: fashionDock.enabled + ? (positionForAnimation === Dock.Top ? DLayerShellWindow.AnchorTop : DLayerShellWindow.AnchorBottom) + : position2Anchors(positionForAnimation) + DLayerShellWindow.topMargin: fashionDock.enabled && positionForAnimation === Dock.Top + ? fashionDock.floatingMargin + : 0 + + DLayerShellWindow.bottomMargin: fashionDock.enabled && positionForAnimation === Dock.Bottom + ? fashionDock.floatingMargin + : 0 DLayerShellWindow.layer: DLayerShellWindow.LayerTop - DLayerShellWindow.exclusionZone: Panel.hideMode === Dock.KeepShowing ? Applet.dockSize : 0 + DLayerShellWindow.exclusionZone: Panel.hideMode === Dock.KeepShowing + ? Applet.dockSize + (fashionDock.enabled ? fashionDock.floatingMargin : 0) + : 0 DLayerShellWindow.scope: "dde-shell/dock" DLayerShellWindow.keyboardInteractivity: DLayerShellWindow.KeyboardInteractivityNone D.DWindow.enabled: true - D.DWindow.windowRadius: 0 + D.DWindow.windowRadius: fashionDock.enabled ? fashionDock.backgroundRadius : 0 //TODO:由于windoweffect处理有BUG,导致动画结束后一致保持无阴影,无borderwidth状态。 无法恢复到最初的阴影和边框 //D.DWindow.windowEffect: hideShowAnimation.running ? D.PlatformHandle.EffectNoShadow | D.PlatformHandle.EffectNoBorder : 0 @@ -92,12 +156,21 @@ Window { D.ColorSelector.family: D.Palette.CrystalColor onDockSizeChanged: { - if (dock.dockSize === Dock.MIN_DOCK_SIZE) { + if (fashionDock.enabled) { + Panel.indicatorStyle = Dock.Fashion + } else if (dock.dockSize === Dock.MIN_DOCK_SIZE) { Panel.indicatorStyle = Dock.Efficient } else { Panel.indicatorStyle = Dock.Fashion } } + Behavior on width { + enabled: fashionDock.enabled && !dock.isDragging + NumberAnimation { + duration: 200 + easing.type: Easing.OutQuad + } + } Binding on itemIconSizeBase { when: !isDragging @@ -108,7 +181,7 @@ Window { PropertyAnimation { id: hideShowAnimation; // Currently, Wayland (Treeland) doesn't support StyledBehindWindowBlur inside the window, thus we keep using the window size approach on Wayland - property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" + property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" && !fashionDock.enabled property bool hiding: false target: useTransformBasedAnimation ? dockTransform : dock; property: { @@ -136,7 +209,7 @@ Window { Connections { target: dockTransform - enabled: Qt.platform.pluginName === "xcb" && hideShowAnimation.running + enabled: hideShowAnimation.useTransformBasedAnimation && hideShowAnimation.running function onXChanged() { if (dock.useColumnLayout) { @@ -153,7 +226,7 @@ Window { Connections { target: dock - enabled: Qt.platform.pluginName !== "xcb" && hideShowAnimation.running + enabled: !hideShowAnimation.useTransformBasedAnimation && hideShowAnimation.running function onWidthChanged() { if (dock.useColumnLayout) { @@ -181,7 +254,7 @@ Window { SequentialAnimation { id: dockAnimation - property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" + property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" && !fashionDock.enabled property bool isShowing: false property bool isPositionChanging: false property var target: useTransformBasedAnimation ? dockTransform : dock @@ -326,6 +399,11 @@ Window { prop: "itemAlignment" value: Dock.CenterAlignment } + EnumPropertyMenuItem { + name: qsTr("Fashion Mode") + prop: "itemAlignment" + value: Dock.FashionAlignment + } } MutuallyExclusiveMenu { title: qsTr("Position") @@ -414,16 +492,16 @@ Window { D.StyledBehindWindowBlur { control: parent anchors.fill: parent - cornerRadius: 0 + cornerRadius: fashionDock.enabled ? fashionDock.backgroundRadius : 0 blendColor: { if (valid) { - return DStyle.Style.control.selectColor(undefined, + return D.Style.control.selectColor(undefined, Qt.rgba(235 / 255.0, 235 / 255.0, 235 / 255.0, dock.blendColorAlpha(0.6)), Qt.rgba(10 / 255, 10 / 255, 10 /255, dock.blendColorAlpha(85 / 255))) } - return DStyle.Style.control.selectColor(undefined, - DStyle.Style.behindWindowBlur.lightNoBlurColor, - DStyle.Style.behindWindowBlur.darkNoBlurColor) + return D.Style.control.selectColor(undefined, + D.Style.behindWindowBlur.lightNoBlurColor, + D.Style.behindWindowBlur.darkNoBlurColor) } } @@ -500,7 +578,7 @@ Window { //此处为边距区域的点击实践特殊处理。 MouseArea { id: leftMarginArea - width: useColumnLayout ? parent.width : gridLayout.columnSpacing + width: useColumnLayout ? parent.width : (fashionDock.enabled ? 0 : gridLayout.columnSpacing) height: useColumnLayout ? gridLayout.rowSpacing : parent.height anchors.left: parent.left anchors.top: parent.top @@ -519,7 +597,12 @@ Window { // TODO: remove GridLayout and use delegatechosser manager all items GridLayout { id: gridLayout - anchors.fill: parent + anchors { + fill: parent + rightMargin: fashionDock.enabled && dockRightPart.visible + ? dockRightPart.implicitWidth + Math.ceil(gridLayout.columnSpacing) + : 0 + } columns: 1 rows: 1 flow: useColumnLayout ? GridLayout.LeftToRight : GridLayout.TopToBottom @@ -529,6 +612,7 @@ Window { Item { id: leftMargin + visible: !fashionDock.enabled implicitWidth: 0 implicitHeight: 0 } @@ -558,14 +642,14 @@ Window { implicitHeight: centerLoader.implicitHeight Accessible.role: Accessible.Grouping Accessible.name: qsTr("Dock Center Area") - Layout.maximumWidth: useColumnLayout ? -1 : dockRawCenterSpace - Layout.maximumHeight: useColumnLayout ? dockRawCenterSpace : -1 + Layout.maximumWidth: useColumnLayout ? -1 : dockEffectiveCenterSpace + Layout.maximumHeight: useColumnLayout ? dockEffectiveCenterSpace : -1 onXChanged: dockCenterPartPosChanged() onYChanged: dockCenterPartPosChanged() - Layout.leftMargin: !useColumnLayout && Panel.itemAlignment === Dock.CenterAlignment ? - Math.max(0, (dock.width - dockCenterPart.implicitWidth) / 2 - (dockLeftPart.implicitWidth + 20) + Math.min((dock.width - dockCenterPart.implicitWidth) / 2 - (dockRightPart.implicitWidth + 20), 0)) : 0 + Layout.leftMargin: !useColumnLayout && !fashionDock.enabled && Panel.itemAlignment === Dock.CenterAlignment ? + Math.max(0, (dock.width - dockCenterPart.implicitWidth) / 2 - (dockLeftPart.implicitWidth + centerAlignmentEdgeSpacing) + Math.min((dock.width - dockCenterPart.implicitWidth) / 2 - (dockRightPart.implicitWidth + centerAlignmentEdgeSpacing), 0)) : 0 Layout.topMargin: useColumnLayout && Panel.itemAlignment === Dock.CenterAlignment ? - Math.max(0, (dock.height - dockCenterPart.implicitHeight) / 2 - (dockLeftPart.implicitHeight + 20) + Math.min((dock.height - dockCenterPart.implicitHeight) / 2 - (dockRightPart.implicitHeight + 20), 0)) : 0 + Math.max(0, (dock.height - dockCenterPart.implicitHeight) / 2 - (dockLeftPart.implicitHeight + centerAlignmentEdgeSpacing) + Math.min((dock.height - dockCenterPart.implicitHeight) / 2 - (dockRightPart.implicitHeight + centerAlignmentEdgeSpacing), 0)) : 0 Behavior on Layout.leftMargin { enabled: !dock.isDragging && !Applet.isResizing @@ -597,8 +681,9 @@ Window { } Item { - Layout.fillWidth: true - Layout.fillHeight: true + Layout.fillWidth: !fashionDock.enabled + Layout.fillHeight: !fashionDock.enabled + visible: !fashionDock.enabled } } diff --git a/panels/dock/showdesktop/package/showdesktop.qml b/panels/dock/showdesktop/package/showdesktop.qml index 7a44d4237..d3926c0e3 100644 --- a/panels/dock/showdesktop/package/showdesktop.qml +++ b/panels/dock/showdesktop/package/showdesktop.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023-2026 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -13,11 +13,12 @@ AppletItem { id: showdesktop readonly property int showDesktopWidth: 10 property bool useColumnLayout: Panel.position % 2 - property int dockSize: Panel.rootObject.dockItemMaxSize + readonly property bool fashionMode: Panel.fashionMode property int dockOrder: 30 - property bool shouldVisible: Applet.visible - implicitWidth: useColumnLayout ? Panel.rootObject.dockSize : showDesktopWidth - implicitHeight: useColumnLayout ? showDesktopWidth : Panel.rootObject.dockSize + property bool shouldVisible: Applet.visible && !fashionMode + visible: shouldVisible + implicitWidth: shouldVisible ? (useColumnLayout ? Panel.rootObject.dockSize : showDesktopWidth) : 0 + implicitHeight: shouldVisible ? (useColumnLayout ? showDesktopWidth : Panel.rootObject.dockSize) : 0 PanelToolTip { id: toolTip diff --git a/panels/dock/taskmanager/CMakeLists.txt b/panels/dock/taskmanager/CMakeLists.txt index 3854b7e27..7f530dc4e 100644 --- a/panels/dock/taskmanager/CMakeLists.txt +++ b/panels/dock/taskmanager/CMakeLists.txt @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS DBus WaylandClient) +find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS DBus Quick WaylandClient) find_package(yaml-cpp REQUIRED) find_package(TreelandProtocols REQUIRED) @@ -84,6 +84,8 @@ add_library(dock-taskmanager SHARED ${DBUS_INTERFACES} treelandwindowmonitor.h taskmanagersettings.cpp taskmanagersettings.h + taskoverflowcontroller.cpp + taskoverflowcontroller.h textcalculator.h textcalculator.cpp ) @@ -98,6 +100,7 @@ target_link_libraries(dock-taskmanager PRIVATE dde-shell-frame Qt${QT_VERSION_MAJOR}::WaylandClientPrivate Qt${QT_VERSION_MAJOR}::Concurrent + Qt${QT_VERSION_MAJOR}::Quick yaml-cpp PkgConfig::WaylandClient ) diff --git a/panels/dock/taskmanager/dockglobalelementmodel.cpp b/panels/dock/taskmanager/dockglobalelementmodel.cpp index 65a7125d7..8fc6589c6 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.cpp +++ b/panels/dock/taskmanager/dockglobalelementmodel.cpp @@ -22,16 +22,40 @@ Q_LOGGING_CATEGORY(dockGlobalElementModelLog, "org.deepin.dde.shell.dock.taskman namespace dock { -DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, DockCombineModel *activeAppModel, QObject *parent) +namespace { + +bool isGroupElement(const QString &id, const QAbstractItemModel *model) +{ + return model == nullptr && id.startsWith(QStringLiteral("internal/folders/")); +} + +int modelRole(const QAbstractItemModel *model, const QByteArray &name, int fallback = -1) +{ + return model ? model->roleNames().key(name, fallback) : fallback; +} + +} + +DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, QAbstractItemModel *groupModel, QObject *groupProvider, + DockCombineModel *activeAppModel, QObject *parent) : QAbstractListModel(parent) , AbstractTaskManagerInterface(nullptr) , m_appsModel(appsModel) + , m_groupModel(groupModel) + , m_groupProvider(groupProvider) , m_activeAppModel(activeAppModel) { connect(TaskManagerSettings::instance(), &TaskManagerSettings::dockedElementsChanged, this, &DockGlobalElementModel::loadDockedElements); connect(TaskManagerSettings::instance(), &TaskManagerSettings::windowSplitChanged, this, &DockGlobalElementModel::groupItemsByApp); connect(TaskManagerSettings::instance(), &TaskManagerSettings::dockedApplicationsEnabledChanged, this, &DockGlobalElementModel::loadDockedElements); + if (m_groupModel) { + connect(m_groupModel, &QAbstractItemModel::modelReset, this, &DockGlobalElementModel::refreshGroupElements, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::rowsInserted, this, &DockGlobalElementModel::refreshGroupElements, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::rowsRemoved, this, &DockGlobalElementModel::handleGroupRowsRemoved, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::dataChanged, this, &DockGlobalElementModel::notifyGroupElementsChanged, Qt::QueuedConnection); + } + connect( m_appsModel, &QAbstractItemModel::rowsRemoved, @@ -229,6 +253,7 @@ QHash DockGlobalElementModel::roleNames() const {TaskManager::WinTitleRole, MODEL_TITLE}, {TaskManager::WinIconRole, MODEL_WINICON}, {TaskManager::WinIdRole, MODEL_WINID}, + {TaskManager::GroupItemsRole, MODEL_GROUPITEMS}, }; } @@ -261,32 +286,45 @@ void DockGlobalElementModel::loadDockedElements() QList> newDocked; if (TaskManagerSettings::instance()->dockedApplicationsEnabled()) { for (auto elementInfo : TaskManagerSettings::instance()->dockedElements()) { - auto pair = elementInfo.split('/'); - if (pair.size() != 2) - continue; + QString type; + QString id; + if (elementInfo.startsWith(QStringLiteral("internal/folders/"))) { + type = QStringLiteral("group"); + id = elementInfo; + } else { + const auto separator = elementInfo.indexOf('/'); + if (separator <= 0 || separator == elementInfo.size() - 1) + continue; - auto type = pair[0]; - auto id = pair[1]; + type = elementInfo.left(separator); + id = elementInfo.mid(separator + 1); + if (type != QStringLiteral("desktop")) + continue; + } auto tmp = std::make_tuple(type, id); // check desktop is installed QAbstractItemModel *model = nullptr; int row = 0; - if (type == "desktop") { + if (type == QStringLiteral("desktop")) { model = m_appsModel; auto res = m_appsModel->match(m_appsModel->index(0, 0), TaskManager::DesktopIdRole, id, 1, Qt::MatchExactly).value(0); if (!res.isValid()) continue; row = res.row(); + } else if (!groupIndex(id).isValid()) { + continue; } newDocked.append(tmp); if (m_dockedElements.contains(tmp)) continue; - auto isRunning = std::any_of(m_data.constBegin(), m_data.constEnd(), [&id](const auto &data) { - return std::get<0>(data) == id; + auto isRunning = std::any_of(m_data.constBegin(), m_data.constEnd(), [&type, &id](const auto &data) { + if (std::get<0>(data) != id) + return false; + return isGroupElement(std::get<0>(data), std::get<1>(data)) == (type == QStringLiteral("group")); }); if (!isRunning) { @@ -301,8 +339,12 @@ void DockGlobalElementModel::loadDockedElements() if (newDocked.contains(*it)) continue; auto type = std::get<0>(*it), id = std::get<1>(*it); - auto dataIt = std::find_if(m_data.begin(), m_data.end(), [this, &id](const auto &data) { - return std::get<0>(data) == id && std::get<1>(data) == m_appsModel; + auto dataIt = std::find_if(m_data.begin(), m_data.end(), [this, &type, &id](const auto &item) { + if (std::get<0>(item) != id) + return false; + if (type == QStringLiteral("group")) + return isGroupElement(std::get<0>(item), std::get<1>(item)); + return std::get<1>(item) == m_appsModel; }); if (dataIt != m_data.end()) { auto pos = (dataIt - m_data.begin()); @@ -329,6 +371,15 @@ QString DockGlobalElementModel::getMenus(const QModelIndex &index) const auto model = std::get<1>(data); auto row = std::get<2>(data); + if (isGroupElement(id, model)) { + QJsonArray menusArray; + if (TaskManagerSettings::instance()->dockedApplicationsEnabled()) { + const bool isDocked = m_dockedElements.contains(std::make_tuple(QStringLiteral("group"), id)); + menusArray.append(QJsonObject{{"id", DOCK_ACTION_DOCK}, {"name", isDocked ? tr("Undock") : tr("Dock")}}); + } + return QJsonDocument(menusArray).toJson(); + } + QJsonArray menusArray; QString appNameInMenu = tr("Open"); if (model == m_activeAppModel) { @@ -379,9 +430,36 @@ QVariant DockGlobalElementModel::data(const QModelIndex &index, int role) const auto model = std::get<1>(data); auto row = std::get<2>(data); + if (isGroupElement(id, model)) { + switch (role) { + case TaskManager::ItemIdRole: + case TaskManager::DesktopIdRole: + return id; + case TaskManager::GroupItemsRole: + return groupApplications(id); + case TaskManager::NameRole: + return groupDisplayName(id); + case TaskManager::IconNameRole: + return QStringLiteral("folder"); + case TaskManager::WindowsRole: + return QStringList{}; + case TaskManager::ActiveRole: + case TaskManager::AttentionRole: + return false; + case TaskManager::DockedRole: + return m_dockedElements.contains(std::make_tuple(QStringLiteral("group"), id)); + case TaskManager::MenusRole: + return getMenus(index); + default: + return {}; + } + } + switch (role) { case TaskManager::ItemIdRole: return id; + case TaskManager::GroupItemsRole: + return QVariantList{}; case TaskManager::WindowsRole: { if (model == m_activeAppModel) { return QStringList{model->index(row, 0).data(TaskManager::WinIdRole).toString()}; @@ -398,6 +476,9 @@ QVariant DockGlobalElementModel::data(const QModelIndex &index, int role) const return false; } + case TaskManager::DockedRole: + return model == nullptr || m_dockedElements.contains(std::make_tuple("desktop", id)); + case TaskManager::MenusRole: { return getMenus(index); } @@ -418,6 +499,9 @@ void DockGlobalElementModel::requestActivate(const QModelIndex &index) const auto sourceModel = std::get<1>(data); auto sourceRow = std::get<2>(data); + if (isGroupElement(id, sourceModel)) + return; + if (sourceModel == m_activeAppModel) { auto sourceIndex = sourceModel->index(sourceRow, 0); m_activeAppModel->requestActivate(sourceIndex); @@ -433,8 +517,15 @@ void DockGlobalElementModel::requestNewInstance(const QModelIndex &index, const { auto data = m_data.value(index.row()); auto id = std::get<0>(data); + auto model = std::get<1>(data); qDebug(dockGlobalElementModelLog) << "Requesting new instance for index:" << index << "with action:" << action << "id:" << id; + if (isGroupElement(id, model)) { + if (action == DOCK_ACTION_DOCK) + TaskManagerSettings::instance()->toggleDockedElement(id); + return; + } + // Handle special actions first (for both active and docked apps) if (action == DOCK_ACTION_DOCK) { TaskManagerSettings::instance()->toggleDockedElement(QStringLiteral("desktop/%1").arg(id)); @@ -461,6 +552,9 @@ void DockGlobalElementModel::requestOpenUrls(const QModelIndex &index, const QLi { auto data = m_data.value(index.row()); auto id = std::get<0>(data); + auto model = std::get<1>(data); + if (isGroupElement(id, model)) + return; QStringList urlStrings; for (const QUrl &url : urls) { @@ -524,6 +618,134 @@ void DockGlobalElementModel::moveItem(int from, int to) endMoveRows(); } +QModelIndex DockGlobalElementModel::groupIndex(const QString &launcherFolderId) const +{ + if (!m_groupModel || m_groupModel->rowCount() == 0 || launcherFolderId == QStringLiteral("internal/folders/0")) + return {}; + + const int desktopIdRole = modelRole(m_groupModel, QByteArray(MODEL_DESKTOPID)); + if (desktopIdRole < 0) + return {}; + + return m_groupModel->match(m_groupModel->index(0, 0), desktopIdRole, + launcherFolderId, 1, Qt::MatchExactly).value(0); +} + +QString DockGlobalElementModel::groupDisplayName(const QString &launcherFolderId) const +{ + QString displayName; + if (m_groupProvider) { + QMetaObject::invokeMethod(m_groupProvider, "groupDisplayName", Qt::DirectConnection, + Q_RETURN_ARG(QString, displayName), Q_ARG(QString, launcherFolderId)); + } + + if (!displayName.isEmpty()) + return displayName; + + const auto sourceIndex = groupIndex(launcherFolderId); + const int displayRole = modelRole(m_groupModel, QByteArrayLiteral("display"), Qt::DisplayRole); + const int nameRole = modelRole(m_groupModel, QByteArray(MODEL_NAME), displayRole); + return sourceIndex.data(nameRole).toString(); +} + +QVariantList DockGlobalElementModel::groupApplications(const QString &launcherFolderId) const +{ + QVariantList applications; + if (m_groupProvider) { + QMetaObject::invokeMethod(m_groupProvider, "groupItemDetails", Qt::DirectConnection, + Q_RETURN_ARG(QVariantList, applications), Q_ARG(QString, launcherFolderId)); + } + + const int desktopIdRole = modelRole(m_appsModel, QByteArray(MODEL_DESKTOPID)); + const int nameRole = modelRole(m_appsModel, QByteArray(MODEL_NAME)); + if (desktopIdRole < 0 || nameRole < 0) + return applications; + + for (QVariant &application : applications) { + QVariantMap details = application.toMap(); + const QString desktopId = details.value(QStringLiteral("desktopId")).toString(); + if (desktopId.isEmpty()) + continue; + + QModelIndex appIndex = m_appsModel->match(m_appsModel->index(0, 0), desktopIdRole, + desktopId, 1, Qt::MatchExactly).value(0); + if (!appIndex.isValid()) { + const QString alternateId = desktopId.endsWith(QStringLiteral(".desktop")) + ? desktopId.chopped(8) + : desktopId + QStringLiteral(".desktop"); + appIndex = m_appsModel->match(m_appsModel->index(0, 0), desktopIdRole, + alternateId, 1, Qt::MatchExactly).value(0); + } + + if (!appIndex.isValid()) + continue; + + const QString localizedName = appIndex.data(nameRole).toString(); + if (!localizedName.isEmpty()) { + details.insert(QStringLiteral("name"), localizedName); + application = details; + } + } + + return applications; +} + +void DockGlobalElementModel::refreshGroupElements() +{ + loadDockedElements(); + notifyGroupElementsChanged(); +} + +void DockGlobalElementModel::handleGroupRowsRemoved() +{ + auto *settings = TaskManagerSettings::instance(); + QStringList dockedElements = settings->dockedElements(); + bool dockedElementsChanged = false; + + for (auto it = dockedElements.begin(); it != dockedElements.end();) { + if (!it->startsWith(QStringLiteral("internal/folders/"))) { + ++it; + continue; + } + + if (groupIndex(*it).isValid()) { + ++it; + continue; + } + + it = dockedElements.erase(it); + dockedElementsChanged = true; + } + + if (dockedElementsChanged) { + settings->setDockedElements(dockedElements); + } else { + loadDockedElements(); + } + notifyGroupElementsChanged(); +} + +void DockGlobalElementModel::notifyGroupElementsChanged() +{ + if (!m_data.isEmpty()) { + Q_EMIT dataChanged(index(0, 0), index(m_data.size() - 1, 0), + {TaskManager::NameRole, TaskManager::GroupItemsRole}); + } +} + +bool DockGlobalElementModel::requestDockGroup(const QString &launcherFolderId) +{ + if (!groupIndex(launcherFolderId).isValid()) + return false; + + auto *settings = TaskManagerSettings::instance(); + if (settings->isDocked(launcherFolderId)) + return false; + + settings->appendDockedElement(launcherFolderId); + return true; +} + void DockGlobalElementModel::groupItemsByApp() { if (m_data.isEmpty()) @@ -533,16 +755,20 @@ void DockGlobalElementModel::groupItemsByApp() return; for (int i = 0; i < m_data.size(); ++i) { + if (std::get<1>(m_data.at(i)) == nullptr) + continue; const QString currentId = std::get<0>(m_data.at(i)); int insertPos = i + 1; - while (insertPos < m_data.size() && std::get<0>(m_data.at(insertPos)) == currentId) { + while (insertPos < m_data.size() + && std::get<1>(m_data.at(insertPos)) != nullptr + && std::get<0>(m_data.at(insertPos)) == currentId) { ++insertPos; } for (int j = insertPos; j < m_data.size(); ++j) { - if (std::get<0>(m_data.at(j)) != currentId) + if (std::get<1>(m_data.at(j)) == nullptr || std::get<0>(m_data.at(j)) != currentId) continue; int destRow = insertPos < j ? insertPos : insertPos + 1; diff --git a/panels/dock/taskmanager/dockglobalelementmodel.h b/panels/dock/taskmanager/dockglobalelementmodel.h index 64a991bcf..97b223570 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.h +++ b/panels/dock/taskmanager/dockglobalelementmodel.h @@ -15,7 +15,8 @@ class DockGlobalElementModel : public QAbstractListModel, public AbstractTaskMan { Q_OBJECT public: - explicit DockGlobalElementModel(QAbstractItemModel *appsModel, DockCombineModel *activeAppModel, QObject *parent = nullptr); + explicit DockGlobalElementModel(QAbstractItemModel *appsModel, QAbstractItemModel *groupModel, QObject *groupProvider, + DockCombineModel *activeAppModel, QObject *parent = nullptr); QHash roleNames() const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; @@ -37,14 +38,21 @@ class DockGlobalElementModel : public QAbstractListModel, public AbstractTaskMan void requestWindowsView(const QModelIndexList &indexes) const override; void moveItem(int from, int to); + bool requestDockGroup(const QString &launcherFolderId); public slots: void initDockedElements(bool unused); private: void loadDockedElements(); + void refreshGroupElements(); + void handleGroupRowsRemoved(); + void notifyGroupElementsChanged(); QString getMenus(const QModelIndex &index) const; void groupItemsByApp(); + QModelIndex groupIndex(const QString &launcherFolderId) const; + QString groupDisplayName(const QString &launcherFolderId) const; + QVariantList groupApplications(const QString &launcherFolderId) const; private: // id, model, and pos @@ -53,6 +61,8 @@ public slots: // type, id QList> m_dockedElements; QAbstractItemModel *m_appsModel; + QAbstractItemModel *m_groupModel; + QPointer m_groupProvider; DockCombineModel *m_activeAppModel; }; } diff --git a/panels/dock/taskmanager/dockgroupmodel.cpp b/panels/dock/taskmanager/dockgroupmodel.cpp index 432453357..46fb15d39 100644 --- a/panels/dock/taskmanager/dockgroupmodel.cpp +++ b/panels/dock/taskmanager/dockgroupmodel.cpp @@ -195,6 +195,14 @@ void DockGroupModel::requestWindowsView(const QModelIndexList &indexes) const void DockGroupModel::requestNewInstance(const QModelIndex &index, const QString &action) const { + const auto itemId = index.data(TaskManager::ItemIdRole).toString(); + const bool isGroup = itemId.startsWith(QStringLiteral("internal/folders/")); + if (isGroup) { + if (action == DOCK_ACTION_DOCK) + TaskManagerSettings::instance()->toggleDockedElement(itemId); + return; + } + if (action == DOCK_ACTION_DOCK) { auto desktopId = index.data(TaskManager::DesktopIdRole).toString(); TaskManagerSettings::instance()->toggleDockedElement(QStringLiteral("desktop/%1").arg(desktopId)); diff --git a/panels/dock/taskmanager/globals.h b/panels/dock/taskmanager/globals.h index 47ec6a9b2..17cc377f5 100644 --- a/panels/dock/taskmanager/globals.h +++ b/panels/dock/taskmanager/globals.h @@ -48,6 +48,7 @@ constexpr auto MODEL_ACTIONS = "actions"; constexpr auto MODEL_MENUS = "menus"; constexpr auto MODEL_DOCKED = "docked"; constexpr auto MODEL_WINDOWS = "windows"; +constexpr auto MODEL_GROUPITEMS = "groupItems"; // default item icon constexpr auto DEFAULT_APP_ICONNAME = "application-default-icon"; diff --git a/panels/dock/taskmanager/package/AppItem.qml b/panels/dock/taskmanager/package/AppItem.qml index f4233a7fb..00694d842 100644 --- a/panels/dock/taskmanager/package/AppItem.qml +++ b/panels/dock/taskmanager/package/AppItem.qml @@ -24,27 +24,45 @@ Item { required property int visualIndex required property var modelIndex required property string title + required property var groupItems + readonly property bool isGroup: itemId.indexOf("internal/folders/") === 0 + readonly property bool useCompositePreview: root.isGroup property real blendOpacity: 1.0 + property bool dragEnabled: true + property string dragSource: "taskbar" + property bool restorePositionAfterDrag: false + property real dragStartX: 0 + property real dragStartY: 0 signal dropFilesOnItem(itemId: string, files: list) - signal dragFinished() + signal dragFinished(int dropAction) - Drag.active: mouseArea.drag.active + Drag.active: root.dragEnabled && mouseArea.drag.active Drag.source: root Drag.hotSpot.x: icon.width / 2 Drag.hotSpot.y: icon.height / 2 Drag.dragType: Drag.Automatic - Drag.mimeData: { "text/x-dde-dock-dnd-appid": itemId, "text/x-dde-dock-dnd-source": "taskbar", "text/x-dde-dock-dnd-winid": windows.length > 0 ? windows[0] : ""} Accessible.role: Accessible.Button Accessible.name: root.name Accessible.description: root.attention ? qsTr("Demands attention") : (root.active ? qsTr("Active") : "") + Drag.supportedActions: Qt.CopyAction | Qt.MoveAction + Drag.mimeData: { "text/x-dde-dock-dnd-appid": itemId, "text/x-dde-dock-dnd-source": dragSource, "text/x-dde-dock-dnd-winid": windows.length > 0 ? windows[0] : ""} + Drag.onDragFinished: function(dropAction) { + if (root.restorePositionAfterDrag) { + root.x = root.dragStartX + root.y = root.dragStartY + } + root.dragFinished(dropAction) + } + property bool useColumnLayout: Panel.rootObject.useColumnLayout property real iconSize: Panel.rootObject.dockItemMaxSize * 9 / 14 property bool enableTitle: false property bool titleActive: enableTitle && titleLoader.active + property bool preservePanelPopupOnContextMenu: false property var iconGlobalPoint: { var a = icon var x = 0, y = 0 @@ -150,6 +168,7 @@ Item { D.DciIcon { id: icon name: root.iconName + visible: !root.useCompositePreview height: iconSize width: iconSize sourceSize: Qt.size(iconSize, iconSize) @@ -230,6 +249,44 @@ Item { running: false } } + + Item { + anchors.centerIn: parent + width: root.iconSize + height: root.iconSize + + Rectangle { + anchors.fill: parent + radius: Math.max(6, Math.round(root.iconSize / 4)) + color: root.colorTheme === Dock.Dark ? + Qt.rgba(1, 1, 1, 0.10) : + Qt.rgba(0, 0, 0, 0.10) + border.width: 1 + border.color: root.colorTheme === Dock.Dark ? + Qt.rgba(1, 1, 1, 0.40) : + Qt.rgba(0, 0, 0, 0.20) + visible: root.useCompositePreview + } + + Grid { + anchors.centerIn: parent + visible: root.useCompositePreview + columns: 2 + spacing: Math.max(1, Math.round(root.iconSize * 0.04)) + + Repeater { + model: Math.min(4, root.groupItems.length) + D.DciIcon { + required property int index + width: Math.round(root.iconSize * 0.34) + height: width + sourceSize: Qt.size(width, height) + name: root.groupItems[index].iconName + retainWhileLoading: true + } + } + } + } } WindowIndicator { @@ -414,7 +471,10 @@ Item { running: false repeat: false onTriggered: { - var pos = icon.mapToItem(null, 0, 0) + const point = icon.mapToItem(null, 0, 0) + icon.DockPositioner.bounding = Qt.rect(point.x, point.y, 0, 0) + icon.DockPositioner.updatePosition() + const pos = Qt.point(icon.DockPositioner.x, icon.DockPositioner.y) taskmanager.Applet.requestUpdateWindowIconGeometry(root.modelIndex, Qt.rect(pos.x, pos.y, icon.width, icon.height), Panel.rootObject) } @@ -428,6 +488,8 @@ Item { property int xOffset: 0 property int yOffset: 0 onTriggered: { + xOffset = icon.DockPanelPositioner.x + yOffset = icon.DockPanelPositioner.y if (root.windows.length != 0 || Qt.platform.pluginName === "wayland") { // 使用基于 modelIndex 的预览API,确保精确匹配 taskmanager.Applet.requestPreview(root.modelIndex, Panel.rootObject, xOffset, yOffset, Panel.position); @@ -437,22 +499,13 @@ Item { function onEntered() { - if (windows.length === 0) { + if (root.isGroup || 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 + const point = icon.mapToItem(null, icon.width / 2, icon.height / 2) + icon.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, 0, 0) previewTimer.start() } @@ -472,6 +525,23 @@ Item { closeItemPreview() } + function toggleGroupPopup() { + toolTip.close() + Panel.requestClosePopup() + const point = icon.mapToItem(null, icon.width / 2, icon.height / 2) + groupPopup.openAt(point) + } + + FolderPopup { + id: groupPopup + groupName: root.name + groupItems: root.groupItems + displayMode: root.displayMode + onLaunchApplicationRequested: function(desktopId) { + taskmanager.Applet.launchGroupApplication(desktopId) + } + } + function closeItemPreview() { if (previewTimer.running) { previewTimer.stop() @@ -481,7 +551,9 @@ Item { } function requestAppItemMenu() { - Panel.requestClosePopup() + if (!root.preservePanelPopupOnContextMenu) { + Panel.requestClosePopup() + } contextMenuLoader.trashEmpty = TaskManager.isTrashEmpty() contextMenuLoader.active = true MenuHelper.openMenu(contextMenuLoader.item) @@ -492,13 +564,14 @@ Item { anchors.fill: parent hoverEnabled: false acceptedButtons: Qt.LeftButton | Qt.RightButton - drag.target: root + drag.target: root.dragEnabled ? root : null drag.onActiveChanged: { if (!drag.active) { Panel.contextDragging = false - root.dragFinished() return } + root.dragStartX = root.x + root.dragStartY = root.y Panel.contextDragging = true } @@ -515,7 +588,7 @@ Item { } onPressed: function (mouse) { isTouchLongPressed = false - if (mouse.button === Qt.LeftButton) { + if (root.dragEnabled && mouse.button === Qt.LeftButton) { appItem.grabToImage(function(result) { root.Drag.imageSource = result.url; }) @@ -534,6 +607,10 @@ Item { if (mouse.button === Qt.RightButton) { requestAppItemMenu() } else { + if (root.isGroup) { + toggleGroupPopup() + return + } if (root.windows.length === 0) { launchAnimation.start(); TaskManager.requestNewInstance(index, ""); @@ -545,15 +622,15 @@ Item { PanelToolTip { id: toolTip - toolTipX: DockPanelPositioner.x - toolTipY: DockPanelPositioner.y + toolTipX: icon.DockPanelPositioner.x + toolTipY: icon.DockPanelPositioner.y } PanelToolTip { id: dragToolTip text: qsTr("Move to Trash") - toolTipX: DockPanelPositioner.x - toolTipY: DockPanelPositioner.y + toolTipX: icon.DockPanelPositioner.x + toolTipY: icon.DockPanelPositioner.y visible: false } @@ -561,9 +638,10 @@ Item { id: toolTipShowTimer interval: 50 onTriggered: { - var point = root.mapToItem(null, root.width / 2, root.height / 2) toolTip.text = root.itemId === "dde-trash" ? root.name + "-" + taskmanager.Applet.getTrashTipText() : root.name - toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) + const point = icon.mapToItem(null, icon.width / 2, icon.height / 2) + icon.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, + toolTip.width, toolTip.height) toolTip.open() } } @@ -585,8 +663,9 @@ Item { if (root.itemId === "dde-trash") { dragToolTipCloseTimer.stop() if (!dragToolTip.toolTipVisible) { - var point = root.mapToItem(null, root.width / 2, root.height / 2) - dragToolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, dragToolTip.width, dragToolTip.height) + const point = icon.mapToItem(null, icon.width / 2, icon.height / 2) + icon.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, + dragToolTip.width, dragToolTip.height) dragToolTip.open() } } diff --git a/panels/dock/taskmanager/package/AppItemTitle.qml b/panels/dock/taskmanager/package/AppItemTitle.qml index 06c6806e9..e71146727 100644 --- a/panels/dock/taskmanager/package/AppItemTitle.qml +++ b/panels/dock/taskmanager/package/AppItemTitle.qml @@ -19,7 +19,7 @@ Item { implicitWidth: titleLoader.width implicitHeight: titleLoader.height - TextCalculator.text: root.text + TextCalculator.text: root.enabled ? root.text : "" Loader { id: titleLoader diff --git a/panels/dock/taskmanager/package/FolderPopup.qml b/panels/dock/taskmanager/package/FolderPopup.qml new file mode 100644 index 000000000..3a8c090ba --- /dev/null +++ b/panels/dock/taskmanager/package/FolderPopup.qml @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +PanelPopup { + id: root + + required property string groupName + required property var groupItems + required property int displayMode + + readonly property int itemCount: root.groupItems ? root.groupItems.length : 0 + readonly property int columnCount: root.itemCount > 12 ? 5 : 4 + readonly property int rowCount: Math.max(1, Math.ceil(root.itemCount / root.columnCount)) + readonly property int visibleRowCount: Math.min(3, root.rowCount) + + readonly property int itemWidth: 96 + readonly property int itemHeight: 104 + readonly property int itemIconSize: 48 + readonly property int itemSpacing: 8 + readonly property int horizontalPadding: 5 + readonly property int titleTopPadding: 12 + readonly property int titleHeight: 24 + readonly property int titleBottomSpacing: 8 + readonly property int bottomPadding: 22 + readonly property int gridWidth: root.columnCount * root.itemWidth + + (root.columnCount - 1) * root.itemSpacing + readonly property int gridHeight: root.rowCount * root.itemHeight + + (root.rowCount - 1) * root.itemSpacing + readonly property int visibleGridHeight: root.visibleRowCount * root.itemHeight + + (root.visibleRowCount - 1) * root.itemSpacing + + property point anchorPoint: Qt.point(0, 0) + + signal launchApplicationRequested(string desktopId) + + width: root.gridWidth + root.horizontalPadding * 2 + height: root.titleTopPadding + root.titleHeight + root.titleBottomSpacing + + root.visibleGridHeight + root.bottomPadding + popupX: DockPanelPositioner.x + popupY: DockPanelPositioner.y + closeOnInactive: MenuHelper.activeMenu === null && !Panel.contextDragging + + function openAt(point) { + root.anchorPoint = point + popupFlickable.contentY = 0 + root.open() + } + + Component.onCompleted: { + DockPanelPositioner.bounding = Qt.binding(function() { + return Qt.rect(root.anchorPoint.x, root.anchorPoint.y, + root.width, root.height) + }) + } + + Control { + id: popupContent + + anchors.fill: parent + leftPadding: root.horizontalPadding + rightPadding: root.horizontalPadding + topPadding: root.titleTopPadding + root.titleHeight + root.titleBottomSpacing + bottomPadding: root.bottomPadding + background: Item {} + + Text { + id: folderTitle + + anchors.top: parent.top + anchors.topMargin: root.titleTopPadding + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: root.horizontalPadding + 8 + anchors.rightMargin: root.horizontalPadding + 8 + height: root.titleHeight + text: root.groupName + textFormat: Text.PlainText + elide: Text.ElideRight + maximumLineCount: 1 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: D.DTK.palette.windowText + font: D.DTK.fontManager.t6 + } + + contentItem: Flickable { + id: popupFlickable + + contentWidth: width + contentHeight: root.gridHeight + clip: true + interactive: root.rowCount > root.visibleRowCount + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + pixelAligned: true + + Grid { + id: popupGrid + + width: popupFlickable.width + height: root.gridHeight + columns: root.columnCount + rowSpacing: root.itemSpacing + columnSpacing: root.itemSpacing + + Repeater { + id: groupRepeater + model: root.groupItems + + delegate: Item { + id: groupAppItem + + required property var modelData + + width: root.itemWidth + height: root.itemHeight + clip: true + + Item { + id: groupAppContent + + anchors.fill: parent + + AppItemBackground { + id: groupAppHoverBackground + + anchors.fill: parent + radius: 8 + enabled: false + isActive: false + windowCount: 0 + displayMode: root.displayMode + D.ColorSelector.hovered: groupAppHoverHandler.hovered + D.ColorSelector.pressed: groupAppMouseArea.pressed + } + + D.DciIcon { + id: groupAppIcon + + width: root.itemIconSize + height: width + anchors.top: parent.top + anchors.topMargin: 7 + anchors.horizontalCenter: parent.horizontalCenter + name: modelData.iconName + sourceSize: Qt.size(width, height) + } + + Text { + id: groupAppTitle + + anchors.top: groupAppIcon.bottom + anchors.topMargin: 4 + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 2 + anchors.rightMargin: 2 + textFormat: Text.PlainText + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + text: modelData.name + color: D.DTK.palette.windowText + font: D.DTK.fontManager.t9 + } + + HoverHandler { + id: groupAppHoverHandler + } + + MouseArea { + id: groupAppMouseArea + + anchors.fill: parent + onClicked: { + root.launchApplicationRequested(modelData.desktopId) + root.close() + } + } + } + } + } + } + + ScrollBar.vertical: ScrollBar { + parent: popupContent + anchors.top: popupFlickable.top + anchors.right: popupContent.right + anchors.bottom: popupFlickable.bottom + anchors.rightMargin: 1 + width: 4 + policy: root.rowCount > root.visibleRowCount + ? ScrollBar.AsNeeded + : ScrollBar.AlwaysOff + } + } + } +} diff --git a/panels/dock/taskmanager/package/TaskManager.qml b/panels/dock/taskmanager/package/TaskManager.qml index 1177a52d9..536bce88f 100644 --- a/panels/dock/taskmanager/package/TaskManager.qml +++ b/panels/dock/taskmanager/package/TaskManager.qml @@ -14,6 +14,7 @@ ContainmentItem { id: taskmanager property bool useColumnLayout: Panel.rootObject.useColumnLayout property int dockOrder: 16 + readonly property bool fashionMode: Panel.fashionMode Accessible.role: Accessible.Grouping Accessible.name: qsTr("Task Manager") @@ -21,10 +22,12 @@ ContainmentItem { function calcRemainingSpace(baseSize) { const otherCount = Panel.rootObject.dockCenterPartCount - 1; const otherOccupied = otherCount > 0 ? otherCount * baseSize * multitaskViewIconRatio : 0; - return Panel.rootObject.dockRawCenterSpace - otherOccupied; + return Panel.rootObject.dockEffectiveCenterSpace - otherOccupied; } - readonly property real remainingSpacesForTaskManager: calcRemainingSpace(Panel.rootObject.dockItemMaxSize) + property real remainingSpacesForTaskManager: fashionMode + ? 0 + : calcRemainingSpace(Panel.rootObject.dockItemMaxSize) readonly property int appTitleSpacing: Math.max(10, Math.round(Panel.rootObject.dockItemMaxSize * 9 / 14) / 3) // Start padding for the app container so that the visual gap // (multitask icon right edge → first app icon left edge) = appTitleSpacing. @@ -37,15 +40,20 @@ ContainmentItem { // Defined in main.qml as dockItemIconSize = dockItemMaxSize * 9 / 14. // At default dock size (56), icon = 36px; at min (37), icon ≈ 24px; at max (100), icon ≈ 64px. readonly property real iconWidthToMaxSizeRatio: 9 / 14 + readonly property int minimumDockItemSize: Math.max( + Dock.MIN_DOCK_SIZE, + Math.round(Panel.rootObject.dockSize * Dock.MIN_DOCK_SIZE / Dock.DEFAULT_DOCK_SIZE)) readonly property real startPadding: Math.max(0, appTitleSpacing - (Panel.rootObject.dockItemMaxSize * (multitaskViewIconRatio - iconWidthToMaxSizeRatio) / 2)) implicitWidth: { + if (fashionMode) return appContainer.implicitWidth + (useColumnLayout ? 0 : startPadding) let extra = useColumnLayout ? 0 : startPadding let w = appContainer.implicitWidth + extra let maxW = Panel.itemAlignment === Dock.LeftAlignment ? Math.max(remainingSpacesForTaskManager, w) : Math.min(remainingSpacesForTaskManager, w) return useColumnLayout ? Panel.rootObject.dockSize : maxW } implicitHeight: { + if (fashionMode && useColumnLayout) return appContainer.implicitHeight + startPadding let extra = useColumnLayout ? startPadding : 0 let h = appContainer.implicitHeight + extra let maxH = Panel.itemAlignment === Dock.LeftAlignment ? Math.max(remainingSpacesForTaskManager, h) : Math.min(remainingSpacesForTaskManager, h) @@ -82,9 +90,22 @@ ContainmentItem { } property real blendOpacity: blendColorAlpha(D.DTK.themeType === D.ApplicationHelper.DarkType ? 0.25 : 1.0) + TaskOverflowController { + id: taskOverflow + // Window-split title layout has priority. Only fall back to adaptive icon + // shrinking and overflow when no title layout can fit the available space. + enabled: !textCalculator.enabled || textCalculator.totalWidth <= 0 + availableExtent: Math.max(0, taskmanager.calcRemainingSpace(Panel.rootObject.dockItemMaxSize) - taskmanager.startPadding) + minimumReached: Panel.rootObject.dockItemMaxSize <= taskmanager.minimumDockItemSize + 0.5 + fallbackItemExtent: Math.max(1, Panel.rootObject.dockItemMaxSize * taskmanager.iconWidthToMaxSizeRatio) + spacing: taskmanager.appTitleSpacing + visualModel: visualModel + dataModel: taskmanager.Applet.dataModel + } + TextCalculator { id: textCalculator - enabled: taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top) + enabled: !fashionMode && taskmanager.Applet.windowSplit && (Panel.position == Dock.Bottom || Panel.position == Dock.Top) dataModel: taskmanager.Applet.dataModel iconSize: Panel.rootObject.dockSize * 9 / 14 spacing: Math.max(10, Math.round(textCalculator.iconSize) / 3) @@ -131,15 +152,30 @@ ContainmentItem { required property string icon // winIconName required property string menus required property list windows + required property var groupItems z: attention ? -1 : 0 property bool visibility: { - let draggedAppId = taskmanager.Applet.desktopIdToAppId(launcherDndDropArea.launcherDndDesktopId) - if (itemId !== draggedAppId) { + const draggedItemId = launcherDndDropArea.dockItemId(launcherDndDropArea.launcherDndDesktopId) + if (itemId !== draggedItemId) { return true } return windows.length > 0 && launcherDndDropArea.launcherDndWinId !== windows[0] } + readonly property bool overflowActive: taskOverflow.popupItems.length > 0 + readonly property bool isOverflowButton: overflowActive + && DelegateModel.itemsIndex === taskOverflow.visibleItemCount + readonly property bool hiddenInOverflow: overflowActive + && DelegateModel.itemsIndex > taskOverflow.visibleItemCount + visible: !hiddenInOverflow + + readonly property real normalDelegateWidth: useColumnLayout + ? taskmanager.implicitWidth + : appItem.implicitWidth + readonly property real normalDelegateHeight: useColumnLayout + ? Panel.rootObject.dockItemMaxSize * 9 / 14 + : taskmanager.implicitHeight + ListView.onAdd: NumberAnimation { target: delegateRoot properties: "scale,opacity" @@ -164,8 +200,8 @@ ContainmentItem { Behavior on opacity { NumberAnimation { duration: 200 } } Behavior on scale { NumberAnimation { duration: 200 } } - implicitWidth: useColumnLayout ? taskmanager.implicitWidth : appItem.implicitWidth - implicitHeight: useColumnLayout ? Panel.rootObject.dockItemMaxSize * 9 / 14 : taskmanager.implicitHeight + implicitWidth: hiddenInOverflow? 0 : normalDelegateWidth + implicitHeight: hiddenInOverflow? 0 : normalDelegateHeight property int visualIndex: DelegateModel.itemsIndex property var modelIndex: visualModel.modelIndex(index) @@ -181,8 +217,29 @@ ContainmentItem { y: delegateRoot.y width: delegateRoot.width height: delegateRoot.height - scale: delegateRoot.scale + readonly property bool hiddenByOverflow: delegateRoot.isOverflowButton + || delegateRoot.hiddenInOverflow + property real overflowTransitionScale: hiddenByOverflow ? 0.8 : 1.0 + scale: delegateRoot.scale * overflowTransitionScale + opacity: hiddenByOverflow ? 0 : 1 + visible: opacity > 0.001 property bool positionAnimationEnabled: false + + Behavior on opacity { + enabled: appItemRect.positionAnimationEnabled + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + Behavior on overflowTransitionScale { + enabled: appItemRect.positionAnimationEnabled + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + Behavior on x { enabled: appItemRect.positionAnimationEnabled NumberAnimation { @@ -217,6 +274,7 @@ ContainmentItem { iconName: delegateRoot.iconName menus: delegateRoot.menus windows: delegateRoot.windows + groupItems: delegateRoot.groupItems visualIndex: delegateRoot.visualIndex modelIndex: delegateRoot.modelIndex blendOpacity: taskmanager.blendOpacity @@ -234,42 +292,117 @@ ContainmentItem { } } + TaskOverflowButton { + id: overflowButton + taskManagerItem: taskmanager + overflow: taskOverflow + } + DropArea { id: launcherDndDropArea anchors.fill: parent z: 3 - keys: ["text/x-dde-dock-dnd-appid"] + keys: ["text/x-dde-dock-dnd-appid", "text/x-dde-launcher-dnd-desktopId"] property string launcherDndDesktopId: "" property string launcherDndDragSource: "" property string launcherDndWinId: "" + property bool launcherDndDocked: false function resetDndState() { launcherDndDesktopId = "" launcherDndDragSource = "" launcherDndWinId = "" + launcherDndDocked = false + } + + function isOverflowButtonIndex(targetIndex) { + return taskOverflow.popupItems.length > 0 + && targetIndex === taskOverflow.visibleItemCount + } + + function targetIndexAt(x, y) { + if (launcherDndDragSource !== "overflow-popup") { + return appContainer.indexAt(x, y) + } + + const visibleCount = taskOverflow.visibleItemCount + if (visibleCount <= 0) { + return -1 + } + + const itemExtent = taskOverflow.fallbackItemExtent + const pitch = itemExtent + taskOverflow.spacing + const position = taskmanager.useColumnLayout ? y : x + const nearestSlot = Math.floor((Math.max(0, position) + taskOverflow.spacing / 2) / pitch) + return Math.max(0, Math.min(visibleCount - 1, nearestSlot)) + } + + function isInternalDrag() { + return launcherDndDragSource === "taskbar" + || launcherDndDragSource === "overflow-popup" + } + + function isLauncherFolderId(id) { + return typeof id === "string" + && id.indexOf("internal/folders/") === 0 + } + + function dockItemId(id) { + return taskmanager.Applet.desktopIdToAppId(id) + } + + function requestDockIfNeeded() { + if (isInternalDrag() || launcherDndDocked) { + return true + } + + if (taskmanager.Applet.requestDockByDesktopId(launcherDndDesktopId) === false) { + resetDndState() + return false + } + + launcherDndDocked = true + return true } onEntered: function(drag) { let desktopId = drag.getDataAsString("text/x-dde-dock-dnd-appid") + if (!desktopId) { + const launcherId = drag.getDataAsString("text/x-dde-launcher-dnd-desktopId") + if (isLauncherFolderId(launcherId)) { + desktopId = launcherId + } + } + if (!desktopId) { + resetDndState() + return + } launcherDndDragSource = drag.getDataAsString("text/x-dde-dock-dnd-source") launcherDndWinId = drag.getDataAsString("text/x-dde-dock-dnd-winid") launcherDndDesktopId = desktopId - if (launcherDndDragSource !== "taskbar" && taskmanager.Applet.requestDockByDesktopId(desktopId) === false) { - drag.accepted = false - resetDndState() + let targetIndex = targetIndexAt(drag.x, drag.y) + if (!isOverflowButtonIndex(targetIndex) && !requestDockIfNeeded()) { + return + } + if (!isOverflowButtonIndex(targetIndex)) { + drag.acceptProposedAction() } } onPositionChanged: function(drag) { if (launcherDndDesktopId === "") return - let targetIndex = appContainer.indexAt(drag.x, drag.y) - let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) - let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(appId, launcherDndWinId) : taskmanager.findAppIndex(appId) + if (launcherDndDragSource === "overflow-popup") return + let targetIndex = targetIndexAt(drag.x, drag.y) + if (isOverflowButtonIndex(targetIndex) || !requestDockIfNeeded()) return + drag.acceptProposedAction() + const itemId = dockItemId(launcherDndDesktopId) + let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(itemId, launcherDndWinId) : taskmanager.findAppIndex(itemId) if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { if (taskmanager.Applet.windowSplit) { taskmanager.Applet.moveItem(currentIndex, targetIndex) } else { visualModel.items.move(currentIndex, targetIndex) + taskOverflow.scheduleSync() } } } @@ -277,14 +410,16 @@ ContainmentItem { onDropped: function(drop) { Panel.contextDragging = false if (launcherDndDesktopId === "") return - let targetIndex = appContainer.indexAt(drop.x, drop.y) - let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) - let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(appId, launcherDndWinId) : taskmanager.findAppIndex(appId) + let targetIndex = targetIndexAt(drop.x, drop.y) + if (!requestDockIfNeeded()) return + const itemId = dockItemId(launcherDndDesktopId) + let currentIndex = taskmanager.Applet.windowSplit ? taskmanager.findAppIndexByWindow(itemId, launcherDndWinId) : taskmanager.findAppIndex(itemId) if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { if (taskmanager.Applet.windowSplit) { taskmanager.Applet.moveItem(currentIndex, targetIndex) } else { visualModel.items.move(currentIndex, targetIndex) + taskOverflow.scheduleSync() } } let appIds = [] @@ -292,11 +427,16 @@ ContainmentItem { appIds.push(visualModel.items.get(i).model.itemId) } taskmanager.Applet.saveDockElementsOrder(appIds) + if (isInternalDrag()) { + drop.accept(Qt.MoveAction) + } else { + drop.acceptProposedAction() + } resetDndState() } onExited: function() { - if (launcherDndDesktopId !== "" && launcherDndDragSource !== "taskbar") { + if (launcherDndDesktopId !== "" && !isInternalDrag() && launcherDndDocked) { taskmanager.Applet.requestUndockByDesktopId(launcherDndDesktopId) } resetDndState() @@ -349,8 +489,10 @@ ContainmentItem { return dockSize; const otherItems = Math.max(0, Panel.rootObject.dockCenterPartCount - 1); - const optimal = solveMaxSizeFull(Panel.rootObject.dockRawCenterSpace, appCount, otherItems); - return optimal >= dockSize ? dockSize : optimal; + const optimal = solveMaxSizeFull(Panel.rootObject.dockEffectiveCenterSpace, appCount, otherItems); + const fitted = optimal >= dockSize ? dockSize : optimal; + return Math.max(taskmanager.minimumDockItemSize, fitted); }) + taskOverflow.scheduleSync() } } diff --git a/panels/dock/taskmanager/package/TaskOverflowButton.qml b/panels/dock/taskmanager/package/TaskOverflowButton.qml new file mode 100644 index 000000000..aabc77d3d --- /dev/null +++ b/panels/dock/taskmanager/package/TaskOverflowButton.qml @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +Item { + id: root + + required property var taskManagerItem + required property var overflow + + readonly property int overflowCount: overflow.popupItems.length + readonly property var primaryOverflowItem: overflow.popupItems.length > 0 ? overflow.popupItems[0] : null + readonly property string overflowIconName: primaryOverflowItem && primaryOverflowItem.iconName ? primaryOverflowItem.iconName : "" + readonly property real popupItemWidth: Math.max(1, overflow.fallbackItemExtent + overflow.spacing) + readonly property real popupItemHeight: Math.max(1, Panel.rootObject ? Panel.rootObject.dockItemMaxSize : overflow.fallbackItemExtent) + readonly property real buttonOffset: overflow.visibleItemCount * (overflow.fallbackItemExtent + overflow.spacing) + readonly property real overflowIconSize: Math.max(12, Math.round(Panel.rootObject.dockItemIconSize)) + readonly property int hoverVerticalSpacing: Math.round(overflowIconSize / 8) + 1 + readonly property int hoverHorizontalSpacing: Math.round(overflowIconSize / 8) + readonly property int hoverBackgroundWidth: Math.round(overflowIconSize + hoverHorizontalSpacing * 2) + readonly property int hoverBackgroundHeight: Math.round(overflowIconSize + hoverVerticalSpacing * 2) + readonly property real dotSize: Math.max(2, Math.round(overflowIconSize * 0.11)) + readonly property real dotSpacing: Math.max(1, Math.round(dotSize * 0.75)) + readonly property color dotColor: D.DTK.themeType === D.ApplicationHelper.DarkType ? "#FFFFFF" : "#000000" + readonly property int popupColumns: Math.min(6, Math.max(1, overflowCount)) + property point popupAnchorPoint: Qt.point(0, 0) + + visible: overflowCount > 0 + x: root.taskManagerItem.useColumnLayout ? 0 : root.buttonOffset + y: root.taskManagerItem.useColumnLayout ? root.buttonOffset : 0 + implicitWidth: root.taskManagerItem.useColumnLayout ? root.taskManagerItem.implicitWidth : overflow.fallbackItemExtent + implicitHeight: root.taskManagerItem.useColumnLayout ? overflow.fallbackItemExtent : root.taskManagerItem.implicitHeight + width: implicitWidth + height: implicitHeight + + function togglePopup() { + toolTipShowTimer.stop() + toolTip.close() + + if (root.overflowCount <= 0) { + return + } + + if (overflowPopup.popupVisible) { + overflowPopup.close() + return + } + + Panel.requestClosePopup() + popupAnchorPoint = root.mapToItem(null, root.width / 2, root.height / 2) + overflowPopup.open() + } + + AppItemBackground { + width: root.hoverBackgroundWidth + height: root.hoverBackgroundHeight + radius: height / 5 + anchors.centerIn: parent + enabled: false + displayMode: Panel.indicatorStyle + windowCount: 0 + isActive: false + D.ColorSelector.hovered: overflowHoverHandler.hovered || overflowPopup.popupVisible + D.ColorSelector.pressed: overflowMouseArea.pressed + } + + Item { + id: overflowIndicator + anchors.centerIn: parent + visible: root.primaryOverflowItem !== null + width: root.overflowIconSize + height: root.overflowIconSize + + D.DciIcon { + anchors.fill: parent + name: root.overflowIconName + sourceSize: Qt.size(width, height) + retainWhileLoading: true + smooth: false + opacity: 0.4 + } + Row { + spacing: root.dotSpacing + anchors.centerIn: parent + + Repeater { + model: 3 + + Rectangle { + width: root.dotSize + height: width + radius: width / 2 + color: root.dotColor + } + } + } + } + + MouseArea { + id: overflowMouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: false + onClicked: root.togglePopup() + } + + HoverHandler { + id: overflowHoverHandler + onHoveredChanged: { + if (hovered && !overflowPopup.popupVisible) { + toolTipShowTimer.start() + } else { + toolTipShowTimer.stop() + toolTip.close() + } + } + } + + PanelToolTip { + id: toolTip + text: qsTr("Show hidden icons") + toolTipX: DockPanelPositioner.x + toolTipY: DockPanelPositioner.y + } + + Timer { + id: toolTipShowTimer + interval: 50 + onTriggered: { + var point = root.mapToItem(null, root.width / 2, root.height / 2) + toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) + toolTip.open() + } + } + + PanelPopup { + id: overflowPopup + width: overflowPopupContent.width + height: overflowPopupContent.height + popupX: DockPanelPositioner.x + popupY: DockPanelPositioner.y + closeOnInactive: MenuHelper.activeMenu === null && !Panel.contextDragging + + Component.onCompleted: { + DockPanelPositioner.bounding = Qt.binding(function () { + return Qt.rect(root.popupAnchorPoint.x, + root.popupAnchorPoint.y, + overflowPopup.width, + overflowPopup.height) + }) + } + + Control { + id: overflowPopupContent + padding: 10 + implicitWidth: overflowGrid.implicitWidth + leftPadding + rightPadding + implicitHeight: overflowGrid.implicitHeight + topPadding + bottomPadding + width: implicitWidth + height: implicitHeight + background: Item {} + + DropArea { + anchors.fill: parent + keys: ["text/x-dde-dock-dnd-appid"] + onDropped: function(drop) { + if (drop.getDataAsString("text/x-dde-dock-dnd-source") === "overflow-popup") { + drop.accept(Qt.MoveAction) + } + } + } + + contentItem: Grid { + id: overflowGrid + columns: root.popupColumns + rowSpacing: Math.max(6, Math.round(root.popupItemWidth * 0.12)) + columnSpacing: rowSpacing + + Repeater { + model: root.overflow.popupItems + + delegate: Item { + required property var modelData + + width: root.popupItemWidth + height: root.popupItemHeight + + AppItem { + width: parent.width + height: parent.height + displayMode: Panel.indicatorStyle + colorTheme: Panel.colorTheme + active: modelData.active + attention: modelData.attention + itemId: modelData.itemId + name: modelData.name + iconName: modelData.iconName + menus: modelData.menus + windows: modelData.windows + groupItems: modelData.groupItems + visualIndex: modelData.visualIndex + modelIndex: modelData.modelIndex + blendOpacity: root.taskManagerItem.blendOpacity + title: modelData.title + enableTitle: false + preservePanelPopupOnContextMenu: true + dragEnabled: modelData.docked + dragSource: "overflow-popup" + restorePositionAfterDrag: true + + Component.onCompleted: { + dropFilesOnItem.connect(root.taskManagerItem.Applet.dropFilesOnItem) + } + onDragFinished: function(dropAction) { + if (dropAction === Qt.IgnoreAction && modelData.docked) { + root.taskManagerItem.Applet.requestUndockByDesktopId(modelData.itemId) + } + } + } + } + } + } + } + } +} diff --git a/panels/dock/taskmanager/taskmanager.cpp b/panels/dock/taskmanager/taskmanager.cpp index bae8101ea..a4741e608 100644 --- a/panels/dock/taskmanager/taskmanager.cpp +++ b/panels/dock/taskmanager/taskmanager.cpp @@ -18,6 +18,7 @@ #include "pluginfactory.h" #include "taskmanager.h" #include "taskmanageradaptor.h" +#include "taskoverflowcontroller.h" #include "taskmanagersettings.h" #include "textcalculator.h" #include "treelandwindowmonitor.h" @@ -52,6 +53,15 @@ Q_LOGGING_CATEGORY(taskManagerLog, "org.deepin.dde.shell.dock.taskmanager", QtDe namespace dock { +static QString canonicalLauncherFolderId(const QString &id) +{ + static const QRegularExpression expression(QStringLiteral("^internal/folders/(\\d+)$")); + const auto match = expression.match(id.trimmed()); + if (!match.hasMatch() || match.captured(1).toInt() <= 0) + return {}; + return QStringLiteral("internal/folders/%1").arg(match.captured(1).toInt()); +} + // 通过AM(Application Manager)匹配应用程序的辅助函数 static QString getDesktopIdByPid(const QStringList &identifies) { @@ -147,6 +157,7 @@ TaskManager::TaskManager(QObject *parent) , AbstractTaskManagerInterface(nullptr) , m_windowFullscreen(false) { + qmlRegisterType("org.deepin.ds.dock.taskmanager", 1, 0, "TaskOverflowController"); qmlRegisterType("org.deepin.ds.dock.taskmanager", 1, 0, "TextCalculator"); qmlRegisterUncreatableType("org.deepin.ds.dock.taskmanager", 1, 0, "TextCalculatorAttached", "TextCalculator Attached"); @@ -183,10 +194,22 @@ bool TaskManager::init() DApplet::init(); - DS_NAMESPACE::DAppletBridge bridge("org.deepin.ds.dde-apps"); + DS_NAMESPACE::DAppletBridge appsBridge("org.deepin.ds.dde-apps"); BoolFilterModel *leftModel = new BoolFilterModel(m_windowMonitor.data(), m_windowMonitor->roleNames().key("shouldSkip"), this); - if (auto applet = bridge.applet()) { + if (auto applet = appsBridge.applet()) { + m_appsApplet = applet; auto model = applet->property("appModel").value(); + + DS_NAMESPACE::DAppletBridge launcherBridge("org.deepin.ds.dock.launcherapplet"); + auto launcherApplet = launcherBridge.applet(); + auto groupModel = launcherApplet + ? launcherApplet->property("itemArrangementModel").value() + : nullptr; + if (groupModel) + qCInfo(taskManagerLog) << "Using live launchpad item arrangement model"; + else + qCWarning(taskManagerLog) << "Launchpad item arrangement model is unavailable"; + Q_ASSERT(model); m_activeAppModel = new DockCombineModel(leftModel, model, TaskManager::IdentityRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { auto roleNames = model->roleNames(); @@ -218,7 +241,7 @@ bool TaskManager::init() return res.value(0); }); - m_dockGlobalElementModel = new DockGlobalElementModel(model, m_activeAppModel, this); + m_dockGlobalElementModel = new DockGlobalElementModel(model, groupModel, launcherApplet, m_activeAppModel, this); m_itemModel = new DockItemModel(m_dockGlobalElementModel, this); // 初始化预览代理模型,基于合并后的数据 @@ -401,8 +424,13 @@ QString TaskManager::desktopIdToAppId(const QString& desktopId) bool TaskManager::requestDockByDesktopId(const QString& desktopID) { - if (!Settings->dockedApplicationsEnabled()) return false; - if (desktopID.startsWith("internal/")) return false; + if (!Settings->dockedApplicationsEnabled()) + return false; + + const auto folderId = canonicalLauncherFolderId(desktopID); + if (!folderId.isEmpty()) + return m_dockGlobalElementModel && m_dockGlobalElementModel->requestDockGroup(folderId); + QString appId = desktopIdToAppId(desktopID); // 检查应用是否已经在任务栏中,如果是则返回 false if (IsDocked(appId)) @@ -413,11 +441,34 @@ bool TaskManager::requestDockByDesktopId(const QString& desktopID) bool TaskManager::requestUndockByDesktopId(const QString& desktopID) { - if (!Settings->dockedApplicationsEnabled()) return false; - if (desktopID.startsWith("internal/")) return false; + if (!Settings->dockedApplicationsEnabled()) + return false; + + const auto folderId = canonicalLauncherFolderId(desktopID); + if (!folderId.isEmpty()) + return requestUndockElement(folderId); + return RequestUndock(desktopIdToAppId(desktopID)); } +bool TaskManager::launchGroupApplication(const QString &desktopId) +{ + if (!m_appsApplet) + return false; + bool launched = false; + QMetaObject::invokeMethod(m_appsApplet, "launchApplication", Qt::DirectConnection, + Q_RETURN_ARG(bool, launched), Q_ARG(QString, desktopId)); + return launched; +} + +bool TaskManager::requestUndockElement(const QString &elementKey) +{ + if (!TaskManagerSettings::instance()->isDocked(elementKey)) + return false; + TaskManagerSettings::instance()->removeDockedElement(elementKey); + return true; +} + bool TaskManager::RequestDock(QString appID) { if (!Settings->dockedApplicationsEnabled()) @@ -502,9 +553,14 @@ void TaskManager::saveDockElementsOrder(const QStringList &appIds) const QStringList &dockedElements = TaskManagerSettings::instance()->dockedElements(); QStringList newDockedElements; for (const auto &appId : appIds) { - auto desktopElement = QString("desktop/%1").arg(appId); - if (dockedElements.contains(desktopElement) && !newDockedElements.contains(desktopElement)) { - newDockedElements.append(desktopElement); + if (appId == QStringLiteral("internal/folders/0")) + continue; + const auto folderId = canonicalLauncherFolderId(appId); + const auto element = folderId.isEmpty() + ? QStringLiteral("desktop/%1").arg(appId) + : folderId; + if (dockedElements.contains(element) && !newDockedElements.contains(element)) { + newDockedElements.append(element); } } TaskManagerSettings::instance()->setDockedElements(newDockedElements); diff --git a/panels/dock/taskmanager/taskmanager.h b/panels/dock/taskmanager/taskmanager.h index 517bd0da3..53f409dfe 100644 --- a/panels/dock/taskmanager/taskmanager.h +++ b/panels/dock/taskmanager/taskmanager.h @@ -65,6 +65,7 @@ class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManage ExecsRole, CategoriesRole, DesktopSourcePathRole, + GroupItemsRole, }; Q_ENUM(Roles) @@ -94,6 +95,8 @@ class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManage Q_INVOKABLE QString desktopIdToAppId(const QString& desktopId); Q_INVOKABLE bool requestDockByDesktopId(const QString& desktopID); Q_INVOKABLE bool requestUndockByDesktopId(const QString& desktopID); + Q_INVOKABLE bool launchGroupApplication(const QString &desktopId); + Q_INVOKABLE bool requestUndockElement(const QString &elementKey); Q_INVOKABLE bool RequestDock(QString appID); Q_INVOKABLE bool IsDocked(QString appID); Q_INVOKABLE bool RequestUndock(QString appID); @@ -126,6 +129,7 @@ private Q_SLOTS: DockItemModel *m_itemModel = nullptr; HoverPreviewProxyModel *m_hoverPreviewModel = nullptr; int queryTrashCount() const; + QPointer m_appsApplet; }; } diff --git a/panels/dock/taskmanager/taskmanagersettings.cpp b/panels/dock/taskmanager/taskmanagersettings.cpp index 4f660ddec..0303480f7 100644 --- a/panels/dock/taskmanager/taskmanagersettings.cpp +++ b/panels/dock/taskmanager/taskmanagersettings.cpp @@ -132,7 +132,7 @@ QStringList TaskManagerSettings::dockedElements() const return m_dockedElements; } -// elementId is like "desktop/sample.app.id" +// elementId is either "desktop/sample.app.id" or "internal/folders/1" bool TaskManagerSettings::isDocked(const QString &elementId) const { return m_dockedElements.contains(elementId); diff --git a/panels/dock/taskmanager/taskoverflowcontroller.cpp b/panels/dock/taskmanager/taskoverflowcontroller.cpp new file mode 100644 index 000000000..1b260b834 --- /dev/null +++ b/panels/dock/taskmanager/taskoverflowcontroller.cpp @@ -0,0 +1,295 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "taskoverflowcontroller.h" + +#include "globals.h" + +#include +#include + +#include + +namespace dock { +namespace { +constexpr qreal OverflowReleaseHysteresis = 8; +} + +TaskOverflowController::TaskOverflowController(QObject *parent) + : QObject(parent) +{ +} + +void TaskOverflowController::setEnabled(bool enabled) +{ + if (m_enabled == enabled) + return; + + m_enabled = enabled; + if (!m_enabled) + m_latched = false; + emit enabledChanged(); + scheduleSync(); +} + +void TaskOverflowController::setAvailableExtent(qreal availableExtent) +{ + if (!qFuzzyCompare(m_availableExtent + 1, availableExtent + 1)) { + m_availableExtent = availableExtent; + scheduleSync(); + } +} + +void TaskOverflowController::setMinimumReached(bool minimumReached) +{ + if (m_minimumReached != minimumReached) { + m_minimumReached = minimumReached; + scheduleSync(); + } +} + +void TaskOverflowController::setFallbackItemExtent(qreal fallbackItemExtent) +{ + if (!qFuzzyCompare(m_fallbackItemExtent + 1, fallbackItemExtent + 1)) { + m_fallbackItemExtent = fallbackItemExtent; + emit fallbackItemExtentChanged(); + scheduleSync(); + } +} + +void TaskOverflowController::setSpacing(qreal spacing) +{ + if (!qFuzzyCompare(m_spacing + 1, spacing + 1)) { + m_spacing = spacing; + emit spacingChanged(); + scheduleSync(); + } +} + +void TaskOverflowController::setVisualModel(QObject *visualModel) +{ + if (m_visualModel == visualModel) + return; + + if (m_visualModel) + disconnect(m_visualModel, nullptr, this, nullptr); + + m_visualModel = visualModel; + if (m_visualModel) + connect(m_visualModel, &QObject::destroyed, this, [this] { + m_visualModel = nullptr; + scheduleSync(); + }); + + scheduleSync(); +} + +void TaskOverflowController::setDataModel(QAbstractItemModel *dataModel) +{ + if (m_dataModel == dataModel) + return; + + disconnectDataModelSignals(); + m_dataModel = dataModel; + connectDataModelSignals(); + scheduleSync(); +} + +void TaskOverflowController::scheduleSync() +{ + if (m_syncPending) + return; + + m_syncPending = true; + QTimer::singleShot(0, this, [this] { + m_syncPending = false; + sync(); + }); +} + +void TaskOverflowController::sync() +{ + const int totalCount = modelCount(); + const qreal fullExtent = totalCount > 0 + ? totalCount * m_fallbackItemExtent + (totalCount - 1) * m_spacing + : 0; + + const bool overflowEnabled = syncEnabledState(totalCount, fullExtent); + if (!overflowEnabled || totalCount <= 0 || !std::isfinite(m_availableExtent) || m_availableExtent <= 0) { + setVisibleItemCount(totalCount); + setPopupItems({}); + return; + } + + qreal usedExtent = 0; + int visibleCount = 0; + for (int index = 0; index < totalCount; ++index) { + const qreal itemExtent = m_fallbackItemExtent; + const qreal leadingSpacing = visibleCount > 0 ? m_spacing : 0; + const int remainingCount = totalCount - (visibleCount + 1); + const qreal reservedButtonExtent = remainingCount > 0 ? m_spacing + m_fallbackItemExtent : 0; + if (usedExtent + leadingSpacing + itemExtent + reservedButtonExtent > m_availableExtent) + break; + + usedExtent += leadingSpacing + itemExtent; + ++visibleCount; + } + + if (visibleCount >= totalCount) { + setVisibleItemCount(totalCount); + setPopupItems({}); + return; + } + + QVariantList overflowItems; + overflowItems.reserve(totalCount - visibleCount); + for (int index = visibleCount; index < totalCount; ++index) { + const auto itemData = entryAt(index); + if (!itemData.isEmpty()) + overflowItems.append(itemData); + } + + setVisibleItemCount(visibleCount); + setPopupItems(overflowItems); +} + +void TaskOverflowController::onDataModelDestroyed() +{ + m_dataModel = nullptr; + scheduleSync(); +} + +int TaskOverflowController::modelCount() const +{ + if (m_visualModel) { + const auto countValue = m_visualModel->property("count"); + if (countValue.isValid()) + return countValue.toInt(); + } + + return m_dataModel ? m_dataModel->rowCount() : 0; +} + +QModelIndex TaskOverflowController::modelIndexAt(int visualIndex) const +{ + if (visualIndex < 0) + return {}; + + if (m_visualModel) { + QVariant modelIndexValue; + const bool ok = QMetaObject::invokeMethod(m_visualModel, + "modelIndex", + Q_RETURN_ARG(QVariant, modelIndexValue), + Q_ARG(int, visualIndex)); + if (ok) { + const auto index = modelIndexValue.value(); + if (index.isValid()) + return index; + } + } + + if (m_dataModel && visualIndex < m_dataModel->rowCount()) + return m_dataModel->index(visualIndex, 0); + + return {}; +} + +QVariant TaskOverflowController::dataForRole(const QModelIndex &index, const QByteArray &roleName) const +{ + if (!m_dataModel || !index.isValid()) + return {}; + + const auto role = m_dataModel->roleNames().key(roleName, -1); + if (role < 0) + return {}; + + return m_dataModel->data(index, role); +} + +QVariantMap TaskOverflowController::entryAt(int visualIndex) const +{ + const auto index = modelIndexAt(visualIndex); + if (!index.isValid()) + return {}; + + auto iconName = dataForRole(index, MODEL_ICONNAME).toString(); + if (iconName.isEmpty()) + iconName = dataForRole(index, MODEL_WINICON).toString(); + if (iconName.isEmpty()) + iconName = DEFAULT_APP_ICONNAME; + + return { + { QStringLiteral("active"), dataForRole(index, MODEL_ACTIVE).toBool() }, + { QStringLiteral("attention"), dataForRole(index, MODEL_ATTENTION).toBool() }, + { QStringLiteral("docked"), dataForRole(index, MODEL_DOCKED).toBool() }, + { QStringLiteral("itemId"), dataForRole(index, MODEL_ITEMID).toString() }, + { QStringLiteral("name"), dataForRole(index, MODEL_NAME).toString() }, + { QStringLiteral("title"), dataForRole(index, MODEL_TITLE).toString() }, + { QStringLiteral("iconName"), iconName }, + { QStringLiteral("menus"), dataForRole(index, MODEL_MENUS).toString() }, + { QStringLiteral("windows"), dataForRole(index, MODEL_WINDOWS).toStringList() }, + { QStringLiteral("groupItems"), dataForRole(index, MODEL_GROUPITEMS).toList() }, + { QStringLiteral("visualIndex"), visualIndex }, + { QStringLiteral("modelIndex"), QVariant::fromValue(index) }, + }; +} + +bool TaskOverflowController::syncEnabledState(int totalCount, qreal fullExtent) +{ + if (!m_enabled || totalCount <= 0 || !std::isfinite(m_availableExtent) || m_availableExtent <= 0) { + m_latched = false; + return false; + } + + // Enter overflow as soon as the content exceeds the available boundary. + // Keep hysteresis only when leaving overflow to avoid rapid toggling near + // the threshold without allowing visible items to overlap first. + const qreal startThreshold = m_availableExtent; + const qreal stopThreshold = std::max(0, m_availableExtent - OverflowReleaseHysteresis); + const bool nextLatched = m_latched + ? fullExtent > stopThreshold + : (m_minimumReached && fullExtent > startThreshold); + m_latched = nextLatched; + return m_latched; +} + +void TaskOverflowController::connectDataModelSignals() +{ + if (!m_dataModel) + return; + + connect(m_dataModel, &QAbstractItemModel::dataChanged, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::rowsInserted, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::rowsMoved, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::rowsRemoved, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::modelReset, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QAbstractItemModel::layoutChanged, this, &TaskOverflowController::scheduleSync); + connect(m_dataModel, &QObject::destroyed, this, &TaskOverflowController::onDataModelDestroyed); +} + +void TaskOverflowController::disconnectDataModelSignals() +{ + if (m_dataModel) + disconnect(m_dataModel, nullptr, this, nullptr); +} + +void TaskOverflowController::setVisibleItemCount(int visibleItemCount) +{ + if (m_visibleItemCount == visibleItemCount) + return; + + m_visibleItemCount = visibleItemCount; + emit visibleItemCountChanged(); +} + +void TaskOverflowController::setPopupItems(const QVariantList &items) +{ + if (m_popupItems == items) + return; + + m_popupItems = items; + emit popupItemsChanged(); +} + +} diff --git a/panels/dock/taskmanager/taskoverflowcontroller.h b/panels/dock/taskmanager/taskoverflowcontroller.h new file mode 100644 index 000000000..d4c5838a5 --- /dev/null +++ b/panels/dock/taskmanager/taskoverflowcontroller.h @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +namespace dock { + +class TaskOverflowController : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(qreal availableExtent READ availableExtent WRITE setAvailableExtent) + Q_PROPERTY(bool minimumReached READ minimumReached WRITE setMinimumReached) + Q_PROPERTY(qreal fallbackItemExtent READ fallbackItemExtent WRITE setFallbackItemExtent NOTIFY fallbackItemExtentChanged) + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + Q_PROPERTY(QObject *visualModel READ visualModel WRITE setVisualModel) + Q_PROPERTY(QAbstractItemModel *dataModel READ dataModel WRITE setDataModel) + Q_PROPERTY(int visibleItemCount READ visibleItemCount NOTIFY visibleItemCountChanged) + Q_PROPERTY(QVariantList popupItems READ popupItems NOTIFY popupItemsChanged) + +public: + explicit TaskOverflowController(QObject *parent = nullptr); + + bool isEnabled() const + { + return m_enabled; + } + void setEnabled(bool enabled); + + qreal availableExtent() const + { + return m_availableExtent; + } + void setAvailableExtent(qreal availableExtent); + + bool minimumReached() const + { + return m_minimumReached; + } + void setMinimumReached(bool minimumReached); + + qreal fallbackItemExtent() const + { + return m_fallbackItemExtent; + } + void setFallbackItemExtent(qreal fallbackItemExtent); + + qreal spacing() const + { + return m_spacing; + } + void setSpacing(qreal spacing); + + QObject *visualModel() const + { + return m_visualModel; + } + void setVisualModel(QObject *visualModel); + + QAbstractItemModel *dataModel() const + { + return m_dataModel; + } + void setDataModel(QAbstractItemModel *dataModel); + + int visibleItemCount() const + { + return m_visibleItemCount; + } + QVariantList popupItems() const + { + return m_popupItems; + } + + Q_INVOKABLE void scheduleSync(); + +Q_SIGNALS: + void enabledChanged(); + void fallbackItemExtentChanged(); + void spacingChanged(); + void visibleItemCountChanged(); + void popupItemsChanged(); + +private Q_SLOTS: + void onDataModelDestroyed(); + +private: + void sync(); + int modelCount() const; + QModelIndex modelIndexAt(int visualIndex) const; + QVariant dataForRole(const QModelIndex &index, const QByteArray &roleName) const; + QVariantMap entryAt(int visualIndex) const; + bool syncEnabledState(int totalCount, qreal fullExtent); + + void connectDataModelSignals(); + void disconnectDataModelSignals(); + + void setVisibleItemCount(int visibleItemCount); + void setPopupItems(const QVariantList &items); + + bool m_enabled = true; + qreal m_availableExtent = 0; + bool m_minimumReached = false; + qreal m_fallbackItemExtent = 1; + qreal m_spacing = 0; + QPointer m_visualModel; + QPointer m_dataModel; + bool m_latched = false; + int m_visibleItemCount = 0; + QVariantList m_popupItems; + bool m_syncPending = false; +}; + +} diff --git a/panels/dock/taskmanager/textcalculator.cpp b/panels/dock/taskmanager/textcalculator.cpp index b3f5c6e4a..0516141b9 100644 --- a/panels/dock/taskmanager/textcalculator.cpp +++ b/panels/dock/taskmanager/textcalculator.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace dock { @@ -169,7 +170,19 @@ void TextCalculator::scheduleCalculation() resetCalculatedWidths(); return; } - calculateOptimalTextWidth(); + + if (m_calculationPending) + return; + + m_calculationPending = true; + QTimer::singleShot(0, this, [this] { + m_calculationPending = false; + if (!m_enabled || !m_dataModel) { + resetCalculatedWidths(); + return; + } + calculateOptimalTextWidth(); + }); } void TextCalculator::resetCalculatedWidths() @@ -267,12 +280,9 @@ void TextCalculator::calculateOptimalTextWidth() const int appCount = titles.size(); if (appCount <= 0 || m_remainingSpace <= 0) { - if (m_optimalSingleTextWidth != 0.0) { + if (!qFuzzyIsNull(m_optimalSingleTextWidth) || !qFuzzyIsNull(m_totalWidth)) { qCDebug(textCalculatorLog) << "Setting optimal width to 0 (no apps or no space)"; - m_optimalSingleTextWidth = 0.0; - m_totalWidth = 0; - emit optimalSingleTextWidthChanged(); - emit totalWidthChanged(); + resetCalculatedWidths(); } return; } @@ -317,15 +327,20 @@ void TextCalculator::calculateOptimalTextWidth() } } - // Update results - if (!qFuzzyCompare(m_optimalSingleTextWidth, newOptimalWidth)) { + // Publish a single, internally consistent layout result. totalWidth can change + // even when the selected character count (and therefore optimal width) does not. + const bool optimalWidthChanged = !qFuzzyCompare(m_optimalSingleTextWidth, newOptimalWidth); + const bool totalWidthDidChange = !qFuzzyCompare(m_totalWidth, newTotalWidth); + if (optimalWidthChanged || totalWidthDidChange) { qCDebug(textCalculatorLog) << "Optimal text width changed from" << m_optimalSingleTextWidth << "to" << newOptimalWidth << "App count:" << appCount << "Remaining space:" << m_remainingSpace << "Total required:" << newTotalWidth << "Char count:" << charCount << "spacing:" << m_spacing; m_optimalSingleTextWidth = newOptimalWidth; - emit optimalSingleTextWidthChanged(); m_totalWidth = newTotalWidth; - emit totalWidthChanged(); + if (optimalWidthChanged) + emit optimalSingleTextWidthChanged(); + if (totalWidthDidChange) + emit totalWidthChanged(); } } diff --git a/panels/dock/taskmanager/textcalculator.h b/panels/dock/taskmanager/textcalculator.h index 6152b3b1d..665e44941 100644 --- a/panels/dock/taskmanager/textcalculator.h +++ b/panels/dock/taskmanager/textcalculator.h @@ -184,6 +184,7 @@ private slots: QPointer m_dataModel; qreal m_remainingSpace; bool m_enabled; + bool m_calculationPending = false; QHash m_baselineWidthCache; // Cache for baseline widths of different character counts };