diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b12d2217..23b9f3562 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,52 @@ if(Qt${QT_VERSION_MAJOR}_VERSION VERSION_GREATER_EQUAL 6.10) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS WaylandClientPrivate WaylandCompositorPrivate REQUIRED) endif() find_package(Dtk${DTK_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui) +set(ICU_USE_STATIC_LIBS OFF) find_package(ICU 74.2 REQUIRED COMPONENTS uc i18n io) + +get_filename_component(DS_ICU_PREFIX "${ICU_INCLUDE_DIR}" DIRECTORY) +function(ds_resolve_icu_shared_library output_var library_basename) + set(search_roots + "${DS_ICU_PREFIX}/lib" + "${DS_ICU_PREFIX}/lib/*" + "/usr/local/lib" + "/usr/local/lib/*" + "/usr/lib" + "/usr/lib/*" + "/lib" + "/lib/*" + ) + + foreach(search_root IN LISTS search_roots) + file(GLOB versioned_candidates LIST_DIRECTORIES false "${search_root}/${library_basename}.so.*") + list(SORT versioned_candidates) + list(REVERSE versioned_candidates) + + foreach(candidate IN LISTS versioned_candidates) + get_filename_component(resolved_library "${candidate}" REALPATH) + if (EXISTS "${resolved_library}") + set(${output_var} "${resolved_library}" PARENT_SCOPE) + return() + endif() + endforeach() + + file(GLOB soname_candidates LIST_DIRECTORIES false "${search_root}/${library_basename}.so") + foreach(candidate IN LISTS soname_candidates) + get_filename_component(resolved_library "${candidate}" REALPATH) + if (EXISTS "${resolved_library}") + set(${output_var} "${resolved_library}" PARENT_SCOPE) + return() + endif() + endforeach() + endforeach() + + message(FATAL_ERROR "Could not resolve an existing ICU shared library for ${library_basename}") +endfunction() + +ds_resolve_icu_shared_library(DS_ICU_UC_LIBRARY libicuuc) +ds_resolve_icu_shared_library(DS_ICU_I18N_LIBRARY libicui18n) +ds_resolve_icu_shared_library(DS_ICU_IO_LIBRARY libicuio) + find_package(WaylandProtocols REQUIRED) find_package(PkgConfig REQUIRED) diff --git a/LICENSES/OFL-1.1.txt b/LICENSES/OFL-1.1.txt new file mode 100644 index 000000000..19387e8ba --- /dev/null +++ b/LICENSES/OFL-1.1.txt @@ -0,0 +1,89 @@ +SIL OPEN FONT LICENSE + +Version 1.1 - 26 February 2007 + +PREAMBLE + +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to any +document created using the fonts or their derivatives. + +DEFINITIONS + +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or in +the appropriate machine-readable metadata fields within text or binary +files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name +as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any Modified +Version, except to acknowledge the contribution(s) of the Copyright +Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this license, and must not be distributed +under any other license. The requirement for fonts to remain under this +license does not apply to any document created using the Font Software. + +TERMINATION + +This license becomes null and void if any of the above conditions are not +met. + +DISCLAIMER + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/REUSE.toml b/REUSE.toml index 35e9e7679..7f091ba92 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -92,3 +92,9 @@ path = "toolGenerate/**/**" precedence = "aggregate" SPDX-FileCopyrightText = "None" SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "shell/fonts/ElmsSans-Regular.ttf" +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 The Elms Sans Project Authors" +SPDX-License-Identifier = "OFL-1.1" diff --git a/applets/dde-appearance/appearanceapplet.cpp b/applets/dde-appearance/appearanceapplet.cpp index 68e3c4a86..1b2f7f2f5 100644 --- a/applets/dde-appearance/appearanceapplet.cpp +++ b/applets/dde-appearance/appearanceapplet.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 @@ -7,6 +7,9 @@ #include "pluginfactory.h" #include +#include +#include +#include #include #include @@ -14,6 +17,14 @@ DCORE_USE_NAMESPACE DS_BEGIN_NAMESPACE namespace dde { +namespace { +bool isOpacityChangeType(const QString &type) +{ + return type.compare(QStringLiteral("opacity"), Qt::CaseInsensitive) == 0 + || type.compare(QStringLiteral("windowopacity"), Qt::CaseInsensitive) == 0; +} +} + AppearanceApplet::AppearanceApplet(QObject *parent) : DApplet(parent) { @@ -39,11 +50,11 @@ bool AppearanceApplet::load() qreal AppearanceApplet::opacity() const { - if (!m_interface) + if (m_opacity < 0) return -1; // The minimum opacity is 0.2 - return std::max(0.2, m_interface->opacity()); + return std::max(0.2, m_opacity); } void AppearanceApplet::initDBusProxy() @@ -59,11 +70,42 @@ void AppearanceApplet::initDBusProxy() return; } - m_interface->setSync(false); - QObject::connect(m_interface.data(), &org::deepin::dde::Appearance1::OpacityChanged, this, &AppearanceApplet::opacityChanged); + QObject::connect(m_interface.data(), &org::deepin::dde::Appearance1::Changed, this, + [this](const QString &type, const QString &) { + if (isOpacityChangeType(type)) { + refreshOpacity(); + Q_EMIT opacityChanged(); + } + }); + QObject::connect(m_interface.data(), &org::deepin::dde::Appearance1::Refreshed, this, + [this](const QString &type) { + if (isOpacityChangeType(type)) { + refreshOpacity(); + Q_EMIT opacityChanged(); + } + }); + refreshOpacity(); Q_EMIT opacityChanged(); } +void AppearanceApplet::refreshOpacity() +{ + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.deepin.dde.Appearance1"), + QStringLiteral("/org/deepin/dde/Appearance1"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("Get")); + message << QStringLiteral("org.deepin.dde.Appearance1") << QStringLiteral("Opacity"); + + QDBusReply reply = QDBusConnection::sessionBus().call(message); + if (!reply.isValid()) { + qWarning() << "Failed to get Appearance opacity, error:" << reply.error(); + m_opacity = -1; + return; + } + + m_opacity = reply.value().variant().toReal(); +} + D_APPLET_CLASS(AppearanceApplet) } DS_END_NAMESPACE diff --git a/applets/dde-appearance/appearanceapplet.h b/applets/dde-appearance/appearanceapplet.h index 956b1e5bf..4f2ab8bc1 100644 --- a/applets/dde-appearance/appearanceapplet.h +++ b/applets/dde-appearance/appearanceapplet.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 @@ -24,8 +24,10 @@ class AppearanceApplet : public DApplet void opacityChanged(); private: void initDBusProxy(); + void refreshOpacity(); private: QScopedPointer m_interface; + qreal m_opacity = -1; }; } diff --git a/applets/dde-apps/CMakeLists.txt b/applets/dde-apps/CMakeLists.txt index e21172bd5..30d561b07 100644 --- a/applets/dde-apps/CMakeLists.txt +++ b/applets/dde-apps/CMakeLists.txt @@ -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 @@ -6,29 +6,46 @@ find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS DB find_package(DDEApplicationManager REQUIRED) find_package(yaml-cpp REQUIRED) +set(DDE_APPS_DBUS_API_FALLBACK_DIR "${PROJECT_SOURCE_DIR}/panels/dock/taskmanager/api/amdbus") +set(DDE_APPS_APPLICATION_XML "${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ApplicationManager1.Application.xml") +set(DDE_APPS_APPLICATION_MANAGER_XML "${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ApplicationManager1.xml") +set(DDE_APPS_OBJECT_MANAGER_XML "${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ObjectManager1.xml") + +if (NOT EXISTS "${DDE_APPS_APPLICATION_XML}") + set(DDE_APPS_APPLICATION_XML "${DDE_APPS_DBUS_API_FALLBACK_DIR}/org.desktopspec.ApplicationManager1.Application.xml") +endif() + +if (NOT EXISTS "${DDE_APPS_APPLICATION_MANAGER_XML}") + set(DDE_APPS_APPLICATION_MANAGER_XML "${DDE_APPS_DBUS_API_FALLBACK_DIR}/org.desktopspec.ApplicationManager1.xml") +endif() + +if (NOT EXISTS "${DDE_APPS_OBJECT_MANAGER_XML}") + set(DDE_APPS_OBJECT_MANAGER_XML "${DDE_APPS_DBUS_API_FALLBACK_DIR}/org.desktopspec.ObjectManager1.xml") +endif() + set_source_files_properties( - ${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ApplicationManager1.Application.xml + ${DDE_APPS_APPLICATION_XML} PROPERTIES INCLUDE api/types/am.h CLASSNAME Application ) set_source_files_properties( - ${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ApplicationManager1.xml + ${DDE_APPS_APPLICATION_MANAGER_XML} PROPERTIES INCLUDE api/types/am.h CLASSNAME ApplicationManager ) set_source_files_properties( - ${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ObjectManager1.xml + ${DDE_APPS_OBJECT_MANAGER_XML} PROPERTIES INCLUDE api/types/am.h CLASSNAME ObjectManager ) qt_add_dbus_interfaces( DBUS_INTERFACES - ${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ApplicationManager1.Application.xml - ${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ApplicationManager1.xml - ${DDE_APPLICATION_MANAGER_DBUS_API_DIR}/org.desktopspec.ObjectManager1.xml + ${DDE_APPS_APPLICATION_XML} + ${DDE_APPS_APPLICATION_MANAGER_XML} + ${DDE_APPS_OBJECT_MANAGER_XML} ) diff --git a/applets/dde-apps/appgroup.cpp b/applets/dde-apps/appgroup.cpp index c37a6125f..ad00f3b7c 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 @@ -14,12 +14,15 @@ 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))) + : AppItem(normalizeGroupId(groupId), AppItemModel::FolderItemType) + , 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}" + QObject::connect(m_itemsPage, &ItemsPage::nameChanged, m_itemsPage, [this]() { + setAppName(m_itemsPage->name()); + }); + // folder id is the numeric suffix of the normalized launcher group id. setFolderId(parseGroupId(groupId)); for (const QStringList &items : appIDs) { @@ -49,19 +52,38 @@ ItemsPage *AppGroup::itemsPage() bool AppGroup::idIsFolder(const QString & id) { - return id.startsWith(QStringLiteral("internal/folder/")); + bool isNumericId = false; + id.toInt(&isNumericId); + + return isNumericId || + id.startsWith(QStringLiteral("internal/folder/")) || + id.startsWith(QStringLiteral("internal/folders/")) || + id.startsWith(QStringLiteral("internal/group/")); +} + +QString AppGroup::normalizeGroupId(const QString &id) +{ + bool isNumericId = false; + const int numericId = id.toInt(&isNumericId); + if (isNumericId) { + return groupIdFromNumber(numericId); + } + + if (!idIsFolder(id)) { + return id; + } + + return QStringLiteral("internal/folders/%1").arg(parseGroupId(id)); } 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(); + return id.section(QLatin1Char('/'), -1).toInt(); } void AppGroup::setItemsPerPage(int number) @@ -75,4 +97,3 @@ void AppGroup::setFolderId(int folderId) } } - diff --git a/applets/dde-apps/appgroup.h b/applets/dde-apps/appgroup.h index 97eb4805c..90cac7dab 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 @@ -19,6 +19,7 @@ class AppGroup : public AppItem ItemsPage * itemsPage(); static bool idIsFolder(const QString & id); + static QString normalizeGroupId(const QString &id); static QString groupIdFromNumber(int groupId); static int parseGroupId(const QString & id); diff --git a/applets/dde-apps/appgroupmanager.cpp b/applets/dde-apps/appgroupmanager.cpp index b5ec71b16..24137d842 100644 --- a/applets/dde-apps/appgroupmanager.cpp +++ b/applets/dde-apps/appgroupmanager.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 @@ -12,6 +12,22 @@ namespace apps { +static QString normalizeResolvableGroupId(const QString &groupId) +{ + bool isNumericGroupId = false; + groupId.toInt(&isNumericGroupId); + + if (AppGroup::idIsFolder(groupId)) { + return AppGroup::normalizeGroupId(groupId); + } + + if (isNumericGroupId) { + return AppGroup::groupIdFromNumber(groupId.toInt()); + } + + return {}; +} + AppGroupManager::AppGroupManager(AMAppItemModel * referenceModel, QObject *parent) : QStandardItemModel(parent) , m_referenceModel(referenceModel) @@ -43,13 +59,37 @@ QVariant AppGroupManager::data(const QModelIndex &index, int role) const if (!index.isValid()) return QVariant(); - if (role == GroupIdRole) { - return index.row(); - } - return QStandardItemModel::data(index, role); } +QHash AppGroupManager::roleNames() const +{ + return { + {GroupIdRole, QByteArrayLiteral("groupId")}, + {GroupItemsPerPageRole, QByteArrayLiteral("groupItemsPerPage")}, + {AppItemModel::DesktopIdRole, QByteArrayLiteral("desktopId")}, + {AppItemModel::NameRole, QByteArrayLiteral("name")}, + {AppItemModel::IconNameRole, QByteArrayLiteral("iconName")}, + {AppItemModel::StartUpWMClassRole, QByteArrayLiteral("startupWMClass")}, + {AppItemModel::NoDisplayRole, QByteArrayLiteral("noDisplay")}, + {AppItemModel::ActionsRole, QByteArrayLiteral("actions")}, + {AppItemModel::DDECategoryRole, QByteArrayLiteral("ddeCategory")}, + {AppItemModel::CategoriesRole, QByteArrayLiteral("categories")}, + {AppItemModel::InstalledTimeRole, QByteArrayLiteral("installedTime")}, + {AppItemModel::LastLaunchedTimeRole, QByteArrayLiteral("lastLaunchedTime")}, + {AppItemModel::LaunchedTimesRole, QByteArrayLiteral("launchedTimes")}, + {AppItemModel::DockedRole, QByteArrayLiteral("docked")}, + {AppItemModel::OnDesktopRole, QByteArrayLiteral("onDesktop")}, + {AppItemModel::AutoStartRole, QByteArrayLiteral("autoStart")}, + {AppItemModel::AppTypeRole, QByteArrayLiteral("appType")}, + {AppItemModel::XLingLongRole, QByteArrayLiteral("isLingLong")}, + {AppItemModel::IdRole, QByteArrayLiteral("id")}, + {AppItemModel::XCreatedByRole, QByteArrayLiteral("xCreatedBy")}, + {AppItemModel::ExecsRole, QByteArrayLiteral("execs")}, + {AppItemModel::DesktopSourcePathRole, QByteArrayLiteral("desktopSourcePath")}, + }; +} + // Find the item's location. If folderId is -1, search all folders. ItemPosition AppGroupManager::findItem(const QString &appId, int folderId) { @@ -121,6 +161,44 @@ ItemsPage * AppGroupManager::groupPages(int groupId) return folder->itemsPage(); } +QStringList AppGroupManager::groupItems(const QString &groupId) const +{ + const QString normalizedGroupId = normalizeResolvableGroupId(groupId); + if (normalizedGroupId.isEmpty()) { + return {}; + } + + for (int i = 0; i < rowCount(); ++i) { + auto folder = static_cast(item(i)); + if (!folder || AppGroup::normalizeGroupId(folder->appId()) != normalizedGroupId) { + continue; + } + + return folder->itemsPage()->allArrangedItems(); + } + + return {}; +} + +QString AppGroupManager::groupDisplayName(const QString &groupId) const +{ + const QString normalizedGroupId = normalizeResolvableGroupId(groupId); + if (normalizedGroupId.isEmpty()) { + return {}; + } + + for (int i = 0; i < rowCount(); ++i) { + auto folder = static_cast(item(i)); + if (!folder || AppGroup::normalizeGroupId(folder->appId()) != normalizedGroupId) { + continue; + } + + return folder->itemsPage() ? folder->itemsPage()->name() : folder->appName(); + } + + return {}; +} + void AppGroupManager::bringToFromt(const QString & id) { const ItemPosition origPos = findItem(id); @@ -342,6 +420,8 @@ void AppGroupManager::loadAppGroupInfo() if (groupId.isEmpty()) { groupId = assignGroupId(); + } else { + groupId = AppGroup::normalizeGroupId(groupId); } appendGroup(groupId, name, items); } @@ -359,7 +439,7 @@ void AppGroupManager::saveAppGroupInfo() for (int i = 0; i < rowCount(); i++) { auto folder = group(index(i, 0)); QVariantMap valueMap; - valueMap.insert("name", folder->data(AppItemModel::NameRole)); + valueMap.insert("name", folder->itemsPage() ? folder->itemsPage()->name() : folder->data(AppItemModel::NameRole)); valueMap.insert("groupId", folder->appId()); valueMap.insert("appItems", fromListOfStringList(folder->pages())); list << valueMap; @@ -373,15 +453,15 @@ QString AppGroupManager::assignGroupId() const QStringList knownGroupIds; for (int i = 0; i < rowCount(); i++) { auto group = index(i, 0); - knownGroupIds.append(group.data(AppItemModel::DesktopIdRole).toString()); + knownGroupIds.append(AppGroup::normalizeGroupId(group.data(AppItemModel::DesktopIdRole).toString())); } int idNumber = 0; - while (knownGroupIds.contains(QString("internal/group/%1").arg(idNumber))) { + while (knownGroupIds.contains(AppGroup::groupIdFromNumber(idNumber))) { idNumber++; } - return QString("internal/group/%1").arg(idNumber); + return AppGroup::groupIdFromNumber(idNumber); } AppGroup * AppGroupManager::appendGroup(int groupId, QString groupName, const QList &appItemIDs) @@ -392,7 +472,7 @@ AppGroup * AppGroupManager::appendGroup(int groupId, QString groupName, const QL AppGroup * AppGroupManager::appendGroup(QString groupId, QString groupName, const QList &appItemIDs) { - auto p = new AppGroup(groupId, groupName, appItemIDs); + auto p = new AppGroup(AppGroup::normalizeGroupId(groupId), groupName, appItemIDs); appendRow(p); return p; } diff --git a/applets/dde-apps/appgroupmanager.h b/applets/dde-apps/appgroupmanager.h index a51745b06..f9694e2fb 100644 --- a/applets/dde-apps/appgroupmanager.h +++ b/applets/dde-apps/appgroupmanager.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 @@ -55,6 +55,7 @@ class AppGroupManager : public QStandardItemModel explicit AppGroupManager(AMAppItemModel * referenceModel, QObject* parent = nullptr); QVariant data(const QModelIndex &index, int role = GroupIdRole) const override; + QHash roleNames() const override; Q_INVOKABLE ItemPosition findItem(const QString &appId, int folderId = -1); Q_INVOKABLE void appendItemToGroup(const QString &appId, int groupId); @@ -63,6 +64,8 @@ class AppGroupManager : public QStandardItemModel AppGroup * group(int groupId); AppGroup * group(QModelIndex idx); Q_INVOKABLE ItemsPage * groupPages(int groupId); + Q_INVOKABLE QStringList groupItems(const QString &groupId) const; + Q_INVOKABLE QString groupDisplayName(const QString &groupId) const; Q_INVOKABLE void bringToFromt(const QString & id); Q_INVOKABLE void commitRearrangeOperation(const QString & dragId, const QString & dropId, DndOperation operation, int pageHint = -1); diff --git a/applets/dde-apps/appsdockedhelper.cpp b/applets/dde-apps/appsdockedhelper.cpp index c1380accf..eaf0f6b6b 100644 --- a/applets/dde-apps/appsdockedhelper.cpp +++ b/applets/dde-apps/appsdockedhelper.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 @@ -11,6 +11,18 @@ #include namespace apps { +namespace { +QString desktopIdFromDockedElement(const QString &element) +{ + static const QString prefix = QStringLiteral("desktop/"); + if (!element.startsWith(prefix)) { + return {}; + } + + return element.mid(prefix.size()).trimmed(); +} +} + AppsDockedHelper* AppsDockedHelper::instance() { static AppsDockedHelper* _instance = nullptr; @@ -29,6 +41,18 @@ AppsDockedHelper::AppsDockedHelper(QObject *parent) // TODO: remove yaml and rewrite this auto updateDockedDesktopfiles = [this](){ m_dockedDesktopIDs.clear(); + const QStringList dockedElements = m_config->value(QStringLiteral("dockedElements")).toStringList(); + for (const QString &element : dockedElements) { + const QString desktopId = desktopIdFromDockedElement(element); + if (!desktopId.isEmpty()) { + m_dockedDesktopIDs.insert(desktopId); + } + } + + if (!m_dockedDesktopIDs.isEmpty() || !m_config->isDefaultValue(QStringLiteral("dockedElements"))) { + return; + } + auto dcokedDesktopFilesStrList = m_config->value("Docked_Items").toStringList(); foreach(auto dcokedDesktopFilesStr, dcokedDesktopFilesStrList) { YAML::Node node; @@ -52,7 +76,7 @@ AppsDockedHelper::AppsDockedHelper(QObject *parent) }; connect(m_config, &DConfig::valueChanged, this, [this, updateDockedDesktopfiles](const QString &key){ - if (key != "Docked_Items") return; + if (key != "Docked_Items" && key != "dockedElements") return; updateDockedDesktopfiles(); }); @@ -71,4 +95,3 @@ void AppsDockedHelper::setDocked(const QString &appId, bool docked) // TODO } } - diff --git a/cmake/DDEShellPackageMacros.cmake b/cmake/DDEShellPackageMacros.cmake index 12741dbcc..89fec2277 100644 --- a/cmake/DDEShellPackageMacros.cmake +++ b/cmake/DDEShellPackageMacros.cmake @@ -14,6 +14,8 @@ macro(ds_build_package) ) set(package_dirs ${PROJECT_BINARY_DIR}/packages/${_config_PACKAGE}/) add_custom_command(TARGET ${_config_PACKAGE}_package POST_BUILD + COMMAND ${CMAKE_COMMAND} -E remove_directory ${package_dirs} + COMMAND ${CMAKE_COMMAND} -E make_directory ${package_dirs} COMMAND ${CMAKE_COMMAND} -E copy_directory ${package_root_dir} ${package_dirs} ) @@ -79,6 +81,8 @@ function(ds_handle_package_translation) ) set(package_dirs ${PROJECT_BINARY_DIR}/packages/${_config_PACKAGE}/) + set(translation_lupdate_target ${_config_PACKAGE}_translation_lupdate) + set(translation_lrelease_target ${_config_PACKAGE}_translation_lrelease) # FIXME: not working on Qt 6.7 # set_source_files_properties(${TRANSLATION_FILES} @@ -93,10 +97,14 @@ function(ds_handle_package_translation) TS_FILES ${TRANSLATION_FILES} SOURCES ${_config_QML_FILES} ${_config_SOURCE_FILES} QM_FILES_OUTPUT_VARIABLE TRANSLATED_FILES + LUPDATE_TARGET ${translation_lupdate_target} + LRELEASE_TARGET ${translation_lrelease_target} LUPDATE_OPTIONS -no-obsolete -no-ui-lines -locations none IMMEDIATE_CALL ) + add_dependencies(${_config_PACKAGE}_translation ${translation_lrelease_target}) + # /usr/share/dde-shell/org.deepin.xxx/translations/org.deepin.xxx.qm install(FILES ${TRANSLATED_FILES} DESTINATION ${DDE_SHELL_TRANSLATION_INSTALL_DIR}/${_config_PACKAGE}/translations) endfunction() diff --git a/debian/dde-shell.install b/debian/dde-shell.install index 62683587a..bd0ff4698 100644 --- a/debian/dde-shell.install +++ b/debian/dde-shell.install @@ -1,4 +1,5 @@ usr/bin/* +usr/lib/*/dde-shell/libtray_loader_font_sync.so usr/lib/*/dde-shell/org.deepin.ds.dde-am* usr/lib/*/dde-shell/org.deepin.ds.dde-appearance* usr/lib/*/dde-shell/org.deepin.ds.dde-apps* @@ -30,6 +31,8 @@ usr/share/dsg/configs/org.deepin.dde.shell/org.deepin.ds.dde-apps.json usr/share/dsg/configs/org.deepin.dde.shell/org.deepin.ds.dock.json usr/share/dsg/configs/org.deepin.dde.shell/org.deepin.ds.dock.taskmanager.json usr/share/dsg/configs/org.deepin.dde.shell/org.deepin.ds.dock.tray.json +usr/share/dsg/configs/org.deepin.dde.dock/ +usr/share/dsg/configs/org.deepin.dde.tray-loader/ usr/share/dsg/configs/org.deepin.ds.dock/ usr/share/deepin-debug-config/deepin-debug-config.d/*.json usr/share/deepin-log-viewer/deepin-log.conf.d/*.json diff --git a/frame/dsqmlglobal.cpp b/frame/dsqmlglobal.cpp index 9e9ab2e6a..1bdad50f1 100644 --- a/frame/dsqmlglobal.cpp +++ b/frame/dsqmlglobal.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 @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,34 @@ DCORE_USE_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(dsLog) +namespace { + +QString shellDataFontFamily() +{ + static QString dataFontFamily; + static bool initialized = false; + + if (initialized) { + return dataFontFamily; + } + + initialized = true; + const int fontId = QFontDatabase::addApplicationFont(QStringLiteral(":/shell/fonts/ElmsSans-Regular.ttf")); + if (fontId < 0) { + qCWarning(dsLog) << "Failed to load shell data font resource"; + return dataFontFamily; + } + + const QStringList families = QFontDatabase::applicationFontFamilies(fontId); + if (!families.isEmpty()) { + dataFontFamily = families.constFirst(); + } + + return dataFontFamily; +} + +} // namespace + class DQmlGlobalPrivate : public DObjectPrivate { public: @@ -87,6 +116,11 @@ DApplet *DQmlGlobal::rootApplet() const return DPluginLoader::instance()->rootApplet(); } +QString DQmlGlobal::dataFontFamily() const +{ + return shellDataFontFamily(); +} + void DQmlGlobal::singleShot(int msec, QJSValue callback) { if (!callback.isCallable()) { diff --git a/frame/layershell/x11dlayershellemulation.cpp b/frame/layershell/x11dlayershellemulation.cpp index 08d6f76ba..54253fd8f 100644 --- a/frame/layershell/x11dlayershellemulation.cpp +++ b/frame/layershell/x11dlayershellemulation.cpp @@ -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 @@ -30,11 +30,16 @@ LayerShellEmulation::LayerShellEmulation(QWindow* window, QObject *parent) { onLayerChanged(); connect(m_dlayerShellWindow, &DLayerShellWindow::layerChanged, this, &LayerShellEmulation::onLayerChanged); + connect(m_window, &QWindow::visibleChanged, this, [this](bool) { + onLayerChanged(); + }); + connect(m_window, &QWindow::visibilityChanged, this, [this](QWindow::Visibility) { + onLayerChanged(); + }); onPositionChanged(); connect(m_dlayerShellWindow, &DLayerShellWindow::anchorsChanged, this, &LayerShellEmulation::onPositionChanged); connect(m_dlayerShellWindow, &DLayerShellWindow::marginsChanged, this, &LayerShellEmulation::onPositionChanged); - connect(m_dlayerShellWindow, &DLayerShellWindow::geometryHintsChanged, this, &LayerShellEmulation::onPositionChanged); onExclusionZoneChanged(); m_exclusionZoneChangedTimer.setSingleShot(true); @@ -72,9 +77,6 @@ LayerShellEmulation::LayerShellEmulation(QWindow* window, QObject *parent) onScopeChanged(); connect(m_dlayerShellWindow, &DLayerShellWindow::scopeChanged, this, &LayerShellEmulation::onScopeChanged); - onInputRegionChanged(); - connect(m_dlayerShellWindow, &DLayerShellWindow::inputRegionChanged, this, &LayerShellEmulation::onInputRegionChanged); - // connect(m_dlayerShellWindow, &DS_NAMESPACE::DLayerShellWindow::keyboardInteractivityChanged, this, &LayerShellEmulation::onKeyboardInteractivityChanged); } @@ -90,6 +92,10 @@ LayerShellEmulation::LayerShellEmulation(QWindow* window, QObject *parent) void LayerShellEmulation::onLayerChanged() { auto xcbWindow = dynamic_cast(m_window->handle()); + if (!xcbWindow) { + return; + } + switch (m_dlayerShellWindow->layer()) { case DLayerShellWindow::LayerBackground: { m_window->setFlags(m_window->flags() & ~Qt::WindowStaysOnBottomHint); @@ -122,30 +128,17 @@ void LayerShellEmulation::onPositionChanged() { auto anchors = m_dlayerShellWindow->anchors(); auto screen = m_window->screen(); - if (!screen) { - return; - } - - int targetWidth = m_window->width(); - int targetHeight = m_window->height(); - if (m_dlayerShellWindow->preferredWidth() > 0) { - targetWidth = m_dlayerShellWindow->preferredWidth(); - } - if (m_dlayerShellWindow->preferredHeight() > 0) { - targetHeight = m_dlayerShellWindow->preferredHeight(); - } - auto screenRect = screen->geometry(); - auto x = screenRect.left() + (screenRect.width() - targetWidth) / 2; - auto y = screenRect.top() + (screenRect.height() - targetHeight) / 2; + auto x = screenRect.left() + (screenRect.width() - m_window->width()) / 2; + auto y = screenRect.top() + (screenRect.height() - m_window->height()) / 2; if (anchors & DLayerShellWindow::AnchorRight) { // https://doc.qt.io/qt-6/qrect.html#right - x = (screen->geometry().right() + 1 - targetWidth - m_dlayerShellWindow->rightMargin()); + x = (screen->geometry().right() + 1 - m_window->width() - m_dlayerShellWindow->rightMargin()); } if (anchors & DLayerShellWindow::AnchorBottom) { // https://doc.qt.io/qt-6/qrect.html#bottom - y = (screen->geometry().bottom() + 1 - targetHeight - m_dlayerShellWindow->bottomMargin()); + y = (screen->geometry().bottom() + 1 - m_window->height() - m_dlayerShellWindow->bottomMargin()); } if (anchors & DLayerShellWindow::AnchorLeft) { x = (screen->geometry().left() + m_dlayerShellWindow->leftMargin()); @@ -154,7 +147,7 @@ void LayerShellEmulation::onPositionChanged() y = (screen->geometry().top() + m_dlayerShellWindow->topMargin()); } - QRect rect(x, y, targetWidth, targetHeight); + QRect rect(x, y, m_window->width(), m_window->height()); const bool horizontallyConstrained = anchors.testFlags({DLayerShellWindow::AnchorLeft, DLayerShellWindow::AnchorRight}); const bool verticallyConstrained = anchors.testFlags({DLayerShellWindow::AnchorTop, DLayerShellWindow::AnchorBottom}); @@ -168,9 +161,8 @@ void LayerShellEmulation::onPositionChanged() rect.setHeight(screen->geometry().height() - m_dlayerShellWindow->topMargin() - m_dlayerShellWindow->bottomMargin()); } - if (m_window->geometry() != rect) { - m_window->setGeometry(rect); - } + m_window->setGeometry(rect); + onLayerChanged(); } /** @@ -333,7 +325,6 @@ void LayerShellEmulation::onInputRegionChanged() } if (m_dlayerShellWindow->inputRegion().isNull()) { - // Reset input region (no shape constraint) xcb_shape_mask(x11Application->connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, m_window->winId(), 0, 0, XCB_NONE); xcb_flush(x11Application->connection()); return; @@ -352,8 +343,6 @@ void LayerShellEmulation::onInputRegionChanged() rects.append(rect); } - // Set the input shape via XCB - // If rects vector is empty, the window will become completely transparent to input clicks (unclickable) xcb_shape_rectangles(x11Application->connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, m_window->winId(), 0, 0, rects.size(), rects.data()); diff --git a/frame/pluginloader.cpp b/frame/pluginloader.cpp index 01ab0bd31..05c617e4d 100644 --- a/frame/pluginloader.cpp +++ b/frame/pluginloader.cpp @@ -130,33 +130,67 @@ class DPluginLoaderPrivate : public DObjectPrivate result << DDE_SHELL_PLUGIN_INSTALL_DIR; + QStringList librarySubdirs {QStringLiteral("lib/dde-shell")}; + const QString installPluginDir = QString::fromLocal8Bit(DDE_SHELL_PLUGIN_INSTALL_DIR); + const int installLibIndex = installPluginDir.lastIndexOf(QStringLiteral("/lib")); + if (installLibIndex >= 0) { + const QString libSubdir = installPluginDir.mid(installLibIndex + 1, installPluginDir.size() - installLibIndex - 1 - QStringLiteral("/dde-shell").size()); + if (!libSubdir.isEmpty()) { + const QString resolvedSubdir = libSubdir + QStringLiteral("/dde-shell"); + if (!librarySubdirs.contains(resolvedSubdir)) { + librarySubdirs.prepend(resolvedSubdir); + } + } + } + + for (const auto &dataDir : QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)) { + if (!dataDir.endsWith(QStringLiteral("/share"))) { + continue; + } + + const QString prefix = dataDir.left(dataDir.size() - QStringLiteral("/share").size()); + for (const auto &librarySubdir : librarySubdirs) { + const QString pluginDir = QDir(prefix).absoluteFilePath(librarySubdir); + if (QDir(pluginDir).exists() && !result.contains(pluginDir)) { + result << pluginDir; + } + } + } + qCDebug(dsLog()) << "Builtin plugin paths" << result; return result; } - bool existPlugin(const DPluginMetaData &data) const + QString pluginFilePath(const DPluginMetaData &data) const { const QString fileName = data.pluginId(); D_QC(DPluginLoader); for (const auto &item : q->pluginDirs()) { const QDir dir(item); - if (dir.exists(fileName + PluginSuffix)) - return true; + const QString absoluteFilePath = dir.absoluteFilePath(fileName + PluginSuffix); + if (QFileInfo::exists(absoluteFilePath)) + return absoluteFilePath; } - return false; + return {}; + } + + bool existPlugin(const DPluginMetaData &data) const + { + return !pluginFilePath(data).isEmpty(); } DAppletFactory *appletFactory(const DPluginMetaData &data) { - if (!existPlugin(data)) + const QString pluginPath = pluginFilePath(data); + if (pluginPath.isEmpty()) { return nullptr; + } DAppletFactory *factory = nullptr; - const QString fileName = data.pluginId(); - QPluginLoader loader(fileName); + QPluginLoader loader(pluginPath); loader.load(); if (!loader.isLoaded()) { - qCWarning(dsLog) << "Load the plugin failed." << loader.errorString(); + qCWarning(dsLog) << "Load the plugin failed." << pluginPath << loader.errorString(); return factory; } @@ -171,12 +205,12 @@ class DPluginLoaderPrivate : public DObjectPrivate break; if (!loader.instance()) { - qWarning(dsLog) << "Load the plugin failed." << loader.errorString(); + qWarning(dsLog) << "Load the plugin failed." << pluginPath << loader.errorString(); break; } factory = qobject_cast(loader.instance()); if (!factory) { - qWarning(dsLog) << "The plugin isn't a DAppletFactory." << fileName; + qWarning(dsLog) << "The plugin isn't a DAppletFactory." << pluginPath; break; } } while (false); diff --git a/frame/private/dsqmlglobal_p.h b/frame/private/dsqmlglobal_p.h index 459096b13..353239919 100644 --- a/frame/private/dsqmlglobal_p.h +++ b/frame/private/dsqmlglobal_p.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 @@ -24,6 +24,7 @@ class DQmlGlobal : public QObject, public DTK_CORE_NAMESPACE::DObject Q_OBJECT D_DECLARE_PRIVATE(DQmlGlobal) Q_PROPERTY(DApplet* rootApplet READ rootApplet NOTIFY rootAppletChanged FINAL) + Q_PROPERTY(QString dataFontFamily READ dataFontFamily CONSTANT FINAL) public: explicit DQmlGlobal(QObject *parent = nullptr); ~DQmlGlobal() override; @@ -36,6 +37,7 @@ class DQmlGlobal : public QObject, public DTK_CORE_NAMESPACE::DObject Q_INVOKABLE void singleShot(int msec, QJSValue callback); DApplet *rootApplet() const; + QString dataFontFamily() const; static DQmlGlobal *instance(); static QList allChildrenWindows(QWindow *target); diff --git a/frame/qml/PanelPopupWindow.qml b/frame/qml/PanelPopupWindow.qml index 4de58dd69..e3a5bffc5 100644 --- a/frame/qml/PanelPopupWindow.qml +++ b/frame/qml/PanelPopupWindow.qml @@ -13,19 +13,20 @@ PopupWindow { property real xOffset: 0 property real yOffset: 0 + property real positionXOffset: xOffset + property real positionYOffset: yOffset property int margins: 10 + property int windowThemeType: D.ApplicationHelper.LightType + readonly property bool darkTheme: root.windowThemeType === D.ApplicationHelper.DarkType property Item currentItem - property int requestedWidth: 10 - property int requestedHeight: 10 + property bool geometryUpdatePending: false signal requestUpdateGeometry() signal updateGeometryFinished() // order to update screen and (x,y) property var updateGeometryer : function updateGeometry() { - if (root.requestedWidth <= 10 || root.requestedHeight <= 10) { - root.width = root.requestedWidth; - root.height = root.requestedHeight; + if (root.width <= 10 || root.height <= 10) { return; } if (!root.transientParent) @@ -36,12 +37,10 @@ PopupWindow { let bounding = Qt.rect(root.screen.virtualX + margins, root.screen.virtualY + margins, root.screen.width - margins * 2, root.screen.height - margins * 2) - let pos = Qt.point(transientParent ? transientParent.x + xOffset : xOffset, - transientParent ? transientParent.y + yOffset : yOffset) - let newX = selectValue(pos.x, bounding.left, bounding.right - root.requestedWidth) - let newY = selectValue(pos.y, bounding.top, bounding.bottom - root.requestedHeight) - - root.setWindowGeometry(newX, newY, root.requestedWidth, root.requestedHeight) + let pos = Qt.point(transientParent ? transientParent.x + positionXOffset : positionXOffset, + transientParent ? transientParent.y + positionYOffset : positionYOffset) + x = selectValue(pos.x, bounding.left, bounding.right - root.width) + y = selectValue(pos.y, bounding.top, bounding.bottom - root.height) } function selectValue(value, min, max) { @@ -76,13 +75,14 @@ PopupWindow { flags: (Qt.platform.pluginName === "xcb" ? (Qt.Tool | Qt.WindowStaysOnTopHint) : Qt.Popup) font: D.DTK.fontManager.t6 D.DWindow.enabled: true + D.DWindow.themeType: root.windowThemeType D.DWindow.windowRadius: D.DTK.platformTheme.windowRadius < 0 ? 4 : D.DTK.platformTheme.windowRadius D.DWindow.enableSystemResize: false D.DWindow.enableSystemMove: false D.DWindow.enableBlurWindow: true // TODO set shadowOffset maunally. D.DWindow.shadowOffset: Qt.point(0, 25) - D.DWindow.shadowColor: D.DTK.themeType === D.ApplicationHelper.DarkType ? Qt.rgba(0, 0, 0, 0.5) : Qt.rgba(0, 0, 0, 0.2) + D.DWindow.shadowColor: root.darkTheme ? Qt.rgba(0, 0, 0, 0.5) : Qt.rgba(0, 0, 0, 0.2) D.ColorSelector.family: D.Palette.CrystalColor color: "transparent" @@ -90,8 +90,6 @@ PopupWindow { if(root.visible) return currentItem = null - root.requestedWidth = 10 - root.requestedHeight = 10 root.width = 10 root.height = 10 DS.closeChildrenWindows(root) @@ -125,18 +123,20 @@ PopupWindow { } } - onRequestedHeightChanged: { - requestUpdateGeometry() - } - onRequestedWidthChanged: { - requestUpdateGeometry() - } - onXOffsetChanged: requestUpdateGeometry() - onYOffsetChanged: requestUpdateGeometry() + onHeightChanged: requestUpdateGeometry() + onWidthChanged: requestUpdateGeometry() + onPositionXOffsetChanged: requestUpdateGeometry() + onPositionYOffsetChanged: requestUpdateGeometry() onRequestUpdateGeometry: { if (updateGeometryer) { + if (geometryUpdatePending) { + return + } + + geometryUpdatePending = true Qt.callLater(function () { + geometryUpdatePending = false updateGeometryer() updateGeometryFinished() }) @@ -153,14 +153,15 @@ PopupWindow { return appearance.opacity } blendColor: { + const isDark = root.darkTheme if (valid) { - return DStyle.Style.control.selectColor(undefined, - Qt.rgba(235 / 255.0, 235 / 255.0, 235 / 255.0, blendColorAlpha(0.6)), - Qt.rgba(0, 0, 0, blendColorAlpha(85 / 255))) + return isDark + ? Qt.rgba(0, 0, 0, blendColorAlpha(85 / 255)) + : Qt.rgba(235 / 255.0, 235 / 255.0, 235 / 255.0, blendColorAlpha(0.6)) } - return DStyle.Style.control.selectColor(undefined, - DStyle.Style.behindWindowBlur.lightNoBlurColor, - DStyle.Style.behindWindowBlur.darkNoBlurColor) + return isDark + ? DStyle.Style.behindWindowBlur.darkNoBlurColor + : DStyle.Style.behindWindowBlur.lightNoBlurColor } } -} \ No newline at end of file +} diff --git a/frame/qml/PanelToolTip.qml b/frame/qml/PanelToolTip.qml index 1ef671329..686488e60 100644 --- a/frame/qml/PanelToolTip.qml +++ b/frame/qml/PanelToolTip.qml @@ -18,6 +18,8 @@ Item { property int toolTipX: 0 property int toolTipY: 0 property bool readyBinding: false + property int closeGraceInterval: 90 + readonly property int toolTipTopFrameInset: 1 // WM_NAME, used for kwin. property string windowTitle: "dde-shell/paneltooltip" width: toolTip.width @@ -25,13 +27,13 @@ Item { Binding { when: readyBinding - target: toolTipWindow; property: "requestedWidth" + target: toolTipWindow; property: "width" value: toolTip.width + toolTip.leftPadding + toolTip.rightPadding } Binding { when: readyBinding - target: toolTipWindow; property: "requestedHeight" - value: toolTip.height + target: toolTipWindow; property: "height" + value: toolTip.height + toolTipTopFrameInset } Binding { when: readyBinding @@ -43,7 +45,7 @@ Item { when: readyBinding delayed: true target: toolTipWindow; property: "yOffset" - value: control.toolTipY + value: control.toolTipY - toolTipTopFrameInset } function open() @@ -51,11 +53,27 @@ Item { if (!toolTipWindow) return + closeTimer.stop() + timer.stop() + + if (toolTipWindow.visible && toolTipWindow.currentItem && toolTipWindow.currentItem !== control) { + toolTipWindow.close() + toolTipWindow.currentItem = null + } + readyBinding = Qt.binding(function () { return toolTipWindow && toolTipWindow.currentItem === control }) - toolTipWindow.currentItem = control + if (toolTipWindow.visible) { + toolTipWindow.title = windowTitle + if ("showAnimated" in toolTipWindow) { + toolTipWindow.showAnimated() + } else { + toolTipWindow.show() + } + return + } timer.start() } @@ -70,7 +88,11 @@ Item { return toolTipWindow.title = windowTitle - toolTipWindow.show() + if ("showAnimated" in toolTipWindow) { + toolTipWindow.showAnimated() + } else { + toolTipWindow.show() + } } } @@ -82,14 +104,45 @@ Item { if (!readyBinding) return - toolTipWindow.close() - toolTipWindow.currentItem = null + if (closeGraceInterval > 0 && toolTipWindow.visible) { + closeTimer.restart() + return + } + + if (toolTipWindow.currentItem !== control) { + return + } + + if ("closeAnimated" in toolTipWindow) { + toolTipWindow.closeAnimated() + } else { + toolTipWindow.close() + toolTipWindow.currentItem = null + } } function hide() { close() } + Timer { + id: closeTimer + interval: control.closeGraceInterval + repeat: false + onTriggered: { + if (!toolTipWindow || toolTipWindow.currentItem !== control) { + return + } + + if ("closeAnimated" in toolTipWindow) { + toolTipWindow.closeAnimated() + } else { + toolTipWindow.close() + toolTipWindow.currentItem = null + } + } + } + Control { id: toolTip visible: readyBinding diff --git a/frame/qml/PanelToolTipWindow.qml b/frame/qml/PanelToolTipWindow.qml index 32a55c849..dd33afcea 100644 --- a/frame/qml/PanelToolTipWindow.qml +++ b/frame/qml/PanelToolTipWindow.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -13,4 +13,55 @@ PanelPopupWindow { flags: Qt.ToolTip | Qt.WindowStaysOnTopHint D.DWindow.windowRadius: 8 D.DWindow.shadowRadius: 8 + D.DWindow.shadowOffset: Qt.point(0, 8) + + function resetVisualState() { + root.opacity = 1.0 + if (root.contentItem) { + root.contentItem.opacity = 1.0 + root.contentItem.scale = 1.0 + } + } + + function showAnimated() { + resetVisualState() + root.positionXOffset = root.xOffset + root.positionYOffset = root.yOffset + root.show() + } + + function closeAnimated() { + if (!root.visible) { + root.currentItem = null + return + } + + root.close() + root.currentItem = null + } + + onXOffsetChanged: root.positionXOffset = root.xOffset + onYOffsetChanged: root.positionYOffset = root.yOffset + + onCurrentItemChanged: { + if (!!root.currentItem && root.visible) { + root.positionXOffset = root.xOffset + root.positionYOffset = root.yOffset + return + } + + root.positionXOffset = root.xOffset + root.positionYOffset = root.yOffset + } + + onVisibleChanged: { + if (!visible) { + resetVisualState() + return + } + + root.positionXOffset = root.xOffset + root.positionYOffset = root.yOffset + resetVisualState() + } } diff --git a/live-overlay/packages/org.deepin.ds.dock b/live-overlay/packages/org.deepin.ds.dock new file mode 120000 index 000000000..418121b81 --- /dev/null +++ b/live-overlay/packages/org.deepin.ds.dock @@ -0,0 +1 @@ +/home/shule/src/dde-shell/panels/dock/package \ No newline at end of file diff --git a/live-overlay/packages/org.deepin.ds.dock.taskmanager b/live-overlay/packages/org.deepin.ds.dock.taskmanager new file mode 120000 index 000000000..07b2f5962 --- /dev/null +++ b/live-overlay/packages/org.deepin.ds.dock.taskmanager @@ -0,0 +1 @@ +/home/shule/src/dde-shell/panels/dock/taskmanager/package \ No newline at end of file diff --git a/live-overlay/packages/org.deepin.ds.dock.tray b/live-overlay/packages/org.deepin.ds.dock.tray new file mode 120000 index 000000000..ea143bb12 --- /dev/null +++ b/live-overlay/packages/org.deepin.ds.dock.tray @@ -0,0 +1 @@ +/home/shule/src/dde-shell/panels/dock/tray/package \ No newline at end of file diff --git a/misc/dde-shell-plugin@.service.in b/misc/dde-shell-plugin@.service.in index fd6ca5729..7cf4a4dc4 100644 --- a/misc/dde-shell-plugin@.service.in +++ b/misc/dde-shell-plugin@.service.in @@ -1,8 +1,27 @@ [Unit] Description=Manage a plugin for dde-shell. +RefuseManualStart=no +RefuseManualStop=no +StartLimitBurst=3 +StartLimitIntervalSec=0 +CollectMode=inactive-or-failed + +Requisite=dde-session-pre.target +After=dde-session-pre.target + +PartOf=dde-session-core.target +Before=dde-session-core.target + +Requires=dbus.socket +After=dbus.socket [Service] +Type=simple ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/dde-shell -p %I +TimeoutStartSec=infinity +Slice=session.slice +Restart=always +RestartSec=1s [Install] -WantedBy=multi-user.target +WantedBy=default.target diff --git a/misc/dde-shell@.service.in b/misc/dde-shell@.service.in index ce950b328..18511c47d 100644 --- a/misc/dde-shell@.service.in +++ b/misc/dde-shell@.service.in @@ -1,8 +1,35 @@ [Unit] Description=Manage an category for dde-shell. +RefuseManualStart=no +RefuseManualStop=no +StartLimitBurst=3 +StartLimitIntervalSec=0 +CollectMode=inactive-or-failed + +Requisite=dde-session-pre.target +After=dde-session-pre.target + +PartOf=dde-session-core.target +Before=dde-session-core.target + +Requires=dbus.socket +After=dbus.socket + +#FIXME: maybe AM is invalid +# old AM +Wants=org.deepin.dde.Application1.Manager.service +After=org.deepin.dde.Application1.Manager.service +# new AM +Wants=org.desktopspec.ApplicationManager1.service +After=org.desktopspec.ApplicationManager1.service [Service] +Type=simple ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/dde-shell -C %I +TimeoutStartSec=infinity +Slice=session.slice +Restart=always +RestartSec=1s [Install] -WantedBy=multi-user.target +WantedBy=default.target diff --git a/misc/restart-dde-shell-core.sh b/misc/restart-dde-shell-core.sh new file mode 100755 index 000000000..8b03d11cc --- /dev/null +++ b/misc/restart-dde-shell-core.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: CC0-1.0 + +set -euo pipefail + +ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +BIN_PATH="${BIN_PATH:-$ROOT_DIR/build-run/shell/dde-shell}" +CORE_ARGS=(-C DDE --serviceName=org.deepin.dde.shell -d org.deepin.ds.desktop) +CORE_MATCH="$BIN_PATH -C DDE --serviceName=org.deepin.dde.shell" +SYSTEMD_UNIT="${SYSTEMD_UNIT:-dde-shell@DDE.service}" +LOG_FILE="${LOG_FILE:-/tmp/dde-shell-core.log}" +START_TIMEOUT="${START_TIMEOUT:-10}" + +core_pids() { + pgrep -f -- "$CORE_MATCH" || true +} + +systemd_unit_exists() { + systemctl --user show "$SYSTEMD_UNIT" -p LoadState --value 2>/dev/null | grep -qx 'loaded' +} + +systemd_main_pid() { + systemctl --user show "$SYSTEMD_UNIT" -p MainPID --value 2>/dev/null || true +} + +status() { + echo "core-pids: $(core_pids | tr '\n' ' ' | sed 's/ $//')" + if systemd_unit_exists; then + echo "systemd:" + systemctl --user show "$SYSTEMD_UNIT" -p MainPID -p ActiveState -p SubState -p Result -p NRestarts 2>/dev/null || true + fi + echo "dbus:" + qdbus --session | grep 'org.deepin.dde.shell' || true + echo "dock-window:" + xwininfo -root -tree | grep 'org.deepin.ds.dock' || true +} + +stop_core() { + if systemd_unit_exists; then + systemctl --user stop "$SYSTEMD_UNIT" >/dev/null 2>&1 || true + fi + + local pids + pids="$(core_pids)" + if [[ -z "$pids" ]]; then + return + fi + + kill $pids 2>/dev/null || true + for _ in $(seq 1 50); do + if [[ -z "$(core_pids)" ]]; then + return + fi + sleep 0.1 + done + + kill -9 $pids 2>/dev/null || true +} + +wait_for_health() { + local deadline + deadline=$((SECONDS + START_TIMEOUT)) + while (( SECONDS < deadline )); do + if qdbus --session | grep -q '^ org\.deepin\.dde\.shell$' \ + && xwininfo -root -tree | grep -q 'org\.deepin\.ds\.dock'; then + if systemd_unit_exists; then + local main_pid + main_pid="$(systemd_main_pid)" + if [[ -z "$main_pid" || "$main_pid" = "0" ]]; then + sleep 0.2 + continue + fi + fi + return 0 + fi + sleep 0.2 + done + return 1 +} + +start_core() { + if systemd_unit_exists; then + systemctl --user reset-failed "$SYSTEMD_UNIT" >/dev/null 2>&1 || true + systemctl --user restart "$SYSTEMD_UNIT" + return + fi + + if [[ ! -x "$BIN_PATH" ]]; then + echo "dde-shell binary not found: $BIN_PATH" >&2 + return 1 + fi + + : >"$LOG_FILE" + setsid -f "$BIN_PATH" "${CORE_ARGS[@]}" >"$LOG_FILE" 2>&1 +} + +restart() { + stop_core + start_core + if wait_for_health; then + status + return 0 + fi + + echo "dde-shell core failed health check" >&2 + status >&2 + echo "last-log:" >&2 + tail -n 80 "$LOG_FILE" >&2 || true + return 1 +} + +case "${1:-restart}" in +status) + status + ;; +restart) + restart + ;; +stop) + stop_core + status + ;; +*) + echo "usage: $0 [restart|status|stop]" >&2 + exit 2 + ;; +esac diff --git a/panels/dock/AppletItemButton.qml b/panels/dock/AppletItemButton.qml index 0b94c3777..383615591 100644 --- a/panels/dock/AppletItemButton.qml +++ b/panels/dock/AppletItemButton.qml @@ -12,7 +12,9 @@ IconButton { id: control property bool isActive property real radius: 4 + property point lastSpotlightPoint: Qt.point(0, 0) property bool autoClosePopup: false + readonly property bool drivesDockSpotlight: Window.window === Panel.rootObject padding: 4 topPadding: undefined @@ -26,6 +28,35 @@ IconButton { icon.width: 16 icon.height: 16 + function mapSpotlightPoint(localPoint) { + if (!drivesDockSpotlight) { + return Qt.point(0, 0) + } + + const point = localPoint || Qt.point(width / 2, height / 2) + return mapToItem(null, point.x, point.y) + } + + function updateSpotlight(localPoint) { + if (!drivesDockSpotlight) { + lastSpotlightPoint = Qt.point(0, 0) + Panel.reportMousePresence(false) + return + } + + lastSpotlightPoint = mapSpotlightPoint(localPoint) + Panel.reportMousePresence(true, lastSpotlightPoint) + } + + function clearSpotlight() { + if (!drivesDockSpotlight) { + Panel.reportMousePresence(false) + return + } + + Panel.reportMousePresence(false, lastSpotlightPoint) + } + Connections { target: control enabled: autoClosePopup @@ -42,4 +73,38 @@ IconButton { Component.onCompleted: { contentItem.smooth = false } + + HoverHandler { + id: spotlightHoverHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + enabled: control.enabled && control.visible && control.hoverEnabled + + onPointChanged: { + if (hovered) { + spotlightClearTimer.stop() + control.updateSpotlight(spotlightHoverHandler.point.position) + } + } + + onHoveredChanged: { + if (hovered) { + spotlightClearTimer.stop() + control.updateSpotlight() + return + } + + spotlightClearTimer.restart() + } + } + + Timer { + id: spotlightClearTimer + interval: 70 + repeat: false + onTriggered: { + if (!spotlightHoverHandler.hovered) { + control.clearSpotlight() + } + } + } } diff --git a/panels/dock/CMakeLists.txt b/panels/dock/CMakeLists.txt index ca8179c12..0b8c09301 100644 --- a/panels/dock/CMakeLists.txt +++ b/panels/dock/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") configure_file(environments.h.in environments.h @ONLY) find_package(PkgConfig REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} COMPONENTS Core DBus Gui Qml WaylandCompositor Widgets WaylandClient) +find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} COMPONENTS Core DBus Gui Network Qml WaylandCompositor Widgets WaylandClient) find_package(DdeTrayLoader REQUIRED) find_package(TreelandProtocols REQUIRED) pkg_check_modules(WaylandClient REQUIRED IMPORTED_TARGET wayland-client) @@ -59,6 +59,25 @@ add_library(dockpanel SHARED ${dock_panel_sources} ) +add_dependencies(dockpanel tray_loader_font_sync) + +add_library(tray_loader_font_sync SHARED + tray_loader_font_sync.cpp + tray_loader_font_sync.qrc +) + +set_target_properties(tray_loader_font_sync PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}" +) + +target_link_libraries(tray_loader_font_sync PRIVATE + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets +) + +install(TARGETS tray_loader_font_sync DESTINATION "${CMAKE_INSTALL_LIBDIR}/dde-shell") + qt_generate_wayland_protocol_client_sources(dockpanel NO_INCLUDE_CORE_ONLY FILES @@ -106,6 +125,8 @@ file( constants.h # dockfilterproxymodel.cpp # dockfilterproxymodel.h + fashionleftpluginprovider.h + fashionleftpluginprovider.cpp pluginmanagerextension_p.h pluginmanagerextension.cpp pluginmanagerintegration_p.h @@ -147,7 +168,10 @@ qt_generate_wayland_protocol_server_sources(dock-plugin target_link_libraries(dock-plugin PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Concurrent + Qt${QT_VERSION_MAJOR}::DBus Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Qml Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::WaylandCompositor @@ -171,3 +195,7 @@ dtk_add_config_meta_files(APPID org.deepin.ds.dock FILES dconfig/org.deepin.ds.d dtk_add_config_meta_files(APPID org.deepin.ds.dock FILES dconfig/org.deepin.ds.dock.tray.json) # compat dtk_add_config_meta_files(APPID org.deepin.dde.shell FILES dconfig/org.deepin.ds.dock.json) dtk_add_config_meta_files(APPID org.deepin.dde.shell FILES dconfig/org.deepin.ds.dock.tray.json) +dtk_add_config_meta_files(APPID org.deepin.dde.dock FILES dconfig/org.deepin.dde.dock.json) +dtk_add_config_meta_files(APPID org.deepin.dde.dock FILES dconfig/org.deepin.dde.dock.plugin.quick-panel.json) +dtk_add_config_meta_files(APPID org.deepin.dde.tray-loader FILES dconfig/org.deepin.dde.dock.json) +dtk_add_config_meta_files(APPID org.deepin.dde.tray-loader FILES dconfig/org.deepin.dde.dock.plugin.quick-panel.json) diff --git a/panels/dock/OverflowContainer.qml b/panels/dock/OverflowContainer.qml index 0c04b2889..4e3d30a42 100644 --- a/panels/dock/OverflowContainer.qml +++ b/panels/dock/OverflowContainer.qml @@ -19,6 +19,8 @@ Item { property alias addDisplaced: listView.addDisplaced property alias removeDisplaced: listView.removeDisplaced property alias moveDisplaced: listView.moveDisplaced + property alias footer: listView.footer + property alias footerPositioning: listView.footerPositioning ListView { id: listView anchors.fill: parent @@ -54,6 +56,30 @@ Item { return listView.indexAt(x, y) } + function childTargetImplicitWidth(child) { + if (!child) { + return 0 + } + + if (child.targetImplicitWidth !== undefined) { + return child.targetImplicitWidth + } + + return child.implicitWidth + } + + function childTargetImplicitHeight(child) { + if (!child) { + return 0 + } + + if (child.targetImplicitHeight !== undefined) { + return child.targetImplicitHeight + } + + return child.implicitHeight + } + implicitWidth: { let width = 0 for (let child of listView.contentItem.visibleChildren) { @@ -70,4 +96,20 @@ Item { } return Math.max(height, 1) } + + readonly property real targetImplicitWidth: { + let width = 0 + for (let child of listView.contentItem.visibleChildren) { + width = calculateImplicitWidth(width, childTargetImplicitWidth(child)) + } + return Math.max(width, 1) + } + + readonly property real targetImplicitHeight: { + let height = 0 + for (let child of listView.contentItem.visibleChildren) { + height = calculateImplicitHeight(height, childTargetImplicitHeight(child)) + } + return Math.max(height, 1) + } } diff --git a/panels/dock/constants.h b/panels/dock/constants.h index bf3cb9372..eabcacb30 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: 2018 - 2026 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later @@ -38,6 +38,12 @@ enum ItemAlignment { LeftAlignment = 1, }; +enum ViewMode { + CenteredMode = 0, + LeftAlignedMode = 1, + FashionMode = 2, +}; + enum ColorTheme { Light = Dtk::Gui::DGuiApplicationHelper::ColorType::LightType, Dark = Dtk::Gui::DGuiApplicationHelper::ColorType::DarkType, @@ -148,6 +154,7 @@ const QString DCCIconPath = CMAKE_INSTALL_PREFIX + QString("/share/dde-dock/icon Q_ENUM_NS(SIZE) Q_ENUM_NS(IndicatorStyle) Q_ENUM_NS(ItemAlignment) +Q_ENUM_NS(ViewMode) Q_ENUM_NS(ColorTheme) Q_ENUM_NS(HideMode) Q_ENUM_NS(Position) @@ -162,6 +169,7 @@ Q_ENUM_NS(TrayPluginSizePolicy) Q_DECLARE_METATYPE(dock::SIZE) Q_DECLARE_METATYPE(dock::IndicatorStyle) Q_DECLARE_METATYPE(dock::ItemAlignment) +Q_DECLARE_METATYPE(dock::ViewMode) Q_DECLARE_METATYPE(dock::ColorTheme) Q_DECLARE_METATYPE(dock::HideMode) Q_DECLARE_METATYPE(dock::HideState) diff --git a/panels/dock/dconfig/org.deepin.dde.dock.json b/panels/dock/dconfig/org.deepin.dde.dock.json new file mode 100644 index 000000000..7ef9e609c --- /dev/null +++ b/panels/dock/dconfig/org.deepin.dde.dock.json @@ -0,0 +1,106 @@ +{ + "magic": "dsg.config.meta", + "version": "1.0", + "contents": { + "Dock_Size": { + "value": 48, + "serial": 0, + "flags": [], + "name": "Dock_Size", + "name[zh_CN]": "*****", + "description": "", + "permissions": "readwrite", + "visibility": "private" + }, + "Position": { + "value": "bottom", + "serial": 0, + "flags": [], + "name": "Position", + "name[zh_CN]": "*****", + "description": "", + "permissions": "readwrite", + "visibility": "private" + }, + "Hide_Mode": { + "value": "keep-showing", + "serial": 0, + "flags": [], + "name": "Hide_Mode", + "name[zh_CN]": "*****", + "description": "The value will influence when the dock is shown or hidden.", + "permissions": "readwrite", + "visibility": "private" + }, + "Item_Alignment": { + "value": "center", + "serial": 0, + "flags": [], + "name": "Item_Alignment", + "name[zh_CN]": "*****", + "description": "", + "permissions": "readwrite", + "visibility": "private" + }, + "View_Mode": { + "value": "center", + "serial": 0, + "flags": [], + "name": "View_Mode", + "name[zh_CN]": "*****", + "description": "", + "permissions": "readwrite", + "visibility": "private" + }, + "Indicator_Style": { + "value": "Fashion", + "serial": 0, + "flags": [], + "name": "Indicator_Style", + "name[zh_CN]": "*****", + "description": "", + "permissions": "readwrite", + "visibility": "private" + }, + "Plugins_Visible": { + "value": {}, + "serial": 0, + "flags": [], + "name": "The visibilities of plugins", + "name[zh_CN]": "插件可见性", + "description": "The loaded plugin which is visible when dock is started.", + "permissions": "readwrite", + "visibility": "private" + }, + "Show_In_Primary": { + "value": true, + "serial": 0, + "flags": [], + "name": "show_in_primary", + "name[zh_CN]": "任务栏显示在主屏幕", + "description": "show dock in primary screen", + "permissions": "readwrite", + "visibility": "private" + }, + "Locked": { + "value": false, + "serial": 0, + "flags": [], + "name": "locked", + "name[zh_CN]": "禁用自由调节", + "description": "lock dock to prevent dragging resize", + "permissions": "readwrite", + "visibility": "private" + }, + "enableShowDesktop": { + "value": true, + "serial": 0, + "flags": [], + "name": "Enable ShowDesktop", + "name[zh_CN]": "启用显示桌面区域", + "description": "Enable or disable the show desktop area on the right side of the dock", + "permissions": "readwrite", + "visibility": "private" + } + } +} diff --git a/panels/dock/dconfig/org.deepin.dde.dock.plugin.quick-panel.json b/panels/dock/dconfig/org.deepin.dde.dock.plugin.quick-panel.json new file mode 100644 index 000000000..53b29d34a --- /dev/null +++ b/panels/dock/dconfig/org.deepin.dde.dock.plugin.quick-panel.json @@ -0,0 +1,20 @@ +{ + "magic": "dsg.config.meta", + "version": "1.0", + "contents": { + "Dock_Quick_Plugins": { + "value": [ + "network-item-key", + "bluetooth-item-key" + ], + "serial": 0, + "flags": [], + "name": "Dock_Quick_Plugins", + "name[zh_CN]": "停靠在任务栏上的快捷面板插件", + "description": "Quick panel plugin item keys that should also stay visible on the dock by default.", + "description[zh_CN]": "默认同时显示在任务栏上的快捷面板插件 item key 列表。", + "permissions": "readwrite", + "visibility": "private" + } + } +} diff --git a/panels/dock/dconfig/org.deepin.ds.dock.json b/panels/dock/dconfig/org.deepin.ds.dock.json index b4843310e..a1f7ea223 100755 --- a/panels/dock/dconfig/org.deepin.ds.dock.json +++ b/panels/dock/dconfig/org.deepin.ds.dock.json @@ -42,6 +42,16 @@ "permissions": "readwrite", "visibility": "private" }, + "View_Mode": { + "value": "center", + "serial": 0, + "flags": [], + "name": "View_Mode", + "name[zh_CN]": "*****", + "description": "", + "permissions": "readwrite", + "visibility": "private" + }, "Indicator_Style": { "value": "Fashion", "serial": 0, diff --git a/panels/dock/dockhelper.cpp b/panels/dock/dockhelper.cpp index bb089675b..5db6e2df9 100644 --- a/panels/dock/dockhelper.cpp +++ b/panels/dock/dockhelper.cpp @@ -6,19 +6,125 @@ #include "constants.h" #include "dockpanel.h" +#include +#include #include +#include +#include +#include namespace dock { +namespace { + +QWindow *topTransientParent(QWindow *window) +{ + QWindow *topLevelWindow = window; + while (topLevelWindow && topLevelWindow->transientParent()) { + topLevelWindow = topLevelWindow->transientParent(); + } + + return topLevelWindow; +} + +bool isDockRelatedWindow(QWindow *window, QWindow *dockWindow) +{ + if (!window || !dockWindow) { + return false; + } + + if (window == dockWindow) { + return true; + } + + return topTransientParent(window) == dockWindow; +} + +QRect expandedGeometry(const QRect &geometry, int margin) +{ + return geometry.adjusted(-margin, -margin, margin, margin); +} + +QRect dockMouseTrackingGeometry(const QRect &geometry, const QRect &screenGeometry, Position position, int margin) +{ + QRect trackingGeometry = expandedGeometry(geometry, margin); + + const int leftGap = qMax(0, geometry.left() - screenGeometry.left()); + const int topGap = qMax(0, geometry.top() - screenGeometry.top()); + const int rightGap = qMax(0, screenGeometry.right() - geometry.right()); + const int bottomGap = qMax(0, screenGeometry.bottom() - geometry.bottom()); + + switch (position) { + case Bottom: + trackingGeometry.adjust(0, 0, 0, qMax(0, bottomGap - margin)); + break; + case Top: + trackingGeometry.adjust(0, -qMax(0, topGap - margin), 0, 0); + break; + case Left: + trackingGeometry.adjust(-qMax(0, leftGap - margin), 0, 0, 0); + break; + case Right: + trackingGeometry.adjust(0, 0, qMax(0, rightGap - margin), 0); + break; + } + + return trackingGeometry; +} + +bool isCursorOnDockWakeEdge(const QPoint &globalCursorPos, const QRect &screenGeometry, Position position) +{ + if (!screenGeometry.isValid()) { + return false; + } + + constexpr int edgeThreshold = 2; + if (!screenGeometry.adjusted(-edgeThreshold, -edgeThreshold, edgeThreshold, edgeThreshold).contains(globalCursorPos)) { + return false; + } + + switch (position) { + case Bottom: + return globalCursorPos.y() >= screenGeometry.bottom() - edgeThreshold; + case Top: + return globalCursorPos.y() <= screenGeometry.top() + edgeThreshold; + case Left: + return globalCursorPos.x() <= screenGeometry.left() + edgeThreshold; + case Right: + return globalCursorPos.x() >= screenGeometry.right() - edgeThreshold; + } + + return false; +} + +bool shouldUseCursorEdgeWakeFallback(DockPanel *panel) +{ + if (!panel) { + return false; + } + + // Fashion mode uses its own wake-up surface. Keeping the generic edge fallback + // enabled there causes premature wake-ups and repeated hide/show loops while + // the cursor is still parked near the screen edge. + return panel->viewMode() != FashionMode; +} + +} + DockHelper::DockHelper(DockPanel *parent) : QObject(parent) , m_hideTimer(new QTimer(this)) , m_showTimer(new QTimer(this)) + , m_cursorMonitorTimer(new QTimer(this)) + , m_edgeWakeHoldTimer(new QTimer(this)) { m_hideTimer->setInterval(400); - m_showTimer->setInterval(400); + m_showTimer->setInterval(120); + m_cursorMonitorTimer->setInterval(16); + m_edgeWakeHoldTimer->setInterval(420); m_hideTimer->setSingleShot(true); m_showTimer->setSingleShot(true); + m_edgeWakeHoldTimer->setSingleShot(true); qApp->installEventFilter(this); QMetaObject::invokeMethod(this, &DockHelper::initAreas, Qt::QueuedConnection); @@ -27,6 +133,10 @@ DockHelper::DockHelper(DockPanel *parent) connect(parent, &DockPanel::rootObjectChanged, this, &DockHelper::initAreas); connect(parent, &DockPanel::showInPrimaryChanged, this, &DockHelper::updateAllDockWakeArea); connect(parent, &DockPanel::hideStateChanged, this, &DockHelper::updateAllDockWakeArea); + connect(parent, &DockPanel::viewModeChanged, this, [this]() { + updateAllDockWakeArea(); + updatePanelMouseState(); + }); connect(parent, &DockPanel::hideModeChanged, m_hideTimer, static_cast(&QTimer::start)); connect(parent, &DockPanel::hideModeChanged, m_showTimer, static_cast(&QTimer::start)); connect(parent, &DockPanel::positionChanged, this, [this](Position pos) { @@ -39,19 +149,34 @@ DockHelper::DockHelper(DockPanel *parent) connect(m_hideTimer, &QTimer::timeout, this, &DockHelper::checkNeedHideOrNot); connect(m_showTimer, &QTimer::timeout, this, &DockHelper::checkNeedShowOrNot); + connect(m_cursorMonitorTimer, &QTimer::timeout, this, &DockHelper::updatePanelMouseState); + connect(m_edgeWakeHoldTimer, &QTimer::timeout, this, &DockHelper::checkNeedHideOrNot); + m_cursorMonitorTimer->start(); connect(this, &DockHelper::isWindowOverlapChanged, this, [this](bool overlap) { if (overlap) { + if (m_showTimer->isActive()) { + m_showTimer->stop(); + } m_hideTimer->start(); } else { + if (m_hideTimer->isActive()) { + m_hideTimer->stop(); + } m_showTimer->start(); } }); connect(this, &DockHelper::currentActiveWindowFullscreenChanged, this, [this] (bool isFullscreen) { + if (m_hideTimer->isActive()) { + m_hideTimer->stop(); + } + if (m_showTimer->isActive()) { + m_showTimer->stop(); + } if (isFullscreen) { - checkNeedHideOrNot(); + m_hideTimer->start(); } else { - checkNeedShowOrNot(); + m_showTimer->start(); } }); } @@ -87,6 +212,10 @@ bool DockHelper::eventFilter(QObject *watched, QEvent *event) switch (event->type()) { case QEvent::Enter: { m_enters.insert(window, true); + updateCursorPosition(event); + if (m_edgeWakeHoldTimer->isActive()) { + m_edgeWakeHoldTimer->stop(); + } if (m_hideTimer->isActive()) { m_hideTimer->stop(); } @@ -94,6 +223,11 @@ bool DockHelper::eventFilter(QObject *watched, QEvent *event) m_showTimer->start(); break; } + case QEvent::MouseMove: + case QEvent::HoverMove: { + updateCursorPosition(event); + break; + } case QEvent::Leave: { m_enters.insert(window, false); if (m_showTimer->isActive()) { @@ -127,6 +261,7 @@ bool DockHelper::eventFilter(QObject *watched, QEvent *event) } } + updatePanelMouseState(); return false; } @@ -145,9 +280,28 @@ bool DockHelper::wakeUpAreaNeedShowOnThisScreen(QScreen *screen) void DockHelper::enterScreen(QScreen *screen) { + if (!screen) { + return; + } + + if (m_edgeWakeLatched && m_edgeWakeScreen == screen) { + return; + } + + m_edgeWakeLatched = true; + m_edgeWakeScreen = screen; + + if (m_hideTimer->isActive()) { + m_hideTimer->stop(); + } + if (m_showTimer->isActive()) { + m_showTimer->stop(); + } + auto nowScreen = parent()->dockScreen(); if (nowScreen == screen) { + m_edgeWakeHoldTimer->start(); parent()->setHideState(Show); return; } @@ -155,12 +309,18 @@ void DockHelper::enterScreen(QScreen *screen) // Do not switch screen if any popup/transient child window is showing for (auto show : m_transientChildShows) { if (show) { + m_edgeWakeHoldTimer->start(); parent()->setHideState(Show); return; } } QTimer::singleShot(200, [this, screen]() { + if (!screen) { + return; + } + + m_edgeWakeHoldTimer->start(); parent()->setDockScreen(screen); parent()->setHideState(Show); updateAllDockWakeArea(); @@ -169,6 +329,9 @@ void DockHelper::enterScreen(QScreen *screen) void DockHelper::leaveScreen() { + if (m_edgeWakeHoldTimer->isActive()) { + m_edgeWakeHoldTimer->stop(); + } m_hideTimer->start(); } @@ -188,8 +351,113 @@ void DockHelper::updateAllDockWakeArea() } } +void DockHelper::updatePanelMouseState() +{ + auto *window = parent()->window(); + const QPoint globalCursorPos = QCursor::pos(); + QScreen *cursorScreen = QGuiApplication::screenAt(globalCursorPos); + if (!cursorScreen && window) { + cursorScreen = window->screen(); + } + + const bool cursorOnWakeEdge = cursorScreen + && isCursorOnDockWakeEdge(globalCursorPos, cursorScreen->geometry(), parent()->position()); + + if (!cursorScreen + || cursorScreen != m_edgeWakeScreen + || !cursorOnWakeEdge) { + m_edgeWakeLatched = false; + m_edgeWakeScreen.clear(); + } + + if (shouldUseCursorEdgeWakeFallback(parent()) + && parent()->hideState() == Hide + && cursorScreen + && (!parent()->showInPrimary() || cursorScreen == qApp->primaryScreen()) + && cursorOnWakeEdge) { + enterScreen(cursorScreen); + } + + if (!window || !window->isVisible()) { + parent()->setContainsMouse(false); + return; + } + + const int geometryMargin = qMax(6, qRound(parent()->dockSize() * 0.14)); + const QRect screenGeometry = window->screen() ? window->screen()->geometry() : QRect(); + const Position panelPosition = parent()->position(); + bool containsMouse = dockMouseTrackingGeometry(window->geometry(), screenGeometry, panelPosition, geometryMargin).contains(globalCursorPos); + + if (!containsMouse) { + const auto topLevelWindows = QGuiApplication::topLevelWindows(); + for (QWindow *candidateWindow : topLevelWindows) { + if (!candidateWindow || !candidateWindow->isVisible() || !isDockRelatedWindow(candidateWindow, window)) { + continue; + } + + const QRect candidateScreenGeometry = candidateWindow->screen() ? candidateWindow->screen()->geometry() : screenGeometry; + if (dockMouseTrackingGeometry(candidateWindow->geometry(), candidateScreenGeometry, panelPosition, geometryMargin).contains(globalCursorPos)) { + containsMouse = true; + break; + } + } + } + + parent()->setContainsMouse(containsMouse); + if (containsMouse) { + m_edgeWakeLatched = false; + m_edgeWakeScreen.clear(); + if (m_edgeWakeHoldTimer->isActive()) { + m_edgeWakeHoldTimer->stop(); + } + parent()->setCursorPosition(globalCursorPos - window->position()); + } +} + +void DockHelper::updateCursorPosition(QEvent *event) +{ + if (!parent()->window() || !event) { + return; + } + + QPointF globalPosition; + bool valid = false; + switch (event->type()) { + case QEvent::Enter: { + auto *enterEvent = static_cast(event); + globalPosition = enterEvent->globalPosition(); + valid = true; + break; + } + case QEvent::MouseMove: { + auto *mouseEvent = static_cast(event); + globalPosition = mouseEvent->globalPosition(); + valid = true; + break; + } + case QEvent::HoverMove: { + auto *hoverEvent = static_cast(event); + globalPosition = hoverEvent->globalPosition(); + valid = true; + break; + } + default: + break; + } + + if (!valid) { + return; + } + + parent()->setCursorPosition(globalPosition - parent()->window()->position()); +} + void DockHelper::checkNeedHideOrNot() { + if (m_edgeWakeHoldTimer->isActive()) { + return; + } + bool needHide; switch (parent()->hideMode()) { case KeepShowing: { @@ -210,6 +478,7 @@ void DockHelper::checkNeedHideOrNot() } needHide &= !parent()->contextDragging(); + needHide &= !parent()->containsMouse(); // any enter will not make hide for (auto enter : m_enters) { @@ -262,8 +531,11 @@ void DockHelper::initAreas() { // clear old area for (auto area : m_areas) { - area->close(); - delete area; + if (!area) { + continue; + } + + destroyArea(area); } m_areas.clear(); diff --git a/panels/dock/dockhelper.h b/panels/dock/dockhelper.h index 778021ad3..f0152314a 100644 --- a/panels/dock/dockhelper.h +++ b/panels/dock/dockhelper.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 @@ -8,6 +8,9 @@ #include "dockpanel.h" #include +#include + +class QTimer; namespace dock { class DockWakeUpArea; @@ -38,6 +41,8 @@ class DockHelper : public QObject bool wakeUpAreaNeedShowOnThisScreen(QScreen *screen); void updateAllDockWakeArea(); + void updatePanelMouseState(); + void updateCursorPosition(QEvent *event); public Q_SLOTS: void checkNeedHideOrNot(); @@ -52,6 +57,10 @@ public Q_SLOTS: QHash m_transientChildShows; QTimer *m_hideTimer; QTimer *m_showTimer; + QTimer *m_cursorMonitorTimer; + QTimer *m_edgeWakeHoldTimer; + bool m_edgeWakeLatched = false; + QPointer m_edgeWakeScreen; }; class DockWakeUpArea @@ -72,4 +81,3 @@ class DockWakeUpArea DockHelper *m_helper; }; } - diff --git a/panels/dock/dockpanel.cpp b/panels/dock/dockpanel.cpp index 469f5da21..f6527b7a4 100644 --- a/panels/dock/dockpanel.cpp +++ b/panels/dock/dockpanel.cpp @@ -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 @@ -16,13 +16,22 @@ #include "dockfrontadaptor.h" #include "dockdaemonadaptor.h" #include "loadtrayplugins.h" - #include #include #include #include #include +#include +#include +#include +#include +#include #include +#include + +#if defined(BUILD_WITH_X11) +#include +#endif #define SETTINGS DockSettings::instance() @@ -30,6 +39,182 @@ Q_LOGGING_CATEGORY(dockLog, "org.deepin.dde.shell.dock") namespace dock { +namespace { +constexpr auto kAppearanceService = "org.deepin.dde.Appearance1"; +constexpr auto kAppearancePath = "/org/deepin/dde/Appearance1"; +constexpr auto kAppearanceInterface = "org.deepin.dde.Appearance1"; +constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"; + +Dtk::Gui::DGuiApplicationHelper::ColorType explicitThemeTypeFromName(const QString &themeName) +{ + const QString normalizedThemeName = themeName.trimmed().toLower(); + if (normalizedThemeName.isEmpty()) + return Dtk::Gui::DGuiApplicationHelper::UnknownType; + + if (normalizedThemeName == QStringLiteral("dark") + || normalizedThemeName.endsWith(QStringLiteral(".dark")) + || normalizedThemeName.endsWith(QStringLiteral("-dark")) + || normalizedThemeName.endsWith(QStringLiteral("_dark"))) { + return Dtk::Gui::DGuiApplicationHelper::DarkType; + } + + if (normalizedThemeName == QStringLiteral("light") + || normalizedThemeName.endsWith(QStringLiteral(".light")) + || normalizedThemeName.endsWith(QStringLiteral("-light")) + || normalizedThemeName.endsWith(QStringLiteral("_light"))) { + return Dtk::Gui::DGuiApplicationHelper::LightType; + } + + return Dtk::Gui::DGuiApplicationHelper::UnknownType; +} + +Dtk::Gui::DGuiApplicationHelper::ColorType currentEffectiveThemeType(Dtk::Gui::DGuiApplicationHelper *helper) +{ + if (!helper) + return Dtk::Gui::DGuiApplicationHelper::LightType; + + const auto currentThemeType = helper->themeType(); + if (currentThemeType != Dtk::Gui::DGuiApplicationHelper::UnknownType) + return currentThemeType; + + const auto currentPaletteType = helper->paletteType(); + if (currentPaletteType != Dtk::Gui::DGuiApplicationHelper::UnknownType) + return currentPaletteType; + + return Dtk::Gui::DGuiApplicationHelper::LightType; +} + +Dtk::Gui::DGuiApplicationHelper::ColorType effectiveThemeTypeFromAppearance( + const QString &globalThemeName, + const QString >kThemeName, + const QString &iconThemeName, + Dtk::Gui::DGuiApplicationHelper *helper) +{ + const auto globalThemeType = explicitThemeTypeFromName(globalThemeName); + if (globalThemeType != Dtk::Gui::DGuiApplicationHelper::UnknownType) + return globalThemeType; + + const auto gtkThemeType = explicitThemeTypeFromName(gtkThemeName); + if (gtkThemeType != Dtk::Gui::DGuiApplicationHelper::UnknownType) + return gtkThemeType; + + const auto iconThemeType = explicitThemeTypeFromName(iconThemeName); + if (iconThemeType != Dtk::Gui::DGuiApplicationHelper::UnknownType) + return iconThemeType; + + return currentEffectiveThemeType(helper); +} + +ColorTheme colorThemeFromHelperThemeType(Dtk::Gui::DGuiApplicationHelper::ColorType themeType) +{ + return themeType == Dtk::Gui::DGuiApplicationHelper::DarkType ? Dark : Light; +} + +QString readAppearanceStringProperty(const QString &propertyName) +{ + QDBusInterface appearanceProperties(kAppearanceService, + kAppearancePath, + kPropertiesInterface, + QDBusConnection::sessionBus()); + if (!appearanceProperties.isValid()) + return QString(); + + const QDBusReply reply = appearanceProperties.call(QStringLiteral("Get"), + QString::fromLatin1(kAppearanceInterface), + propertyName); + return reply.isValid() ? reply.value().variant().toString() : QString(); +} + +#if defined(BUILD_WITH_X11) +xcb_window_t nativeTopLevelWindow(xcb_connection_t *connection, xcb_window_t window) +{ + xcb_window_t current = window; + + while (current != XCB_WINDOW_NONE) { + xcb_query_tree_reply_t *reply = xcb_query_tree_reply(connection, xcb_query_tree(connection, current), nullptr); + if (!reply) { + return current; + } + + const bool isTopLevel = reply->parent == reply->root || reply->parent == XCB_WINDOW_NONE; + const xcb_window_t parent = reply->parent; + free(reply); + + if (isTopLevel) { + return current; + } + + current = parent; + } + + return window; +} + +bool raiseX11Window(QWindow *window) +{ + if (!window) { + return false; + } + + window->create(); + const auto windowId = static_cast(window->winId()); + if (windowId == XCB_WINDOW_NONE) { + return false; + } + + auto *x11App = qGuiApp->nativeInterface(); + if (!x11App || !x11App->connection()) { + return false; + } + + const auto nativeWindowId = nativeTopLevelWindow(x11App->connection(), windowId); + const uint32_t values[] = { XCB_STACK_MODE_ABOVE }; + xcb_configure_window(x11App->connection(), nativeWindowId, XCB_CONFIG_WINDOW_STACK_MODE, values); + xcb_flush(x11App->connection()); + return true; +} +#endif + +void syncApplicationTheme(Dtk::Gui::DGuiApplicationHelper *helper, + const QString &globalThemeName, + const QString >kThemeName, + const QString &iconThemeName) +{ + if (!helper) + return; + + const auto explicitThemeType = explicitThemeTypeFromName(globalThemeName); + const bool followSystemTheme = explicitThemeType == Dtk::Gui::DGuiApplicationHelper::UnknownType; + const auto targetPaletteType = followSystemTheme ? Dtk::Gui::DGuiApplicationHelper::UnknownType : explicitThemeType; + + if (auto *applicationTheme = helper->applicationTheme()) { + const QByteArray gtkThemeNameBytes = gtkThemeName.toUtf8(); + if (!gtkThemeNameBytes.isEmpty() && applicationTheme->themeName() != gtkThemeNameBytes) { + applicationTheme->setThemeName(gtkThemeNameBytes); + } + + const QByteArray iconThemeNameBytes = iconThemeName.toUtf8(); + if (!iconThemeNameBytes.isEmpty() && applicationTheme->iconThemeName() != iconThemeNameBytes) { + applicationTheme->setIconThemeName(iconThemeNameBytes); + } + } + + if (helper->paletteType() != targetPaletteType) { + helper->setPaletteType(targetPaletteType); + } + + const auto targetThemeType = effectiveThemeTypeFromAppearance(globalThemeName, gtkThemeName, iconThemeName, helper); + const QPalette targetPalette = helper->applicationPalette(targetThemeType); + if (helper->applicationPalette() != targetPalette) { + helper->setApplicationPalette(targetPalette); + } + + if (!iconThemeName.isEmpty() && QIcon::themeName() != iconThemeName) { + QIcon::setThemeName(iconThemeName); + } +} +} + DockPanel::DockPanel(QObject *parent) : DPanel(parent) , m_theme(ColorTheme::Dark) @@ -38,9 +223,40 @@ DockPanel::DockPanel(QObject *parent) , m_loadTrayPlugins(new LoadTrayPlugins(this)) , m_compositorReady(false) , m_launcherShown(false) + , m_themeSyncTimer(new QTimer(this)) + , m_launcherRaiseTimer(new QTimer(this)) + , m_launcherRaisePasses(0) , m_contextDragging(false) + , m_containsMouse(false) + , m_reportedContainsMouse(false) , m_isResizing(false) -{ + , m_cursorPosition(0, 0) + , m_reportedCursorPosition(0, 0) +{ + m_themeSyncTimer->setSingleShot(true); + m_themeSyncTimer->setInterval(80); + connect(m_themeSyncTimer, &QTimer::timeout, this, &DockPanel::syncColorThemeWithSystem); + + m_launcherRaiseTimer->setInterval(80); + connect(m_launcherRaiseTimer, &QTimer::timeout, this, [this] { + if (!m_launcherShown || m_launcherRaisePasses <= 0) { + m_launcherRaiseTimer->stop(); + return; + } + + if (window()) { + window()->raise(); +#if defined(BUILD_WITH_X11) + raiseX11Window(window()); +#endif + } + + --m_launcherRaisePasses; + if (m_launcherRaisePasses <= 0) { + m_launcherRaiseTimer->stop(); + } + }); + connect(this, &DockPanel::compositorReadyChanged, this, [this] { if (!m_compositorReady) return; m_loadTrayPlugins->loadDockPlugins(); @@ -110,6 +326,13 @@ bool DockPanel::init() connect(this, &DockPanel::frontendWindowRectChanged, dockDaemonAdaptor, &DockDaemonAdaptor::FrontendWindowRectChanged); connect(SETTINGS, &DockSettings::dockSizeChanged, this, &DockPanel::dockSizeChanged); connect(SETTINGS, &DockSettings::hideModeChanged, this, &DockPanel::hideModeChanged); + connect(SETTINGS, &DockSettings::viewModeChanged, this, [this](ViewMode mode) { + Q_EMIT viewModeChanged(mode); + + if (window()) { + QMetaObject::invokeMethod(this, &DockPanel::onWindowGeometryChanged, Qt::QueuedConnection); + } + }); connect(SETTINGS, &DockSettings::itemAlignmentChanged, this, &DockPanel::itemAlignmentChanged); connect(SETTINGS, &DockSettings::indicatorStyleChanged, this, &DockPanel::indicatorStyleChanged); connect(SETTINGS, &DockSettings::lockedChanged, this, &DockPanel::lockedChanged); @@ -134,9 +357,10 @@ bool DockPanel::init() QObject::connect(this, &DApplet::rootObjectChanged, this, [this]() { if (rootObject()) { // those connections need connect after DPanel::init() which create QQuickWindow - // xChanged yChanged not worked on wayland, so use above positionChanged instead - // connect(window(), &QQuickWindow::xChanged, this, &DockPanel::onWindowGeometryChanged); - // connect(window(), &QQuickWindow::yChanged, this, &DockPanel::onWindowGeometryChanged); + if (QGuiApplication::platformName() == QStringLiteral("xcb")) { + connect(window(), &QQuickWindow::xChanged, this, &DockPanel::onWindowGeometryChanged); + connect(window(), &QQuickWindow::yChanged, this, &DockPanel::onWindowGeometryChanged); + } connect(window(), &QQuickWindow::widthChanged, this, &DockPanel::onWindowGeometryChanged); connect(window(), &QQuickWindow::heightChanged, this, &DockPanel::onWindowGeometryChanged); QMetaObject::invokeMethod(this, &DockPanel::onWindowGeometryChanged); @@ -150,16 +374,23 @@ bool DockPanel::init() } }); - m_theme = static_cast(Dtk::Gui::DGuiApplicationHelper::instance()->themeType()); + auto *guiHelper = Dtk::Gui::DGuiApplicationHelper::instance(); + QObject::connect(guiHelper, &Dtk::Gui::DGuiApplicationHelper::themeTypeChanged, + this, &DockPanel::syncColorThemeWithSystem); + if (auto *platformTheme = guiHelper->applicationTheme()) { + QObject::connect(platformTheme, &Dtk::Gui::DPlatformTheme::themeNameChanged, + this, &DockPanel::syncColorThemeWithSystem); + } + QDBusConnection::sessionBus().connect(kAppearanceService, kAppearancePath, kAppearanceInterface, + "Changed", this, SLOT(onAppearanceChanged(QString,QString))); + QDBusConnection::sessionBus().connect(kAppearanceService, kAppearancePath, kAppearanceInterface, + "Refreshed", this, SLOT(onAppearanceRefreshed(QString))); + syncColorThemeWithSystem(); + auto platformName = QGuiApplication::platformName(); if (QStringLiteral("wayland") == platformName) { - // TODO: support get color type from wayland m_helper = new WaylandDockHelper(this); } else if (QStringLiteral("xcb") == platformName) { - QObject::connect(Dtk::Gui::DGuiApplicationHelper::instance(), &Dtk::Gui::DGuiApplicationHelper::themeTypeChanged, - this, [this](){ - setColorTheme(static_cast(Dtk::Gui::DGuiApplicationHelper::instance()->themeType())); - }); m_helper = new X11DockHelper(this); } @@ -193,28 +424,13 @@ void DockPanel::setFrontendWindowRect(int transformOffsetX, int transformOffsetY auto ratio = window()->devicePixelRatio(); auto screenGeometry = window()->screen()->geometry(); auto geometry = window()->geometry(); - auto xOffset = 0, yOffset = 0; - - switch (position()) { - case Top: - xOffset = (screenGeometry.width() - geometry.width()) / 2; - yOffset = transformOffsetY; - break; - case Bottom: - xOffset = (screenGeometry.width() - geometry.width()) / 2; - yOffset = screenGeometry.height() - geometry.height() + transformOffsetY; - break; - case Right: - xOffset = screenGeometry.width() - geometry.width() + transformOffsetX; - yOffset = (screenGeometry.height() - geometry.height()) / 2; - break; - case Left: - xOffset = transformOffsetX; - yOffset = (screenGeometry.height() - geometry.height()) / 2; - break; - } + const int xOffset = geometry.x() - screenGeometry.x() + transformOffsetX; + const int yOffset = geometry.y() - screenGeometry.y() + transformOffsetY; - m_frontendWindowRect = QRect(screenGeometry.x() + xOffset * ratio, screenGeometry.y() + yOffset * ratio, geometry.width() * ratio, geometry.height() * ratio); + m_frontendWindowRect = QRect(screenGeometry.x() + xOffset * ratio, + screenGeometry.y() + yOffset * ratio, + geometry.width() * ratio, + geometry.height() * ratio); } ColorTheme DockPanel::colorTheme() @@ -230,6 +446,63 @@ void DockPanel::setColorTheme(const ColorTheme& theme) Q_EMIT this->colorThemeChanged(theme); } +void DockPanel::onAppearanceChanged(const QString &type, const QString &value) +{ + Q_UNUSED(value) + static const QStringList kTrackedAppearanceKeys{ + QStringLiteral("GlobalTheme"), + QStringLiteral("GtkTheme"), + QStringLiteral("IconTheme"), + QStringLiteral("QtActiveColor") + }; + + if (!kTrackedAppearanceKeys.contains(type, Qt::CaseInsensitive)) + return; + + m_themeSyncTimer->start(); +} + +void DockPanel::onAppearanceRefreshed(const QString &type) +{ + static const QStringList kTrackedAppearanceKeys{ + QStringLiteral("GlobalTheme"), + QStringLiteral("GtkTheme"), + QStringLiteral("IconTheme"), + QStringLiteral("QtActiveColor") + }; + + if (!type.isEmpty() && !kTrackedAppearanceKeys.contains(type, Qt::CaseInsensitive)) + return; + + m_themeSyncTimer->start(); +} + +void DockPanel::syncColorThemeWithSystem() +{ + auto *guiHelper = Dtk::Gui::DGuiApplicationHelper::instance(); + QDBusInterface appearanceProperties(kAppearanceService, + kAppearancePath, + kPropertiesInterface, + QDBusConnection::sessionBus()); + if (appearanceProperties.isValid()) { + const QDBusReply reply = appearanceProperties.call(QStringLiteral("Get"), + QString::fromLatin1(kAppearanceInterface), + QStringLiteral("GlobalTheme")); + if (reply.isValid()) { + const QString globalThemeName = reply.value().variant().toString(); + const QString gtkThemeName = readAppearanceStringProperty(QStringLiteral("GtkTheme")); + const QString iconThemeName = readAppearanceStringProperty(QStringLiteral("IconTheme")); + syncApplicationTheme(guiHelper, globalThemeName, gtkThemeName, iconThemeName); + setColorTheme(colorThemeFromHelperThemeType( + effectiveThemeTypeFromAppearance(globalThemeName, gtkThemeName, iconThemeName, guiHelper))); + return; + } + } + + syncApplicationTheme(guiHelper, QString(), QString(), QString()); + setColorTheme(colorThemeFromHelperThemeType(currentEffectiveThemeType(guiHelper))); +} + uint DockPanel::dockSize() { return SETTINGS->dockSize(); @@ -260,6 +533,11 @@ Position DockPanel::position() return SETTINGS->position(); } +ViewMode DockPanel::viewMode() +{ + return SETTINGS->viewMode(); +} + void DockPanel::setPosition(const Position& position) { if (position == SETTINGS->position()) return; @@ -269,6 +547,35 @@ void DockPanel::setPosition(const Position& position) // Directly commit the position change SETTINGS->setPosition(position); + + if (SETTINGS->viewMode() == ViewMode::FashionMode) { + SETTINGS->setItemAlignment(position == Position::Bottom + ? ItemAlignment::LeftAlignment + : ItemAlignment::CenterAlignment); + } +} + +void DockPanel::setViewMode(const ViewMode &mode) +{ + if (SETTINGS->viewMode() == mode) + return; + + SETTINGS->setViewMode(mode); + + switch (mode) { + case ViewMode::CenteredMode: + SETTINGS->setItemAlignment(ItemAlignment::CenterAlignment); + break; + case ViewMode::LeftAlignedMode: + SETTINGS->setItemAlignment(ItemAlignment::LeftAlignment); + break; + case ViewMode::FashionMode: + SETTINGS->setItemAlignment(SETTINGS->position() == Position::Bottom + ? ItemAlignment::LeftAlignment + : ItemAlignment::CenterAlignment); + SETTINGS->setIndicatorStyle(IndicatorStyle::Fashion); + break; + } } ItemAlignment DockPanel::itemAlignment() @@ -278,6 +585,9 @@ ItemAlignment DockPanel::itemAlignment() void DockPanel::setItemAlignment(const ItemAlignment& alignment) { + if (SETTINGS->viewMode() != ViewMode::FashionMode) { + SETTINGS->setViewMode(alignment == ItemAlignment::CenterAlignment ? ViewMode::CenteredMode : ViewMode::LeftAlignedMode); + } SETTINGS->setItemAlignment(alignment); } @@ -358,6 +668,35 @@ void DockPanel::notifyDockPositionChanged(int offsetX, int offsetY) Q_EMIT frontendWindowRectChanged(m_frontendWindowRect); } +void DockPanel::reportMousePresence(bool containsMouse, const QPointF &cursorPosition) +{ + const bool previousContainsMouse = this->containsMouse(); + const QPointF previousCursorPosition = this->cursorPosition(); + + const auto isSameReportedPosition = [](const QPointF &lhs, const QPointF &rhs) { + constexpr qreal epsilon = 0.5; + return qAbs(lhs.x() - rhs.x()) <= epsilon && qAbs(lhs.y() - rhs.y()) <= epsilon; + }; + + // Ignore delayed clear requests from a previously hovered delegate once a new + // delegate has already taken over spotlight reporting. + if (!containsMouse && m_reportedContainsMouse && cursorPosition != QPointF() && !isSameReportedPosition(m_reportedCursorPosition, cursorPosition)) { + return; + } + + m_reportedContainsMouse = containsMouse; + if (containsMouse) { + m_reportedCursorPosition = cursorPosition; + } + + if (previousContainsMouse != this->containsMouse()) { + emit containsMouseChanged(this->containsMouse()); + } + if (previousCursorPosition != this->cursorPosition()) { + emit cursorPositionChanged(this->cursorPosition()); + } +} + void DockPanel::launcherVisibleChanged(bool visible) { if (visible == m_launcherShown) return; @@ -369,6 +708,14 @@ void DockPanel::launcherVisibleChanged(bool visible) if (newHideState != oldHideState) { Q_EMIT hideStateChanged(newHideState); } + + if (m_launcherShown) { + m_launcherRaisePasses = 10; + m_launcherRaiseTimer->start(); + } else { + m_launcherRaisePasses = 0; + m_launcherRaiseTimer->stop(); + } } void DockPanel::updateDockScreen() @@ -468,6 +815,44 @@ void DockPanel::setContextDragging(bool newContextDragging) emit contextDraggingChanged(); } +bool DockPanel::containsMouse() const +{ + return m_containsMouse || m_reportedContainsMouse; +} + +QPointF DockPanel::cursorPosition() const +{ + return m_reportedContainsMouse ? m_reportedCursorPosition : m_cursorPosition; +} + +void DockPanel::setContainsMouse(bool containsMouse) +{ + const bool previousContainsMouse = this->containsMouse(); + const QPointF previousCursorPosition = this->cursorPosition(); + if (m_containsMouse == containsMouse) + return; + + m_containsMouse = containsMouse; + if (previousContainsMouse != this->containsMouse()) { + emit containsMouseChanged(this->containsMouse()); + } + if (previousCursorPosition != this->cursorPosition()) { + emit cursorPositionChanged(this->cursorPosition()); + } +} + +void DockPanel::setCursorPosition(const QPointF &cursorPosition) +{ + const QPointF previousCursorPosition = this->cursorPosition(); + if (m_cursorPosition == cursorPosition) + return; + + m_cursorPosition = cursorPosition; + if (previousCursorPosition != this->cursorPosition()) { + emit cursorPositionChanged(this->cursorPosition()); + } +} + bool DockPanel::isResizing() const { return m_isResizing; diff --git a/panels/dock/dockpanel.h b/panels/dock/dockpanel.h index 3bd31485c..16b8fe8d1 100644 --- a/panels/dock/dockpanel.h +++ b/panels/dock/dockpanel.h @@ -11,6 +11,8 @@ #include #include +class QTimer; + namespace dock { class DockHelper; class LoadTrayPlugins; @@ -29,6 +31,7 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext Q_PROPERTY(uint dockSize READ dockSize WRITE setDockSize NOTIFY dockSizeChanged FINAL) Q_PROPERTY(HideMode hideMode READ hideMode WRITE setHideMode NOTIFY hideModeChanged FINAL) Q_PROPERTY(Position position READ position WRITE setPosition NOTIFY positionChanged FINAL) + Q_PROPERTY(ViewMode viewMode READ viewMode WRITE setViewMode NOTIFY viewModeChanged FINAL) Q_PROPERTY(ItemAlignment itemAlignment READ itemAlignment WRITE setItemAlignment NOTIFY itemAlignmentChanged FINAL) Q_PROPERTY(IndicatorStyle indicatorStyle READ indicatorStyle WRITE setIndicatorStyle NOTIFY indicatorStyleChanged FINAL) Q_PROPERTY(bool showInPrimary READ showInPrimary WRITE setShowInPrimary NOTIFY showInPrimaryChanged FINAL) @@ -41,6 +44,8 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext Q_PROPERTY(bool debugMode READ debugMode FINAL CONSTANT) Q_PROPERTY(bool contextDragging READ contextDragging WRITE setContextDragging NOTIFY contextDraggingChanged FINAL) + Q_PROPERTY(bool containsMouse READ containsMouse NOTIFY containsMouseChanged FINAL) + Q_PROPERTY(QPointF cursorPosition READ cursorPosition NOTIFY cursorPositionChanged FINAL) public: explicit DockPanel(QObject *parent = nullptr); @@ -69,6 +74,9 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext Position position(); void setPosition(const Position& position); + ViewMode viewMode(); + void setViewMode(const ViewMode& mode); + ItemAlignment itemAlignment(); void setItemAlignment(const ItemAlignment& alignment); @@ -83,6 +91,7 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext Q_INVOKABLE void openDockSettings() const; Q_INVOKABLE void notifyDockPositionChanged(int offsetX, int offsetY); + Q_INVOKABLE void reportMousePresence(bool containsMouse, const QPointF &cursorPosition = QPointF()); bool showInPrimary() const; void setShowInPrimary(bool newShowInPrimary); @@ -100,6 +109,11 @@ class DockPanel : public DS_NAMESPACE::DPanel, public QDBusContext bool contextDragging() const; void setContextDragging(bool newContextDragging); + bool containsMouse() const; + QPointF cursorPosition() const; + void setContainsMouse(bool containsMouse); + void setCursorPosition(const QPointF &cursorPosition); + bool isResizing() const; void setIsResizing(bool resizing); @@ -110,6 +124,9 @@ private Q_SLOTS: void onWindowGeometryChanged(); void launcherVisibleChanged(bool visible); void updateDockScreen(); + void onAppearanceChanged(const QString &type, const QString &value); + void onAppearanceRefreshed(const QString &type); + void syncColorThemeWithSystem(); Q_SIGNALS: void geometryChanged(QRect geometry); @@ -122,6 +139,7 @@ private Q_SLOTS: void hideModeChanged(HideMode mode); void beforePositionChanged(Position beforePosition); void positionChanged(Position position); + void viewModeChanged(ViewMode mode); void itemAlignmentChanged(ItemAlignment alignment); void indicatorStyleChanged(IndicatorStyle style); void showInPrimaryChanged(bool showInPrimary); @@ -133,6 +151,8 @@ private Q_SLOTS: void lockedChanged(bool locked); void contextDraggingChanged(); + void containsMouseChanged(bool containsMouse); + void cursorPositionChanged(const QPointF &cursorPosition); void isResizingChanged(bool isResizing); private: @@ -143,8 +163,15 @@ private Q_SLOTS: LoadTrayPlugins *m_loadTrayPlugins; bool m_compositorReady; bool m_launcherShown; + QTimer *m_themeSyncTimer; + QTimer *m_launcherRaiseTimer; + int m_launcherRaisePasses; bool m_contextDragging; + bool m_containsMouse; + bool m_reportedContainsMouse; bool m_isResizing; + QPointF m_cursorPosition; + QPointF m_reportedCursorPosition; QRect m_frontendWindowRect; }; diff --git a/panels/dock/docksettings.cpp b/panels/dock/docksettings.cpp index 0ae5a5c5f..b0428946c 100644 --- a/panels/dock/docksettings.cpp +++ b/panels/dock/docksettings.cpp @@ -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 @@ -6,6 +6,8 @@ #include "constants.h" #include "docksettings.h" +#include +#include #include #include @@ -92,6 +94,50 @@ static ItemAlignment string2ItenAlignment(const QString& alignmentStr) return ItemAlignment::CenterAlignment; } +static QString viewMode2String(const ViewMode& mode) +{ + switch (mode) { + case ViewMode::LeftAlignedMode: + return "left"; + case ViewMode::FashionMode: + return "fashion"; + case ViewMode::CenteredMode: + default: + return "center"; + } +} + +static ViewMode inferViewMode(const Position &position, const IndicatorStyle &style, const ItemAlignment &alignment) +{ + if (position == Position::Bottom && style == IndicatorStyle::Fashion && alignment == ItemAlignment::LeftAlignment) + return ViewMode::FashionMode; + + return alignment == ItemAlignment::LeftAlignment ? ViewMode::LeftAlignedMode : ViewMode::CenteredMode; +} + +static ViewMode string2ViewMode(const QString &modeStr, const Position &position, const IndicatorStyle &style, const ItemAlignment &alignment) +{ + if (modeStr == "left") + return ViewMode::LeftAlignedMode; + if (modeStr == "fashion") + return ViewMode::FashionMode; + if (modeStr == "center") + return ViewMode::CenteredMode; + + return inferViewMode(position, style, alignment); +} + +static QString localViewModeSettingsPath() +{ + QString configRoot = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); + if (configRoot.isEmpty()) + configRoot = QDir::homePath() + QStringLiteral("/.config"); + + QDir configDir(configRoot); + configDir.mkpath(QStringLiteral("dde-shell")); + return configDir.filePath(QStringLiteral("dde-shell/dock-view-mode.ini")); +} + static QString indicatorStyle2String(IndicatorStyle mode) { switch (mode) { @@ -126,10 +172,12 @@ DockSettings* DockSettings::instance() DockSettings::DockSettings(QObject* parent) : QObject(parent) , m_dockConfig(DConfig::create("org.deepin.dde.shell", "org.deepin.ds.dock", QString(), this)) + , m_viewModeSettings(new QSettings(localViewModeSettingsPath(), QSettings::IniFormat)) , m_writeTimer(new QTimer(this)) , m_dockSize(dock::DEFAULT_DOCK_SIZE) , m_hideMode(dock::KeepShowing) , m_dockPosition(dock::Bottom) + , m_viewMode(dock::CenteredMode) , m_alignment(dock::CenterAlignment) , m_style(dock::Fashion) , m_locked(false) @@ -147,6 +195,8 @@ void DockSettings::init() m_dockPosition = string2Position(m_dockConfig->value(keyPosition).toString()); m_alignment = string2ItenAlignment(m_dockConfig->value(keyItemAlignment).toString()); m_style = string2IndicatorStyle(m_dockConfig->value(keyIndicatorStyle).toString()); + const QString persistedViewMode = m_viewModeSettings ? m_viewModeSettings->value(QStringLiteral("ViewMode")).toString() : QString(); + m_viewMode = string2ViewMode(persistedViewMode, m_dockPosition, m_style, m_alignment); m_pluginsVisible = m_dockConfig->value(keyPluginsVisible).toMap(); m_showInPrimary = m_dockConfig->value(keyShowInPrimary).toBool(); m_locked = m_dockConfig->value(keyLocked).toBool(); @@ -230,6 +280,11 @@ Position DockSettings::position() return m_dockPosition; } +ViewMode DockSettings::viewMode() +{ + return m_viewMode; +} + void DockSettings::setPosition(const Position& position) { if (position == m_dockPosition) return; @@ -239,6 +294,15 @@ void DockSettings::setPosition(const Position& position) addWriteJob(positionJob); } +void DockSettings::setViewMode(const ViewMode &mode) +{ + if (mode == m_viewMode) return; + + m_viewMode = mode; + Q_EMIT viewModeChanged(m_viewMode); + addWriteJob(viewModeJob); +} + ItemAlignment DockSettings::itemAlignment() { return m_alignment; @@ -350,6 +414,16 @@ void DockSettings::checkWriteJob() }); break; } + case viewModeJob: { + connect(m_writeTimer, &QTimer::timeout, this, [this](){ + if (m_viewModeSettings) { + m_viewModeSettings->setValue(QStringLiteral("ViewMode"), viewMode2String(m_viewMode)); + m_viewModeSettings->sync(); + } + checkWriteJob(); + }); + break; + } case itemAlignmentJob: { connect(m_writeTimer, &QTimer::timeout, this, [this](){ m_dockConfig->setValue(keyItemAlignment, itemAlignment2String(m_alignment)); diff --git a/panels/dock/docksettings.h b/panels/dock/docksettings.h index 8635a81e1..0f035f42f 100644 --- a/panels/dock/docksettings.h +++ b/panels/dock/docksettings.h @@ -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 @@ -10,6 +10,7 @@ #include #include #include +#include DCORE_USE_NAMESPACE @@ -22,6 +23,7 @@ class DockSettings : public QObject Q_PROPERTY(uint dockSize READ dockSize WRITE setDockSize NOTIFY dockSizeChanged FINAL) Q_PROPERTY(HideMode hidemode READ hideMode WRITE setHideMode NOTIFY hideModeChanged FINAL) Q_PROPERTY(Position position READ position WRITE setPosition NOTIFY positionChanged FINAL) + Q_PROPERTY(ViewMode viewMode READ viewMode WRITE setViewMode NOTIFY viewModeChanged FINAL) Q_PROPERTY(ItemAlignment itemAlignment READ itemAlignment WRITE setItemAlignment NOTIFY itemAlignmentChanged FINAL) Q_PROPERTY(IndicatorStyle indicatorStyle READ indicatorStyle WRITE setIndicatorStyle NOTIFY indicatorStyleChanged FINAL) Q_PROPERTY(QVariantMap pluginsVisible READ pluginsVisible WRITE setPluginsVisible NOTIFY pluginsVisibleChanged FINAL) @@ -34,6 +36,7 @@ class DockSettings : public QObject uint dockSize(); HideMode hideMode(); Position position(); + ViewMode viewMode(); ItemAlignment itemAlignment(); IndicatorStyle indicatorStyle(); QVariantMap pluginsVisible(); @@ -43,6 +46,7 @@ class DockSettings : public QObject void setDockSize(const uint& size); void setHideMode(const HideMode& mode); void setPosition(const Position& position); + void setViewMode(const ViewMode& mode); void setItemAlignment(const ItemAlignment& alignment); void setIndicatorStyle(const IndicatorStyle& style); void setPluginsVisible(const QVariantMap & pluginsVisible); @@ -54,8 +58,9 @@ class DockSettings : public QObject dockSizeJob = 0, hideModeJob = 1, positionJob = 2, - itemAlignmentJob = 3, - indicatorStyleJob = 4, + viewModeJob = 3, + itemAlignmentJob = 4, + indicatorStyleJob = 5, }; explicit DockSettings(QObject *parent = nullptr); @@ -68,6 +73,7 @@ class DockSettings : public QObject void dockSizeChanged(uint size); void hideModeChanged(HideMode mode); void positionChanged(Position position); + void viewModeChanged(ViewMode mode); void itemAlignmentChanged(ItemAlignment alignment); void indicatorStyleChanged(IndicatorStyle style); void pluginsVisibleChanged(const QVariantMap &pluginsVisible); @@ -77,12 +83,14 @@ class DockSettings : public QObject private: QScopedPointer m_dockConfig; + QScopedPointer m_viewModeSettings; QTimer* m_writeTimer; QList m_writeJob; uint m_dockSize; HideMode m_hideMode; Position m_dockPosition; + ViewMode m_viewMode; ItemAlignment m_alignment; IndicatorStyle m_style; QVariantMap m_pluginsVisible; diff --git a/panels/dock/environments.h.in b/panels/dock/environments.h.in index 032f4708c..63fb79c53 100644 --- a/panels/dock/environments.h.in +++ b/panels/dock/environments.h.in @@ -5,4 +5,7 @@ #pragma once #define CMAKE_INSTALL_FULL_LIBEXECDIR "@CMAKE_INSTALL_FULL_LIBEXECDIR@" +#define CMAKE_INSTALL_FULL_LIBDIR "@CMAKE_INSTALL_FULL_LIBDIR@" #define CMAKE_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@" +#define TRAY_LOADER_FONT_SYNC_BUILD_PATH "@PROJECT_BINARY_DIR@/libtray_loader_font_sync.so" +#define TRAY_LOADER_FONT_SYNC_INSTALL_PATH "@CMAKE_INSTALL_FULL_LIBDIR@/dde-shell/libtray_loader_font_sync.so" diff --git a/panels/dock/fashionleftpluginprovider.cpp b/panels/dock/fashionleftpluginprovider.cpp new file mode 100644 index 000000000..acfb79ebe --- /dev/null +++ b/panels/dock/fashionleftpluginprovider.cpp @@ -0,0 +1,4377 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "fashionleftpluginprovider.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace dock { + +namespace { + +constexpr auto NotificationService = "org.deepin.dde.Notification1"; +constexpr auto NotificationPath = "/org/deepin/dde/Notification1"; +constexpr auto NotificationInterface = "org.deepin.dde.Notification1"; + +constexpr auto MailService = "org.deepin.mail"; +constexpr auto MailPath = "/org/deepin/mail"; +constexpr auto MailInterface = "org.deepin.mail"; + +constexpr auto MprisPath = "/org/mpris/MediaPlayer2"; +constexpr auto MprisRootInterface = "org.mpris.MediaPlayer2"; +constexpr auto MprisPlayerInterface = "org.mpris.MediaPlayer2.Player"; +constexpr auto DBusPropertiesInterface = "org.freedesktop.DBus.Properties"; + +constexpr auto SystemMonitorService = "org.deepin.SystemMonitorDaemon"; +constexpr auto SystemMonitorPath = "/org/deepin/SystemMonitorDaemon"; +constexpr auto SystemMonitorInterface = "org.deepin.SystemMonitorDaemon"; + +constexpr auto ControlCenterService = "org.deepin.dde.ControlCenter1"; +constexpr auto ControlCenterPath = "/org/deepin/dde/ControlCenter1"; +constexpr auto ControlCenterInterface = "org.deepin.dde.ControlCenter1"; + +constexpr auto StatusNotifierWatcherService = "org.kde.StatusNotifierWatcher"; +constexpr auto StatusNotifierWatcherPath = "/StatusNotifierWatcher"; +constexpr auto StatusNotifierWatcherInterface = "org.kde.StatusNotifierWatcher"; +constexpr auto StatusNotifierItemInterface = "org.kde.StatusNotifierItem"; + +constexpr auto SystemMonitorServerService = "com.deepin.SystemMonitorServer"; +constexpr auto SystemMonitorServerPath = "/com/deepin/SystemMonitorServer"; +constexpr auto SystemMonitorServerInterface = "com.deepin.SystemMonitorServer"; +constexpr auto SystemMonitorMainService = "com.deepin.SystemMonitorMain"; +constexpr auto SystemMonitorMainPath = "/com/deepin/SystemMonitorMain"; +constexpr auto SystemMonitorMainInterface = "com.deepin.SystemMonitorMain"; + +const QString WeatherAppIconPath = QStringLiteral("/usr/share/icons/hicolor/scalable/apps/org.deepin.weather.svg"); +const QString WeatherDockPluginPath = QStringLiteral("/usr/lib/dde-dock/plugins/system-trays/libdeepin-weather-dock-plugin.so"); +const QString MessageIconPath = QStringLiteral("/usr/share/icons/bloom/actions/24/mail-unread-new.svg"); +const QString DockPackageDataPath = QStringLiteral("/usr/share/dde-shell/org.deepin.ds.dock"); +const QString DefaultMailIconName = QStringLiteral("deepin-mail"); +const QString DefaultMusicIconName = QStringLiteral("audio-x-generic"); +constexpr int WeatherPopupTaskbarGap = 10; + +using WeatherCodeToDescriptionFunction = QString (*)(int); +using WeatherCodeToIconNameFunction = QString (*)(int, bool); + +struct MusicSnapshot +{ + QString service; + QString desktopEntry; + QString executablePath; + QString appName; + QString title = QStringLiteral("未检测到音乐"); + QString subtitle = QStringLiteral("打开播放器开始播放"); + QUrl artSource; + bool available = false; + bool playing = false; + bool canRaise = false; + bool canGoPrevious = false; + bool canGoNext = false; + bool canTogglePlayback = false; + int score = std::numeric_limits::min(); +}; + +struct MprisMetadataFallback +{ + QString title; + QString artist; + QString artUrl; +}; + +struct AiCliProcessInfo +{ + qint64 pid = 0; + qint64 parentPid = 0; + QString toolId; + QString toolName; + QString workingDirectory; + QStringList arguments; + QString sessionLogPath; +}; + +enum class AiCliTaskState +{ + Unknown, + Running, + Completed, +}; + +struct AiCliSessionLogSnapshot +{ + AiCliTaskState taskState = AiCliTaskState::Unknown; + QString eventType; + QDateTime eventTimeUtc; +}; + +struct AiCliSessionLogCacheEntry +{ + qint64 fileSize = -1; + qint64 lastModifiedMs = -1; + AiCliSessionLogSnapshot snapshot; +}; + +struct MusicDesktopCandidate +{ + QString desktopFilePath; + QString desktopId; + QString executablePath; + QString executableBaseName; + QString appName; +}; + +struct MetadataFallbackCacheEntry +{ + MprisMetadataFallback metadata; + qint64 capturedAtMs = 0; +}; + +struct RunningMusicPlayerSnapshotCacheEntry +{ + MusicSnapshot snapshot; + QString previousDesktopEntry; + qint64 capturedAtMs = 0; +}; + +constexpr qint64 AiCliRecentCompletedWindowMs = 30LL * 60LL * 1000LL; + +QString aiCliSessionRootPath() +{ + return QDir::cleanPath(QDir::homePath() + QStringLiteral("/.codex/sessions")); +} + +QString canonicalExecutablePath(const QString &path); +QString desktopEntryText(const QString &desktopFilePath, const QString &key); +QString localizedDesktopEntryText(const QString &desktopFilePath, const QString &key); +QString desktopEntryExecutable(const QString &desktopFilePath); +bool isBrowserDesktopId(const QString &desktopId); +QString bestWindowIdForPid(qint64 pid, bool onlyVisible = true); + +QHash &aiCliSessionLogCache() +{ + static QHash cache; + return cache; +} + +QMutex &aiCliSessionLogCacheMutex() +{ + static QMutex mutex; + return mutex; +} + +QHash &desktopFileByExecutableCache() +{ + static QHash cache; + return cache; +} + +QMutex &desktopFileByExecutableCacheMutex() +{ + static QMutex mutex; + return mutex; +} + +QHash &metadataFallbackCache() +{ + static QHash cache; + return cache; +} + +QMutex &metadataFallbackCacheMutex() +{ + static QMutex mutex; + return mutex; +} + +RunningMusicPlayerSnapshotCacheEntry &runningMusicPlayerSnapshotCache() +{ + static RunningMusicPlayerSnapshotCacheEntry cache; + return cache; +} + +QVariantMap variantMapFromIntHash(const QHash &hash) +{ + QVariantMap map; + for (auto it = hash.cbegin(); it != hash.cend(); ++it) { + map.insert(it.key(), it.value()); + } + return map; +} + +QVariantMap variantMapFromLongLongHash(const QHash &hash) +{ + QVariantMap map; + for (auto it = hash.cbegin(); it != hash.cend(); ++it) { + map.insert(it.key(), static_cast(it.value())); + } + return map; +} + +QVariantMap variantMapFromStringHash(const QHash &hash) +{ + QVariantMap map; + for (auto it = hash.cbegin(); it != hash.cend(); ++it) { + map.insert(it.key(), it.value()); + } + return map; +} + +QHash intHashFromVariantMap(const QVariantMap &map) +{ + QHash hash; + for (auto it = map.cbegin(); it != map.cend(); ++it) { + hash.insert(it.key(), it.value().toInt()); + } + return hash; +} + +QHash longLongHashFromVariantMap(const QVariantMap &map) +{ + QHash hash; + for (auto it = map.cbegin(); it != map.cend(); ++it) { + hash.insert(it.key(), it.value().toLongLong()); + } + return hash; +} + +QHash stringHashFromVariantMap(const QVariantMap &map) +{ + QHash hash; + for (auto it = map.cbegin(); it != map.cend(); ++it) { + hash.insert(it.key(), it.value().toString()); + } + return hash; +} + +QString aiCliTaskStateKey(AiCliTaskState state) +{ + switch (state) { + case AiCliTaskState::Running: + return QStringLiteral("running"); + case AiCliTaskState::Completed: + return QStringLiteral("completed"); + default: + return QStringLiteral("unknown"); + } +} + +QStringList localDesktopSearchDirectories() +{ + QStringList directories = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation); + directories << QStringLiteral("/var/lib/linglong/entries/share/applications") + << QStringLiteral("/usr/local/share/applications") + << QStringLiteral("/usr/share/applications") + << QDir::home().filePath(QStringLiteral(".local/share/applications")); + + QStringList uniqueDirectories; + for (const QString &directory : directories) { + const QString cleanPath = QDir::cleanPath(directory); + if (!QFileInfo(cleanPath).isDir() || uniqueDirectories.contains(cleanPath)) { + continue; + } + + uniqueDirectories << cleanPath; + } + + return uniqueDirectories; +} + +bool desktopEntryBoolValue(const QString &desktopFilePath, const QString &key) +{ + if (desktopFilePath.isEmpty() || key.isEmpty()) { + return false; + } + + QSettings settings(desktopFilePath, QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + return settings.value(key).toBool(); +} + +bool containsKeyword(const QString &text, const QStringList &keywords) +{ + const QString normalizedText = text.trimmed().toLower(); + if (normalizedText.isEmpty()) { + return false; + } + + for (const QString &keyword : keywords) { + if (normalizedText.contains(keyword)) { + return true; + } + } + + return false; +} + +bool desktopEntryLooksLikeMusicPlayer(const QString &desktopFilePath) +{ + if (desktopFilePath.isEmpty() + || desktopEntryBoolValue(desktopFilePath, QStringLiteral("Hidden")) + || desktopEntryBoolValue(desktopFilePath, QStringLiteral("NoDisplay"))) { + return false; + } + + const QString desktopId = QFileInfo(desktopFilePath).fileName(); + if (isBrowserDesktopId(desktopId)) { + return false; + } + + const QString executablePath = desktopEntryExecutable(desktopFilePath); + if (executablePath.isEmpty()) { + return false; + } + + const QString categories = desktopEntryText(desktopFilePath, QStringLiteral("Categories")).toLower(); + const QString name = localizedDesktopEntryText(desktopFilePath, QStringLiteral("Name")).toLower(); + const QString genericName = localizedDesktopEntryText(desktopFilePath, QStringLiteral("GenericName")).toLower(); + const QString combinedText = name + QLatin1Char(' ') + genericName + QLatin1Char(' ') + categories; + + if (containsKeyword(combinedText, { + QStringLiteral("record"), + QStringLiteral("recorder"), + QStringLiteral("capture"), + QStringLiteral("mixer"), + QStringLiteral("volume"), + QStringLiteral("effect"), + QStringLiteral("effects"), + QStringLiteral("equalizer"), + QStringLiteral("enhancer"), + QStringLiteral("processor"), + QStringLiteral("filter"), + QStringLiteral("converter"), + QStringLiteral("editor"), + QStringLiteral("podcast"), + QStringLiteral("剪辑"), + QStringLiteral("录音"), + QStringLiteral("混音"), + QStringLiteral("音效"), + QStringLiteral("均衡器"), + })) { + return false; + } + + return containsKeyword(combinedText, { + QStringLiteral("music"), + QStringLiteral("spotify"), + QStringLiteral("netease"), + QStringLiteral("cloudmusic"), + QStringLiteral("rhythmbox"), + QStringLiteral("audacious"), + QStringLiteral("feeluown"), + QStringLiteral("listen1"), + QStringLiteral("qqmusic"), + QStringLiteral("kugou"), + QStringLiteral("musicplayer"), + QStringLiteral("音乐"), + QStringLiteral("云音乐"), + QStringLiteral("网易"), + QStringLiteral("酷狗"), + QStringLiteral("播放器"), + }); +} + +const QHash> &musicDesktopCandidatesByExecutableBaseName() +{ + static QHash> candidateIndex; + static bool initialized = false; + if (initialized) { + return candidateIndex; + } + + initialized = true; + for (const QString &directory : localDesktopSearchDirectories()) { + QDirIterator iterator(directory, + {QStringLiteral("*.desktop")}, + QDir::Files | QDir::Readable, + QDirIterator::Subdirectories); + while (iterator.hasNext()) { + const QString desktopFilePath = iterator.next(); + if (!desktopEntryLooksLikeMusicPlayer(desktopFilePath)) { + continue; + } + + const QString executablePath = canonicalExecutablePath(desktopEntryExecutable(desktopFilePath)); + const QString executableBaseName = QFileInfo(executablePath).fileName().trimmed().toLower(); + if (executableBaseName.isEmpty()) { + continue; + } + + MusicDesktopCandidate candidate; + candidate.desktopFilePath = desktopFilePath; + candidate.desktopId = QFileInfo(desktopFilePath).fileName(); + candidate.executablePath = executablePath; + candidate.executableBaseName = executableBaseName; + candidate.appName = localizedDesktopEntryText(desktopFilePath, QStringLiteral("Name")); + if (candidate.appName.isEmpty()) { + candidate.appName = QFileInfo(desktopFilePath).completeBaseName(); + } + + candidateIndex[candidate.executableBaseName].append(candidate); + } + } + + return candidateIndex; +} + +bool isRecognizedMusicDesktopId(const QString &desktopId) +{ + QString normalizedDesktopId = desktopId.trimmed(); + if (normalizedDesktopId.endsWith(QStringLiteral(".desktop"))) { + normalizedDesktopId.chop(QStringLiteral(".desktop").size()); + } + if (normalizedDesktopId.isEmpty()) { + return false; + } + + const auto &candidateIndex = musicDesktopCandidatesByExecutableBaseName(); + for (auto it = candidateIndex.cbegin(); it != candidateIndex.cend(); ++it) { + for (const MusicDesktopCandidate &candidate : it.value()) { + QString candidateDesktopId = candidate.desktopId.trimmed(); + if (candidateDesktopId.endsWith(QStringLiteral(".desktop"))) { + candidateDesktopId.chop(QStringLiteral(".desktop").size()); + } + if (candidateDesktopId == normalizedDesktopId) { + return true; + } + } + } + + return false; +} + +bool isRecognizedMusicExecutable(const QString &executablePath) +{ + const QString executableBaseName = QFileInfo(executablePath).fileName().trimmed().toLower(); + if (executableBaseName.isEmpty()) { + return false; + } + + return musicDesktopCandidatesByExecutableBaseName().contains(executableBaseName); +} + +QStringList sessionLogPathsForProcess(qint64 pid) +{ + if (pid <= 0) { + return {}; + } + + const QString sessionsRootPath = aiCliSessionRootPath(); + QDir fdDirectory(QStringLiteral("/proc/%1/fd").arg(pid)); + const QStringList fdEntries = fdDirectory.entryList(QDir::AllEntries | QDir::NoDotAndDotDot, + QDir::Name); + QStringList sessionLogPaths; + for (const QString &fdEntry : fdEntries) { + const QString targetPath = QFile::symLinkTarget(fdDirectory.filePath(fdEntry)); + if (targetPath.isEmpty()) { + continue; + } + + const QString cleanTargetPath = QDir::cleanPath(targetPath); + if (!cleanTargetPath.startsWith(sessionsRootPath + QLatin1Char('/')) + || !cleanTargetPath.endsWith(QStringLiteral(".jsonl"))) { + continue; + } + + if (!sessionLogPaths.contains(cleanTargetPath)) { + sessionLogPaths << cleanTargetPath; + } + } + + return sessionLogPaths; +} + +QString primarySessionLogPath(const QStringList &sessionLogPaths) +{ + QString selectedPath; + qint64 bestModifiedMs = std::numeric_limits::min(); + qint64 bestFileSize = std::numeric_limits::min(); + for (const QString &sessionLogPath : sessionLogPaths) { + const QFileInfo fileInfo(sessionLogPath); + if (!fileInfo.isFile()) { + continue; + } + + const qint64 modifiedMs = fileInfo.lastModified().toMSecsSinceEpoch(); + const qint64 fileSize = fileInfo.size(); + if (selectedPath.isEmpty() + || modifiedMs > bestModifiedMs + || (modifiedMs == bestModifiedMs && fileSize > bestFileSize) + || (modifiedMs == bestModifiedMs && fileSize == bestFileSize && sessionLogPath > selectedPath)) { + selectedPath = sessionLogPath; + bestModifiedMs = modifiedMs; + bestFileSize = fileSize; + } + } + + return selectedPath; +} + +QByteArray readTailOfFile(const QString &filePath, qint64 maxBytes) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return {}; + } + + const qint64 fileSize = file.size(); + const qint64 offset = qMax(0, fileSize - maxBytes); + if (!file.seek(offset)) { + return {}; + } + + QByteArray data = file.readAll(); + if (offset > 0) { + const int firstNewlineIndex = data.indexOf('\n'); + if (firstNewlineIndex >= 0) { + data.remove(0, firstNewlineIndex + 1); + } + } + + return data; +} + +AiCliSessionLogSnapshot parseAiCliSessionLogSnapshot(const QByteArray &tailData) +{ + const QList lines = tailData.split('\n'); + for (int index = lines.size() - 1; index >= 0; --index) { + const QByteArray line = lines.at(index).trimmed(); + if (line.isEmpty()) { + continue; + } + + const QJsonDocument document = QJsonDocument::fromJson(line); + if (!document.isObject()) { + continue; + } + + const QJsonObject object = document.object(); + if (object.value(QStringLiteral("type")).toString() != QStringLiteral("event_msg")) { + continue; + } + + const QJsonObject payload = object.value(QStringLiteral("payload")).toObject(); + const QString eventType = payload.value(QStringLiteral("type")).toString().trimmed(); + if (eventType.isEmpty()) { + continue; + } + + AiCliSessionLogSnapshot snapshot; + snapshot.eventType = eventType; + snapshot.taskState = eventType == QStringLiteral("task_complete") + ? AiCliTaskState::Completed + : AiCliTaskState::Running; + + const QString timestampText = object.value(QStringLiteral("timestamp")).toString().trimmed(); + snapshot.eventTimeUtc = QDateTime::fromString(timestampText, Qt::ISODateWithMs); + if (!snapshot.eventTimeUtc.isValid()) { + snapshot.eventTimeUtc = QDateTime::fromString(timestampText, Qt::ISODate); + } + if (snapshot.eventTimeUtc.isValid()) { + snapshot.eventTimeUtc = snapshot.eventTimeUtc.toUTC(); + } + + return snapshot; + } + + return {}; +} + +AiCliSessionLogSnapshot sessionLogSnapshot(const QString &sessionLogPath) +{ + const QFileInfo fileInfo(sessionLogPath); + if (!fileInfo.isFile()) { + return {}; + } + + const qint64 fileSize = fileInfo.size(); + const qint64 lastModifiedMs = fileInfo.lastModified().toMSecsSinceEpoch(); + + auto &cache = aiCliSessionLogCache(); + { + QMutexLocker locker(&aiCliSessionLogCacheMutex()); + const auto cachedEntry = cache.constFind(sessionLogPath); + if (cachedEntry != cache.cend() + && cachedEntry->fileSize == fileSize + && cachedEntry->lastModifiedMs == lastModifiedMs) { + return cachedEntry->snapshot; + } + } + + AiCliSessionLogSnapshot snapshot; + const QList probeSizes = { + 64 * 1024, + 256 * 1024, + 1024 * 1024, + }; + for (qint64 probeSize : probeSizes) { + snapshot = parseAiCliSessionLogSnapshot(readTailOfFile(sessionLogPath, probeSize)); + if (snapshot.taskState != AiCliTaskState::Unknown || fileSize <= probeSize) { + break; + } + } + + AiCliSessionLogCacheEntry cacheEntry; + cacheEntry.fileSize = fileSize; + cacheEntry.lastModifiedMs = lastModifiedMs; + cacheEntry.snapshot = snapshot; + { + QMutexLocker locker(&aiCliSessionLogCacheMutex()); + cache.insert(sessionLogPath, cacheEntry); + } + return snapshot; +} + +bool shouldCountCompletedSession(const AiCliSessionLogSnapshot &snapshot, const QDateTime &nowUtc) +{ + if (snapshot.taskState != AiCliTaskState::Completed || !snapshot.eventTimeUtc.isValid()) { + return false; + } + + const qint64 ageMs = snapshot.eventTimeUtc.msecsTo(nowUtc); + return ageMs >= 0 && ageMs <= AiCliRecentCompletedWindowMs; +} + +QString firstExistingPath(const QStringList &paths) +{ + for (const QString &path : paths) { + if (QFileInfo::exists(path)) { + return path; + } + } + + return {}; +} + +QString readTrimmedTextFile(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + + return QString::fromLocal8Bit(file.readAll()).trimmed(); +} + +QStringList readProcCommandLine(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return {}; + } + + const QList rawArguments = file.readAll().split('\0'); + QStringList arguments; + for (const QByteArray &rawArgument : rawArguments) { + if (rawArgument.isEmpty()) { + continue; + } + + arguments << QString::fromLocal8Bit(rawArgument); + } + + return arguments; +} + +QString aiToolDisplayName(const QString &toolId) +{ + if (toolId == QStringLiteral("codex")) { + return QStringLiteral("Codex"); + } + if (toolId == QStringLiteral("claude")) { + return QStringLiteral("Claude Code"); + } + + return {}; +} + +QString detectAiCliToolId(const QString &candidate) +{ + const QString normalized = candidate.trimmed().toLower(); + if (normalized.isEmpty()) { + return {}; + } + + if (normalized == QStringLiteral("codex") + || normalized.startsWith(QStringLiteral("codex-")) + || normalized.startsWith(QStringLiteral("codex."))) { + return QStringLiteral("codex"); + } + + if (normalized == QStringLiteral("claude") + || normalized == QStringLiteral("claude-code") + || normalized == QStringLiteral("claude_code") + || normalized.startsWith(QStringLiteral("claude-")) + || normalized.startsWith(QStringLiteral("claude."))) { + return QStringLiteral("claude"); + } + + return {}; +} + +QString detectAiCliToolId(const QString &executableName, + const QString &commName, + const QStringList &arguments) +{ + const QStringList candidates = [&arguments, &executableName, &commName] { + QStringList values; + values << executableName << commName; + for (const QString &argument : arguments) { + values << QFileInfo(argument).fileName(); + } + return values; + }(); + + for (const QString &candidate : candidates) { + const QString toolId = detectAiCliToolId(candidate); + if (!toolId.isEmpty()) { + return toolId; + } + } + + return {}; +} + +QString shortenSingleLineText(const QString &text, int maxLength = 32) +{ + QString singleLine = text; + singleLine.replace(QRegularExpression(QStringLiteral("\\s+")), QStringLiteral(" ")); + singleLine = singleLine.trimmed(); + + if (singleLine.size() <= maxLength) { + return singleLine; + } + + return singleLine.left(qMax(0, maxLength - 1)).trimmed() + QChar(0x2026); +} + +QString promptSummaryFromAiArguments(const QStringList &arguments) +{ + if (arguments.size() < 2) { + return {}; + } + + for (int index = 1; index + 1 < arguments.size(); ++index) { + const QString argument = arguments.at(index); + if (argument == QStringLiteral("-p") + || argument == QStringLiteral("--prompt") + || argument == QStringLiteral("--message") + || argument == QStringLiteral("--task")) { + return shortenSingleLineText(arguments.at(index + 1)); + } + } + + for (int index = 1; index + 1 < arguments.size(); ++index) { + const QString argument = arguments.at(index); + if ((argument == QStringLiteral("exec") + || argument == QStringLiteral("run") + || argument == QStringLiteral("ask")) + && !arguments.at(index + 1).startsWith(QLatin1Char('-'))) { + return shortenSingleLineText(arguments.at(index + 1)); + } + } + + return {}; +} + +QString homeRelativePath(const QString &path) +{ + const QString cleanPath = QDir::cleanPath(path); + const QString homePath = QDir::cleanPath(QDir::homePath()); + if (cleanPath == homePath) { + return QStringLiteral("~"); + } + if (cleanPath.startsWith(homePath + QLatin1Char('/'))) { + return QStringLiteral("~") + cleanPath.mid(homePath.size()); + } + + return cleanPath; +} + +QString workingDirectoryLabel(const QString &workingDirectory) +{ + const QString normalizedPath = homeRelativePath(workingDirectory); + if (normalizedPath.isEmpty()) { + return {}; + } + + QFileInfo fileInfo(normalizedPath); + if (!fileInfo.fileName().isEmpty()) { + return fileInfo.fileName(); + } + + return normalizedPath; +} + +qint64 parentPidForProcess(qint64 pid) +{ + if (pid <= 0) { + return 0; + } + + QFile statFile(QStringLiteral("/proc/%1/stat").arg(pid)); + if (!statFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + return 0; + } + + const QString statText = QString::fromLocal8Bit(statFile.readAll()).trimmed(); + const int closingParenIndex = statText.lastIndexOf(QLatin1Char(')')); + if (closingParenIndex < 0 || closingParenIndex + 2 >= statText.size()) { + return 0; + } + + const QString trailingFields = statText.mid(closingParenIndex + 2); + const QStringList fields = trailingFields.split(QLatin1Char(' '), Qt::SkipEmptyParts); + if (fields.size() < 3) { + return 0; + } + + bool ppidOk = false; + const qint64 ppid = fields.at(1).toLongLong(&ppidOk); + return ppidOk ? ppid : 0; +} + +bool processHasAncestor(qint64 pid, qint64 ancestorPid) +{ + if (pid <= 0 || ancestorPid <= 0) { + return false; + } + + qint64 currentPid = pid; + int guard = 0; + while (currentPid > 1 && guard < 64) { + if (currentPid == ancestorPid) { + return true; + } + + currentPid = parentPidForProcess(currentPid); + ++guard; + } + + return currentPid == ancestorPid; +} + +qint64 activeWindowPidForX11() +{ + if (QGuiApplication::platformName() != QLatin1String("xcb")) { + return 0; + } + + auto runCommand = [](const QString &program, const QStringList &arguments) { + QProcess process; + process.start(program, arguments); + if (!process.waitForStarted(500) || !process.waitForFinished(500)) { + return QString(); + } + + return QString::fromLocal8Bit(process.readAllStandardOutput()).trimmed(); + }; + + const QString activeWindowOutput = runCommand(QStringLiteral("xprop"), + {QStringLiteral("-root"), + QStringLiteral("_NET_ACTIVE_WINDOW")}); + const QRegularExpression activeWindowPattern(QStringLiteral("0x[0-9a-fA-F]+")); + const QRegularExpressionMatch activeWindowMatch = activeWindowPattern.match(activeWindowOutput); + if (!activeWindowMatch.hasMatch()) { + return 0; + } + + const QString windowId = activeWindowMatch.captured(0); + const QString windowPidOutput = runCommand(QStringLiteral("xprop"), + {QStringLiteral("-id"), + windowId, + QStringLiteral("_NET_WM_PID")}); + const QRegularExpression pidPattern(QStringLiteral("(\\d+)")); + const QRegularExpressionMatch pidMatch = pidPattern.match(windowPidOutput); + if (!pidMatch.hasMatch()) { + return 0; + } + + bool pidOk = false; + const qint64 pid = pidMatch.captured(1).toLongLong(&pidOk); + return pidOk ? pid : 0; +} + +QList currentAiCliProcesses() +{ + QList detectedProcesses; + + const QDir procDirectory(QStringLiteral("/proc")); + const QStringList pidEntries = procDirectory.entryList({QStringLiteral("[0-9]*")}, + QDir::Dirs | QDir::NoDotAndDotDot, + QDir::Name); + for (const QString &pidEntry : pidEntries) { + bool pidOk = false; + const qint64 pid = pidEntry.toLongLong(&pidOk); + if (!pidOk || pid <= 0) { + continue; + } + + const QString procBasePath = procDirectory.filePath(pidEntry); + const QStringList arguments = readProcCommandLine(procBasePath + QStringLiteral("/cmdline")); + const QString executableName = QFileInfo(QFile::symLinkTarget(procBasePath + QStringLiteral("/exe"))).fileName(); + const QString commName = readTrimmedTextFile(procBasePath + QStringLiteral("/comm")); + const QString toolId = detectAiCliToolId(executableName, commName, arguments); + if (toolId.isEmpty()) { + continue; + } + + AiCliProcessInfo processInfo; + processInfo.pid = pid; + processInfo.parentPid = parentPidForProcess(pid); + processInfo.toolId = toolId; + processInfo.toolName = aiToolDisplayName(toolId); + processInfo.workingDirectory = QFile::symLinkTarget(procBasePath + QStringLiteral("/cwd")); + processInfo.arguments = arguments; + processInfo.sessionLogPath = primarySessionLogPath(sessionLogPathsForProcess(pid)); + detectedProcesses << processInfo; + } + + QList processes; + for (const AiCliProcessInfo &process : std::as_const(detectedProcesses)) { + bool hasSameToolDescendant = false; + for (const AiCliProcessInfo &candidate : std::as_const(detectedProcesses)) { + if (candidate.pid == process.pid || candidate.toolId != process.toolId) { + continue; + } + + if (processHasAncestor(candidate.pid, process.pid)) { + hasSameToolDescendant = true; + break; + } + } + + if (!hasSameToolDescendant) { + processes << process; + } + } + + std::sort(processes.begin(), processes.end(), [](const AiCliProcessInfo &left, const AiCliProcessInfo &right) { + return left.pid > right.pid; + }); + return processes; +} + +MusicSnapshot runningMusicPlayerSnapshot(const QString &previousDesktopEntry) +{ + const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); + RunningMusicPlayerSnapshotCacheEntry &snapshotCache = runningMusicPlayerSnapshotCache(); + if (snapshotCache.capturedAtMs > 0 + && nowMs - snapshotCache.capturedAtMs <= 2500 + && snapshotCache.previousDesktopEntry == previousDesktopEntry) { + return snapshotCache.snapshot; + } + + MusicSnapshot bestSnapshot; + qint64 bestPid = 0; + QString previousDesktopId = previousDesktopEntry.trimmed(); + if (previousDesktopId.endsWith(QStringLiteral(".desktop"))) { + previousDesktopId.chop(QStringLiteral(".desktop").size()); + } + + const auto &candidateIndex = musicDesktopCandidatesByExecutableBaseName(); + if (candidateIndex.isEmpty()) { + return bestSnapshot; + } + + const QDir procDirectory(QStringLiteral("/proc")); + const QStringList pidEntries = procDirectory.entryList({QStringLiteral("[0-9]*")}, + QDir::Dirs | QDir::NoDotAndDotDot, + QDir::Name); + for (const QString &pidEntry : pidEntries) { + bool pidOk = false; + const qint64 pid = pidEntry.toLongLong(&pidOk); + if (!pidOk || pid <= 0) { + continue; + } + + const QString executablePath = canonicalExecutablePath(QFile::symLinkTarget(procDirectory.filePath(pidEntry + QStringLiteral("/exe")))); + const QString executableBaseName = QFileInfo(executablePath).fileName().trimmed().toLower(); + if (executableBaseName.isEmpty()) { + continue; + } + + const QList candidates = candidateIndex.value(executableBaseName); + for (const MusicDesktopCandidate &candidate : candidates) { + if (bestWindowIdForPid(pid).isEmpty()) { + continue; + } + + int score = !candidate.executablePath.isEmpty() && candidate.executablePath == executablePath ? 300 : 220; + QString candidateDesktopId = candidate.desktopId; + if (candidateDesktopId.endsWith(QStringLiteral(".desktop"))) { + candidateDesktopId.chop(QStringLiteral(".desktop").size()); + } + if (!previousDesktopId.isEmpty() && candidateDesktopId == previousDesktopId) { + score += 80; + } + + if (!bestSnapshot.available || score > bestSnapshot.score || (score == bestSnapshot.score && pid > bestPid)) { + bestSnapshot.available = true; + bestSnapshot.desktopEntry = candidate.desktopId; + bestSnapshot.executablePath = executablePath; + bestSnapshot.appName = candidate.appName; + bestSnapshot.canRaise = true; + bestSnapshot.score = score; + bestPid = pid; + } + } + } + + snapshotCache.snapshot = bestSnapshot; + snapshotCache.previousDesktopEntry = previousDesktopEntry; + snapshotCache.capturedAtMs = nowMs; + return bestSnapshot; +} + +QString aiToolProcessLabel(const QString &toolId) +{ + if (toolId == QStringLiteral("codex")) { + return QStringLiteral("codex"); + } + if (toolId == QStringLiteral("claude")) { + return QStringLiteral("claude code"); + } + + return QStringLiteral("ai"); +} + +bool isShellOrWrapperExecutable(const QString &name) +{ + const QString normalizedName = name.trimmed().toLower(); + return normalizedName == QStringLiteral("bash") + || normalizedName == QStringLiteral("sh") + || normalizedName == QStringLiteral("dash") + || normalizedName == QStringLiteral("zsh") + || normalizedName == QStringLiteral("fish") + || normalizedName == QStringLiteral("ksh") + || normalizedName == QStringLiteral("nu") + || normalizedName == QStringLiteral("tmux") + || normalizedName == QStringLiteral("screen") + || normalizedName == QStringLiteral("sudo") + || normalizedName == QStringLiteral("doas") + || normalizedName == QStringLiteral("su") + || normalizedName == QStringLiteral("env") + || normalizedName == QStringLiteral("flatpak-spawn") + || normalizedName == QStringLiteral("systemd") + || normalizedName == QStringLiteral("login"); +} + +void sendDesktopNotification(const QString &summary, + const QString &body, + const QString &appIcon = QStringLiteral("computer")) +{ + if (summary.trimmed().isEmpty()) { + return; + } + + QDBusInterface notificationInterface(QStringLiteral("org.freedesktop.Notifications"), + QStringLiteral("/org/freedesktop/Notifications"), + QStringLiteral("org.freedesktop.Notifications"), + QDBusConnection::sessionBus()); + if (!notificationInterface.isValid()) { + return; + } + + notificationInterface.call(QStringLiteral("Notify"), + QStringLiteral("dde-shell"), + 0U, + appIcon, + summary, + body, + QStringList(), + QVariantMap(), + 4000); +} + +QStringList uniqueExistingDirectories(const QStringList &paths) +{ + QStringList directories; + for (const QString &path : paths) { + const QString cleanPath = QDir(path).absolutePath(); + if (directories.contains(cleanPath) || !QFileInfo(cleanPath).isDir()) { + continue; + } + + directories << cleanPath; + } + + return directories; +} + +bool querySystemMonitorUsage(const char *methodName, int *value) +{ + if (!value) { + return false; + } + + QDBusInterface systemMonitor(SystemMonitorService, + SystemMonitorPath, + SystemMonitorInterface, + QDBusConnection::sessionBus()); + if (!systemMonitor.isValid()) { + return false; + } + + const QDBusReply reply = systemMonitor.call(QString::fromLatin1(methodName)); + if (!reply.isValid()) { + return false; + } + + *value = qBound(0, reply.value(), 100); + return true; +} + +bool isUpAndRunningInterface(const QNetworkInterface &networkInterface) +{ + const auto flags = networkInterface.flags(); + return (flags & QNetworkInterface::IsUp) + && (flags & QNetworkInterface::IsRunning) + && !(flags & QNetworkInterface::IsLoopBack) + && !networkInterface.name().trimmed().isEmpty(); +} + +bool isLikelyPhysicalTrafficInterface(const QNetworkInterface &networkInterface) +{ + if (!isUpAndRunningInterface(networkInterface)) { + return false; + } + + const QDir interfaceDirectory(QStringLiteral("/sys/class/net/%1").arg(networkInterface.name().trimmed())); + if (!interfaceDirectory.exists()) { + return false; + } + + return QFileInfo(interfaceDirectory.filePath(QStringLiteral("device"))).exists() + || QFileInfo(interfaceDirectory.filePath(QStringLiteral("wireless"))).exists(); +} + +QStringList defaultRouteInterfaceNames() +{ + QStringList routeInterfaceNames; + QFile routeFile(QStringLiteral("/proc/net/route")); + if (!routeFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + return routeInterfaceNames; + } + + while (!routeFile.atEnd()) { + const QString line = QString::fromUtf8(routeFile.readLine()).simplified(); + if (line.startsWith(QStringLiteral("Iface"))) { + continue; + } + + const QStringList fields = line.split(QLatin1Char(' '), Qt::SkipEmptyParts); + if (fields.size() < 2) { + continue; + } + + const QString interfaceName = fields.at(0).trimmed(); + const QString destination = fields.at(1).trimmed(); + if (destination == QStringLiteral("00000000") + && interfaceName != QStringLiteral("lo") + && !routeInterfaceNames.contains(interfaceName)) { + routeInterfaceNames << interfaceName; + } + } + + return routeInterfaceNames; +} + +QVariantMap variantMapFromDBusArgument(const QDBusArgument &argument) +{ + QVariantMap result; + if (argument.currentType() != QDBusArgument::MapType) { + return result; + } + + argument.beginMap(); + while (!argument.atEnd()) { + QString key; + QDBusVariant value; + argument.beginMapEntry(); + argument >> key >> value; + argument.endMapEntry(); + result.insert(key, value.variant()); + } + argument.endMap(); + return result; +} + +QVariant unwrapDBusValue(const QVariant &value) +{ + if (value.canConvert()) { + return qvariant_cast(value).variant(); + } + + if (value.userType() == qMetaTypeId()) { + const QDBusArgument argument = qvariant_cast(value); + if (argument.currentType() == QDBusArgument::VariantType) { + QDBusVariant unwrappedValue; + argument >> unwrappedValue; + return unwrapDBusValue(unwrappedValue.variant()); + } + } + + return value; +} + +QVariantMap dbusProperties(const QString &service, const QString &path, const QString &interfaceName) +{ + QDBusInterface propertiesInterface(service, + path, + DBusPropertiesInterface, + QDBusConnection::sessionBus()); + const QDBusReply reply = propertiesInterface.call(QStringLiteral("GetAll"), interfaceName); + return reply.isValid() ? reply.value() : QVariantMap(); +} + +QVariantMap dbusProperties(const QString &service, const QString &interfaceName) +{ + return dbusProperties(service, QLatin1String(MprisPath), interfaceName); +} + +QString stringFromDBusValue(const QVariant &value) +{ + return unwrapDBusValue(value).toString().trimmed(); +} + +QStringList stringListFromDBusValue(const QVariant &value) +{ + const QVariant unwrappedValue = unwrapDBusValue(value); + if (unwrappedValue.canConvert()) { + return unwrappedValue.toStringList(); + } + + QStringList strings; + const QVariantList values = unwrappedValue.toList(); + for (const QVariant &entry : values) { + const QString text = stringFromDBusValue(entry); + if (!text.isEmpty()) { + strings << text; + } + } + + return strings; +} + +bool boolFromDBusValue(const QVariant &value) +{ + return unwrapDBusValue(value).toBool(); +} + +QVariantMap mapFromDBusValue(const QVariant &value) +{ + const QVariant unwrappedValue = unwrapDBusValue(value); + if (unwrappedValue.userType() == qMetaTypeId()) { + return variantMapFromDBusArgument(qvariant_cast(unwrappedValue)); + } + + if (unwrappedValue.canConvert()) { + return unwrappedValue.toMap(); + } + + return {}; +} + +bool callDBusMethod(const QString &service, + const QString &path, + const QString &interfaceName, + const QString &method, + const QVariantList &arguments = {}, + QDBus::CallMode callMode = QDBus::Block) +{ + QDBusInterface interface(service, path, interfaceName, QDBusConnection::sessionBus()); + if (!interface.isValid()) { + return false; + } + + const QDBusMessage reply = interface.callWithArgumentList(callMode, method, arguments); + if (callMode == QDBus::NoBlock) { + return true; + } + return reply.type() != QDBusMessage::ErrorMessage; +} + +bool splitStatusNotifierItemReference(const QString &itemReference, QString *service, QString *path) +{ + if (!service || !path) { + return false; + } + + const int pathSeparatorIndex = itemReference.indexOf(QLatin1Char('/')); + if (pathSeparatorIndex <= 0) { + return false; + } + + *service = itemReference.left(pathSeparatorIndex).trimmed(); + *path = itemReference.mid(pathSeparatorIndex).trimmed(); + return !service->isEmpty() && !path->isEmpty(); +} + +QString canonicalExecutablePath(const QString &path) +{ + if (path.isEmpty()) { + return {}; + } + + const QFileInfo fileInfo(path); + const QString canonicalPath = fileInfo.canonicalFilePath(); + if (!canonicalPath.isEmpty()) { + return canonicalPath; + } + + return fileInfo.exists() ? fileInfo.absoluteFilePath() : QString(); +} + +QString executablePathForPid(uint pid) +{ + if (pid == 0) { + return {}; + } + + const QString executableLinkPath = QStringLiteral("/proc/%1/exe").arg(pid); + return canonicalExecutablePath(QFileInfo(executableLinkPath).symLinkTarget()); +} + +QString preferredWeatherExecutablePath() +{ + const QString resolvedExecutablePath = canonicalExecutablePath(QStandardPaths::findExecutable(QStringLiteral("deepin-weather"))); + if (!resolvedExecutablePath.isEmpty()) { + return resolvedExecutablePath; + } + + return canonicalExecutablePath(QStringLiteral("/usr/bin/deepin-weather")); +} + +uint serviceProcessId(const QString &serviceName); + +bool findStatusNotifierItem(const QString &expectedId, + QString *service, + QString *path, + const QString &preferredExecutablePath = {}) +{ + if (!service || !path) { + return false; + } + + const QVariantMap watcherProperties = dbusProperties(QLatin1String(StatusNotifierWatcherService), + QLatin1String(StatusNotifierWatcherPath), + QLatin1String(StatusNotifierWatcherInterface)); + const QStringList registeredItems = stringListFromDBusValue(watcherProperties.value(QStringLiteral("RegisteredStatusNotifierItems"))); + const QString preferredBaseName = QFileInfo(preferredExecutablePath).fileName(); + QString bestService; + QString bestPath; + int bestScore = std::numeric_limits::min(); + uint bestPid = 0; + + for (const QString &itemReference : registeredItems) { + QString candidateService; + QString candidatePath; + if (!splitStatusNotifierItemReference(itemReference, &candidateService, &candidatePath)) { + continue; + } + + const QVariantMap itemProperties = dbusProperties(candidateService, + candidatePath, + QLatin1String(StatusNotifierItemInterface)); + if (stringFromDBusValue(itemProperties.value(QStringLiteral("Id"))) != expectedId) { + continue; + } + + const uint pid = serviceProcessId(candidateService); + const QString executablePath = executablePathForPid(pid); + int score = 0; + if (!preferredExecutablePath.isEmpty() && executablePath == preferredExecutablePath) { + score += 300; + } else if (!preferredBaseName.isEmpty() && QFileInfo(executablePath).fileName() == preferredBaseName) { + score += 200; + } else if (!executablePath.isEmpty()) { + score += 100; + } + + if (score > bestScore || (score == bestScore && pid >= bestPid)) { + bestScore = score; + bestPid = pid; + bestService = candidateService; + bestPath = candidatePath; + } + } + + if (bestService.isEmpty() || bestPath.isEmpty()) { + return false; + } + + *service = bestService; + *path = bestPath; + return true; +} + +bool activateStatusNotifierItem(const QString &expectedId, + int x, + int y, + QDBus::CallMode callMode = QDBus::Block, + const QString &preferredExecutablePath = {}) +{ + QString service; + QString path; + if (!findStatusNotifierItem(expectedId, &service, &path, preferredExecutablePath)) { + return false; + } + + return callDBusMethod(service, + path, + QLatin1String(StatusNotifierItemInterface), + QStringLiteral("Activate"), + {x, y}, + callMode); +} + +bool runCommand(const QString &program, const QStringList &arguments = {}, int timeoutMs = 1500) +{ + QProcess process; + process.start(program, arguments); + if (!process.waitForFinished(timeoutMs)) { + process.kill(); + process.waitForFinished(500); + return false; + } + + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +QString processOutput(const QString &program, const QStringList &arguments = {}, int timeoutMs = 1500) +{ + QProcess process; + process.start(program, arguments); + if (!process.waitForFinished(timeoutMs)) { + process.kill(); + process.waitForFinished(500); + return {}; + } + + if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { + return {}; + } + + return QString::fromUtf8(process.readAllStandardOutput()).trimmed(); +} + +QString desktopEntryText(const QString &desktopFilePath, const QString &key) +{ + if (desktopFilePath.isEmpty() || key.isEmpty()) { + return {}; + } + + QSettings settings(desktopFilePath, QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + return settings.value(key).toString().trimmed(); +} + +QString localizedDesktopEntryText(const QString &desktopFilePath, const QString &key) +{ + if (desktopFilePath.isEmpty() || key.isEmpty()) { + return {}; + } + + QSettings settings(desktopFilePath, QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + + QStringList localizedKeys; + const QStringList uiLanguages = QLocale::system().uiLanguages(); + for (const QString &uiLanguage : uiLanguages) { + QString normalizedLanguage = uiLanguage.trimmed(); + if (normalizedLanguage.isEmpty()) { + continue; + } + + normalizedLanguage.replace(QLatin1Char('-'), QLatin1Char('_')); + const QString fullKey = QStringLiteral("%1[%2]").arg(key, normalizedLanguage); + if (!localizedKeys.contains(fullKey)) { + localizedKeys << fullKey; + } + + const int separatorIndex = normalizedLanguage.indexOf(QLatin1Char('_')); + if (separatorIndex > 0) { + const QString baseLanguageKey = QStringLiteral("%1[%2]").arg(key, normalizedLanguage.left(separatorIndex)); + if (!localizedKeys.contains(baseLanguageKey)) { + localizedKeys << baseLanguageKey; + } + } + } + + for (const QString &localizedKey : localizedKeys) { + const QString localizedValue = settings.value(localizedKey).toString().trimmed(); + if (!localizedValue.isEmpty()) { + return localizedValue; + } + } + + return settings.value(key).toString().trimmed(); +} + +QString desktopEntryExecutable(const QString &desktopFilePath) +{ + const QStringList commandLines = { + desktopEntryText(desktopFilePath, QStringLiteral("TryExec")), + desktopEntryText(desktopFilePath, QStringLiteral("Exec")), + }; + + for (const QString &commandLine : commandLines) { + const QStringList commandParts = QProcess::splitCommand(commandLine); + for (const QString &part : commandParts) { + const QString token = part.trimmed(); + if (token.isEmpty() || token == QStringLiteral("env") || token.startsWith(QLatin1Char('%'))) { + continue; + } + + if (token.contains(QLatin1Char('=')) + && !token.startsWith(QLatin1Char('/')) + && !token.contains(QDir::separator())) { + continue; + } + + return token; + } + } + + return {}; +} + +bool canManipulateWindows() +{ + static const bool available = !QStandardPaths::findExecutable(QStringLiteral("xdotool")).isEmpty(); + static const bool x11Session = qEnvironmentVariable("XDG_SESSION_TYPE").compare(QStringLiteral("x11"), Qt::CaseInsensitive) == 0 + || QGuiApplication::platformName().contains(QStringLiteral("xcb"), Qt::CaseInsensitive); + return available && x11Session; +} + +QPoint nativePointForScreen(QScreen *screen, const QPoint &logicalPoint) +{ + if (!screen) { + return logicalPoint; + } + + const QRect screenGeometry = screen->geometry(); + const qreal scale = screen->devicePixelRatio(); + return QPoint(qRound((logicalPoint.x() - screenGeometry.x()) * scale + screenGeometry.x()), + qRound((logicalPoint.y() - screenGeometry.y()) * scale + screenGeometry.y())); +} + +QRect nativeRectForScreen(QScreen *screen, const QRect &logicalRect) +{ + if (!screen) { + return logicalRect; + } + + const QPoint topLeft = nativePointForScreen(screen, logicalRect.topLeft()); + const qreal scale = screen->devicePixelRatio(); + return QRect(topLeft, + QSize(qRound(logicalRect.width() * scale), + qRound(logicalRect.height() * scale))); +} + +QScreen *screenForNativePoint(const QPoint &nativePoint) +{ + const QList screens = QGuiApplication::screens(); + for (QScreen *screen : screens) { + if (nativeRectForScreen(screen, screen->geometry()).contains(nativePoint)) { + return screen; + } + } + + return QGuiApplication::primaryScreen(); +} + +uint serviceProcessId(const QString &serviceName) +{ + if (serviceName.isEmpty()) { + return 0; + } + + QDBusConnectionInterface *connectionInterface = QDBusConnection::sessionBus().interface(); + if (!connectionInterface) { + return 0; + } + + const QDBusReply pidReply = connectionInterface->servicePid(serviceName); + return pidReply.isValid() ? pidReply.value() : 0; +} + +QStringList outputLines(const QString &output) +{ + QStringList lines = output.split(QLatin1Char('\n'), Qt::SkipEmptyParts); + for (QString &line : lines) { + line.remove(QLatin1Char('\r')); + line = line.trimmed(); + } + lines.removeAll(QString()); + return lines; +} + +bool windowGeometry(const QString &windowId, QRect *geometry) +{ + if (!geometry || windowId.isEmpty() || !canManipulateWindows()) { + return false; + } + + const QString output = processOutput(QStringLiteral("xdotool"), + {QStringLiteral("getwindowgeometry"), + QStringLiteral("--shell"), + windowId}, + 1200); + if (output.isEmpty()) { + return false; + } + + const QRegularExpression xPattern(QStringLiteral("(?:^|\\n)X=(-?\\d+)(?:\\n|$)")); + const QRegularExpression yPattern(QStringLiteral("(?:^|\\n)Y=(-?\\d+)(?:\\n|$)")); + const QRegularExpression widthPattern(QStringLiteral("(?:^|\\n)WIDTH=(\\d+)(?:\\n|$)")); + const QRegularExpression heightPattern(QStringLiteral("(?:^|\\n)HEIGHT=(\\d+)(?:\\n|$)")); + + const auto xMatch = xPattern.match(output); + const auto yMatch = yPattern.match(output); + const auto widthMatch = widthPattern.match(output); + const auto heightMatch = heightPattern.match(output); + if (!xMatch.hasMatch() || !yMatch.hasMatch() || !widthMatch.hasMatch() || !heightMatch.hasMatch()) { + return false; + } + + *geometry = QRect(xMatch.captured(1).toInt(), + yMatch.captured(1).toInt(), + widthMatch.captured(1).toInt(), + heightMatch.captured(1).toInt()); + return geometry->isValid(); +} + +QString bestWindowIdForSearches(const QList &searches, bool onlyVisible = true) +{ + if (!canManipulateWindows()) { + return {}; + } + + QString bestWindowId; + int bestArea = -1; + for (const QStringList &searchArguments : searches) { + QStringList arguments = {QStringLiteral("search")}; + if (onlyVisible) { + arguments << QStringLiteral("--onlyvisible"); + } + arguments << searchArguments; + const QStringList windowIds = outputLines(processOutput(QStringLiteral("xdotool"), + arguments, + 1200)); + for (const QString &windowId : windowIds) { + QRect geometry; + if (!windowGeometry(windowId, &geometry)) { + continue; + } + + const int area = geometry.width() * geometry.height(); + if (area > bestArea) { + bestArea = area; + bestWindowId = windowId; + } + } + } + + return bestWindowId; +} + +QString weatherWindowId(bool onlyVisible = true, const QString &preferredExecutablePath = {}) +{ + QString service; + QString path; + if (findStatusNotifierItem(QStringLiteral("org.deepin.weather"), &service, &path, preferredExecutablePath)) { + const uint pid = serviceProcessId(service); + if (pid > 0) { + const QString windowId = bestWindowIdForSearches({QStringList{QStringLiteral("--pid"), QString::number(pid)}}, + onlyVisible); + if (!windowId.isEmpty()) { + return windowId; + } + } + } + + return bestWindowIdForSearches({ + QStringList{QStringLiteral("--class"), QStringLiteral("deepin-weather")}, + QStringList{QStringLiteral("--class"), QStringLiteral("org.deepin.weather")}, + QStringList{QStringLiteral("--name"), QStringLiteral("org.deepin.weather")}, + }, onlyVisible); +} + +bool activateWindow(const QString &windowId) +{ + return !windowId.isEmpty() + && runCommand(QStringLiteral("xdotool"), {QStringLiteral("windowactivate"), QStringLiteral("--sync"), windowId}, 1200); +} + +bool activateWindowWithoutSync(const QString &windowId) +{ + return !windowId.isEmpty() + && runCommand(QStringLiteral("xdotool"), {QStringLiteral("windowactivate"), windowId}, 400); +} + +bool raiseWindow(const QString &windowId) +{ + return !windowId.isEmpty() + && runCommand(QStringLiteral("xdotool"), {QStringLiteral("windowraise"), windowId}, 400); +} + +bool moveWindow(const QString &windowId, int x, int y) +{ + return !windowId.isEmpty() + && runCommand(QStringLiteral("xdotool"), + {QStringLiteral("windowmove"), windowId, QString::number(x), QString::number(y)}, + 1200); +} + +QString bestWindowIdForDesktopEntry(const QString &desktopFilePath, + const QString &fallbackAppName, + const QString &fallbackExecutablePath, + bool onlyVisible = true) +{ + Q_UNUSED(fallbackAppName) + QList searches; + if (!desktopFilePath.isEmpty()) { + const QString startupWmClass = desktopEntryText(desktopFilePath, QStringLiteral("StartupWMClass")); + if (!startupWmClass.isEmpty()) { + searches << QStringList{QStringLiteral("--class"), startupWmClass}; + } + + const QString desktopExecutable = desktopEntryExecutable(desktopFilePath); + const QString executableBaseName = QFileInfo(desktopExecutable).fileName(); + if (!executableBaseName.isEmpty()) { + searches << QStringList{QStringLiteral("--class"), executableBaseName}; + } + } + + const QString fallbackExecutableBaseName = QFileInfo(fallbackExecutablePath).fileName(); + if (!fallbackExecutableBaseName.isEmpty()) { + searches << QStringList{QStringLiteral("--class"), fallbackExecutableBaseName}; + } + + return bestWindowIdForSearches(searches, onlyVisible); +} + +bool activateWindowForServiceOrDesktop(const QString &serviceName, + const QString &desktopFilePath, + const QString &fallbackAppName, + const QString &fallbackExecutablePath) +{ + const uint pid = serviceProcessId(serviceName); + if (pid > 0) { + QString windowId = bestWindowIdForSearches({QStringList{QStringLiteral("--pid"), QString::number(pid)}}, false); + if (windowId.isEmpty()) { + windowId = bestWindowIdForSearches({QStringList{QStringLiteral("--pid"), QString::number(pid)}}); + } + if (activateWindow(windowId)) { + return true; + } + } + + QString windowId = bestWindowIdForDesktopEntry(desktopFilePath, fallbackAppName, fallbackExecutablePath, false); + if (windowId.isEmpty()) { + windowId = bestWindowIdForDesktopEntry(desktopFilePath, fallbackAppName, fallbackExecutablePath); + } + + return activateWindow(windowId); +} + +QString bestWindowIdForPid(qint64 pid, bool onlyVisible) +{ + if (pid <= 0) { + return {}; + } + + return bestWindowIdForSearches({QStringList{QStringLiteral("--pid"), QString::number(pid)}}, onlyVisible); +} + +bool activateWindowForPidOrDesktop(qint64 pid, + const QString &desktopFilePath, + const QString &fallbackAppName, + const QString &fallbackExecutablePath) +{ + QString windowId = bestWindowIdForPid(pid, false); + if (activateWindow(windowId)) { + return true; + } + + windowId = bestWindowIdForPid(pid); + if (activateWindow(windowId)) { + return true; + } + + return activateWindowForServiceOrDesktop(QString(), desktopFilePath, fallbackAppName, fallbackExecutablePath); +} + +bool moveWeatherWindowToRequestedPosition(int taskbarLeft, + int taskbarTop, + const QString &preferredExecutablePath = {}) +{ + QString windowId = weatherWindowId(true, preferredExecutablePath); + if (windowId.isEmpty()) { + windowId = weatherWindowId(false, preferredExecutablePath); + } + if (windowId.isEmpty()) { + return false; + } + + QRect geometry; + if (!windowGeometry(windowId, &geometry)) { + return false; + } + + QScreen *screen = screenForNativePoint(geometry.center()); + if (!screen) { + screen = screenForNativePoint(QPoint(taskbarLeft, taskbarTop)); + } + if (!screen) { + screen = QGuiApplication::primaryScreen(); + } + + const QRect availableGeometry = screen ? nativeRectForScreen(screen, screen->availableGeometry()) + : QRect(taskbarLeft, geometry.y(), geometry.width(), geometry.height()); + int targetX = taskbarLeft; + int targetY = taskbarTop - geometry.height() - WeatherPopupTaskbarGap; + const int maxX = availableGeometry.left() + qMax(0, availableGeometry.width() - geometry.width()); + const int maxY = availableGeometry.top() + qMax(0, availableGeometry.height() - geometry.height()); + + targetX = qBound(availableGeometry.left(), targetX, maxX); + targetY = qBound(availableGeometry.top(), targetY, maxY); + + if ((geometry.x() != targetX || geometry.y() != targetY) + && !moveWindow(windowId, targetX, targetY)) { + return false; + } + + raiseWindow(windowId); + return activateWindowWithoutSync(windowId); +} + +MprisMetadataFallback metadataFallbackFromCommand(const QString &service) +{ + if (service.isEmpty()) { + return {}; + } + + const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); + { + QMutexLocker locker(&metadataFallbackCacheMutex()); + const auto cachedEntry = metadataFallbackCache().constFind(service); + if (cachedEntry != metadataFallbackCache().cend() + && nowMs - cachedEntry->capturedAtMs <= 2500) { + return cachedEntry->metadata; + } + } + + QProcess process; + process.start(QStringLiteral("dbus-send"), + { + QStringLiteral("--session"), + QStringLiteral("--dest=%1").arg(service), + QStringLiteral("--print-reply"), + QStringLiteral("/org/mpris/MediaPlayer2"), + QStringLiteral("org.freedesktop.DBus.Properties.Get"), + QStringLiteral("string:org.mpris.MediaPlayer2.Player"), + QStringLiteral("string:Metadata"), + }); + if (!process.waitForFinished(800)) { + process.kill(); + process.waitForFinished(200); + return {}; + } + + if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { + return {}; + } + + const QString output = QString::fromUtf8(process.readAllStandardOutput()); + const QRegularExpression titlePattern(QStringLiteral(R"mpris(string "xesam:title"\s+variant\s+string "([^"]*)")mpris"), + QRegularExpression::DotMatchesEverythingOption); + const QRegularExpression artistPattern(QStringLiteral(R"mpris(string "xesam:artist"\s+variant\s+array \[\s+string "([^"]*)")mpris"), + QRegularExpression::DotMatchesEverythingOption); + const QRegularExpression artPattern(QStringLiteral(R"mpris(string "mpris:artUrl"\s+variant\s+string "([^"]*)")mpris"), + QRegularExpression::DotMatchesEverythingOption); + + MprisMetadataFallback metadata; + const QRegularExpressionMatch titleMatch = titlePattern.match(output); + if (titleMatch.hasMatch()) { + metadata.title = titleMatch.captured(1).trimmed(); + } + + const QRegularExpressionMatch artistMatch = artistPattern.match(output); + if (artistMatch.hasMatch()) { + metadata.artist = artistMatch.captured(1).trimmed(); + } + + const QRegularExpressionMatch artMatch = artPattern.match(output); + if (artMatch.hasMatch()) { + metadata.artUrl = artMatch.captured(1).trimmed(); + } + + { + QMutexLocker locker(&metadataFallbackCacheMutex()); + MetadataFallbackCacheEntry cacheEntry; + cacheEntry.metadata = metadata; + cacheEntry.capturedAtMs = nowMs; + metadataFallbackCache().insert(service, cacheEntry); + } + + return metadata; +} + +int browserPenaltyForIdentity(const QString &identity) +{ + static const QRegularExpression browserPattern(QStringLiteral("(browser|chrome|chromium|firefox|edge|safari|浏览器)"), + QRegularExpression::CaseInsensitiveOption); + return browserPattern.match(identity).hasMatch() ? -40 : 0; +} + +bool isBrowserIdentity(const QString &identity) +{ + static const QRegularExpression browserPattern(QStringLiteral("(browser|chrome|chromium|firefox|edge|safari|浏览器)"), + QRegularExpression::CaseInsensitiveOption); + return browserPattern.match(identity.trimmed()).hasMatch(); +} + +bool isBrowserDesktopId(const QString &desktopId) +{ + static const QRegularExpression browserPattern(QStringLiteral("(^|[._-])(browser|chrome|chromium|firefox|edge|brave|vivaldi|opera)([._-]|$)"), + QRegularExpression::CaseInsensitiveOption); + QString normalizedDesktopId = desktopId.trimmed(); + if (normalizedDesktopId.endsWith(QStringLiteral(".desktop"))) { + normalizedDesktopId.chop(QStringLiteral(".desktop").size()); + } + + return browserPattern.match(normalizedDesktopId).hasMatch(); +} + +bool isBrowserServiceName(const QString &serviceName) +{ + return isBrowserDesktopId(serviceName); +} + +bool isDedicatedMusicIdentity(const QString &identity) +{ + const QString trimmedIdentity = identity.trimmed(); + return !trimmedIdentity.isEmpty() && !isBrowserIdentity(trimmedIdentity); +} + +QString iconNameForMusicAppName(const QString &appName) +{ + const QString normalizedName = appName.trimmed().toLower(); + if (normalizedName.isEmpty()) { + return {}; + } + + if (normalizedName.contains(QStringLiteral("网易云")) + || normalizedName.contains(QStringLiteral("网易")) + || normalizedName.contains(QStringLiteral("云音乐")) + || normalizedName.contains(QStringLiteral("netease"))) { + return QStringLiteral("netease-cloud-music"); + } + + if (normalizedName.contains(QStringLiteral("deepin music")) + || normalizedName.contains(QStringLiteral("deepin-music")) + || normalizedName.contains(QStringLiteral("com.deepin.music")) + || normalizedName.contains(QStringLiteral("deepin音乐")) + || normalizedName == QStringLiteral("音乐")) { + return QStringLiteral("deepin-music"); + } + + return {}; +} + +QUrl stableMusicArtSource(const QString &artUrl) +{ + const QUrl sourceUrl(artUrl); + if (!sourceUrl.isValid() || sourceUrl.isEmpty()) { + return {}; + } + + if (!sourceUrl.isLocalFile()) { + return sourceUrl; + } + + const QString sourcePath = sourceUrl.toLocalFile(); + const QFileInfo sourceInfo(sourcePath); + if (!sourceInfo.exists() || !sourceInfo.isFile() || !sourceInfo.isReadable()) { + return sourceUrl; + } + + QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + if (cacheRoot.isEmpty()) { + cacheRoot = QDir::home().filePath(QStringLiteral(".cache/dde-shell")); + } + + QDir cacheDirectory(QDir(cacheRoot).filePath(QStringLiteral("fashion-left-plugin/music-art"))); + if (!cacheDirectory.exists() && !cacheDirectory.mkpath(QStringLiteral("."))) { + return sourceUrl; + } + + static qint64 lastPruneMs = 0; + const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); + if (lastPruneMs <= 0 || nowMs - lastPruneMs >= 12LL * 60LL * 60LL * 1000LL) { + lastPruneMs = nowMs; + const QFileInfoList cacheFiles = cacheDirectory.entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::Time | QDir::Reversed); + constexpr qint64 kMaxAgeMs = 7LL * 24LL * 60LL * 60LL * 1000LL; + constexpr int kMaxCachedFiles = 128; + int preservedFiles = 0; + for (const QFileInfo &cacheFileInfo : cacheFiles) { + const bool expired = cacheFileInfo.lastModified().msecsTo(QDateTime::currentDateTime()) > kMaxAgeMs; + if (expired || preservedFiles >= kMaxCachedFiles) { + QFile::remove(cacheFileInfo.absoluteFilePath()); + continue; + } + + ++preservedFiles; + } + } + + const QByteArray imageFormat = QImageReader::imageFormat(sourcePath).toLower(); + const QString suffix = !imageFormat.isEmpty() + ? QString::fromLatin1(imageFormat) + : sourceInfo.suffix().trimmed().toLower(); + + QByteArray hashSeed = sourcePath.toUtf8(); + hashSeed += QByteArray::number(sourceInfo.size()); + hashSeed += QByteArray::number(sourceInfo.lastModified().toMSecsSinceEpoch()); + const QString hash = QString::fromLatin1(QCryptographicHash::hash(hashSeed, QCryptographicHash::Sha1).toHex()); + const QString cacheFileName = suffix.isEmpty() ? hash : QStringLiteral("%1.%2").arg(hash, suffix); + const QString cachePath = cacheDirectory.filePath(cacheFileName); + + if (!QFileInfo::exists(cachePath)) { + QFile::remove(cachePath); + if (!QFile::copy(sourcePath, cachePath)) { + return sourceUrl; + } + } + + return QUrl::fromLocalFile(cachePath); +} + +QString joinMusicSubtitleParts(const QStringList &parts) +{ + QStringList filteredParts; + for (const QString &part : parts) { + const QString trimmedPart = part.trimmed(); + if (!trimmedPart.isEmpty() && !filteredParts.contains(trimmedPart)) { + filteredParts << trimmedPart; + } + } + + return filteredParts.join(QStringLiteral(" · ")); +} + +QString desktopEntryFromMprisService(const QString &serviceName) +{ + constexpr auto prefix = "org.mpris.MediaPlayer2."; + if (!serviceName.startsWith(QLatin1String(prefix))) { + return {}; + } + + QString desktopId = serviceName.mid(static_cast(strlen(prefix))); + const qsizetype instanceSeparatorIndex = desktopId.indexOf(QStringLiteral(".instance")); + if (instanceSeparatorIndex > 0) { + desktopId.truncate(instanceSeparatorIndex); + } + + const qsizetype nestedServiceSeparatorIndex = desktopId.indexOf(QLatin1Char('.')); + if (nestedServiceSeparatorIndex > 0) { + desktopId.truncate(nestedServiceSeparatorIndex); + } + + return desktopId.trimmed(); +} + +QString iconPathForName(const QString &iconName) +{ + if (iconName.isEmpty()) { + return {}; + } + + static QHash cache; + const auto cachedResult = cache.constFind(iconName); + if (cachedResult != cache.cend()) { + return cachedResult.value(); + } + + const QFileInfo iconFileInfo(iconName); + if (iconFileInfo.exists() && iconFileInfo.isFile()) { + const QString absolutePath = iconFileInfo.absoluteFilePath(); + cache.insert(iconName, absolutePath); + return absolutePath; + } + + const QString preferredIconPath = firstExistingPath({ + QStringLiteral("/usr/share/icons/Win11/apps/scalable/%1.svg").arg(iconName), + QStringLiteral("/usr/share/icons/Win11/mimes/scalable/%1.svg").arg(iconName), + QStringLiteral("/usr/share/icons/Win11/mimes/22/%1.svg").arg(iconName), + QStringLiteral("/usr/share/icons/Win11/mimes/16/%1.svg").arg(iconName), + QStringLiteral("/usr/share/icons/hicolor/scalable/apps/%1.svg").arg(iconName), + QStringLiteral("/var/lib/linglong/entries/share/icons/hicolor/scalable/apps/%1.svg").arg(iconName), + QStringLiteral("/usr/share/pixmaps/%1.png").arg(iconName), + QStringLiteral("/usr/share/pixmaps/%1.svg").arg(iconName), + }); + if (!preferredIconPath.isEmpty()) { + cache.insert(iconName, preferredIconPath); + return preferredIconPath; + } + + const QStringList searchRoots = { + QDir::home().filePath(QStringLiteral(".local/share/icons")), + QStringLiteral("/usr/share/icons"), + QStringLiteral("/usr/share/pixmaps"), + }; + const QStringList nameFilters = { + iconName, + iconName + QStringLiteral(".png"), + iconName + QStringLiteral(".svg"), + iconName + QStringLiteral(".xpm"), + }; + + for (const QString &rootPath : searchRoots) { + if (!QFileInfo(rootPath).exists()) { + continue; + } + + QDirIterator iterator(rootPath, + nameFilters, + QDir::Files | QDir::Readable, + QDirIterator::Subdirectories); + while (iterator.hasNext()) { + const QString path = iterator.next(); + cache.insert(iconName, path); + return path; + } + } + + cache.insert(iconName, QString()); + return {}; +} + +MusicSnapshot currentMusicSnapshot(const QString &previousService) +{ + MusicSnapshot bestSnapshot; + + QDBusConnectionInterface *connectionInterface = QDBusConnection::sessionBus().interface(); + if (!connectionInterface) { + return bestSnapshot; + } + + const QDBusReply namesReply = connectionInterface->registeredServiceNames(); + if (!namesReply.isValid()) { + return bestSnapshot; + } + + const QStringList serviceNames = namesReply.value(); + for (const QString &serviceName : serviceNames) { + if (!serviceName.startsWith(QStringLiteral("org.mpris.MediaPlayer2."))) { + continue; + } + + const QVariantMap playerProperties = dbusProperties(serviceName, QLatin1String(MprisPlayerInterface)); + if (playerProperties.isEmpty()) { + continue; + } + + const QVariantMap rootProperties = dbusProperties(serviceName, QLatin1String(MprisRootInterface)); + const uint servicePid = serviceProcessId(serviceName); + if (servicePid == 0) { + continue; + } + + const QString identity = stringFromDBusValue(rootProperties.value(QStringLiteral("Identity"))); + const QString playbackStatus = stringFromDBusValue(playerProperties.value(QStringLiteral("PlaybackStatus"))); + const QVariantMap metadata = mapFromDBusValue(playerProperties.value(QStringLiteral("Metadata"))); + + MusicSnapshot snapshot; + snapshot.service = serviceName; + snapshot.desktopEntry = stringFromDBusValue(rootProperties.value(QStringLiteral("DesktopEntry"))); + snapshot.executablePath = executablePathForPid(servicePid); + if (snapshot.desktopEntry.isEmpty()) { + snapshot.desktopEntry = desktopEntryFromMprisService(serviceName); + } + + const bool recognizedMusicService = isRecognizedMusicDesktopId(snapshot.desktopEntry) + || isRecognizedMusicExecutable(snapshot.executablePath); + if (!recognizedMusicService) { + continue; + } + + snapshot.appName = identity; + snapshot.available = true; + snapshot.playing = playbackStatus == QStringLiteral("Playing"); + snapshot.canRaise = boolFromDBusValue(rootProperties.value(QStringLiteral("CanRaise"))); + snapshot.canGoPrevious = boolFromDBusValue(playerProperties.value(QStringLiteral("CanGoPrevious"))); + snapshot.canGoNext = boolFromDBusValue(playerProperties.value(QStringLiteral("CanGoNext"))); + snapshot.canTogglePlayback = boolFromDBusValue(playerProperties.value(QStringLiteral("CanPause"))) + || boolFromDBusValue(playerProperties.value(QStringLiteral("CanPlay"))); + + QString title = stringFromDBusValue(metadata.value(QStringLiteral("xesam:title"))); + QString subtitle = joinMusicSubtitleParts(stringListFromDBusValue(metadata.value(QStringLiteral("xesam:artist")))); + QString artUrl = stringFromDBusValue(metadata.value(QStringLiteral("mpris:artUrl"))); + if (title.isEmpty() || subtitle.isEmpty() || artUrl.isEmpty()) { + const MprisMetadataFallback fallbackMetadata = metadataFallbackFromCommand(serviceName); + if (title.isEmpty()) { + title = fallbackMetadata.title; + } + if (subtitle.isEmpty()) { + subtitle = fallbackMetadata.artist; + } + if (artUrl.isEmpty()) { + artUrl = fallbackMetadata.artUrl; + } + } + + snapshot.title = title; + snapshot.subtitle = subtitle; + + if (!artUrl.isEmpty()) { + snapshot.artSource = stableMusicArtSource(artUrl); + } + + const bool hasWindow = !bestWindowIdForPid(servicePid).isEmpty(); + if (!hasWindow && !snapshot.playing) { + continue; + } + + snapshot.score = 0; + if (playbackStatus == QStringLiteral("Playing")) { + snapshot.score += 300; + } else if (playbackStatus == QStringLiteral("Paused")) { + snapshot.score += 200; + } else { + snapshot.score += 100; + } + + if (!title.isEmpty()) { + snapshot.score += 30; + } + if (snapshot.artSource.isValid()) { + snapshot.score += 15; + } + if (boolFromDBusValue(playerProperties.value(QStringLiteral("CanControl")))) { + snapshot.score += 10; + } + if (serviceName == previousService) { + snapshot.score += 5; + } + + const bool dedicatedMusicIdentity = isDedicatedMusicIdentity(identity); + const bool browserShellService = (isBrowserDesktopId(snapshot.desktopEntry) || isBrowserServiceName(serviceName)) + && !dedicatedMusicIdentity; + if (browserShellService) { + continue; + } + + if (snapshot.canGoPrevious || snapshot.canGoNext) { + snapshot.score += 80; + } + if (dedicatedMusicIdentity) { + snapshot.score += 100; + } + snapshot.score += browserPenaltyForIdentity(identity); + + if (!bestSnapshot.available || snapshot.score > bestSnapshot.score) { + bestSnapshot = snapshot; + } + } + + return bestSnapshot; +} + +QLibrary &weatherDockPluginLibrary() +{ + static QLibrary library(WeatherDockPluginPath); + static bool loaded = false; + if (!loaded) { + loaded = true; + library.load(); + } + + return library; +} + +WeatherCodeToDescriptionFunction weatherCodeToDescriptionFunction() +{ + static const auto function = reinterpret_cast( + weatherDockPluginLibrary().resolve("_ZN17WeatherController24weatherCodeToDescriptionEi")); + return function; +} + +WeatherCodeToIconNameFunction weatherCodeToIconNameFunction() +{ + static const auto function = reinterpret_cast( + weatherDockPluginLibrary().resolve("_ZN17WeatherController21weatherCodeToIconNameEib")); + return function; +} + +QString weatherAssetPath(const QString &assetName) +{ + const auto packageAssetPath = [assetName]() { + if (assetName.isEmpty()) { + return QString(); + } + + return firstExistingPath({ + QDir(QGuiApplication::applicationDirPath()).filePath( + QStringLiteral("../share/dde-shell/org.deepin.ds.dock/icons/%1.svg").arg(assetName)), + QStringLiteral("%1/icons/%2.svg").arg(DockPackageDataPath, assetName), + }); + }; + + return firstExistingPath({ + // Keep the fashion weather iconography stable across machines. + // Some systems only provide symbolic status weather icons, which look + // like enlarged tray icons when rendered in the left fashion panel. + packageAssetPath(), + QStringLiteral("/usr/share/icons/Win11/status/32/%1.svg").arg(assetName), + QStringLiteral("/usr/share/icons/Win11/status/16/%1.svg").arg(assetName), + QStringLiteral("/usr/share/icons/Adwaita/symbolic/status/%1-symbolic.svg").arg(assetName), + WeatherAppIconPath, + }); +} + +QString weatherAssetNameFor(const QString &iconName, bool isDay) +{ + const QString normalizedIconName = iconName.trimmed().toLower().replace(QLatin1Char('-'), QLatin1Char('_')); + + if (normalizedIconName.contains(QStringLiteral("tornado"))) { + return QStringLiteral("weather-storm-tornado"); + } + + if (normalizedIconName.contains(QStringLiteral("thunder")) + || normalizedIconName.contains(QStringLiteral("storm"))) { + return isDay ? QStringLiteral("weather-storm") : QStringLiteral("weather-storm-night"); + } + + if (normalizedIconName.contains(QStringLiteral("hail"))) { + return QStringLiteral("weather-hail"); + } + + if (normalizedIconName.contains(QStringLiteral("freezing")) + || normalizedIconName.contains(QStringLiteral("sleet"))) { + return QStringLiteral("weather-freezing-rain"); + } + + if ((normalizedIconName.contains(QStringLiteral("snow")) && normalizedIconName.contains(QStringLiteral("rain"))) + || normalizedIconName.contains(QStringLiteral("rain_snow")) + || normalizedIconName.contains(QStringLiteral("snow_rain"))) { + return QStringLiteral("weather-snow-rain"); + } + + if (normalizedIconName.contains(QStringLiteral("snow"))) { + return isDay ? QStringLiteral("weather-snow") : QStringLiteral("weather-snow-night"); + } + + if (normalizedIconName.contains(QStringLiteral("fog")) + || normalizedIconName.contains(QStringLiteral("mist")) + || normalizedIconName.contains(QStringLiteral("haze")) + || normalizedIconName.contains(QStringLiteral("smog"))) { + return QStringLiteral("weather-fog"); + } + + if (normalizedIconName.contains(QStringLiteral("overcast"))) { + return isDay ? QStringLiteral("weather-overcast") : QStringLiteral("weather-overcast-night"); + } + + if (normalizedIconName.contains(QStringLiteral("wind"))) { + return QStringLiteral("weather-windy"); + } + + if (normalizedIconName.contains(QStringLiteral("few")) + || normalizedIconName.contains(QStringLiteral("partly"))) { + return isDay ? QStringLiteral("weather-few-clouds") : QStringLiteral("weather-few-clouds-night"); + } + + if (normalizedIconName.contains(QStringLiteral("cloud")) + || normalizedIconName.contains(QStringLiteral("cloudy")) + || normalizedIconName.contains(QStringLiteral("many_clouds"))) { + return isDay ? QStringLiteral("weather-clouds") : QStringLiteral("weather-clouds-night"); + } + + if (normalizedIconName.contains(QStringLiteral("drizzle")) + || normalizedIconName.contains(QStringLiteral("shower")) + || normalizedIconName.contains(QStringLiteral("showers")) + || normalizedIconName.contains(QStringLiteral("rain"))) { + return isDay ? QStringLiteral("weather-showers-day") : QStringLiteral("weather-showers-night"); + } + + if (normalizedIconName.contains(QStringLiteral("clear")) + || normalizedIconName.contains(QStringLiteral("sun")) + || normalizedIconName.contains(QStringLiteral("sunny")) + || normalizedIconName.contains(QStringLiteral("fine"))) { + return isDay ? QStringLiteral("weather-clear") : QStringLiteral("weather-clear-night"); + } + + return QStringLiteral("weather-none-available"); +} + +QString deepinWeatherDescription(int weatherCode) +{ + const auto function = weatherCodeToDescriptionFunction(); + if (!function) { + return {}; + } + + return function(weatherCode).trimmed(); +} + +QString deepinWeatherIconName(int weatherCode, bool isDay) +{ + const auto function = weatherCodeToIconNameFunction(); + if (!function) { + return {}; + } + + return function(weatherCode, isDay).trimmed(); +} + +} // namespace + +FashionLeftPluginProvider::FashionLeftPluginProvider(QObject *parent) + : QObject(parent) +{ + auto clockTimer = new QTimer(this); + clockTimer->setInterval(1000); + connect(clockTimer, &QTimer::timeout, this, &FashionLeftPluginProvider::refreshClock); + clockTimer->start(); + + auto notificationTimer = new QTimer(this); + notificationTimer->setInterval(15000); + connect(notificationTimer, &QTimer::timeout, this, &FashionLeftPluginProvider::refreshNotificationCount); + notificationTimer->start(); + + auto mailTimer = new QTimer(this); + mailTimer->setInterval(15000); + connect(mailTimer, &QTimer::timeout, this, &FashionLeftPluginProvider::refreshMailState); + mailTimer->start(); + + auto musicTimer = new QTimer(this); + musicTimer->setInterval(1000); + connect(musicTimer, &QTimer::timeout, this, &FashionLeftPluginProvider::refreshMusicState); + musicTimer->start(); + + auto statsTimer = new QTimer(this); + statsTimer->setInterval(1000); + connect(statsTimer, &QTimer::timeout, this, &FashionLeftPluginProvider::refreshSystemStats); + statsTimer->start(); + + auto aiTimer = new QTimer(this); + aiTimer->setInterval(2000); + connect(aiTimer, &QTimer::timeout, this, &FashionLeftPluginProvider::refreshAiState); + aiTimer->start(); + + auto weatherTimer = new QTimer(this); + weatherTimer->setInterval(300000); + connect(weatherTimer, &QTimer::timeout, this, &FashionLeftPluginProvider::refreshWeather); + weatherTimer->start(); + + m_weatherWatcher = new QFileSystemWatcher(this); + connect(m_weatherWatcher, &QFileSystemWatcher::fileChanged, this, [this] { + refreshWeather(); + }); + connect(m_weatherWatcher, &QFileSystemWatcher::directoryChanged, this, [this] { + refreshWeather(); + }); + + m_aiRefreshWatcher = new QFutureWatcher(this); + connect(m_aiRefreshWatcher, &QFutureWatcher::finished, this, [this]() { + applyAiRefreshResult(m_aiRefreshWatcher->result()); + if (m_aiRefreshPending) { + m_aiRefreshPending = false; + refreshAiState(); + } + }); + + QDBusConnection::sessionBus().connect(NotificationService, + NotificationPath, + NotificationInterface, + QStringLiteral("RecordCountChanged"), + this, + SLOT(onNotificationCountChanged(uint))); + + refreshClock(); + refreshNotificationCount(); + refreshMailClient(); + refreshMailState(); + refreshMusicState(); + refreshSystemStats(); + refreshAiState(); + refreshWeather(); +} + +QString FashionLeftPluginProvider::timeText() const +{ + return m_timeText; +} + +QString FashionLeftPluginProvider::dateText() const +{ + return m_dateText; +} + +int FashionLeftPluginProvider::notificationCount() const +{ + return m_notificationCount; +} + +QString FashionLeftPluginProvider::notificationCountText() const +{ + if (m_notificationCount > 99) { + return QStringLiteral("99+"); + } + return QString::number(m_notificationCount); +} + +int FashionLeftPluginProvider::mailUnreadCount() const +{ + return m_mailUnreadCount; +} + +QString FashionLeftPluginProvider::mailUnreadCountText() const +{ + return QString::number(m_mailUnreadCount); +} + +QString FashionLeftPluginProvider::mailSummaryText() const +{ + return m_mailSummaryText; +} + +bool FashionLeftPluginProvider::mailConfigured() const +{ + return m_mailConfigured; +} + +QString FashionLeftPluginProvider::mailIconName() const +{ + return m_mailIconName; +} + +QString FashionLeftPluginProvider::mailClientName() const +{ + return m_mailClientName; +} + +bool FashionLeftPluginProvider::musicAvailable() const +{ + return m_musicAvailable; +} + +QString FashionLeftPluginProvider::musicTitleText() const +{ + return m_musicTitleText; +} + +QString FashionLeftPluginProvider::musicSubtitleText() const +{ + return m_musicSubtitleText; +} + +QString FashionLeftPluginProvider::musicAppName() const +{ + return m_musicAppName; +} + +QUrl FashionLeftPluginProvider::musicArtSource() const +{ + return m_musicArtSource; +} + +QString FashionLeftPluginProvider::musicPlayerIconName() const +{ + return m_musicPlayerIconName; +} + +QUrl FashionLeftPluginProvider::musicPlayerIconSource() const +{ + return m_musicPlayerIconSource; +} + +bool FashionLeftPluginProvider::musicHasArt() const +{ + return m_musicArtSource.isValid() && !m_musicArtSource.isEmpty(); +} + +bool FashionLeftPluginProvider::musicPlaying() const +{ + return m_musicPlaying; +} + +bool FashionLeftPluginProvider::musicCanGoPrevious() const +{ + return m_musicCanGoPrevious; +} + +bool FashionLeftPluginProvider::musicCanGoNext() const +{ + return m_musicCanGoNext; +} + +bool FashionLeftPluginProvider::musicCanTogglePlayback() const +{ + return m_musicCanTogglePlayback; +} + +int FashionLeftPluginProvider::cpuUsage() const +{ + return m_cpuUsage; +} + +int FashionLeftPluginProvider::memoryUsage() const +{ + return m_memoryUsage; +} + +QString FashionLeftPluginProvider::downloadSpeedText() const +{ + return m_downloadSpeedText; +} + +QString FashionLeftPluginProvider::uploadSpeedText() const +{ + return m_uploadSpeedText; +} + +int FashionLeftPluginProvider::aiRunningCount() const +{ + return m_aiRunningCount; +} + +QString FashionLeftPluginProvider::aiRunningCountText() const +{ + if (m_aiRunningCount > 99) { + return QStringLiteral("99+"); + } + + return QString::number(m_aiRunningCount); +} + +QString FashionLeftPluginProvider::aiHeadlineText() const +{ + return m_aiHeadlineText; +} + +QString FashionLeftPluginProvider::aiSummaryText() const +{ + return m_aiSummaryText; +} + +QVariantList FashionLeftPluginProvider::aiToolEntries() const +{ + return m_aiToolEntries; +} + +QString FashionLeftPluginProvider::aiPrimaryToolId() const +{ + return m_aiPrimaryToolId; +} + +QString FashionLeftPluginProvider::weatherCityText() const +{ + return m_weatherCityText; +} + +QString FashionLeftPluginProvider::weatherTemperatureText() const +{ + return m_weatherTemperatureText; +} + +QString FashionLeftPluginProvider::weatherSummaryText() const +{ + return m_weatherSummaryText; +} + +QUrl FashionLeftPluginProvider::weatherIconSource() const +{ + return m_weatherIconSource; +} + +QUrl FashionLeftPluginProvider::messageIconSource() const +{ + return QUrl::fromLocalFile(MessageIconPath); +} + +void FashionLeftPluginProvider::openWeatherPage() +{ + const QString desktopFilePath = locateDesktopFile(QStringLiteral("org.deepin.weather.desktop")); + if (!desktopFilePath.isEmpty() && launchDesktopEntry(desktopFilePath)) { + return; + } + + if (launchCommand(QStringLiteral("deepin-weather"))) { + return; + } + + launchCommand(QStringLiteral("gtk-launch"), {QStringLiteral("org.deepin.weather")}); +} + +void FashionLeftPluginProvider::openWeatherPopup(int taskbarLeft, int taskbarTop, int activationX, int activationY) +{ + const QPoint logicalPopupPoint(qMax(0, taskbarLeft), qMax(0, taskbarTop)); + QScreen *popupScreen = QGuiApplication::screenAt(logicalPopupPoint); + if (!popupScreen) { + popupScreen = QGuiApplication::primaryScreen(); + } + + const QPoint popupPoint = nativePointForScreen(popupScreen, logicalPopupPoint); + const QPoint activationPoint = nativePointForScreen(popupScreen, + QPoint(qMax(0, activationX), qMax(0, activationY))); + const int popupLeft = popupPoint.x(); + const int popupTop = popupPoint.y(); + const int popupX = activationPoint.x(); + const int popupY = activationPoint.y(); + const QString weatherExecutablePath = preferredWeatherExecutablePath(); + const auto scheduleWeatherWindowPlacement = [this, popupLeft, popupTop, weatherExecutablePath] { + for (const int delay : {0, 40, 140, 320}) { + QTimer::singleShot(delay, this, [popupLeft, popupTop, weatherExecutablePath] { + moveWeatherWindowToRequestedPosition(popupLeft, popupTop, weatherExecutablePath); + }); + } + }; + const auto scheduleWeatherActivationRetry = [this, + popupLeft, + popupTop, + popupX, + popupY, + scheduleWeatherWindowPlacement, + weatherExecutablePath] { + QTimer::singleShot(700, + this, + [this, popupLeft, popupTop, popupX, popupY, scheduleWeatherWindowPlacement, weatherExecutablePath] { + if (moveWeatherWindowToRequestedPosition(popupLeft, popupTop, weatherExecutablePath)) { + scheduleWeatherWindowPlacement(); + return; + } + + if (activateStatusNotifierItem(QStringLiteral("org.deepin.weather"), + popupX, + popupY, + QDBus::NoBlock, + weatherExecutablePath)) { + scheduleWeatherWindowPlacement(); + } + }); + }; + const auto openWeatherPageWithRetry = [this, scheduleWeatherActivationRetry] { + openWeatherPage(); + scheduleWeatherActivationRetry(); + }; + + if (activateStatusNotifierItem(QStringLiteral("org.deepin.weather"), + popupX, + popupY, + QDBus::NoBlock, + weatherExecutablePath)) { + scheduleWeatherWindowPlacement(); + QTimer::singleShot(450, + this, + [this, popupLeft, popupTop, scheduleWeatherWindowPlacement, openWeatherPageWithRetry, weatherExecutablePath] { + if (moveWeatherWindowToRequestedPosition(popupLeft, popupTop, weatherExecutablePath)) { + scheduleWeatherWindowPlacement(); + return; + } + + openWeatherPageWithRetry(); + }); + return; + } + + if (moveWeatherWindowToRequestedPosition(popupLeft, popupTop, weatherExecutablePath)) { + scheduleWeatherWindowPlacement(); + return; + } + + openWeatherPageWithRetry(); +} + +void FashionLeftPluginProvider::openMailClient() +{ + refreshMailState(); + if (!m_mailConfigured) { + return; + } + + refreshMailClient(); + + if (!m_mailDesktopFilePath.isEmpty() + && launchCommand(QStringLiteral("gio"), {QStringLiteral("launch"), m_mailDesktopFilePath})) { + return; + } + + if (!m_mailDesktopId.isEmpty()) { + const QString desktopFilePath = locateDesktopFile(m_mailDesktopId); + if (!desktopFilePath.isEmpty() + && launchCommand(QStringLiteral("gio"), {QStringLiteral("launch"), desktopFilePath})) { + return; + } + } + + if (launchCommand(QStringLiteral("xdg-open"), {QStringLiteral("mailto:")})) { + return; + } + + launchCommand(QStringLiteral("ll-cli"), + {QStringLiteral("run"), + QStringLiteral("org.deepin.mail"), + QStringLiteral("--"), + QStringLiteral("/opt/apps/org.deepin.mail/files/bin/deepin-mail")}); +} + +void FashionLeftPluginProvider::openNotificationPage() +{ + showControlCenterPage(QStringLiteral("system/notification")); +} + +void FashionLeftPluginProvider::openMusicPlayer() +{ + if (m_musicService.isEmpty() + && m_musicDesktopEntry.trimmed().isEmpty() + && m_musicExecutablePath.trimmed().isEmpty()) { + return; + } + + QString desktopId = m_musicDesktopEntry.trimmed(); + if (!desktopId.isEmpty() && !desktopId.endsWith(QStringLiteral(".desktop"))) { + desktopId += QStringLiteral(".desktop"); + } + + QString executablePath = m_musicExecutablePath; + if (executablePath.isEmpty() && !m_musicService.isEmpty()) { + executablePath = executablePathForService(m_musicService); + } + QString desktopFilePath; + if (!desktopId.isEmpty()) { + desktopFilePath = locateDesktopFile(desktopId); + } + if (desktopFilePath.isEmpty()) { + desktopFilePath = locateDesktopFileByExecutable(executablePath); + } + if (executablePath.isEmpty()) { + executablePath = desktopCommandExecutable(desktopFilePath); + } + + const auto scheduleMusicWindowActivation = [this, desktopFilePath, executablePath] { + QTimer::singleShot(120, this, [this, desktopFilePath, executablePath] { + if (!activateWindowForServiceOrDesktop(m_musicService, desktopFilePath, m_musicAppName, executablePath)) { + activateWindowForPidOrDesktop(0, desktopFilePath, m_musicAppName, executablePath); + } + }); + QTimer::singleShot(420, this, [this, desktopFilePath, executablePath] { + if (!activateWindowForServiceOrDesktop(m_musicService, desktopFilePath, m_musicAppName, executablePath)) { + activateWindowForPidOrDesktop(0, desktopFilePath, m_musicAppName, executablePath); + } + }); + }; + + if (activateWindowForServiceOrDesktop(m_musicService, desktopFilePath, m_musicAppName, executablePath) + || activateWindowForPidOrDesktop(0, desktopFilePath, m_musicAppName, executablePath)) { + return; + } + + if (!m_musicService.isEmpty()) { + QDBusInterface rootInterface(m_musicService, + MprisPath, + MprisRootInterface, + QDBusConnection::sessionBus()); + if (rootInterface.isValid()) { + const QDBusMessage raiseReply = rootInterface.call(QStringLiteral("Raise")); + if (raiseReply.type() != QDBusMessage::ErrorMessage) { + scheduleMusicWindowActivation(); + return; + } + } + } + + if (!desktopFilePath.isEmpty() && launchDesktopEntry(desktopFilePath)) { + scheduleMusicWindowActivation(); + return; + } + + if (!executablePath.isEmpty() && launchCommand(executablePath)) { + scheduleMusicWindowActivation(); + return; + } + + const QString desktopBaseId = QFileInfo(desktopId).completeBaseName(); + if (!desktopBaseId.isEmpty() && launchCommand(QStringLiteral("gtk-launch"), {desktopBaseId})) { + scheduleMusicWindowActivation(); + return; + } + + if (!desktopId.isEmpty() && launchCommand(QStringLiteral("gtk-launch"), {desktopId})) { + scheduleMusicWindowActivation(); + } +} + +QString FashionLeftPluginProvider::musicControlThemeIconSource(const QString &iconName, bool darkTheme) const +{ + const QString normalizedIconName = iconName.trimmed(); + if (normalizedIconName.isEmpty()) { + return {}; + } + + const QString preferredTheme = darkTheme ? QStringLiteral("Win11-dark") : QStringLiteral("Win11"); + const QString fallbackTheme = darkTheme ? QStringLiteral("Win11") : QStringLiteral("Win11-dark"); + const QString iconPath = firstExistingPath({ + QStringLiteral("/usr/share/icons/%1/actions/16/%2.svg").arg(preferredTheme, normalizedIconName), + QStringLiteral("/usr/share/icons/%1/actions/22/%2.svg").arg(preferredTheme, normalizedIconName), + QStringLiteral("/usr/share/icons/%1/actions/24/%2.svg").arg(preferredTheme, normalizedIconName), + QStringLiteral("/usr/share/icons/%1/actions/16/%2.svg").arg(fallbackTheme, normalizedIconName), + QStringLiteral("/usr/share/icons/%1/actions/22/%2.svg").arg(fallbackTheme, normalizedIconName), + QStringLiteral("/usr/share/icons/%1/actions/24/%2.svg").arg(fallbackTheme, normalizedIconName), + QStringLiteral("/usr/share/icons/bloom/actions/24/%1.svg").arg(normalizedIconName), + QStringLiteral("/usr/share/icons/bloom/actions/22/%1.svg").arg(normalizedIconName), + QStringLiteral("/usr/share/icons/Adwaita/symbolic/actions/%1-symbolic.svg").arg(normalizedIconName), + }); + if (iconPath.isEmpty()) { + return {}; + } + + return QUrl::fromLocalFile(iconPath).toString(); +} + +void FashionLeftPluginProvider::playPreviousTrack() +{ + if (m_musicService.isEmpty() || !m_musicCanGoPrevious) { + return; + } + + QDBusInterface playerInterface(m_musicService, + MprisPath, + MprisPlayerInterface, + QDBusConnection::sessionBus()); + playerInterface.call(QStringLiteral("Previous")); + refreshMusicState(); +} + +void FashionLeftPluginProvider::toggleMusicPlayback() +{ + if (m_musicService.isEmpty() || !m_musicCanTogglePlayback) { + return; + } + + QDBusInterface playerInterface(m_musicService, + MprisPath, + MprisPlayerInterface, + QDBusConnection::sessionBus()); + playerInterface.call(QStringLiteral("PlayPause")); + refreshMusicState(); +} + +void FashionLeftPluginProvider::playNextTrack() +{ + if (m_musicService.isEmpty() || !m_musicCanGoNext) { + return; + } + + QDBusInterface playerInterface(m_musicService, + MprisPath, + MprisPlayerInterface, + QDBusConnection::sessionBus()); + playerInterface.call(QStringLiteral("Next")); + refreshMusicState(); +} + +void FashionLeftPluginProvider::openAiClientHost() +{ + if (activateWindowForPidOrDesktop(m_aiPrimaryHostPid, + m_aiPrimaryHostDesktopFilePath, + m_aiPrimaryHostAppName, + m_aiPrimaryHostExecutablePath)) { + return; + } + + if (!m_aiPrimaryHostDesktopFilePath.isEmpty() && launchDesktopEntry(m_aiPrimaryHostDesktopFilePath)) { + return; + } + + if (!m_aiPrimaryHostExecutablePath.isEmpty() && launchCommand(m_aiPrimaryHostExecutablePath)) { + return; + } + + openSystemMonitorPage(); +} + +void FashionLeftPluginProvider::openSystemMonitorPage() +{ + const QString desktopFilePath = locateDesktopFile(QStringLiteral("deepin-system-monitor.desktop")); + const QString monitorName = localizedDesktopEntryValue(desktopFilePath, QStringLiteral("Name")); + const QString monitorExecutable = desktopCommandExecutable(desktopFilePath); + const auto requestRaiseWindow = [this, desktopFilePath, monitorName, monitorExecutable] { + QTimer::singleShot(150, this, [desktopFilePath, monitorName, monitorExecutable] { + callDBusMethod(QLatin1String(SystemMonitorMainService), + QLatin1String(SystemMonitorMainPath), + QLatin1String(SystemMonitorMainInterface), + QStringLiteral("slotRaiseWindow")); + activateWindowForServiceOrDesktop(QString(), desktopFilePath, monitorName, monitorExecutable); + }); + QTimer::singleShot(500, this, [desktopFilePath, monitorName, monitorExecutable] { + callDBusMethod(QLatin1String(SystemMonitorMainService), + QLatin1String(SystemMonitorMainPath), + QLatin1String(SystemMonitorMainInterface), + QStringLiteral("slotRaiseWindow")); + activateWindowForServiceOrDesktop(QString(), desktopFilePath, monitorName, monitorExecutable); + }); + }; + + if (activateWindowForServiceOrDesktop(QString(), desktopFilePath, monitorName, monitorExecutable)) { + return; + } + + if (callDBusMethod(QLatin1String(SystemMonitorMainService), + QLatin1String(SystemMonitorMainPath), + QLatin1String(SystemMonitorMainInterface), + QStringLiteral("slotRaiseWindow"))) { + requestRaiseWindow(); + return; + } + + if (callDBusMethod(QLatin1String(SystemMonitorServerService), + QLatin1String(SystemMonitorServerPath), + QLatin1String(SystemMonitorServerInterface), + QStringLiteral("showDeepinSystemMoniter"))) { + requestRaiseWindow(); + return; + } + + if (callDBusMethod(QLatin1String(SystemMonitorService), + QLatin1String(SystemMonitorPath), + QLatin1String(SystemMonitorInterface), + QStringLiteral("showDeepinSystemMoniter"))) { + requestRaiseWindow(); + return; + } + + if (!desktopFilePath.isEmpty() && launchDesktopEntry(desktopFilePath)) { + requestRaiseWindow(); + return; + } + + launchCommand(QStringLiteral("deepin-system-monitor")); + requestRaiseWindow(); +} + +void FashionLeftPluginProvider::refreshClock() +{ + const QDateTime now = QDateTime::currentDateTime(); + const QString nextTimeText = now.time().toString(QStringLiteral("HH:mm")); + const QString nextDateText = QLocale().toString(now.date(), QStringLiteral("M/d dddd")); + + if (m_timeText == nextTimeText && m_dateText == nextDateText) { + return; + } + + m_timeText = nextTimeText; + m_dateText = nextDateText; + emit clockChanged(); +} + +void FashionLeftPluginProvider::refreshNotificationCount() +{ + QDBusInterface notificationInterface(NotificationService, + NotificationPath, + NotificationInterface, + QDBusConnection::sessionBus()); + if (!notificationInterface.isValid()) { + return; + } + + const QDBusReply reply = notificationInterface.call(QStringLiteral("recordCount")); + if (!reply.isValid()) { + return; + } + + onNotificationCountChanged(reply.value()); +} + +void FashionLeftPluginProvider::refreshMailState() +{ + QDBusInterface mailInterface(MailService, + MailPath, + MailInterface, + QDBusConnection::sessionBus()); + + int nextMailUnreadCount = 0; + QString nextMailSummaryText = QStringLiteral("邮箱信息不可用"); + bool nextMailConfigured = false; + + if (mailInterface.isValid()) { + const QDBusReply accountsReply = mailInterface.call(QStringLiteral("GetAccounts")); + const QStringList accountIds = accountsReply.isValid() + ? mailAccountIdsFromJson(accountsReply.value()) + : QStringList(); + + if (accountIds.isEmpty()) { + nextMailSummaryText = QStringLiteral("未配置邮箱账户"); + } else { + nextMailConfigured = true; + nextMailSummaryText = accountIds.size() == 1 + ? accountIds.constFirst() + : QStringLiteral("%1个邮箱").arg(accountIds.size()); + + for (const QString &accountId : accountIds) { + const QDBusReply unreadReply = mailInterface.call(QStringLiteral("GetUnread"), accountId); + if (!unreadReply.isValid()) { + continue; + } + + bool unreadOk = false; + const int unreadCount = unreadCountFromJson(unreadReply.value(), &unreadOk); + if (!unreadOk) { + continue; + } + + nextMailUnreadCount += unreadCount; + } + } + } + + if (m_mailConfigured == nextMailConfigured + && m_mailUnreadCount == nextMailUnreadCount + && m_mailSummaryText == nextMailSummaryText) { + return; + } + + m_mailConfigured = nextMailConfigured; + m_mailUnreadCount = nextMailUnreadCount; + m_mailSummaryText = nextMailSummaryText; + emit mailStateChanged(); +} + +void FashionLeftPluginProvider::refreshMusicState() +{ + MusicSnapshot snapshot = currentMusicSnapshot(m_musicService); + if (!snapshot.available) { + snapshot = runningMusicPlayerSnapshot(m_musicDesktopEntry); + } + + QString nextDesktopEntry = snapshot.desktopEntry.trimmed(); + if (!nextDesktopEntry.isEmpty() && !nextDesktopEntry.endsWith(QStringLiteral(".desktop"))) { + nextDesktopEntry += QStringLiteral(".desktop"); + } + + const QString nextDesktopFilePath = locateDesktopFile(nextDesktopEntry); + QString nextMusicAppName = snapshot.appName; + if (nextMusicAppName.isEmpty()) { + nextMusicAppName = localizedDesktopEntryValue(nextDesktopFilePath, QStringLiteral("Name")); + } + QString nextMusicExecutablePath = snapshot.executablePath; + if (nextMusicExecutablePath.isEmpty()) { + nextMusicExecutablePath = desktopCommandExecutable(nextDesktopFilePath); + } + const QString nextMusicPlayerIconName = musicPlayerIconNameForDesktopEntry(nextDesktopEntry, + nextMusicAppName, + snapshot.service); + const QUrl nextMusicPlayerIconSource = iconSourceForName(nextMusicPlayerIconName); + if (m_musicService == snapshot.service + && m_musicDesktopEntry == nextDesktopEntry + && m_musicExecutablePath == nextMusicExecutablePath + && m_musicTitleText == snapshot.title + && m_musicSubtitleText == snapshot.subtitle + && m_musicAppName == nextMusicAppName + && m_musicArtSource == snapshot.artSource + && m_musicPlayerIconName == nextMusicPlayerIconName + && m_musicPlayerIconSource == nextMusicPlayerIconSource + && m_musicAvailable == snapshot.available + && m_musicPlaying == snapshot.playing + && m_musicCanRaise == snapshot.canRaise + && m_musicCanGoPrevious == snapshot.canGoPrevious + && m_musicCanGoNext == snapshot.canGoNext + && m_musicCanTogglePlayback == snapshot.canTogglePlayback) { + return; + } + + m_musicService = snapshot.service; + m_musicDesktopEntry = nextDesktopEntry; + m_musicExecutablePath = nextMusicExecutablePath; + m_musicTitleText = snapshot.title; + m_musicSubtitleText = snapshot.subtitle; + m_musicAppName = nextMusicAppName; + m_musicArtSource = snapshot.artSource; + m_musicPlayerIconName = nextMusicPlayerIconName; + m_musicPlayerIconSource = nextMusicPlayerIconSource; + m_musicAvailable = snapshot.available; + m_musicPlaying = snapshot.playing; + m_musicCanRaise = snapshot.canRaise; + m_musicCanGoPrevious = snapshot.canGoPrevious; + m_musicCanGoNext = snapshot.canGoNext; + m_musicCanTogglePlayback = snapshot.canTogglePlayback; + emit musicStateChanged(); +} + +void FashionLeftPluginProvider::refreshSystemStats() +{ + int nextCpuUsage = m_cpuUsage; + quint64 totalCpuTime = 0; + quint64 idleCpuTime = 0; + if (readCpuTimes(&totalCpuTime, &idleCpuTime)) { + if (m_previousCpuTotalTime > 0 && totalCpuTime > m_previousCpuTotalTime) { + const quint64 totalDelta = totalCpuTime - m_previousCpuTotalTime; + const quint64 idleDelta = idleCpuTime - m_previousCpuIdleTime; + const double busyRatio = totalDelta > 0 + ? (static_cast(totalDelta - qMin(idleDelta, totalDelta)) * 100.0 / totalDelta) + : 0.0; + nextCpuUsage = qBound(0, qRound(busyRatio), 100); + } + + m_previousCpuTotalTime = totalCpuTime; + m_previousCpuIdleTime = idleCpuTime; + } + + int nextMemoryUsage = systemMemoryUsagePercent(); + querySystemMonitorUsage("getMemoryUsage", &nextMemoryUsage); + + const QStringList activeInterfaces = preferredNetworkInterfaces(); + const quint64 receiveBytes = totalInterfaceBytes(true, activeInterfaces); + const quint64 transmitBytes = totalInterfaceBytes(false, activeInterfaces); + const quint64 aggregateReceiveBytes = activeInterfaces.isEmpty() ? receiveBytes : totalInterfaceBytes(true, {}); + const quint64 aggregateTransmitBytes = activeInterfaces.isEmpty() ? transmitBytes : totalInterfaceBytes(false, {}); + + QString nextDownloadSpeed = m_downloadSpeedText; + QString nextUploadSpeed = m_uploadSpeedText; + if (m_networkSampleTimer.isValid()) { + const double elapsedSeconds = qMax(0.001, m_networkSampleTimer.restart() / 1000.0); + quint64 receiveDelta = receiveBytes >= m_previousReceiveBytes + ? (receiveBytes - m_previousReceiveBytes) + : 0; + quint64 transmitDelta = transmitBytes >= m_previousTransmitBytes + ? (transmitBytes - m_previousTransmitBytes) + : 0; + const quint64 aggregateReceiveDelta = aggregateReceiveBytes >= m_previousAggregateReceiveBytes + ? (aggregateReceiveBytes - m_previousAggregateReceiveBytes) + : 0; + const quint64 aggregateTransmitDelta = aggregateTransmitBytes >= m_previousAggregateTransmitBytes + ? (aggregateTransmitBytes - m_previousAggregateTransmitBytes) + : 0; + + // Some environments route user traffic through overlay/tunnel interfaces. + // Fall back to total active traffic when the preferred interface delta stays at zero. + if (receiveDelta == 0 && aggregateReceiveDelta > 0) { + receiveDelta = aggregateReceiveDelta; + } + if (transmitDelta == 0 && aggregateTransmitDelta > 0) { + transmitDelta = aggregateTransmitDelta; + } + + nextDownloadSpeed = formatTransferRate(receiveDelta / elapsedSeconds); + nextUploadSpeed = formatTransferRate(transmitDelta / elapsedSeconds); + } else { + m_networkSampleTimer.start(); + } + + m_previousReceiveBytes = receiveBytes; + m_previousTransmitBytes = transmitBytes; + m_previousAggregateReceiveBytes = aggregateReceiveBytes; + m_previousAggregateTransmitBytes = aggregateTransmitBytes; + + if (m_cpuUsage == nextCpuUsage + && m_memoryUsage == nextMemoryUsage + && m_downloadSpeedText == nextDownloadSpeed + && m_uploadSpeedText == nextUploadSpeed) { + return; + } + + m_cpuUsage = nextCpuUsage; + m_memoryUsage = nextMemoryUsage; + m_downloadSpeedText = nextDownloadSpeed; + m_uploadSpeedText = nextUploadSpeed; + emit systemStatsChanged(); +} + +void FashionLeftPluginProvider::refreshAiState() +{ + if (!m_aiRefreshWatcher) { + return; + } + + if (m_aiRefreshWatcher->isRunning()) { + m_aiRefreshPending = true; + return; + } + + const QHash previousLastSeenPidByTool = m_aiLastSeenPidByTool; + const QString previousLastPrimaryToolId = m_aiLastPrimaryToolId; + m_aiRefreshWatcher->setFuture(QtConcurrent::run([previousLastSeenPidByTool, previousLastPrimaryToolId]() -> QVariantMap { + const QList processes = currentAiCliProcesses(); + QHash currentRunningCounts; + QHash currentCompletedCounts; + QHash currentActivityMsByTool; + QHash currentSessionStates; + QHash nextLastSeenPidByTool = previousLastSeenPidByTool; + QVariantList completedSessions; + const QDateTime nowUtc = QDateTime::currentDateTimeUtc(); + const qint64 nowMs = nowUtc.toMSecsSinceEpoch(); + + for (const AiCliProcessInfo &process : processes) { + nextLastSeenPidByTool[process.toolId] = qMax(nextLastSeenPidByTool.value(process.toolId), process.pid); + + AiCliTaskState taskState = AiCliTaskState::Running; + AiCliSessionLogSnapshot snapshot; + bool hasSessionSnapshot = false; + if (process.toolId == QStringLiteral("codex") && !process.sessionLogPath.isEmpty()) { + snapshot = sessionLogSnapshot(process.sessionLogPath); + if (snapshot.taskState != AiCliTaskState::Unknown) { + taskState = snapshot.taskState; + hasSessionSnapshot = true; + } + } + + const bool counted = !hasSessionSnapshot + || taskState == AiCliTaskState::Running + || shouldCountCompletedSession(snapshot, nowUtc); + if (counted) { + if (taskState == AiCliTaskState::Completed) { + currentCompletedCounts[process.toolId] = currentCompletedCounts.value(process.toolId) + 1; + } else { + currentRunningCounts[process.toolId] = currentRunningCounts.value(process.toolId) + 1; + } + + const qint64 activityMs = snapshot.eventTimeUtc.isValid() + ? snapshot.eventTimeUtc.toMSecsSinceEpoch() + : nowMs; + currentActivityMsByTool[process.toolId] = qMax(currentActivityMsByTool.value(process.toolId), activityMs); + } + + const QString sessionKey = !process.sessionLogPath.isEmpty() + ? process.sessionLogPath + : QStringLiteral("%1:%2").arg(process.toolId).arg(process.pid); + currentSessionStates.insert(sessionKey, aiCliTaskStateKey(taskState)); + if (counted && taskState == AiCliTaskState::Completed) { + QVariantMap completedSession; + completedSession.insert(QStringLiteral("key"), sessionKey); + completedSession.insert(QStringLiteral("toolId"), process.toolId); + completedSessions << completedSession; + } + } + + const qint64 activeWindowPid = activeWindowPidForX11(); + QString currentPrimaryToolId; + const AiCliProcessInfo *primaryProcess = nullptr; + if (activeWindowPid > 0) { + for (const AiCliProcessInfo &process : processes) { + if (processHasAncestor(process.pid, activeWindowPid)) { + currentPrimaryToolId = process.toolId; + primaryProcess = &process; + break; + } + } + } + if (currentPrimaryToolId.isEmpty() && !processes.isEmpty()) { + currentPrimaryToolId = processes.constFirst().toolId; + primaryProcess = &processes.constFirst(); + } + + QString nextPrimaryToolId = currentPrimaryToolId; + if (nextPrimaryToolId.isEmpty()) { + nextPrimaryToolId = previousLastPrimaryToolId; + } + QString nextLastPrimaryToolId = previousLastPrimaryToolId; + if (!currentPrimaryToolId.isEmpty()) { + nextLastPrimaryToolId = currentPrimaryToolId; + } + + if (!primaryProcess && !nextPrimaryToolId.isEmpty()) { + for (const AiCliProcessInfo &process : processes) { + if (process.toolId == nextPrimaryToolId) { + primaryProcess = &process; + break; + } + } + } + + qint64 nextAiPrimaryHostPid = 0; + QString nextAiPrimaryHostDesktopFilePath; + QString nextAiPrimaryHostExecutablePath; + QString nextAiPrimaryHostAppName; + const auto resolveAiHostApplication = [](const AiCliProcessInfo &process, + qint64 *hostPid, + QString *hostDesktopFilePath, + QString *hostExecutablePath, + QString *hostAppName) { + if (!hostPid || !hostDesktopFilePath || !hostExecutablePath || !hostAppName) { + return; + } + + qint64 currentPid = process.pid; + for (int depth = 0; currentPid > 1 && depth < 12; ++depth) { + const QString executablePath = executablePathForPid(static_cast(currentPid)); + const QString executableName = QFileInfo(executablePath).fileName(); + const QString commName = readTrimmedTextFile(QStringLiteral("/proc/%1/comm").arg(currentPid)); + const QStringList arguments = readProcCommandLine(QStringLiteral("/proc/%1/cmdline").arg(currentPid)); + + if (isShellOrWrapperExecutable(executableName) + || isShellOrWrapperExecutable(commName) + || !detectAiCliToolId(executableName, commName, arguments).isEmpty()) { + currentPid = parentPidForProcess(currentPid); + continue; + } + + const QString desktopFilePath = FashionLeftPluginProvider::locateDesktopFileByExecutable(executablePath); + const QString appName = !desktopFilePath.isEmpty() + ? FashionLeftPluginProvider::localizedDesktopEntryValue(desktopFilePath, QStringLiteral("Name")) + : QString(); + const bool hasWindow = !bestWindowIdForPid(currentPid, false).isEmpty() + || !bestWindowIdForPid(currentPid).isEmpty(); + if (!hasWindow && desktopFilePath.isEmpty()) { + currentPid = parentPidForProcess(currentPid); + continue; + } + + *hostPid = currentPid; + *hostDesktopFilePath = desktopFilePath; + *hostExecutablePath = executablePath; + *hostAppName = !appName.isEmpty() ? appName + : (!commName.trimmed().isEmpty() ? commName.trimmed() : executableName); + return; + } + }; + if (primaryProcess) { + resolveAiHostApplication(*primaryProcess, + &nextAiPrimaryHostPid, + &nextAiPrimaryHostDesktopFilePath, + &nextAiPrimaryHostExecutablePath, + &nextAiPrimaryHostAppName); + } + + QStringList candidateToolIds = currentRunningCounts.keys(); + for (auto it = currentCompletedCounts.cbegin(); it != currentCompletedCounts.cend(); ++it) { + if ((it.value() > 0 || currentRunningCounts.value(it.key()) > 0) && !candidateToolIds.contains(it.key())) { + candidateToolIds << it.key(); + } + } + if (!nextPrimaryToolId.isEmpty() && !candidateToolIds.contains(nextPrimaryToolId)) { + candidateToolIds.prepend(nextPrimaryToolId); + } + + std::sort(candidateToolIds.begin(), candidateToolIds.end(), [¤tRunningCounts, ¤tActivityMsByTool, &nextLastSeenPidByTool, &nextPrimaryToolId](const QString &left, const QString &right) { + if (left == right) { + return false; + } + if (left == nextPrimaryToolId) { + return true; + } + if (right == nextPrimaryToolId) { + return false; + } + + const bool leftRunning = currentRunningCounts.value(left) > 0; + const bool rightRunning = currentRunningCounts.value(right) > 0; + if (leftRunning != rightRunning) { + return leftRunning; + } + + const qint64 leftActivityMs = currentActivityMsByTool.value(left); + const qint64 rightActivityMs = currentActivityMsByTool.value(right); + if (leftActivityMs != rightActivityMs) { + return leftActivityMs > rightActivityMs; + } + + const qint64 leftPid = nextLastSeenPidByTool.value(left); + const qint64 rightPid = nextLastSeenPidByTool.value(right); + if (leftPid != rightPid) { + return leftPid > rightPid; + } + + return left < right; + }); + + QVariantList nextToolEntries; + for (const QString &toolId : candidateToolIds) { + if (nextToolEntries.size() >= 2) { + break; + } + + const int runningCount = currentRunningCounts.value(toolId); + const int completedCount = currentCompletedCounts.value(toolId); + const int totalCount = runningCount + completedCount; + if (runningCount <= 0 && totalCount <= 0) { + continue; + } + + QVariantMap entry; + entry.insert(QStringLiteral("toolId"), toolId); + entry.insert(QStringLiteral("displayName"), aiToolDisplayName(toolId)); + entry.insert(QStringLiteral("processLabel"), aiToolProcessLabel(toolId)); + entry.insert(QStringLiteral("runningCount"), runningCount); + entry.insert(QStringLiteral("completedCount"), completedCount); + entry.insert(QStringLiteral("totalCount"), totalCount); + entry.insert(QStringLiteral("progressText"), + QStringLiteral("%1/%2").arg(completedCount).arg(totalCount)); + nextToolEntries << entry; + } + + int nextRunningCount = 0; + for (auto it = currentRunningCounts.cbegin(); it != currentRunningCounts.cend(); ++it) { + nextRunningCount += it.value(); + } + + QString nextHeadlineText; + QString nextSummaryText; + if (nextToolEntries.isEmpty()) { + nextHeadlineText = QStringLiteral("0 条任务"); + nextSummaryText = QStringLiteral("ai cli"); + } else if (nextToolEntries.size() == 1) { + const QVariantMap entry = nextToolEntries.constFirst().toMap(); + nextHeadlineText = entry.value(QStringLiteral("progressText")).toString() + QStringLiteral(" 条任务"); + nextSummaryText = entry.value(QStringLiteral("processLabel")).toString(); + } else { + QStringList parts; + for (const QVariant &entryValue : nextToolEntries) { + const QVariantMap entry = entryValue.toMap(); + parts << QStringLiteral("%1 %2") + .arg(entry.value(QStringLiteral("displayName")).toString()) + .arg(entry.value(QStringLiteral("progressText")).toString()); + } + nextHeadlineText = QStringLiteral("%1 个 CLI").arg(nextToolEntries.size()); + nextSummaryText = parts.join(QStringLiteral(" · ")); + } + + QVariantMap result; + result.insert(QStringLiteral("runningCount"), nextRunningCount); + result.insert(QStringLiteral("headlineText"), nextHeadlineText); + result.insert(QStringLiteral("summaryText"), nextSummaryText); + result.insert(QStringLiteral("toolEntries"), nextToolEntries); + result.insert(QStringLiteral("primaryToolId"), nextPrimaryToolId); + result.insert(QStringLiteral("lastPrimaryToolId"), nextLastPrimaryToolId); + result.insert(QStringLiteral("primaryHostPid"), static_cast(nextAiPrimaryHostPid)); + result.insert(QStringLiteral("primaryHostDesktopFilePath"), nextAiPrimaryHostDesktopFilePath); + result.insert(QStringLiteral("primaryHostExecutablePath"), nextAiPrimaryHostExecutablePath); + result.insert(QStringLiteral("primaryHostAppName"), nextAiPrimaryHostAppName); + result.insert(QStringLiteral("observedSessionStates"), variantMapFromStringHash(currentSessionStates)); + result.insert(QStringLiteral("lastSeenPidByTool"), variantMapFromLongLongHash(nextLastSeenPidByTool)); + result.insert(QStringLiteral("runningCounts"), variantMapFromIntHash(currentRunningCounts)); + result.insert(QStringLiteral("completedCounts"), variantMapFromIntHash(currentCompletedCounts)); + result.insert(QStringLiteral("completedSessions"), completedSessions); + return result; + })); +} + +void FashionLeftPluginProvider::applyAiRefreshResult(const QVariantMap &result) +{ + const int nextRunningCount = result.value(QStringLiteral("runningCount")).toInt(); + const QString nextHeadlineText = result.value(QStringLiteral("headlineText")).toString(); + const QString nextSummaryText = result.value(QStringLiteral("summaryText")).toString(); + const QVariantList nextToolEntries = result.value(QStringLiteral("toolEntries")).toList(); + const QString nextPrimaryToolId = result.value(QStringLiteral("primaryToolId")).toString(); + const QString nextLastPrimaryToolId = result.value(QStringLiteral("lastPrimaryToolId")).toString(); + const qint64 nextAiPrimaryHostPid = result.value(QStringLiteral("primaryHostPid")).toLongLong(); + const QString nextAiPrimaryHostDesktopFilePath = result.value(QStringLiteral("primaryHostDesktopFilePath")).toString(); + const QString nextAiPrimaryHostExecutablePath = result.value(QStringLiteral("primaryHostExecutablePath")).toString(); + const QString nextAiPrimaryHostAppName = result.value(QStringLiteral("primaryHostAppName")).toString(); + const QHash currentSessionStates = stringHashFromVariantMap(result.value(QStringLiteral("observedSessionStates")).toMap()); + const QHash nextLastSeenPidByTool = longLongHashFromVariantMap(result.value(QStringLiteral("lastSeenPidByTool")).toMap()); + const QHash currentRunningCounts = intHashFromVariantMap(result.value(QStringLiteral("runningCounts")).toMap()); + const QHash currentCompletedCounts = intHashFromVariantMap(result.value(QStringLiteral("completedCounts")).toMap()); + const QVariantList completedSessions = result.value(QStringLiteral("completedSessions")).toList(); + + for (const QVariant &sessionValue : completedSessions) { + const QVariantMap session = sessionValue.toMap(); + const QString sessionKey = session.value(QStringLiteral("key")).toString(); + const QString toolId = session.value(QStringLiteral("toolId")).toString(); + const QString previousState = m_aiObservedSessionStates.value(sessionKey); + if (m_aiStateInitialized && previousState != aiCliTaskStateKey(AiCliTaskState::Completed)) { + const int completedCount = currentCompletedCounts.value(toolId); + const int totalCount = completedCount + currentRunningCounts.value(toolId); + sendDesktopNotification(QStringLiteral("%1 已结束").arg(aiToolDisplayName(toolId)), + QStringLiteral("%1/%2 条任务 · %3") + .arg(completedCount) + .arg(totalCount) + .arg(aiToolProcessLabel(toolId)), + QStringLiteral("computer")); + } + } + + const bool stateChanged = m_aiRunningCount != nextRunningCount + || m_aiHeadlineText != nextHeadlineText + || m_aiSummaryText != nextSummaryText + || m_aiPrimaryToolId != nextPrimaryToolId + || m_aiToolEntries != nextToolEntries; + + m_aiLastSeenPidByTool = nextLastSeenPidByTool; + m_aiLastPrimaryToolId = nextLastPrimaryToolId; + m_aiPrimaryHostPid = nextAiPrimaryHostPid; + m_aiPrimaryHostDesktopFilePath = nextAiPrimaryHostDesktopFilePath; + m_aiPrimaryHostExecutablePath = nextAiPrimaryHostExecutablePath; + m_aiPrimaryHostAppName = nextAiPrimaryHostAppName; + m_aiObservedSessionStates = currentSessionStates; + if (!m_aiStateInitialized) { + m_aiStateInitialized = true; + } + + if (!stateChanged) { + return; + } + + m_aiRunningCount = nextRunningCount; + m_aiHeadlineText = nextHeadlineText; + m_aiSummaryText = nextSummaryText; + m_aiPrimaryToolId = nextPrimaryToolId; + m_aiToolEntries = nextToolEntries; + emit aiStateChanged(); +} + +void FashionLeftPluginProvider::refreshWeather() +{ + ensureWeatherWatchPaths(); + + const QString configPath = weatherConfigPath(); + + QString nextWeatherCityText; + QString nextWeatherTemperatureText = QStringLiteral("--°"); + QString nextWeatherSummaryText = QStringLiteral("天气信息不可用"); + QUrl nextWeatherIconSource = QUrl::fromLocalFile(weatherIconPathFor(QString(), true)); + + if (QFileInfo::exists(configPath)) { + QSettings settings(configPath, QSettings::IniFormat); + nextWeatherCityText = settings.value(QStringLiteral("cache/city")).toString().trimmed(); + + bool currentTempOk = false; + const double currentTemp = settings.value(QStringLiteral("cache/currentTemp")).toDouble(¤tTempOk); + if (currentTempOk) { + nextWeatherTemperatureText = QStringLiteral("%1°").arg(qRound(currentTemp)); + } + + bool minTempOk = false; + bool maxTempOk = false; + bool weatherCodeOk = false; + int minTemp = qRound(settings.value(QStringLiteral("cache/todayMin")).toDouble(&minTempOk)); + int maxTemp = qRound(settings.value(QStringLiteral("cache/todayMax")).toDouble(&maxTempOk)); + const int weatherCode = settings.value(QStringLiteral("cache/weatherCode")).toInt(&weatherCodeOk); + const bool isDay = settings.value(QStringLiteral("cache/isDay"), true).toBool(); + + QString descriptionText = weatherCodeOk ? deepinWeatherDescription(weatherCode) : QString(); + QString iconName = weatherCodeOk ? deepinWeatherIconName(weatherCode, isDay) : QString(); + const QByteArray dailyData = settings.value(QStringLiteral("cache/daily")).toByteArray(); + const QJsonDocument dailyDocument = QJsonDocument::fromJson(dailyData); + if (dailyDocument.isArray() && !dailyDocument.array().isEmpty()) { + const QJsonObject todayObject = dailyDocument.array().at(0).toObject(); + if (descriptionText.isEmpty()) { + descriptionText = todayObject.value(QStringLiteral("description")).toString().trimmed(); + } + if (iconName.isEmpty()) { + iconName = todayObject.value(QStringLiteral("iconName")).toString().trimmed(); + } + + if (!minTempOk) { + minTemp = qRound(todayObject.value(QStringLiteral("minTempC")).toDouble()); + minTempOk = todayObject.contains(QStringLiteral("minTempC")); + } + + if (!maxTempOk) { + maxTemp = qRound(todayObject.value(QStringLiteral("maxTempC")).toDouble()); + maxTempOk = todayObject.contains(QStringLiteral("maxTempC")); + } + } + + if (maxTempOk && minTempOk && maxTemp < minTemp) { + qSwap(maxTemp, minTemp); + } + + const QString rangeText = (minTempOk && maxTempOk) + ? QStringLiteral("%1~%2°C").arg(minTemp).arg(maxTemp) + : QString(); + if (!descriptionText.isEmpty() && !rangeText.isEmpty()) { + nextWeatherSummaryText = QStringLiteral("%1 %2").arg(descriptionText, rangeText); + } else if (!descriptionText.isEmpty()) { + nextWeatherSummaryText = descriptionText; + } else if (!rangeText.isEmpty()) { + nextWeatherSummaryText = rangeText; + } + + nextWeatherIconSource = QUrl::fromLocalFile(weatherIconPathFor(iconName, isDay)); + } + + if (m_weatherCityText == nextWeatherCityText + && m_weatherTemperatureText == nextWeatherTemperatureText + && m_weatherSummaryText == nextWeatherSummaryText + && m_weatherIconSource == nextWeatherIconSource) { + return; + } + + m_weatherCityText = nextWeatherCityText; + m_weatherTemperatureText = nextWeatherTemperatureText; + m_weatherSummaryText = nextWeatherSummaryText; + m_weatherIconSource = nextWeatherIconSource; + emit weatherChanged(); +} + +void FashionLeftPluginProvider::onNotificationCountChanged(uint count) +{ + if (m_notificationCount == static_cast(count)) { + return; + } + + m_notificationCount = static_cast(count); + emit notificationCountChanged(); +} + +bool FashionLeftPluginProvider::launchCommand(const QString &program, const QStringList &arguments) +{ + if (program.isEmpty()) { + return false; + } + + return QProcess::startDetached(program, arguments); +} + +bool FashionLeftPluginProvider::showControlCenterPage(const QString &pagePath) +{ + QDBusMessage message = QDBusMessage::createMethodCall(ControlCenterService, + ControlCenterPath, + ControlCenterInterface, + QStringLiteral("ShowPage")); + message << pagePath; + const QDBusMessage reply = QDBusConnection::sessionBus().call(message); + return reply.type() != QDBusMessage::ErrorMessage; +} + +QString FashionLeftPluginProvider::commandOutput(const QString &program, const QStringList &arguments, int timeoutMs) +{ + QProcess process; + process.start(program, arguments); + if (!process.waitForFinished(timeoutMs)) { + process.kill(); + process.waitForFinished(500); + return {}; + } + + if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { + return {}; + } + + return QString::fromUtf8(process.readAllStandardOutput()).trimmed(); +} + +QString FashionLeftPluginProvider::executablePathForService(const QString &serviceName) +{ + if (serviceName.isEmpty()) { + return {}; + } + + QDBusConnectionInterface *connectionInterface = QDBusConnection::sessionBus().interface(); + if (!connectionInterface) { + return {}; + } + + const QDBusReply pidReply = connectionInterface->servicePid(serviceName); + if (!pidReply.isValid() || pidReply.value() == 0) { + return {}; + } + + const QString executableLinkPath = QStringLiteral("/proc/%1/exe").arg(pidReply.value()); + const QString executablePath = QFileInfo(executableLinkPath).symLinkTarget(); + return QFileInfo(executablePath).exists() ? executablePath : QString(); +} + +QStringList FashionLeftPluginProvider::desktopSearchDirectories() +{ + QStringList searchDirectories = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation); + searchDirectories << QStringLiteral("/var/lib/linglong/entries/share/applications") + << QStringLiteral("/usr/local/share/applications") + << QStringLiteral("/usr/share/applications") + << QDir::home().filePath(QStringLiteral(".local/share/applications")); + return uniqueExistingDirectories(searchDirectories); +} + +QString FashionLeftPluginProvider::locateDesktopFile(const QString &desktopId) +{ + if (desktopId.isEmpty()) { + return {}; + } + + if (QFileInfo::exists(desktopId)) { + return QFileInfo(desktopId).absoluteFilePath(); + } + + for (const QString &directory : desktopSearchDirectories()) { + const QString candidate = QDir(directory).filePath(desktopId); + if (QFileInfo::exists(candidate)) { + return candidate; + } + } + + return {}; +} + +QString FashionLeftPluginProvider::desktopEntryValue(const QString &desktopFilePath, const QString &key) +{ + if (desktopFilePath.isEmpty()) { + return {}; + } + + QSettings settings(desktopFilePath, QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + return settings.value(key).toString().trimmed(); +} + +QString FashionLeftPluginProvider::desktopCommandExecutable(const QString &desktopFilePath) +{ + const QStringList commandLines = { + desktopEntryValue(desktopFilePath, QStringLiteral("TryExec")), + desktopEntryValue(desktopFilePath, QStringLiteral("Exec")), + }; + + for (const QString &commandLine : commandLines) { + const QStringList commandParts = QProcess::splitCommand(commandLine); + for (const QString &part : commandParts) { + const QString token = part.trimmed(); + if (token.isEmpty() || token == QStringLiteral("env") || token.startsWith(QLatin1Char('%'))) { + continue; + } + + if (token.contains(QLatin1Char('=')) + && !token.startsWith(QLatin1Char('/')) + && !token.contains(QDir::separator())) { + continue; + } + + return token; + } + } + + return {}; +} + +QString FashionLeftPluginProvider::locateDesktopFileByExecutable(const QString &executablePath) +{ + if (executablePath.isEmpty()) { + return {}; + } + + const QFileInfo executableInfo(executablePath); + const QString canonicalExecutablePath = executableInfo.canonicalFilePath(); + const QString normalizedExecutablePath = canonicalExecutablePath.isEmpty() + ? executableInfo.absoluteFilePath() + : canonicalExecutablePath; + const QString executableName = QFileInfo(normalizedExecutablePath).fileName(); + if (executableName.isEmpty()) { + return {}; + } + + { + QMutexLocker locker(&desktopFileByExecutableCacheMutex()); + const auto cachedResult = desktopFileByExecutableCache().constFind(normalizedExecutablePath); + if (cachedResult != desktopFileByExecutableCache().cend()) { + return cachedResult.value(); + } + } + + QString matchedDesktopFilePath; + + for (const QString &directory : desktopSearchDirectories()) { + QDirIterator iterator(directory, + {QStringLiteral("*.desktop")}, + QDir::Files | QDir::Readable, + QDirIterator::Subdirectories); + while (iterator.hasNext()) { + const QString desktopFilePath = iterator.next(); + const QString desktopExecutable = desktopCommandExecutable(desktopFilePath); + if (desktopExecutable.isEmpty()) { + continue; + } + + const QFileInfo desktopExecutableInfo(desktopExecutable); + const QString canonicalDesktopExecutable = desktopExecutableInfo.canonicalFilePath(); + const QString normalizedDesktopExecutable = canonicalDesktopExecutable.isEmpty() + ? desktopExecutableInfo.absoluteFilePath() + : canonicalDesktopExecutable; + if ((!normalizedDesktopExecutable.isEmpty() && normalizedDesktopExecutable == normalizedExecutablePath) + || desktopExecutableInfo.fileName() == executableName) { + matchedDesktopFilePath = desktopFilePath; + break; + } + } + + if (!matchedDesktopFilePath.isEmpty()) { + break; + } + } + + { + QMutexLocker locker(&desktopFileByExecutableCacheMutex()); + desktopFileByExecutableCache().insert(normalizedExecutablePath, matchedDesktopFilePath); + } + return matchedDesktopFilePath; +} + +bool FashionLeftPluginProvider::launchDesktopEntry(const QString &desktopFilePath) +{ + if (desktopFilePath.isEmpty()) { + return false; + } + + if (launchCommand(QStringLiteral("gio"), {QStringLiteral("launch"), desktopFilePath})) { + return true; + } + + const QString desktopId = QFileInfo(desktopFilePath).completeBaseName(); + if (!desktopId.isEmpty() && launchCommand(QStringLiteral("gtk-launch"), {desktopId})) { + return true; + } + + const QString desktopExecutable = desktopCommandExecutable(desktopFilePath); + if (!desktopExecutable.isEmpty() && launchCommand(desktopExecutable)) { + return true; + } + + return false; +} + +QString FashionLeftPluginProvider::localizedDesktopEntryValue(const QString &desktopFilePath, const QString &key) +{ + if (desktopFilePath.isEmpty() || key.isEmpty()) { + return {}; + } + + QSettings settings(desktopFilePath, QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + + QStringList localizedKeys; + const QStringList uiLanguages = QLocale::system().uiLanguages(); + for (const QString &uiLanguage : uiLanguages) { + QString normalizedLanguage = uiLanguage.trimmed(); + if (normalizedLanguage.isEmpty()) { + continue; + } + + normalizedLanguage.replace(QLatin1Char('-'), QLatin1Char('_')); + const QString fullKey = QStringLiteral("%1[%2]").arg(key, normalizedLanguage); + if (!localizedKeys.contains(fullKey)) { + localizedKeys << fullKey; + } + + const int separatorIndex = normalizedLanguage.indexOf(QLatin1Char('_')); + if (separatorIndex > 0) { + const QString baseLanguageKey = QStringLiteral("%1[%2]").arg(key, normalizedLanguage.left(separatorIndex)); + if (!localizedKeys.contains(baseLanguageKey)) { + localizedKeys << baseLanguageKey; + } + } + } + + for (const QString &localizedKey : localizedKeys) { + const QString localizedValue = settings.value(localizedKey).toString().trimmed(); + if (!localizedValue.isEmpty()) { + return localizedValue; + } + } + + return settings.value(key).toString().trimmed(); +} + +QString FashionLeftPluginProvider::musicPlayerIconNameForDesktopEntry(const QString &desktopId, + const QString &appName, + const QString &serviceName) +{ + QStringList candidateIconNames; + const auto appendCandidate = [&candidateIconNames](const QString &iconName) { + const QString trimmedIconName = iconName.trimmed(); + if (!trimmedIconName.isEmpty() && !candidateIconNames.contains(trimmedIconName)) { + candidateIconNames << trimmedIconName; + } + }; + const auto appendMusicAppCandidates = [&appendCandidate](const QString &name) { + const QString normalizedName = name.trimmed().toLower(); + if (normalizedName.isEmpty()) { + return; + } + + if (normalizedName.contains(QStringLiteral("网易云")) + || normalizedName.contains(QStringLiteral("网易")) + || normalizedName.contains(QStringLiteral("云音乐")) + || normalizedName.contains(QStringLiteral("netease")) + || normalizedName.contains(QStringLiteral("cloudmusic"))) { + appendCandidate(QStringLiteral("netease-cloud-music")); + appendCandidate(QStringLiteral("com.netease.cloudmusic")); + } + + if (normalizedName.contains(QStringLiteral("deepin music")) + || normalizedName.contains(QStringLiteral("deepin-music")) + || normalizedName.contains(QStringLiteral("com.deepin.music")) + || normalizedName.contains(QStringLiteral("deepin音乐")) + || normalizedName == QStringLiteral("音乐")) { + appendCandidate(QStringLiteral("deepin-music")); + appendCandidate(QStringLiteral("deepin-music-player")); + } + }; + + appendMusicAppCandidates(appName); + + const QString normalizedDesktopId = desktopId.trimmed().toLower(); + if (normalizedDesktopId.contains(QStringLiteral("netease")) + || desktopId.contains(QStringLiteral("网易云"))) { + appendCandidate(QStringLiteral("netease-cloud-music")); + appendCandidate(QStringLiteral("com.netease.cloudmusic")); + } + if (normalizedDesktopId.contains(QStringLiteral("deepin-music")) + || normalizedDesktopId.contains(QStringLiteral("com.deepin.music")) + || desktopId.contains(QStringLiteral("deepin-music")) + || desktopId.contains(QStringLiteral("Deepin Music"))) { + appendCandidate(QStringLiteral("deepin-music")); + appendCandidate(QStringLiteral("deepin-music-player")); + } + + const QString desktopFilePath = locateDesktopFile(desktopId); + const QString iconName = desktopEntryValue(desktopFilePath, QStringLiteral("Icon")); + appendCandidate(iconName); + + QString fallbackIconName = desktopId; + if (fallbackIconName.endsWith(QStringLiteral(".desktop"))) { + fallbackIconName.chop(QStringLiteral(".desktop").size()); + } + appendCandidate(fallbackIconName); + + for (const QString &candidateIconName : std::as_const(candidateIconNames)) { + if (!iconPathForName(candidateIconName).isEmpty()) { + return candidateIconName; + } + } + + if (!candidateIconNames.isEmpty()) { + return candidateIconNames.constFirst(); + } + + if (isBrowserDesktopId(desktopId) || isBrowserServiceName(serviceName) || desktopId.isEmpty()) { + return DefaultMusicIconName; + } + + return fallbackIconName.isEmpty() ? DefaultMusicIconName : fallbackIconName; +} + +QUrl FashionLeftPluginProvider::iconSourceForName(const QString &iconName) +{ + const QString iconPath = iconPathForName(iconName); + if (!iconPath.isEmpty()) { + return QUrl::fromLocalFile(iconPath); + } + + return QUrl::fromLocalFile(iconPathForName(DefaultMusicIconName)); +} + +QStringList FashionLeftPluginProvider::mailAccountIdsFromJson(const QString &jsonText) +{ + const QJsonDocument document = QJsonDocument::fromJson(jsonText.toUtf8()); + if (!document.isObject()) { + return {}; + } + + QStringList accountIds; + const QJsonArray accounts = document.object().value(QStringLiteral("accounts")).toArray(); + for (const QJsonValue &accountValue : accounts) { + const QString accountId = accountValue.toObject().value(QStringLiteral("id")).toString().trimmed(); + if (!accountId.isEmpty()) { + accountIds << accountId; + } + } + + return accountIds; +} + +int FashionLeftPluginProvider::unreadCountFromJson(const QString &jsonText, bool *ok) +{ + const QJsonDocument document = QJsonDocument::fromJson(jsonText.toUtf8()); + if (!document.isObject()) { + if (ok) { + *ok = false; + } + return 0; + } + + const QJsonValue countValue = document.object().value(QStringLiteral("count")); + if (!countValue.isDouble()) { + if (ok) { + *ok = false; + } + return 0; + } + + if (ok) { + *ok = true; + } + return countValue.toInt(); +} + +bool FashionLeftPluginProvider::readCpuTimes(quint64 *totalTime, quint64 *idleTime) +{ + if (!totalTime || !idleTime) { + return false; + } + + QFile file(QStringLiteral("/proc/stat")); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + + const QString line = QString::fromUtf8(file.readLine()).simplified(); + const QStringList fields = line.split(QLatin1Char(' '), Qt::SkipEmptyParts); + if (fields.size() < 5 || fields.first() != QStringLiteral("cpu")) { + return false; + } + + quint64 total = 0; + for (qsizetype index = 1; index < fields.size(); ++index) { + total += fields.at(index).toULongLong(); + } + + const quint64 idle = fields.value(4).toULongLong() + fields.value(5).toULongLong(); + *totalTime = total; + *idleTime = idle; + return true; +} + +int FashionLeftPluginProvider::systemMemoryUsagePercent() +{ + QFile file(QStringLiteral("/proc/meminfo")); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return 0; + } + + quint64 totalMemory = 0; + quint64 availableMemory = 0; + while (!file.atEnd()) { + const QString line = QString::fromUtf8(file.readLine()); + if (line.startsWith(QStringLiteral("MemTotal:"))) { + totalMemory = line.section(QLatin1Char(':'), 1).simplified().section(QLatin1Char(' '), 0, 0).toULongLong(); + } else if (line.startsWith(QStringLiteral("MemAvailable:"))) { + availableMemory = line.section(QLatin1Char(':'), 1).simplified().section(QLatin1Char(' '), 0, 0).toULongLong(); + } + + if (totalMemory > 0 && availableMemory > 0) { + break; + } + } + + if (totalMemory == 0) { + return 0; + } + + const quint64 usedMemory = totalMemory > availableMemory ? (totalMemory - availableMemory) : 0; + return qBound(0, qRound(static_cast(usedMemory) * 100.0 / totalMemory), 100); +} + +QStringList FashionLeftPluginProvider::preferredNetworkInterfaces() +{ + const QList allInterfaces = QNetworkInterface::allInterfaces(); + QHash interfacesByName; + QStringList physicalInterfaceNames; + QStringList runningInterfaceNames; + + for (const QNetworkInterface &networkInterface : allInterfaces) { + const QString interfaceName = networkInterface.name().trimmed(); + if (!interfaceName.isEmpty()) { + interfacesByName.insert(interfaceName, networkInterface); + } + + if (!isUpAndRunningInterface(networkInterface)) { + continue; + } + + if (!runningInterfaceNames.contains(interfaceName)) { + runningInterfaceNames << interfaceName; + } + + if (isLikelyPhysicalTrafficInterface(networkInterface) + && !physicalInterfaceNames.contains(interfaceName)) { + physicalInterfaceNames << interfaceName; + } + } + + QStringList defaultPhysicalInterfaceNames; + QStringList defaultRunningInterfaceNames; + const QStringList routeInterfaceNames = defaultRouteInterfaceNames(); + for (const QString &interfaceName : routeInterfaceNames) { + const auto interfaceIterator = interfacesByName.constFind(interfaceName); + if (interfaceIterator == interfacesByName.cend()) { + continue; + } + + const QNetworkInterface networkInterface = interfaceIterator.value(); + if (!isUpAndRunningInterface(networkInterface)) { + continue; + } + + if (!defaultRunningInterfaceNames.contains(interfaceName)) { + defaultRunningInterfaceNames << interfaceName; + } + + if (isLikelyPhysicalTrafficInterface(networkInterface) + && !defaultPhysicalInterfaceNames.contains(interfaceName)) { + defaultPhysicalInterfaceNames << interfaceName; + } + } + + if (!defaultPhysicalInterfaceNames.isEmpty()) { + return defaultPhysicalInterfaceNames; + } + + if (!defaultRunningInterfaceNames.isEmpty()) { + return defaultRunningInterfaceNames; + } + + if (!physicalInterfaceNames.isEmpty()) { + return physicalInterfaceNames; + } + + if (!runningInterfaceNames.isEmpty()) { + return runningInterfaceNames; + } + + return routeInterfaceNames; +} + +void FashionLeftPluginProvider::refreshMailClient() +{ + const QString nextMailDesktopId = commandOutput(QStringLiteral("xdg-mime"), + {QStringLiteral("query"), + QStringLiteral("default"), + QStringLiteral("x-scheme-handler/mailto")}); + const QString nextMailDesktopFilePath = locateDesktopFile(nextMailDesktopId); + QString nextMailIconName = desktopEntryValue(nextMailDesktopFilePath, QStringLiteral("Icon")); + QString nextMailClientName = localizedDesktopEntryValue(nextMailDesktopFilePath, QStringLiteral("Name")); + if (nextMailIconName.isEmpty()) { + nextMailIconName = DefaultMailIconName; + } + if (nextMailClientName.isEmpty()) { + nextMailClientName = QStringLiteral("邮箱"); + } + + if (m_mailDesktopId == nextMailDesktopId + && m_mailDesktopFilePath == nextMailDesktopFilePath + && m_mailIconName == nextMailIconName + && m_mailClientName == nextMailClientName) { + return; + } + + m_mailDesktopId = nextMailDesktopId; + m_mailDesktopFilePath = nextMailDesktopFilePath; + m_mailIconName = nextMailIconName; + m_mailClientName = nextMailClientName; + emit mailClientChanged(); +} + +QString FashionLeftPluginProvider::weatherConfigPath() +{ + return QDir::home().filePath(QStringLiteral(".config/deepin/org.deepin.weather.conf")); +} + +QString FashionLeftPluginProvider::weatherIconPathFor(const QString &iconName, bool isDay) +{ + const QString mappedPath = weatherAssetPath(weatherAssetNameFor(iconName, isDay)); + if (!mappedPath.isEmpty()) { + return mappedPath; + } + + return WeatherAppIconPath; +} + +QString FashionLeftPluginProvider::formatTransferRate(double bytesPerSecond) +{ + const double kilobytes = 1024.0; + const double megabytes = kilobytes * 1024.0; + + if (bytesPerSecond >= megabytes) { + return QString::number(bytesPerSecond / megabytes, 'f', 1) + QStringLiteral("mb/s"); + } + + if (bytesPerSecond >= kilobytes) { + return QString::number(bytesPerSecond / kilobytes, 'f', 1) + QStringLiteral("kb/s"); + } + + return QString::number(qRound(bytesPerSecond)) + QStringLiteral("b/s"); +} + +quint64 FashionLeftPluginProvider::totalInterfaceBytes(bool receiveBytes, const QStringList &preferredInterfaces) +{ + QFile file(QStringLiteral("/proc/net/dev")); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return 0; + } + + const QList lines = file.readAll().split('\n'); + quint64 totalBytes = 0; + for (const QByteArray &rawLine : lines) { + const QString line = QString::fromUtf8(rawLine).trimmed(); + if (!line.contains(QLatin1Char(':'))) { + continue; + } + + const QStringList nameAndValues = line.split(QLatin1Char(':'), Qt::KeepEmptyParts); + if (nameAndValues.size() != 2) { + continue; + } + + const QString interfaceName = nameAndValues.at(0).trimmed(); + if (interfaceName == QStringLiteral("lo")) { + continue; + } + + if (!preferredInterfaces.isEmpty() && !preferredInterfaces.contains(interfaceName)) { + continue; + } + + const QStringList values = nameAndValues.at(1).simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts); + if (values.size() < 16) { + continue; + } + + totalBytes += receiveBytes ? values.at(0).toULongLong() : values.at(8).toULongLong(); + } + + return totalBytes; +} + +void FashionLeftPluginProvider::ensureWeatherWatchPaths() +{ + if (!m_weatherWatcher) { + return; + } + + const QString configPath = weatherConfigPath(); + const QString configDirectory = QFileInfo(configPath).absolutePath(); + + if (!m_weatherWatcher->directories().contains(configDirectory)) { + m_weatherWatcher->addPath(configDirectory); + } + + if (QFileInfo::exists(configPath) && !m_weatherWatcher->files().contains(configPath)) { + m_weatherWatcher->addPath(configPath); + } +} + +} // namespace dock diff --git a/panels/dock/fashionleftpluginprovider.h b/panels/dock/fashionleftpluginprovider.h new file mode 100644 index 000000000..640a1f647 --- /dev/null +++ b/panels/dock/fashionleftpluginprovider.h @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dock { + +class FashionLeftPluginProvider : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString timeText READ timeText NOTIFY clockChanged FINAL) + Q_PROPERTY(QString dateText READ dateText NOTIFY clockChanged FINAL) + Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged FINAL) + Q_PROPERTY(QString notificationCountText READ notificationCountText NOTIFY notificationCountChanged FINAL) + Q_PROPERTY(int mailUnreadCount READ mailUnreadCount NOTIFY mailStateChanged FINAL) + Q_PROPERTY(QString mailUnreadCountText READ mailUnreadCountText NOTIFY mailStateChanged FINAL) + Q_PROPERTY(QString mailSummaryText READ mailSummaryText NOTIFY mailStateChanged FINAL) + Q_PROPERTY(bool mailConfigured READ mailConfigured NOTIFY mailStateChanged FINAL) + Q_PROPERTY(QString mailIconName READ mailIconName NOTIFY mailClientChanged FINAL) + Q_PROPERTY(QString mailClientName READ mailClientName NOTIFY mailClientChanged FINAL) + Q_PROPERTY(bool musicAvailable READ musicAvailable NOTIFY musicStateChanged FINAL) + Q_PROPERTY(QString musicTitleText READ musicTitleText NOTIFY musicStateChanged FINAL) + Q_PROPERTY(QString musicSubtitleText READ musicSubtitleText NOTIFY musicStateChanged FINAL) + Q_PROPERTY(QString musicAppName READ musicAppName NOTIFY musicStateChanged FINAL) + Q_PROPERTY(QUrl musicArtSource READ musicArtSource NOTIFY musicStateChanged FINAL) + Q_PROPERTY(QString musicPlayerIconName READ musicPlayerIconName NOTIFY musicStateChanged FINAL) + Q_PROPERTY(QUrl musicPlayerIconSource READ musicPlayerIconSource NOTIFY musicStateChanged FINAL) + Q_PROPERTY(bool musicHasArt READ musicHasArt NOTIFY musicStateChanged FINAL) + Q_PROPERTY(bool musicPlaying READ musicPlaying NOTIFY musicStateChanged FINAL) + Q_PROPERTY(bool musicCanGoPrevious READ musicCanGoPrevious NOTIFY musicStateChanged FINAL) + Q_PROPERTY(bool musicCanGoNext READ musicCanGoNext NOTIFY musicStateChanged FINAL) + Q_PROPERTY(bool musicCanTogglePlayback READ musicCanTogglePlayback NOTIFY musicStateChanged FINAL) + Q_PROPERTY(int cpuUsage READ cpuUsage NOTIFY systemStatsChanged FINAL) + Q_PROPERTY(int memoryUsage READ memoryUsage NOTIFY systemStatsChanged FINAL) + Q_PROPERTY(QString downloadSpeedText READ downloadSpeedText NOTIFY systemStatsChanged FINAL) + Q_PROPERTY(QString uploadSpeedText READ uploadSpeedText NOTIFY systemStatsChanged FINAL) + Q_PROPERTY(int aiRunningCount READ aiRunningCount NOTIFY aiStateChanged FINAL) + Q_PROPERTY(QString aiRunningCountText READ aiRunningCountText NOTIFY aiStateChanged FINAL) + Q_PROPERTY(QString aiHeadlineText READ aiHeadlineText NOTIFY aiStateChanged FINAL) + Q_PROPERTY(QString aiSummaryText READ aiSummaryText NOTIFY aiStateChanged FINAL) + Q_PROPERTY(QVariantList aiToolEntries READ aiToolEntries NOTIFY aiStateChanged FINAL) + Q_PROPERTY(QString aiPrimaryToolId READ aiPrimaryToolId NOTIFY aiStateChanged FINAL) + Q_PROPERTY(QString weatherCityText READ weatherCityText NOTIFY weatherChanged FINAL) + Q_PROPERTY(QString weatherTemperatureText READ weatherTemperatureText NOTIFY weatherChanged FINAL) + Q_PROPERTY(QString weatherSummaryText READ weatherSummaryText NOTIFY weatherChanged FINAL) + Q_PROPERTY(QUrl weatherIconSource READ weatherIconSource NOTIFY weatherChanged FINAL) + Q_PROPERTY(QUrl messageIconSource READ messageIconSource CONSTANT FINAL) + QML_NAMED_ELEMENT(FashionLeftPluginProvider) + +public: + explicit FashionLeftPluginProvider(QObject *parent = nullptr); + + QString timeText() const; + QString dateText() const; + + int notificationCount() const; + QString notificationCountText() const; + int mailUnreadCount() const; + QString mailUnreadCountText() const; + QString mailSummaryText() const; + bool mailConfigured() const; + QString mailIconName() const; + QString mailClientName() const; + bool musicAvailable() const; + QString musicTitleText() const; + QString musicSubtitleText() const; + QString musicAppName() const; + QUrl musicArtSource() const; + QString musicPlayerIconName() const; + QUrl musicPlayerIconSource() const; + bool musicHasArt() const; + bool musicPlaying() const; + bool musicCanGoPrevious() const; + bool musicCanGoNext() const; + bool musicCanTogglePlayback() const; + + int cpuUsage() const; + int memoryUsage() const; + QString downloadSpeedText() const; + QString uploadSpeedText() const; + int aiRunningCount() const; + QString aiRunningCountText() const; + QString aiHeadlineText() const; + QString aiSummaryText() const; + QVariantList aiToolEntries() const; + QString aiPrimaryToolId() const; + + QString weatherCityText() const; + QString weatherTemperatureText() const; + QString weatherSummaryText() const; + QUrl weatherIconSource() const; + QUrl messageIconSource() const; + + Q_INVOKABLE void openWeatherPage(); + Q_INVOKABLE void openWeatherPopup(int taskbarLeft, int taskbarTop, int activationX, int activationY); + Q_INVOKABLE void openMailClient(); + Q_INVOKABLE void openMusicPlayer(); + Q_INVOKABLE QString musicControlThemeIconSource(const QString &iconName, bool darkTheme) const; + Q_INVOKABLE void playPreviousTrack(); + Q_INVOKABLE void toggleMusicPlayback(); + Q_INVOKABLE void playNextTrack(); + Q_INVOKABLE void openAiClientHost(); + Q_INVOKABLE void openNotificationPage(); + Q_INVOKABLE void openSystemMonitorPage(); + +signals: + void clockChanged(); + void notificationCountChanged(); + void mailStateChanged(); + void mailClientChanged(); + void musicStateChanged(); + void systemStatsChanged(); + void aiStateChanged(); + void weatherChanged(); + +private slots: + void refreshClock(); + void refreshNotificationCount(); + void refreshMailState(); + void refreshMusicState(); + void refreshSystemStats(); + void refreshAiState(); + void refreshWeather(); + void onNotificationCountChanged(uint count); + +private: + static bool launchCommand(const QString &program, const QStringList &arguments = {}); + static bool showControlCenterPage(const QString &pagePath); + static QString commandOutput(const QString &program, const QStringList &arguments, int timeoutMs = 1500); + static QString executablePathForService(const QString &serviceName); + static QStringList desktopSearchDirectories(); + static QString locateDesktopFile(const QString &desktopId); + static QString locateDesktopFileByExecutable(const QString &executablePath); + static QString desktopEntryValue(const QString &desktopFilePath, const QString &key); + static QString desktopCommandExecutable(const QString &desktopFilePath); + static bool launchDesktopEntry(const QString &desktopFilePath); + static QString localizedDesktopEntryValue(const QString &desktopFilePath, const QString &key); + static QString musicPlayerIconNameForDesktopEntry(const QString &desktopId, + const QString &appName, + const QString &serviceName); + static QUrl iconSourceForName(const QString &iconName); + static QStringList mailAccountIdsFromJson(const QString &jsonText); + static int unreadCountFromJson(const QString &jsonText, bool *ok = nullptr); + static bool readCpuTimes(quint64 *totalTime, quint64 *idleTime); + static int systemMemoryUsagePercent(); + static QStringList preferredNetworkInterfaces(); + static QString weatherConfigPath(); + static QString weatherIconPathFor(const QString &iconName, bool isDay); + static QString formatTransferRate(double bytesPerSecond); + static quint64 totalInterfaceBytes(bool receiveBytes, const QStringList &preferredInterfaces = {}); + void applyAiRefreshResult(const QVariantMap &result); + void ensureWeatherWatchPaths(); + void refreshMailClient(); + + QString m_timeText; + QString m_dateText; + int m_notificationCount = 0; + int m_mailUnreadCount = 0; + QString m_mailSummaryText = QStringLiteral("邮箱信息不可用"); + bool m_mailConfigured = false; + QString m_mailDesktopId; + QString m_mailDesktopFilePath; + QString m_mailIconName = QStringLiteral("deepin-mail"); + QString m_mailClientName = QStringLiteral("邮箱"); + QString m_musicService; + QString m_musicDesktopEntry; + QString m_musicExecutablePath; + QString m_musicTitleText = QStringLiteral("未检测到音乐"); + QString m_musicSubtitleText = QStringLiteral("打开播放器开始播放"); + QString m_musicAppName = QStringLiteral("音乐"); + QUrl m_musicArtSource; + QString m_musicPlayerIconName = QStringLiteral("audio-x-generic"); + QUrl m_musicPlayerIconSource; + bool m_musicAvailable = false; + bool m_musicPlaying = false; + bool m_musicCanRaise = false; + bool m_musicCanGoPrevious = false; + bool m_musicCanGoNext = false; + bool m_musicCanTogglePlayback = false; + int m_cpuUsage = 0; + int m_memoryUsage = 0; + QString m_downloadSpeedText = QStringLiteral("0kb/s"); + QString m_uploadSpeedText = QStringLiteral("0kb/s"); + int m_aiRunningCount = 0; + QString m_aiHeadlineText = QStringLiteral("AI 后台任务"); + QString m_aiSummaryText = QStringLiteral("当前空闲"); + QVariantList m_aiToolEntries; + QString m_aiPrimaryToolId; + QString m_aiLastPrimaryToolId; + qint64 m_aiPrimaryHostPid = 0; + QString m_aiPrimaryHostDesktopFilePath; + QString m_aiPrimaryHostExecutablePath; + QString m_aiPrimaryHostAppName; + QHash m_aiObservedSessionStates; + QHash m_aiLastSeenPidByTool; + bool m_aiStateInitialized = false; + QString m_weatherCityText; + QString m_weatherTemperatureText = QStringLiteral("--°"); + QString m_weatherSummaryText = QStringLiteral("天气信息不可用"); + QUrl m_weatherIconSource; + quint64 m_previousCpuTotalTime = 0; + quint64 m_previousCpuIdleTime = 0; + quint64 m_previousReceiveBytes = 0; + quint64 m_previousTransmitBytes = 0; + quint64 m_previousAggregateReceiveBytes = 0; + quint64 m_previousAggregateTransmitBytes = 0; + QElapsedTimer m_networkSampleTimer; + QFileSystemWatcher *m_weatherWatcher = nullptr; + QFutureWatcher *m_aiRefreshWatcher = nullptr; + bool m_aiRefreshPending = false; +}; + +} // namespace dock diff --git a/panels/dock/loadtrayplugins.cpp b/panels/dock/loadtrayplugins.cpp index 6672bcb0d..5a8a1e636 100644 --- a/panels/dock/loadtrayplugins.cpp +++ b/panels/dock/loadtrayplugins.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 @@ -10,11 +10,33 @@ #include #include +#include #include #include namespace dock { +namespace { + +QString trayLoaderFontSyncPath() +{ + const QStringList candidates = { + qEnvironmentVariable("TRAY_LOADER_FONT_SYNC_PATH"), + QString::fromLatin1(TRAY_LOADER_FONT_SYNC_BUILD_PATH), + QString::fromLatin1(TRAY_LOADER_FONT_SYNC_INSTALL_PATH) + }; + + for (const QString &candidate : candidates) { + if (!candidate.isEmpty() && QFileInfo::exists(candidate)) { + return candidate; + } + } + + return QString(); +} + +} // namespace + LoadTrayPlugins::LoadTrayPlugins(QObject *parent) : QObject(parent) { @@ -98,6 +120,15 @@ void LoadTrayPlugins::setProcessEnv(QProcess *process) // TODO: use protocols to determine the environment instead of environment variables env.remove("DDE_CURRENT_COMPOSITOR"); + const QString fontSyncPath = trayLoaderFontSyncPath(); + if (!fontSyncPath.isEmpty()) { + QStringList preloadEntries = env.value(QStringLiteral("LD_PRELOAD")).split(QLatin1Char(':'), Qt::SkipEmptyParts); + if (!preloadEntries.contains(fontSyncPath)) { + preloadEntries.prepend(fontSyncPath); + } + env.insert(QStringLiteral("LD_PRELOAD"), preloadEntries.join(QLatin1Char(':'))); + } + process->setProcessEnvironment(env); } @@ -105,7 +136,8 @@ QString LoadTrayPlugins::loaderPath() const { QStringList execPaths; execPaths << qEnvironmentVariable("TRAY_LOADER_EXECUTE_PATH") - << QString("%1/trayplugin-loader").arg(CMAKE_INSTALL_FULL_LIBEXECDIR); + << QString("%1/trayplugin-loader").arg(CMAKE_INSTALL_FULL_LIBEXECDIR) + << QStringLiteral("/usr/libexec/trayplugin-loader"); QString validExePath; for (const QString &execPath : execPaths) { diff --git a/panels/dock/multitaskview/package/multitaskview.qml b/panels/dock/multitaskview/package/multitaskview.qml index cbd081f22..4cd88db44 100644 --- a/panels/dock/multitaskview/package/multitaskview.qml +++ b/panels/dock/multitaskview/package/multitaskview.qml @@ -20,6 +20,12 @@ AppletDockItem { toolTipY: DockPanelPositioner.y } + function showToolTipNow() { + var point = toggleworkspace.mapToItem(null, toggleworkspace.width / 2, toggleworkspace.height / 2) + toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) + toolTip.open() + } + D.DciIcon { id: icon anchors.centerIn: parent @@ -32,11 +38,7 @@ AppletDockItem { Timer { id: toolTipShowTimer interval: 50 - onTriggered: { - var point = toggleworkspace.mapToItem(null, toggleworkspace.width / 2, toggleworkspace.height / 2) - toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) - toolTip.open() - } + onTriggered: toggleworkspace.showToolTipNow() } MouseArea { @@ -52,7 +54,11 @@ AppletDockItem { HoverHandler { onHoveredChanged: { if (hovered) { - toolTipShowTimer.start() + if (toolTip.toolTipWindow && toolTip.toolTipWindow.visible) { + toggleworkspace.showToolTipNow() + } else { + toolTipShowTimer.start() + } } else { if (toolTipShowTimer.running) { toolTipShowTimer.stop() diff --git a/panels/dock/package/DockPartAppletModel.qml b/panels/dock/package/DockPartAppletModel.qml index 206781270..d230fbbf3 100644 --- a/panels/dock/package/DockPartAppletModel.qml +++ b/panels/dock/package/DockPartAppletModel.qml @@ -1,10 +1,11 @@ -// 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 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 D.SortFilterModel { @@ -12,19 +13,123 @@ D.SortFilterModel { property int leftDockOrder: 0 property int rightDockOrder: 0 + property var acceptItem: null + property var sortOrderProvider: null model: Applet.appletItems filterAcceptsItem: function(item) { + if (acceptItem) { + return acceptItem(item) + } return item.data.dockOrder > leftDockOrder && item.data.dockOrder <= rightDockOrder && (item.data.shouldVisible === undefined || item.data.shouldVisible) } lessThan: function(leftItem, rightItem) { - return parseInt(leftItem.data.dockOrder) <= parseInt(rightItem.data.dockOrder) + const leftOrder = sortOrderProvider ? sortOrderProvider(leftItem) : parseInt(leftItem.data.dockOrder) + const rightOrder = sortOrderProvider ? sortOrderProvider(rightItem) : parseInt(rightItem.data.dockOrder) + if (leftOrder !== rightOrder) { + return leftOrder < rightOrder + } + + const leftDockOrder = parseInt(leftItem.data.dockOrder) + const rightDockOrder = parseInt(rightItem.data.dockOrder) + if (leftDockOrder !== rightDockOrder) { + return leftDockOrder < rightDockOrder + } + + const leftPluginId = leftItem.data && leftItem.data.applet ? leftItem.data.applet.pluginId : "" + const rightPluginId = rightItem.data && rightItem.data.applet ? rightItem.data.applet.pluginId : "" + return leftPluginId < rightPluginId } delegate: Control { - contentItem: model.data - Component.onCompleted: { - contentItem.parent = this + id: delegateRoot + + property var appletItem: model.data + property var attachedAppletItem: null + readonly property string pluginId: appletItem && appletItem.applet ? appletItem.applet.pluginId : "" + readonly property bool useUnifiedDockHoverBackground: [ + "org.deepin.ds.dock.launcherapplet", + "org.deepin.ds.dock.aibar", + "org.deepin.ds.dock.searchitem", + "org.deepin.ds.dock.multitaskview" + ].indexOf(pluginId) >= 0 + readonly property int unifiedHoverBackgroundSize: Math.round((Panel.rootObject ? Panel.rootObject.dockItemMaxSize * 9 / 14 : 0) + 8) + implicitWidth: appletItem ? appletItem.implicitWidth : 0 + implicitHeight: appletItem ? appletItem.implicitHeight : 0 + + contentItem: appletItem + background: AppletItemBackground { + anchors.centerIn: parent + width: delegateRoot.unifiedHoverBackgroundSize + height: delegateRoot.unifiedHoverBackgroundSize + radius: height / 5 + enabled: false + visible: delegateRoot.useUnifiedDockHoverBackground + opacity: delegateHoverHandler.hovered ? 1 : 0 + D.ColorSelector.hovered: delegateHoverHandler.hovered + + Behavior on opacity { + NumberAnimation { duration: 150 } + } + } + + HoverHandler { + id: delegateHoverHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + enabled: delegateRoot.useUnifiedDockHoverBackground && delegateRoot.visible + } + + Timer { + id: syncAppletItemTimer + interval: 0 + repeat: false + onTriggered: delegateRoot.syncAppletItem() + } + + function scheduleSyncAppletItem() { + if (appletItem) { + syncAppletItemTimer.restart() + } + } + + function syncAppletItem() { + if (attachedAppletItem && attachedAppletItem !== appletItem && attachedAppletItem.parent === delegateRoot) { + attachedAppletItem.parent = null + } + + if (appletItem) { + appletItem.parent = delegateRoot + attachedAppletItem = appletItem + } else { + attachedAppletItem = null + } + } + + onAppletItemChanged: { + syncAppletItem() + scheduleSyncAppletItem() + } + onVisibleChanged: scheduleSyncAppletItem() + onWindowChanged: scheduleSyncAppletItem() + onParentChanged: scheduleSyncAppletItem() + + Component.onCompleted: syncAppletItem() + + Component.onDestruction: { + if (attachedAppletItem && attachedAppletItem.parent === delegateRoot) { + attachedAppletItem.parent = null + } + attachedAppletItem = null + } + + Connections { + target: Panel + + function onHideStateChanged() { + if (Panel.hideState !== Dock.Hide) { + delegateRoot.scheduleSyncAppletItem() + } + } } } } diff --git a/panels/dock/package/FashionBackgroundInnerBorder.qml b/panels/dock/package/FashionBackgroundInnerBorder.qml new file mode 100644 index 000000000..abc617462 --- /dev/null +++ b/panels/dock/package/FashionBackgroundInnerBorder.qml @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 + +Item { + id: root + + property real cornerRadius: 0 + property real devicePixelRatio: Screen.devicePixelRatio > 0 ? Screen.devicePixelRatio : 1.0 + property color topColor: Qt.rgba(1, 1, 1, 0.3) + property color bottomColor: Qt.rgba(1, 1, 1, 0.1) + readonly property real borderWidth: 1 / Math.max(1, devicePixelRatio) + + enabled: false + + function clampRadius(radius, width, height) { + return Math.max(0, Math.min(radius, Math.min(width, height) / 2)) + } + + function roundedRectPath(ctx, x, y, width, height, radius) { + if (radius <= 0) { + ctx.rect(x, y, width, height) + return + } + + const right = x + width + const bottom = y + height + ctx.moveTo(x + radius, y) + ctx.lineTo(right - radius, y) + ctx.arcTo(right, y, right, y + radius, radius) + ctx.lineTo(right, bottom - radius) + ctx.arcTo(right, bottom, right - radius, bottom, radius) + ctx.lineTo(x + radius, bottom) + ctx.arcTo(x, bottom, x, bottom - radius, radius) + ctx.lineTo(x, y + radius) + ctx.arcTo(x, y, x + radius, y, radius) + } + + Canvas { + id: borderCanvas + anchors.fill: parent + antialiasing: true + + onPaint: { + const ctx = getContext("2d") + ctx.clearRect(0, 0, width, height) + + if (width <= 0 || height <= 0 || root.borderWidth <= 0) { + return + } + + const inset = root.borderWidth / 2 + const drawWidth = Math.max(0, width - root.borderWidth) + const drawHeight = Math.max(0, height - root.borderWidth) + const radius = root.clampRadius(root.cornerRadius - inset, drawWidth, drawHeight) + + const gradient = ctx.createLinearGradient(0, 0, 0, height) + gradient.addColorStop(0, root.topColor) + gradient.addColorStop(1, root.bottomColor) + + ctx.strokeStyle = gradient + ctx.lineWidth = root.borderWidth + ctx.beginPath() + root.roundedRectPath(ctx, inset, inset, drawWidth, drawHeight, radius) + ctx.stroke() + } + + Connections { + target: root + + function onCornerRadiusChanged() { + borderCanvas.requestPaint() + } + + function onDevicePixelRatioChanged() { + borderCanvas.requestPaint() + } + + function onTopColorChanged() { + borderCanvas.requestPaint() + } + + function onBottomColorChanged() { + borderCanvas.requestPaint() + } + } + + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + Component.onCompleted: requestPaint() + } +} diff --git a/panels/dock/package/FashionLeftDockArea.qml b/panels/dock/package/FashionLeftDockArea.qml new file mode 100644 index 000000000..d29015c2c --- /dev/null +++ b/panels/dock/package/FashionLeftDockArea.qml @@ -0,0 +1,1762 @@ +// 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.Layouts 2.15 +import QtQuick.Window 2.15 +import Qt5Compat.GraphicalEffects + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +Control { + id: root + + readonly property int dockSize: Panel.rootObject.dockSize + readonly property int hoverInset: 3 + readonly property real taskbarRadius: Panel.rootObject && Panel.rootObject.fashionBackgroundRadius !== undefined + ? Panel.rootObject.fashionBackgroundRadius + : Math.round(dockSize / 5) + readonly property real hoverBackgroundRadius: Math.max(0, taskbarRadius - hoverInset) + readonly property int hoverFadeDuration: 150 + readonly property int pageSlideDuration: 260 + readonly property int leftContentPadding: hoverInset + readonly property int rightContentPadding: Math.max(10, Math.round(dockSize * 0.2)) + readonly property int widthExpansion: 0 + readonly property int foregroundContentOffset: 8 + readonly property int systemForegroundContentOffset: 10 + readonly property int textVerticalLift: 1 + readonly property int weatherTextVerticalLift: 2 + readonly property int verticalInset: Math.max(5, Math.round(dockSize * 0.16)) + readonly property int pageContentHeight: Math.max(24, dockSize - verticalInset * 2) + readonly property int pageSpacing: Math.max(8, Math.round(dockSize * 0.14)) + readonly property int tightSpacing: Math.max(1, Math.round(dockSize * 0.04)) + readonly property int mediumIconSize: Math.max(18, Math.round(pageContentHeight * 0.62)) + readonly property int pluginRowLeftMargin: root.leftContentPadding + root.foregroundContentOffset + readonly property int pluginLeadingIconSize: root.mediumIconSize + readonly property int compactPluginLeadingSlotSize: Math.max(root.pluginLeadingIconSize, root.pageContentHeight + 2) + readonly property int pluginLeadingSlotSize: root.singleLineCompactMode + ? root.compactPluginLeadingSlotSize + : Math.max(32, root.pluginLeadingIconSize) + readonly property int musicArtSize: root.pluginLeadingIconSize + readonly property real musicArtRadius: 4 + readonly property int musicControlIconSize: 16 + readonly property int musicControlButtonSize: root.singleLineCompactMode ? 26 : 30 + readonly property real musicControlButtonRadius: musicControlButtonSize / 2 + readonly property int musicControlSpacing: Math.max(3, Math.round(pageContentHeight * 0.07)) + readonly property int musicTitleButtonSpacing: Math.max(2, root.tightSpacing - 1) + readonly property int musicTextSpacing: 1 + readonly property int musicRowSpacing: root.singleLineCompactMode ? Math.max(8, root.pageSpacing - 1) : 10 + readonly property bool singleLineCompactMode: root.pageContentHeight <= 28 + readonly property bool showSecondaryLine: !root.singleLineCompactMode + readonly property int sharedPageVerticalOffset: 0 + readonly property int weatherPageVerticalOffset: 0 + readonly property int aiPageVerticalOffset: 0 + readonly property int musicPageVerticalOffset: 0 + readonly property int sharedTextVerticalOffset: root.showSecondaryLine ? -root.textVerticalLift : 0 + readonly property int weatherInfoVerticalOffset: root.showSecondaryLine + ? -root.weatherTextVerticalLift + : 0 + readonly property int musicControlsWidth: root.musicControlButtonSize * 3 + root.musicControlSpacing * 2 + readonly property real musicArtworkDevicePixelRatio: Screen.devicePixelRatio > 0 ? Screen.devicePixelRatio : 1.0 + readonly property int timeFontSize: Math.max(14, Math.round(pageContentHeight * 0.47)) + readonly property int temperatureUnitFontSize: timeFontSize - 2 + readonly property int headlineFontSize: Math.max(13, Math.round(pageContentHeight * 0.4)) + readonly property int metricFontSize: Math.max(13, Math.round(pageContentHeight * 0.4)) + readonly property int secondaryFontSize: Math.max(9, Math.round(pageContentHeight * 0.24)) + readonly property int captionFontSize: Math.max(9, Math.round(pageContentHeight * 0.2)) + readonly property int weatherSecondaryFontSize: secondaryFontSize + 2 + readonly property int monitorDetailFontSize: captionFontSize + 2 + readonly property int aiProcessFontSize: captionFontSize + 2 + readonly property int aiPrimaryCountFontSize: root.metricFontSize + 1 + readonly property int aiSecondaryCountFontSize: root.captionFontSize + 2 + readonly property int aiStatusBarHeight: 2 + readonly property int aiStatusBarTopMargin: root.aiShowSecondaryLine ? 1 : 0 + readonly property int aiStatusBarAnimationDuration: 1150 + readonly property bool aiShowSecondaryLine: root.pageContentHeight > 34 + readonly property int aiTextVerticalOffset: root.aiShowSecondaryLine ? -root.textVerticalLift : 0 + readonly property int aiMetricColumnSpacing: root.aiShowSecondaryLine ? root.tightSpacing : 0 + readonly property color aiIconTintColor: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.92) : Qt.rgba(0, 0, 0, 0.86) + readonly property int weatherTextSpacing: root.tightSpacing - 2 + readonly property bool musicPageVisible: provider.musicAvailable + readonly property bool mailPageVisible: provider.mailConfigured + readonly property bool aiPageVisible: provider.aiRunningCount > 0 && aiEntries.length > 0 + readonly property var pageIds: { + const ids = ["weather"] + if (aiPageVisible) { + ids.push("ai") + } + if (musicPageVisible) { + ids.push("music") + } + if (mailPageVisible) { + ids.push("mail") + } + ids.push("system") + return ids + } + readonly property int pageCount: pageIds.length + readonly property int pageIndicatorSize: 2 + readonly property int pageIndicatorSpacing: 4 + readonly property int pageIndicatorRightInset: Math.max(4, root.hoverInset + 3) + readonly property real hiddenPageOffset: root.dockSize + Math.max(8, root.verticalInset * 2) + readonly property bool autoRotateEnabled: false + readonly property int autoRotateInterval: 4000 + readonly property int maxMusicTitleWidth: Math.max(root.musicControlsWidth + Math.max(6, root.musicControlSpacing * 2), + Math.round(dockSize * 1.58)) + readonly property string weatherTraySurfaceId: "deepin-weather::weather" + property string currentPageId: "weather" + property string manualPageId: "weather" + property string musicReturnPageId: "weather" + property string mailReturnPageId: "weather" + property bool musicAutoActive: false + property bool mailAutoActive: false + property bool previousMusicAvailable: provider.musicAvailable + property int previousMailUnreadCount: provider.mailUnreadCount + property string transitionFromPageId: "" + property string transitionToPageId: "" + property int transitionDirection: 1 + property point lastSpotlightPoint: Qt.point(0, 0) + property real musicHoverProgress: rootHoverHandler.hovered ? 1 : 0 + property real wheelDeltaAccumulator: 0 + property int wheelDirectionLatch: 0 + property bool wheelStepTriggeredInGesture: false + readonly property int wheelStepThreshold: 120 + readonly property int wheelGestureResetInterval: 180 + readonly property int currentIndex: Math.max(0, root.pageIds.indexOf(root.normalizedPageId(root.currentPageId))) + readonly property int indicatorIndex: transitionToPageId.length > 0 + ? Math.max(0, root.pageIds.indexOf(root.normalizedPageId(root.transitionToPageId))) + : root.currentIndex + readonly property string tooltipPageId: transitionToPageId.length > 0 + ? root.normalizedPageId(root.transitionToPageId) + : root.normalizedPageId(root.currentPageId) + readonly property var aiEntries: provider.aiToolEntries || [] + readonly property int aiEntryCount: Math.min(2, aiEntries.length) + readonly property bool aiDualColumnMode: aiEntryCount > 1 + readonly property string aiPrimaryDisplayToolId: provider.aiPrimaryToolId && provider.aiPrimaryToolId.length > 0 + ? provider.aiPrimaryToolId + : ((aiEntries.length > 0 && aiEntries[0].toolId) ? String(aiEntries[0].toolId) : "") + readonly property string currentTooltipText: { + switch (root.tooltipPageId) { + case "ai": + return root.aiTooltipText() + case "music": + return root.musicTooltipText() + case "mail": + return root.mailTooltipText() + case "system": + return root.systemTooltipText() + default: + return root.weatherTooltipText() + } + } + + Behavior on musicHoverProgress { + NumberAnimation { + duration: root.hoverFadeDuration + 70 + easing.type: Easing.OutCubic + } + } + + readonly property color primaryTextColor: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.96) : Qt.rgba(0, 0, 0, 0.92) + readonly property color secondaryTextColor: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.68) : Qt.rgba(0, 0, 0, 0.58) + readonly property color musicControlForegroundColor: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 1) : Qt.rgba(0, 0, 0, 1) + readonly property string dataFontFamily: DS.dataFontFamily.length > 0 ? DS.dataFontFamily : D.DTK.fontManager.t6.family + readonly property int taskbarWidth: Panel.rootObject && Panel.rootObject.adaptiveFashionLeftWidth !== undefined + ? Panel.rootObject.adaptiveFashionLeftWidth + : 160 + implicitWidth: taskbarWidth + width: taskbarWidth + implicitHeight: dockSize + padding: 0 + + FashionLeftPluginProvider { + id: provider + } + + Item { + id: textProbeLayer + visible: false + width: 0 + height: 0 + + Text { + id: systemMetricValueProbe + text: "100%" + font.family: root.dataFontFamily + font.pixelSize: root.metricFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + } + + Text { + id: systemTrafficProbe + text: "999.9mb/s ↓" + font.family: root.dataFontFamily + font.pixelSize: root.monitorDetailFontSize + renderType: Text.NativeRendering + } + } + + function mapSpotlightPoint(localPoint) { + const point = localPoint || Qt.point(width / 2, height / 2) + return mapToItem(null, point.x, point.y) + } + + function updateSpotlight(localPoint) { + lastSpotlightPoint = mapSpotlightPoint(localPoint) + Panel.reportMousePresence(true, lastSpotlightPoint) + } + + function clearSpotlight() { + Panel.reportMousePresence(false, lastSpotlightPoint) + } + + function prepareWeatherTraySurface() { + const surface = DockCompositor.findSurface(root.weatherTraySurfaceId) + if (!surface) { + return + } + + const panelPoint = weatherPage.mapToItem(null, 0, 0) + const globalPoint = weatherPage.mapToGlobal(0, 0) + surface.updatePluginGeometry(Qt.rect(Math.round(panelPoint.x), + Math.round(panelPoint.y), + Math.round(weatherPage.width), + Math.round(weatherPage.height))) + surface.setGlobalPos(Qt.point(Math.round(globalPoint.x), Math.round(globalPoint.y))) + } + + function resetWheelGesture() { + root.wheelDeltaAccumulator = 0 + root.wheelDirectionLatch = 0 + root.wheelStepTriggeredInGesture = false + } + + function handleWheelPaging(deltaY) { + if (deltaY === 0) { + return + } + + const direction = deltaY < 0 ? 1 : -1 + if (root.wheelDirectionLatch !== 0 && root.wheelDirectionLatch !== direction) { + root.resetWheelGesture() + } + + root.wheelDirectionLatch = direction + wheelGestureResetTimer.restart() + + if (root.wheelStepTriggeredInGesture + || pageTransitionAnimation.running + || root.transitionToPageId.length > 0) { + return + } + + root.wheelDeltaAccumulator += Math.abs(deltaY) + if (root.wheelDeltaAccumulator < root.wheelStepThreshold) { + return + } + + root.wheelStepTriggeredInGesture = true + root.wheelDeltaAccumulator = 0 + root.step(direction) + } + + function visiblePageIds() { + return root.pageIds + } + + function normalizedPageId(pageId) { + return root.pageIds.indexOf(pageId) >= 0 ? pageId : "weather" + } + + function pageItemForId(pageId) { + switch (pageId) { + case "ai": + return aiPage + case "music": + return musicPage + case "mail": + return mailPage + case "system": + return systemPage + default: + return weatherPage + } + } + + function canAutoRotate() { + return root.autoRotateEnabled + && root.pageCount > 1 + && !rootHoverHandler.hovered + && !root.musicAutoActive + && !root.mailAutoActive + } + + function restartAutoRotateTimer() { + autoRotateTimer.stop() + if (root.canAutoRotate()) { + autoRotateTimer.start() + } + } + + function syncPageLayout() { + const visibleIds = root.visiblePageIds() + const inTransition = pageTransitionAnimation.running || root.transitionToPageId.length > 0 + const activePageId = root.normalizedPageId(root.currentPageId) + const items = [weatherPage, aiPage, musicPage, mailPage, systemPage] + for (let index = 0; index < items.length; ++index) { + const item = items[index] + const available = visibleIds.indexOf(item.pageId) >= 0 + const active = inTransition + ? (item.pageId === root.transitionFromPageId || item.pageId === root.transitionToPageId) + : item.pageId === activePageId + item.visible = available && active + if (!item.visible) { + item.y = root.hiddenPageOffset + } else if (!inTransition) { + item.y = 0 + } + } + } + + function completePageTransition() { + if (root.transitionToPageId.length > 0) { + root.currentPageId = root.normalizedPageId(root.transitionToPageId) + } + + root.transitionFromPageId = "" + root.transitionToPageId = "" + root.transitionDirection = 1 + root.syncPageLayout() + root.restartAutoRotateTimer() + } + + function transitionDirectionForTarget(fromIndex, targetIndex) { + const ids = root.visiblePageIds() + if (ids.length <= 1) { + return 1 + } + + if (fromIndex === ids.length - 1 && targetIndex === 0) { + return 1 + } + + if (fromIndex === 0 && targetIndex === ids.length - 1) { + return -1 + } + + return targetIndex >= fromIndex ? 1 : -1 + } + + function showPage(pageId, animated) { + const shouldAnimate = animated === undefined ? true : animated + const ids = root.visiblePageIds() + const targetPageId = root.normalizedPageId(pageId) + const targetIndex = ids.indexOf(targetPageId) + const fromPageId = root.transitionToPageId.length > 0 + ? root.normalizedPageId(root.transitionToPageId) + : root.normalizedPageId(root.currentPageId) + const fromIndex = Math.max(0, ids.indexOf(fromPageId)) + + autoRotateTimer.stop() + + if (targetIndex < 0) { + return + } + + if (pageTransitionAnimation.running) { + pageTransitionAnimation.stop() + } + + if (!shouldAnimate || ids.length < 2 || targetPageId === fromPageId) { + root.currentPageId = targetPageId + root.transitionFromPageId = "" + root.transitionToPageId = "" + root.transitionDirection = 1 + root.syncPageLayout() + root.restartAutoRotateTimer() + return + } + + const fromItem = root.pageItemForId(fromPageId) + const toItem = root.pageItemForId(targetPageId) + if (!fromItem || !toItem) { + root.currentPageId = targetPageId + root.transitionFromPageId = "" + root.transitionToPageId = "" + root.transitionDirection = 1 + root.syncPageLayout() + return + } + + root.transitionFromPageId = fromPageId + root.transitionToPageId = targetPageId + root.transitionDirection = root.transitionDirectionForTarget(fromIndex, targetIndex) + root.syncPageLayout() + + fromItem.y = 0 + toItem.y = root.transitionDirection > 0 ? root.height : -root.height + + transitionFromAnimation.target = fromItem + transitionFromAnimation.from = 0 + transitionFromAnimation.to = -root.transitionDirection * root.height + + transitionToAnimation.target = toItem + transitionToAnimation.from = root.transitionDirection > 0 ? root.height : -root.height + transitionToAnimation.to = 0 + + pageTransitionAnimation.start() + } + + function showManualPage(pageId) { + root.dismissAutoFocus() + const nextPageId = root.normalizedPageId(pageId) + root.manualPageId = nextPageId + root.showPage(nextPageId) + } + + function step(delta) { + const ids = root.visiblePageIds() + const nextIndex = (root.currentIndex + delta + ids.length) % ids.length + root.dismissAutoFocus() + root.manualPageId = ids[nextIndex] + root.showPage(ids[nextIndex]) + } + + function weatherTemperatureValue(text) { + if (!text) { + return "--" + } + + return text.replace(/\s*°[CF]?$/, "") + } + + function musicControlIconSource(iconName) { + const themeIconSource = provider.musicControlThemeIconSource(iconName, Panel.colorTheme === Dock.Dark) + return themeIconSource && themeIconSource.length > 0 + ? themeIconSource + : Qt.resolvedUrl("icons/" + iconName + ".svg") + } + + function weatherTooltipText() { + const cityText = provider.weatherCityText && provider.weatherCityText.length > 0 ? provider.weatherCityText : qsTr("当前城市") + const temperatureText = provider.weatherTemperatureText && provider.weatherTemperatureText.length > 0 + ? provider.weatherTemperatureText.replace(/°$/, "°C") + : "--°C" + return cityText + " " + temperatureText + } + + function musicTooltipText() { + const titleText = provider.musicTitleText || "" + const appText = provider.musicAppName || "" + if (titleText.length > 0 && appText.length > 0) { + return titleText + " - " + appText + } + + return titleText.length > 0 ? titleText : appText + } + + function aiIconSource(toolId) { + const normalizedToolId = toolId ? String(toolId).toLowerCase() : "" + switch (normalizedToolId) { + case "codex": + case "openai": + return Qt.resolvedUrl("icons/openai.svg") + case "claude": + case "claude-code": + return Qt.resolvedUrl("icons/claude.svg") + case "doubao": + return Qt.resolvedUrl("icons/doubao.svg") + case "gemini": + return Qt.resolvedUrl("icons/gemini.svg") + case "qwen": + return Qt.resolvedUrl("icons/qwen.svg") + case "aistudio": + case "google-ai-studio": + return Qt.resolvedUrl("icons/aistudio.svg") + default: + return Qt.resolvedUrl("icons/ai.svg") + } + } + + function aiTooltipText() { + if (root.aiEntries.length <= 0) { + return qsTr("AI") + } + + let parts = [] + for (let index = 0; index < Math.min(2, root.aiEntries.length); ++index) { + const entry = root.aiEntries[index] + const progressText = entry && entry.progressText ? String(entry.progressText) : "0/0" + const processLabel = entry && entry.processLabel ? String(entry.processLabel) : "ai" + parts.push(progressText + qsTr(" 条任务") + " · " + processLabel) + } + return parts.join(" · ") + } + + function mailTooltipText() { + const clientText = provider.mailClientName && provider.mailClientName.length > 0 ? provider.mailClientName : qsTr("邮箱") + return qsTr("%1 %2 封未读").arg(clientText).arg(provider.mailUnreadCount) + } + + function systemTooltipText() { + return qsTr("CPU %1% · 内存 %2%").arg(provider.cpuUsage).arg(provider.memoryUsage) + } + + function updateToolTipGeometry() { + const point = root.mapToItem(null, root.width / 2, root.height / 2) + pageToolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, pageToolTip.width, pageToolTip.height) + } + + function showToolTip() { + if (!rootHoverHandler.hovered || !root.currentTooltipText || root.currentTooltipText.length === 0) { + return + } + + pageToolTip.text = root.currentTooltipText + root.updateToolTipGeometry() + pageToolTip.open() + } + + function hideToolTip() { + if (toolTipShowTimer.running) { + toolTipShowTimer.stop() + } + pageToolTip.close() + } + + function dismissAutoFocus() { + root.musicAutoActive = false + root.mailAutoActive = false + } + + function handleMusicAvailabilityChange() { + if (provider.musicAvailable === root.previousMusicAvailable) { + return + } + + if (provider.musicAvailable) { + if (!root.mailAutoActive && root.currentPageId !== "music") { + root.musicReturnPageId = root.normalizedPageId(root.currentPageId) + root.musicAutoActive = true + root.showPage("music") + } + } else { + if (root.mailAutoActive && root.mailReturnPageId === "music") { + root.mailReturnPageId = root.normalizedPageId(root.musicReturnPageId) + } + + const fallbackPageId = root.normalizedPageId(root.musicAutoActive ? root.musicReturnPageId : root.manualPageId) + if (root.currentPageId === "music") { + root.showPage(fallbackPageId) + } + + if (root.manualPageId === "music") { + root.manualPageId = "weather" + } + + root.musicAutoActive = false + } + + root.previousMusicAvailable = provider.musicAvailable + } + + function handleMailUnreadCountChange() { + if (!provider.mailConfigured) { + root.previousMailUnreadCount = provider.mailUnreadCount + return + } + + const unreadIncreased = provider.mailUnreadCount > root.previousMailUnreadCount + if (unreadIncreased && root.currentPageId !== "mail") { + root.mailReturnPageId = root.normalizedPageId(root.currentPageId) + root.mailAutoActive = true + root.showPage("mail") + } + + root.previousMailUnreadCount = provider.mailUnreadCount + } + + function handleMailVisibilityChange() { + if (provider.mailConfigured) { + return + } + + const fallbackPageId = root.normalizedPageId(root.mailAutoActive ? root.mailReturnPageId : root.manualPageId) + if (root.currentPageId === "mail" + || root.transitionFromPageId === "mail" + || root.transitionToPageId === "mail") { + root.showPage(fallbackPageId, false) + } + + if (root.manualPageId === "mail") { + root.manualPageId = "weather" + } + + root.mailAutoActive = false + root.previousMailUnreadCount = provider.mailUnreadCount + } + + function completeMailAutoFocus() { + if (!root.mailAutoActive) { + return + } + + root.mailAutoActive = false + root.showPage(root.mailReturnPageId) + } + + HoverHandler { + id: rootHoverHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + + onPointChanged: { + if (hovered) { + spotlightClearTimer.stop() + root.updateSpotlight(rootHoverHandler.point.position) + } + } + + onHoveredChanged: { + if (hovered) { + spotlightClearTimer.stop() + root.updateSpotlight() + autoRotateTimer.stop() + toolTipShowTimer.stop() + toolTipShowTimer.start() + } else { + spotlightClearTimer.restart() + root.hideToolTip() + root.restartAutoRotateTimer() + } + } + } + + Timer { + id: spotlightClearTimer + interval: 70 + repeat: false + onTriggered: { + if (!rootHoverHandler.hovered) { + root.clearSpotlight() + } + } + } + + Connections { + target: provider + + function onMusicStateChanged() { + root.handleMusicAvailabilityChange() + if (rootHoverHandler.hovered) { + if (pageToolTip.toolTipVisible) { + root.showToolTip() + } else { + toolTipShowTimer.stop() + toolTipShowTimer.start() + } + } + } + + function onMailStateChanged() { + root.handleMailVisibilityChange() + root.handleMailUnreadCountChange() + if (rootHoverHandler.hovered && root.currentPageId === "mail") { + if (pageToolTip.toolTipVisible) { + root.showToolTip() + } else { + toolTipShowTimer.stop() + toolTipShowTimer.start() + } + } + } + + function onWeatherChanged() { + if (rootHoverHandler.hovered && root.currentPageId === "weather") { + if (pageToolTip.toolTipVisible) { + root.showToolTip() + } else { + toolTipShowTimer.stop() + toolTipShowTimer.start() + } + } + } + + function onSystemStatsChanged() { + if (rootHoverHandler.hovered && root.currentPageId === "system") { + if (pageToolTip.toolTipVisible) { + root.showToolTip() + } + } + } + + function onAiStateChanged() { + if (rootHoverHandler.hovered && root.currentPageId === "ai") { + if (pageToolTip.toolTipVisible) { + root.showToolTip() + } else { + toolTipShowTimer.stop() + toolTipShowTimer.start() + } + } + } + } + + onCurrentPageIdChanged: { + if (!rootHoverHandler.hovered) { + root.restartAutoRotateTimer() + return + } + + if (pageToolTip.toolTipVisible) { + root.showToolTip() + } else { + toolTipShowTimer.stop() + toolTipShowTimer.start() + } + + root.restartAutoRotateTimer() + } + + onCurrentIndexChanged: { + if (!pageTransitionAnimation.running && root.transitionToPageId.length === 0) { + root.syncPageLayout() + } + } + + onTransitionToPageIdChanged: { + if (!rootHoverHandler.hovered) { + return + } + + if (transitionToPageId.length > 0 || pageToolTip.toolTipVisible) { + root.showToolTip() + } else { + toolTipShowTimer.stop() + toolTipShowTimer.start() + } + } + + onPageCountChanged: { + root.syncPageLayout() + root.restartAutoRotateTimer() + } + onHeightChanged: root.syncPageLayout() + onMusicAutoActiveChanged: root.restartAutoRotateTimer() + onMailAutoActiveChanged: root.restartAutoRotateTimer() + + background: Item { + AppletItemBackground { + x: root.hoverInset + y: root.hoverInset + width: Math.max(0, parent.width - root.hoverInset) + height: Math.max(0, parent.height - root.hoverInset * 2) + radius: root.hoverBackgroundRadius + enabled: false + opacity: rootHoverHandler.hovered ? 1 : 0 + D.ColorSelector.hovered: rootHoverHandler.hovered + + Behavior on opacity { + NumberAnimation { + duration: root.hoverFadeDuration + easing.type: Easing.OutCubic + } + } + } + + Item { + anchors.right: parent.right + anchors.rightMargin: root.pageIndicatorRightInset + anchors.verticalCenter: parent.verticalCenter + visible: rootHoverHandler.hovered && root.pageCount > 1 + implicitWidth: root.pageIndicatorSize + implicitHeight: indicatorColumn.implicitHeight + + Column { + id: indicatorColumn + anchors.centerIn: parent + spacing: root.pageIndicatorSpacing + + Repeater { + model: root.pageCount + + Rectangle { + width: root.pageIndicatorSize + height: root.pageIndicatorSize + radius: width / 2 + color: root.primaryTextColor + opacity: index === root.indicatorIndex ? 1 : 0.35 + } + } + } + } + } + + PanelToolTip { + id: pageToolTip + toolTipX: DockPanelPositioner.x + toolTipY: DockPanelPositioner.y + } + + Timer { + id: autoRotateTimer + interval: root.autoRotateInterval + repeat: true + running: false + onTriggered: { + if (!root.canAutoRotate() || pageTransitionAnimation.running || root.transitionToPageId.length > 0) { + return + } + + root.step(1) + } + } + + Timer { + id: toolTipShowTimer + interval: 50 + onTriggered: root.showToolTip() + } + + Timer { + id: wheelGestureResetTimer + interval: root.wheelGestureResetInterval + repeat: false + onTriggered: root.resetWheelGesture() + } + + WheelHandler { + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: function(event) { + const deltaY = event.angleDelta.y !== 0 ? event.angleDelta.y : event.pixelDelta.y + root.handleWheelPaging(deltaY) + + if (rootHoverHandler.hovered) { + root.showToolTip() + } + event.accepted = true + } + } + + component PageShell: Item { + id: pageShell + + property string pageId: "" + + signal clicked(real globalX, real globalY) + + width: root.width + height: root.height + + MouseArea { + id: pageMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: function(mouse) { + const globalPos = pageMouseArea.mapToGlobal(mouse.x, mouse.y) + root.hideToolTip() + pageShell.clicked(globalPos.x, globalPos.y) + } + } + } + + component MusicActionButton: Item { + id: actionButton + + property string iconName: "" + property bool actionEnabled: false + property int layoutIndex: 0 + property int hoverDelay: 0 + property bool hoverTarget: rootHoverHandler.hovered + property real revealProgress: hoverTarget ? 1 : 0 + readonly property real collapsedX: (root.musicControlsWidth - width) / 2 + readonly property real expandedX: layoutIndex * (root.musicControlButtonSize + root.musicControlSpacing) + readonly property bool interactive: actionEnabled && revealProgress > 0.55 + signal clicked() + + implicitWidth: root.musicControlButtonSize + implicitHeight: root.musicControlButtonSize + x: collapsedX + (expandedX - collapsedX) * revealProgress + y: 0 + scale: 0.7 + revealProgress * 0.3 + opacity: revealProgress + z: layoutIndex === 1 ? 3 : 2 - Math.abs(layoutIndex - 1) + + onHoverTargetChanged: revealProgress = hoverTarget ? 1 : 0 + Component.onCompleted: revealProgress = hoverTarget ? 1 : 0 + + Behavior on revealProgress { + SequentialAnimation { + PauseAnimation { + duration: actionButton.hoverTarget ? actionButton.hoverDelay : 0 + } + NumberAnimation { + duration: root.hoverFadeDuration + 110 + easing.type: Easing.OutCubic + } + } + } + + HoverHandler { + id: actionHoverHandler + enabled: actionButton.interactive + } + + Rectangle { + anchors.fill: parent + radius: root.musicControlButtonRadius + color: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(0, 0, 0, 0.08) + opacity: actionHoverHandler.hovered ? actionButton.revealProgress : 0 + + Behavior on opacity { + NumberAnimation { + duration: root.hoverFadeDuration + easing.type: Easing.OutCubic + } + } + } + + Image { + id: actionIconImage + anchors.centerIn: parent + width: root.musicControlIconSize + height: root.musicControlIconSize + source: root.musicControlIconSource(actionButton.iconName) + sourceSize: Qt.size(width, height) + fillMode: Image.PreserveAspectFit + smooth: true + visible: false + } + + ColorOverlay { + anchors.fill: actionIconImage + source: actionIconImage + color: root.musicControlForegroundColor + opacity: actionButton.actionEnabled ? 1 : 0.38 + } + + MouseArea { + anchors.fill: parent + enabled: actionButton.interactive + hoverEnabled: true + cursorShape: actionButton.interactive ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: function(mouse) { + mouse.accepted = true + if (actionButton.actionEnabled) { + actionButton.clicked() + } + } + } + } + + ParallelAnimation { + id: pageTransitionAnimation + + NumberAnimation { + id: transitionFromAnimation + property: "y" + duration: root.pageSlideDuration + easing.type: Easing.OutCubic + } + + NumberAnimation { + id: transitionToAnimation + property: "y" + duration: root.pageSlideDuration + easing.type: Easing.OutCubic + } + + onStopped: root.completePageTransition() + } + + contentItem: Item { + clip: true + + Component.onCompleted: root.syncPageLayout() + + PageShell { + id: weatherPage + pageId: "weather" + + implicitWidth: weatherRow.implicitWidth + onClicked: function(globalX, globalY) { + const taskbarTopLeft = root.mapToGlobal(0, 0) + root.prepareWeatherTraySurface() + provider.openWeatherPopup(Math.round(taskbarTopLeft.x), + Math.round(taskbarTopLeft.y), + Math.round(globalX), + Math.round(globalY)) + } + + RowLayout { + id: weatherRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: root.weatherPageVerticalOffset + anchors.leftMargin: root.pluginRowLeftMargin + anchors.rightMargin: root.rightContentPadding + height: root.pageContentHeight + spacing: root.pageSpacing + + Item { + implicitWidth: root.pluginLeadingSlotSize + implicitHeight: implicitWidth + Layout.alignment: Qt.AlignVCenter + + Image { + anchors.centerIn: parent + width: root.pluginLeadingIconSize + height: root.pluginLeadingIconSize + source: provider.weatherIconSource && provider.weatherIconSource.toString().length > 0 + ? provider.weatherIconSource + : Qt.resolvedUrl("icons/weather-none-available.svg") + sourceSize: Qt.size(width, height) + fillMode: Image.PreserveAspectFit + smooth: true + asynchronous: true + } + } + + Item { + implicitWidth: weatherTextColumn.implicitWidth + implicitHeight: weatherTextColumn.implicitHeight + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + Layout.maximumHeight: root.pageContentHeight + + ColumnLayout { + id: weatherTextColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: root.weatherInfoVerticalOffset + spacing: root.weatherTextSpacing + + Item { + implicitWidth: temperatureValueText.implicitWidth + temperatureUnitText.implicitWidth + implicitHeight: temperatureValueText.implicitHeight + + Text { + id: temperatureValueText + text: root.weatherTemperatureValue(provider.weatherTemperatureText) + color: root.primaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.timeFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + } + + Text { + id: temperatureUnitText + anchors.left: temperatureValueText.right + anchors.baseline: temperatureValueText.baseline + text: "°C" + color: root.primaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.temperatureUnitFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + } + } + + Text { + text: provider.weatherSummaryText + color: root.secondaryTextColor + font.pixelSize: root.weatherSecondaryFontSize + renderType: Text.NativeRendering + Layout.fillWidth: true + elide: Text.ElideRight + visible: root.showSecondaryLine + } + } + } + } + } + + PageShell { + id: aiPage + pageId: "ai" + + implicitWidth: aiRow.implicitWidth + onClicked: provider.openAiClientHost() + + RowLayout { + id: aiRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: root.aiPageVerticalOffset + anchors.leftMargin: root.pluginRowLeftMargin + anchors.rightMargin: root.rightContentPadding + height: root.pageContentHeight + spacing: root.aiDualColumnMode ? 0 : root.pageSpacing + + Item { + implicitWidth: root.aiDualColumnMode ? 0 : root.pluginLeadingSlotSize + implicitHeight: implicitWidth + Layout.alignment: Qt.AlignVCenter + visible: !root.aiDualColumnMode + + Image { + id: aiPrimaryIcon + anchors.centerIn: parent + width: root.pluginLeadingIconSize + height: root.pluginLeadingIconSize + source: root.aiIconSource(root.aiPrimaryDisplayToolId) + sourceSize: Qt.size(width, height) + fillMode: Image.PreserveAspectFit + smooth: true + asynchronous: true + visible: false + } + + ColorOverlay { + anchors.fill: aiPrimaryIcon + source: aiPrimaryIcon + color: root.aiIconTintColor + visible: aiPrimaryIcon.source !== "" + } + + Rectangle { + anchors.centerIn: parent + width: root.pluginLeadingIconSize + height: root.pluginLeadingIconSize + radius: width / 3 + color: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0, 0, 0, 0.06) + border.width: 1 + border.color: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(0, 0, 0, 0.08) + visible: aiPrimaryIcon.source === "" + } + + Text { + anchors.centerIn: parent + text: "AI" + color: root.primaryTextColor + font.pixelSize: root.captionFontSize + 1 + font.weight: Font.DemiBold + renderType: Text.NativeRendering + visible: aiPrimaryIcon.source === "" + } + } + + Item { + implicitWidth: aiMetricsRow.implicitWidth + implicitHeight: aiMetricsRow.implicitHeight + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + Layout.maximumHeight: root.pageContentHeight + + RowLayout { + id: aiMetricsRow + property real dualSharedStatusBarWidth: 24 + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: root.aiDualColumnMode ? 8 : 0 + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: root.aiTextVerticalOffset + spacing: root.aiEntryCount > 1 ? 10 : root.pageSpacing + + function updateDualSharedStatusBarWidth() { + if (!root.aiDualColumnMode) { + dualSharedStatusBarWidth = 24 + return + } + + let maxWidth = 24 + for (let index = 0; index < aiMetricsRepeater.count; ++index) { + const item = aiMetricsRepeater.itemAt(index) + if (item && item.headlineWidth !== undefined) { + maxWidth = Math.max(maxWidth, item.headlineWidth) + } + } + + dualSharedStatusBarWidth = maxWidth + } + + Repeater { + id: aiMetricsRepeater + model: root.aiEntries + + delegate: Item { + id: aiMetricDelegate + required property var modelData + + readonly property string processLabel: modelData && modelData.processLabel ? String(modelData.processLabel) : "ai" + readonly property int runningCount: modelData && modelData.runningCount !== undefined ? Number(modelData.runningCount) : 0 + readonly property int completedCount: modelData && modelData.completedCount !== undefined ? Number(modelData.completedCount) : 0 + readonly property int totalCount: modelData && modelData.totalCount !== undefined ? Number(modelData.totalCount) : 0 + readonly property bool running: runningCount > 0 + readonly property bool completed: !running && totalCount > 0 && completedCount >= totalCount + readonly property string completedText: String(completedCount) + readonly property string totalText: String(totalCount) + readonly property int headlineSpacing: Math.max(2, Math.round(root.pageContentHeight * 0.06)) + readonly property real headlineWidth: aiMetricHeadlineRow.implicitWidth + + implicitWidth: aiMetricColumn.implicitWidth + implicitHeight: aiMetricColumn.implicitHeight + Layout.fillWidth: false + Layout.preferredWidth: implicitWidth + Layout.minimumWidth: 0 + + Component.onCompleted: aiMetricsRow.updateDualSharedStatusBarWidth() + onHeadlineWidthChanged: aiMetricsRow.updateDualSharedStatusBarWidth() + + ColumnLayout { + id: aiMetricColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: root.aiMetricColumnSpacing + + Item { + id: aiMetricHeadlineRow + implicitWidth: taskCountLabel.visible + ? taskCountLabel.x + taskCountLabel.implicitWidth + : totalTextItem.x + totalTextItem.implicitWidth + implicitHeight: Math.max(completedTextItem.implicitHeight, + taskCountLabel.visible ? taskCountLabel.implicitHeight : 0) + + Text { + id: completedTextItem + anchors.left: parent.left + anchors.bottom: parent.bottom + text: aiMetricDelegate.completedText + color: root.primaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.aiPrimaryCountFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + } + + Text { + id: slashTextItem + anchors.left: completedTextItem.right + anchors.leftMargin: aiMetricDelegate.headlineSpacing + anchors.bottom: completedTextItem.bottom + text: "/" + color: root.primaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.aiPrimaryCountFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + } + + Text { + id: totalTextItem + anchors.left: slashTextItem.right + anchors.leftMargin: aiMetricDelegate.headlineSpacing + anchors.bottom: completedTextItem.bottom + text: aiMetricDelegate.totalText + color: root.primaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.aiSecondaryCountFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + } + + Text { + id: taskCountLabel + anchors.left: totalTextItem.right + anchors.leftMargin: aiMetricDelegate.headlineSpacing + anchors.bottom: completedTextItem.bottom + text: qsTr("条任务") + color: root.secondaryTextColor + font.pixelSize: root.captionFontSize + font.weight: Font.Medium + renderType: Text.NativeRendering + visible: root.aiEntryCount === 1 + } + } + + Rectangle { + id: aiStatusTrack + implicitWidth: root.aiDualColumnMode + ? aiMetricsRow.dualSharedStatusBarWidth + : aiMetricHeadlineRow.implicitWidth + implicitHeight: root.aiStatusBarHeight + radius: 0.1 + color: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.16) : Qt.rgba(0, 0, 0, 0.12) + Layout.topMargin: root.aiStatusBarTopMargin + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Qt.rgba(0.37, 0.83, 0.42, 0.95) + visible: aiMetricDelegate.completed + } + + Item { + id: aiRunningViewport + anchors.fill: parent + clip: true + visible: aiMetricDelegate.running + + Item { + id: aiRunningMarquee + width: aiStatusTrack.width * 2 + height: aiStatusTrack.height + y: 0 + x: -aiStatusTrack.width + + Repeater { + model: 2 + + delegate: Rectangle { + x: index * aiStatusTrack.width + width: aiStatusTrack.width + height: aiStatusTrack.height + radius: 0.1 + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: "#ff5f6d" } + GradientStop { position: 0.18; color: "#ff9966" } + GradientStop { position: 0.36; color: "#ffd84d" } + GradientStop { position: 0.54; color: "#59f2c1" } + GradientStop { position: 0.74; color: "#52a8ff" } + GradientStop { position: 1.0; color: "#d16cff" } + } + } + } + + NumberAnimation on x { + running: aiMetricDelegate.running + loops: Animation.Infinite + from: -aiStatusTrack.width + to: 0 + duration: root.aiStatusBarAnimationDuration + easing.type: Easing.Linear + } + } + } + } + + Text { + text: aiMetricDelegate.processLabel + color: root.secondaryTextColor + font.pixelSize: root.aiProcessFontSize + renderType: Text.NativeRendering + Layout.fillWidth: true + Layout.minimumWidth: 0 + elide: Text.ElideRight + visible: root.aiShowSecondaryLine + } + } + } + } + } + } + } + } + + PageShell { + id: musicPage + pageId: "music" + + implicitWidth: root.musicPageVisible ? musicRow.implicitWidth : 0 + implicitHeight: root.musicPageVisible ? root.dockSize : 0 + onClicked: provider.openMusicPlayer() + + RowLayout { + id: musicRow + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: root.pluginRowLeftMargin + anchors.rightMargin: root.rightContentPadding + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: root.musicPageVerticalOffset + height: root.pageContentHeight + spacing: root.musicRowSpacing + + Item { + implicitWidth: root.pluginLeadingSlotSize + implicitHeight: root.pluginLeadingSlotSize + Layout.alignment: Qt.AlignVCenter + + Rectangle { + id: musicArtworkBackground + anchors.centerIn: parent + width: root.musicArtSize + height: root.musicArtSize + radius: root.musicArtRadius + color: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0, 0, 0, 0.06) + visible: provider.musicHasArt && musicArtworkSource.status === Image.Ready + } + + D.DciIcon { + anchors.centerIn: parent + width: root.musicArtSize + height: root.musicArtSize + name: provider.musicPlayerIconName + sourceSize: Qt.size(width, height) + retainWhileLoading: true + smooth: false + visible: !provider.musicHasArt || musicArtworkSource.status !== Image.Ready + } + + Image { + id: musicArtworkSource + anchors.fill: musicArtworkBackground + visible: false + source: provider.musicArtSource + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + sourceSize: Qt.size(Math.round(root.musicArtSize * root.musicArtworkDevicePixelRatio), Math.round(root.musicArtSize * root.musicArtworkDevicePixelRatio)) + mipmap: true + smooth: true + } + + Rectangle { + id: musicArtworkMask + anchors.fill: musicArtworkBackground + radius: root.musicArtRadius + visible: false + } + + OpacityMask { + anchors.fill: musicArtworkBackground + source: musicArtworkSource + maskSource: musicArtworkMask + visible: provider.musicHasArt && musicArtworkSource.status === Image.Ready + cached: false + } + + Rectangle { + anchors.fill: musicArtworkBackground + radius: root.musicArtRadius + color: "transparent" + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.2) + visible: provider.musicHasArt && musicArtworkSource.status === Image.Ready + } + } + + Item { + id: musicInfoArea + property int summaryWidth: Math.max(root.musicControlsWidth, Math.min(root.maxMusicTitleWidth, Math.ceil(Math.max(musicTitleItem.implicitWidth, (root.showSecondaryLine && musicSubtitleWrapper.visible) ? musicSubtitleItem.implicitWidth : 0)))) + clip: true + implicitWidth: Math.ceil(summaryWidth + (root.musicControlsWidth - summaryWidth) * root.musicHoverProgress) + width: implicitWidth + implicitHeight: Math.max(musicInfoColumn.implicitHeight, musicControlsRow.implicitHeight) + Layout.alignment: Qt.AlignVCenter + + Column { + id: musicInfoColumn + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: parent.width + spacing: root.musicTextSpacing + opacity: 1 - root.musicHoverProgress + scale: 1 - root.musicHoverProgress * 0.08 + x: -10 * root.musicHoverProgress + visible: opacity > 0.01 + + Item { + id: musicTitleWrapper + width: parent.width + implicitWidth: width + implicitHeight: musicTitleItem.implicitHeight + visible: provider.musicTitleText.length > 0 + + Text { + id: musicTitleItem + anchors.fill: parent + text: provider.musicTitleText + color: root.primaryTextColor + font.pixelSize: Math.max(11, root.headlineFontSize - 2) + font.weight: Font.Normal + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + } + + Item { + id: musicSubtitleWrapper + width: parent.width + implicitWidth: width + implicitHeight: musicSubtitleItem.implicitHeight + visible: root.showSecondaryLine && provider.musicSubtitleText.length > 0 + + Text { + id: musicSubtitleItem + anchors.fill: parent + text: provider.musicSubtitleText + color: root.secondaryTextColor + font.pixelSize: root.secondaryFontSize + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + } + } + + Item { + id: musicControlsRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + implicitWidth: root.musicControlsWidth + implicitHeight: root.musicControlButtonSize + width: implicitWidth + height: implicitHeight + enabled: root.musicHoverProgress > 0.35 + + MusicActionButton { + layoutIndex: 0 + hoverDelay: 24 + iconName: "media-skip-backward" + actionEnabled: provider.musicCanGoPrevious + onClicked: provider.playPreviousTrack() + } + + MusicActionButton { + layoutIndex: 1 + hoverDelay: 0 + iconName: provider.musicPlaying ? "media-playback-pause" : "media-playback-start" + actionEnabled: provider.musicCanTogglePlayback + onClicked: provider.toggleMusicPlayback() + } + + MusicActionButton { + layoutIndex: 2 + hoverDelay: 48 + iconName: "media-skip-forward" + actionEnabled: provider.musicCanGoNext + onClicked: provider.playNextTrack() + } + } + } + } + } + + PageShell { + id: mailPage + pageId: "mail" + + implicitWidth: mailRow.implicitWidth + onClicked: { + provider.openMailClient() + root.completeMailAutoFocus() + } + + RowLayout { + id: mailRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: root.sharedPageVerticalOffset + anchors.leftMargin: root.pluginRowLeftMargin + anchors.rightMargin: root.rightContentPadding + height: root.pageContentHeight + spacing: root.pageSpacing + + Item { + implicitWidth: root.pluginLeadingSlotSize + implicitHeight: implicitWidth + Layout.alignment: Qt.AlignVCenter + + D.DciIcon { + anchors.centerIn: parent + name: provider.mailIconName + width: root.pluginLeadingIconSize + height: root.pluginLeadingIconSize + sourceSize: Qt.size(width, height) + smooth: false + } + } + + Item { + implicitWidth: mailTextColumn.implicitWidth + implicitHeight: mailTextColumn.implicitHeight + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + Layout.maximumHeight: root.pageContentHeight + + ColumnLayout { + id: mailTextColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: root.sharedTextVerticalOffset + spacing: root.tightSpacing + + RowLayout { + spacing: Math.max(3, Math.round(root.pageContentHeight * 0.1)) + + Text { + text: provider.mailUnreadCountText + color: root.primaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.headlineFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + } + + Text { + text: qsTr("封未读") + color: root.secondaryTextColor + font.pixelSize: root.secondaryFontSize + font.weight: Font.Medium + renderType: Text.NativeRendering + Layout.alignment: root.showSecondaryLine ? Qt.AlignBottom : Qt.AlignVCenter + } + } + + Text { + text: provider.mailSummaryText + color: root.secondaryTextColor + font.pixelSize: root.captionFontSize + renderType: Text.NativeRendering + Layout.fillWidth: true + elide: Text.ElideRight + visible: root.showSecondaryLine + } + } + } + } + } + + PageShell { + id: systemPage + pageId: "system" + + implicitWidth: systemColumn.implicitWidth + onClicked: provider.openSystemMonitorPage() + + ColumnLayout { + id: systemColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: root.sharedPageVerticalOffset + root.sharedTextVerticalOffset + anchors.leftMargin: root.leftContentPadding + root.foregroundContentOffset + root.systemForegroundContentOffset + anchors.rightMargin: root.rightContentPadding + height: root.pageContentHeight + spacing: root.tightSpacing + + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + spacing: root.pageSpacing + + Item { + Layout.fillWidth: true + implicitHeight: cpuMetricRow.implicitHeight + + RowLayout { + id: cpuMetricRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Math.max(3, Math.round(root.pageContentHeight * 0.08)) + + Text { + width: systemMetricValueProbe.implicitWidth + text: qsTr("%1%").arg(provider.cpuUsage) + color: root.primaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.metricFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignRight + elide: Text.ElideRight + } + + Text { + text: qsTr("CPU") + color: root.secondaryTextColor + font.pixelSize: root.captionFontSize + font.weight: Font.Medium + renderType: Text.NativeRendering + Layout.alignment: root.showSecondaryLine ? Qt.AlignBottom : Qt.AlignVCenter + } + } + } + + Item { + Layout.fillWidth: true + implicitHeight: memoryMetricRow.implicitHeight + + RowLayout { + id: memoryMetricRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Math.max(3, Math.round(root.pageContentHeight * 0.08)) + + Text { + width: systemMetricValueProbe.implicitWidth + text: qsTr("%1%").arg(provider.memoryUsage) + color: root.primaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.metricFontSize + font.weight: Font.DemiBold + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignRight + elide: Text.ElideRight + } + + Text { + text: qsTr("内存") + color: root.secondaryTextColor + font.pixelSize: root.captionFontSize + font.weight: Font.Medium + renderType: Text.NativeRendering + Layout.alignment: root.showSecondaryLine ? Qt.AlignBottom : Qt.AlignVCenter + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + spacing: root.pageSpacing + visible: root.showSecondaryLine + + Item { + Layout.fillWidth: true + implicitHeight: downloadSpeedTextItem.implicitHeight + + Text { + id: downloadSpeedTextItem + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: systemTrafficProbe.implicitWidth + text: qsTr("%1 ↓").arg(provider.downloadSpeedText) + color: root.secondaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.monitorDetailFontSize + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + } + + Item { + Layout.fillWidth: true + implicitHeight: uploadSpeedTextItem.implicitHeight + + Text { + id: uploadSpeedTextItem + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: systemTrafficProbe.implicitWidth + text: qsTr("%1 ↑").arg(provider.uploadSpeedText) + color: root.secondaryTextColor + font.family: root.dataFontFamily + font.pixelSize: root.monitorDetailFontSize + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + } + } + } + } + } +} diff --git a/panels/dock/package/FashionLeftDockSwitcher.qml b/panels/dock/package/FashionLeftDockSwitcher.qml new file mode 100644 index 000000000..c0753fb99 --- /dev/null +++ b/panels/dock/package/FashionLeftDockSwitcher.qml @@ -0,0 +1,165 @@ +// 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.Layouts 2.15 + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 + +Control { + id: root + + required property var model + + property int currentIndex: 0 + + readonly property int itemCount: model ? model.count : 0 + readonly property bool hasMultipleItems: itemCount > 1 + readonly property var currentDelegate: switcherRepeater.itemAt(currentIndex) + readonly property int switchButtonSize: Math.max(18, Math.round(Panel.rootObject.dockItemMaxSize * 0.52)) + readonly property color switchButtonTextColor: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.9) : Qt.rgba(0, 0, 0, 0.85) + readonly property color switchButtonHoverColor: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.16) : Qt.rgba(0, 0, 0, 0.08) + readonly property color switchButtonPressedColor: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.24) : Qt.rgba(0, 0, 0, 0.14) + readonly property color switchButtonBorderColor: Panel.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(0, 0, 0, 0.08) + + visible: itemCount > 0 + implicitWidth: rowLayout.implicitWidth + implicitHeight: currentDelegate ? currentDelegate.implicitHeight : Panel.rootObject.dockSize + + function switchBy(delta) { + if (!hasMultipleItems) { + return + } + + const nextIndex = (currentIndex + delta + itemCount) % itemCount + currentIndex = nextIndex + } + + onItemCountChanged: { + if (itemCount === 0) { + currentIndex = 0 + } else if (currentIndex >= itemCount) { + currentIndex = itemCount - 1 + } + } + + background: null + + component SwitchButton: Item { + id: buttonRoot + + required property string glyph + property bool hovered: buttonArea.containsMouse + property bool pressed: buttonArea.pressed + + signal clicked() + + implicitWidth: root.switchButtonSize + implicitHeight: root.switchButtonSize + opacity: enabled ? 1 : 0.45 + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: !buttonRoot.enabled ? "transparent" : (buttonRoot.pressed ? root.switchButtonPressedColor : + (buttonRoot.hovered ? root.switchButtonHoverColor : "transparent")) + border.width: (buttonRoot.hovered || buttonRoot.pressed) ? 1 : 0 + border.color: root.switchButtonBorderColor + antialiasing: true + } + + Text { + anchors.centerIn: parent + text: buttonRoot.glyph + color: root.switchButtonTextColor + font.pixelSize: Math.max(14, Math.round(parent.height * 0.6)) + font.weight: Font.Normal + renderType: Text.NativeRendering + } + + MouseArea { + id: buttonArea + anchors.fill: parent + enabled: buttonRoot.enabled + hoverEnabled: true + onClicked: buttonRoot.clicked() + } + } + + contentItem: RowLayout { + id: rowLayout + spacing: Math.max(4, Math.round(Panel.rootObject.dockItemMaxSize * 0.08)) + + SwitchButton { + id: previousButton + visible: root.hasMultipleItems + enabled: root.hasMultipleItems + glyph: "\u2039" + onClicked: root.switchBy(-1) + } + + Item { + id: viewport + Layout.alignment: Qt.AlignVCenter + implicitWidth: root.currentDelegate ? root.currentDelegate.implicitWidth : 0 + implicitHeight: root.currentDelegate ? root.currentDelegate.implicitHeight : Panel.rootObject.dockSize + + Repeater { + id: switcherRepeater + model: root.model + + delegate: Item { + id: delegateRoot + + required property int index + + property var appletItem: model.data + property var attachedAppletItem: null + + visible: index === root.currentIndex + width: implicitWidth + height: implicitHeight + implicitWidth: appletItem ? appletItem.implicitWidth : 0 + implicitHeight: appletItem ? appletItem.implicitHeight : Panel.rootObject.dockSize + + function attachAppletItem() { + if (attachedAppletItem && attachedAppletItem !== appletItem && attachedAppletItem.parent === delegateRoot) { + attachedAppletItem.parent = null + } + + if (appletItem) { + appletItem.parent = delegateRoot + attachedAppletItem = appletItem + } else { + attachedAppletItem = null + } + } + + onAppletItemChanged: attachAppletItem() + + Component.onCompleted: { + attachAppletItem() + } + + Component.onDestruction: { + if (attachedAppletItem && attachedAppletItem.parent === delegateRoot) { + attachedAppletItem.parent = null + } + attachedAppletItem = null + } + } + } + } + + SwitchButton { + id: nextButton + visible: root.hasMultipleItems + enabled: root.hasMultipleItems + glyph: "\u203A" + onClicked: root.switchBy(1) + } + } +} diff --git a/panels/dock/package/SpotlightGradient.png b/panels/dock/package/SpotlightGradient.png new file mode 100644 index 000000000..985fa5395 Binary files /dev/null and b/panels/dock/package/SpotlightGradient.png differ diff --git a/panels/dock/package/SpotlightGradient.png.license b/panels/dock/package/SpotlightGradient.png.license new file mode 100644 index 000000000..570b42d7d --- /dev/null +++ b/panels/dock/package/SpotlightGradient.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. + +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/panels/dock/package/SpotlightGradient.svg b/panels/dock/package/SpotlightGradient.svg new file mode 100644 index 000000000..3b7c1f4ad --- /dev/null +++ b/panels/dock/package/SpotlightGradient.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/panels/dock/package/SpotlightGradient.svg.license b/panels/dock/package/SpotlightGradient.svg.license new file mode 100644 index 000000000..570b42d7d --- /dev/null +++ b/panels/dock/package/SpotlightGradient.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. + +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/panels/dock/package/icons/ai.svg b/panels/dock/package/icons/ai.svg new file mode 100644 index 000000000..3b27dc57c --- /dev/null +++ b/panels/dock/package/icons/ai.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/panels/dock/package/icons/aistudio.svg b/panels/dock/package/icons/aistudio.svg new file mode 100644 index 000000000..350d95b42 --- /dev/null +++ b/panels/dock/package/icons/aistudio.svg @@ -0,0 +1,10 @@ + + + aistudio + + + + + + + \ No newline at end of file diff --git a/panels/dock/package/icons/claude.svg b/panels/dock/package/icons/claude.svg new file mode 100644 index 000000000..b4a4ce7ff --- /dev/null +++ b/panels/dock/package/icons/claude.svg @@ -0,0 +1,9 @@ + + + claude + + + + + + \ No newline at end of file diff --git a/panels/dock/package/icons/doubao.svg b/panels/dock/package/icons/doubao.svg new file mode 100644 index 000000000..c2de62692 --- /dev/null +++ b/panels/dock/package/icons/doubao.svg @@ -0,0 +1,11 @@ + + + doubao + + + + + + + + \ No newline at end of file diff --git a/panels/dock/package/icons/gemini.svg b/panels/dock/package/icons/gemini.svg new file mode 100644 index 000000000..53cf5001a --- /dev/null +++ b/panels/dock/package/icons/gemini.svg @@ -0,0 +1,9 @@ + + + gemini + + + + + + \ No newline at end of file diff --git a/panels/dock/package/icons/media-playback-pause.svg b/panels/dock/package/icons/media-playback-pause.svg new file mode 100644 index 000000000..bd9dfe527 --- /dev/null +++ b/panels/dock/package/icons/media-playback-pause.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/panels/dock/package/icons/media-playback-start.svg b/panels/dock/package/icons/media-playback-start.svg new file mode 100644 index 000000000..8a101d84b --- /dev/null +++ b/panels/dock/package/icons/media-playback-start.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/panels/dock/package/icons/media-skip-backward.svg b/panels/dock/package/icons/media-skip-backward.svg new file mode 100644 index 000000000..6c7e85720 --- /dev/null +++ b/panels/dock/package/icons/media-skip-backward.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/panels/dock/package/icons/media-skip-forward.svg b/panels/dock/package/icons/media-skip-forward.svg new file mode 100644 index 000000000..6a757755d --- /dev/null +++ b/panels/dock/package/icons/media-skip-forward.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/panels/dock/package/icons/openai.svg b/panels/dock/package/icons/openai.svg new file mode 100644 index 000000000..c9d9fee6c --- /dev/null +++ b/panels/dock/package/icons/openai.svg @@ -0,0 +1,9 @@ + + + openai + + + + + + \ No newline at end of file diff --git a/panels/dock/package/icons/qwen.svg b/panels/dock/package/icons/qwen.svg new file mode 100644 index 000000000..c1ac0ed0d --- /dev/null +++ b/panels/dock/package/icons/qwen.svg @@ -0,0 +1,9 @@ + + + qwen + + + + + + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-clear-night.svg b/panels/dock/package/icons/weather-clear-night.svg new file mode 100644 index 000000000..1c56d8c1e --- /dev/null +++ b/panels/dock/package/icons/weather-clear-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-clear.svg b/panels/dock/package/icons/weather-clear.svg new file mode 100644 index 000000000..7da66b94c --- /dev/null +++ b/panels/dock/package/icons/weather-clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-clouds-night.svg b/panels/dock/package/icons/weather-clouds-night.svg new file mode 100644 index 000000000..c6cf30c86 --- /dev/null +++ b/panels/dock/package/icons/weather-clouds-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-clouds.svg b/panels/dock/package/icons/weather-clouds.svg new file mode 100644 index 000000000..cc2e2b511 --- /dev/null +++ b/panels/dock/package/icons/weather-clouds.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-few-clouds-night.svg b/panels/dock/package/icons/weather-few-clouds-night.svg new file mode 100644 index 000000000..7448ada8b --- /dev/null +++ b/panels/dock/package/icons/weather-few-clouds-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-few-clouds.svg b/panels/dock/package/icons/weather-few-clouds.svg new file mode 100644 index 000000000..23f386a4a --- /dev/null +++ b/panels/dock/package/icons/weather-few-clouds.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-fog.svg b/panels/dock/package/icons/weather-fog.svg new file mode 100644 index 000000000..0838f14ce --- /dev/null +++ b/panels/dock/package/icons/weather-fog.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/panels/dock/package/icons/weather-freezing-rain.svg b/panels/dock/package/icons/weather-freezing-rain.svg new file mode 100644 index 000000000..ac8a18bf5 --- /dev/null +++ b/panels/dock/package/icons/weather-freezing-rain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-hail.svg b/panels/dock/package/icons/weather-hail.svg new file mode 100644 index 000000000..13f4cdf1c --- /dev/null +++ b/panels/dock/package/icons/weather-hail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-none-available.svg b/panels/dock/package/icons/weather-none-available.svg new file mode 100644 index 000000000..cc31231ab --- /dev/null +++ b/panels/dock/package/icons/weather-none-available.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/panels/dock/package/icons/weather-overcast-night.svg b/panels/dock/package/icons/weather-overcast-night.svg new file mode 100644 index 000000000..e830d22fb --- /dev/null +++ b/panels/dock/package/icons/weather-overcast-night.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/panels/dock/package/icons/weather-overcast.svg b/panels/dock/package/icons/weather-overcast.svg new file mode 100644 index 000000000..27d0593d9 --- /dev/null +++ b/panels/dock/package/icons/weather-overcast.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/panels/dock/package/icons/weather-showers-day.svg b/panels/dock/package/icons/weather-showers-day.svg new file mode 100644 index 000000000..de8527ee0 --- /dev/null +++ b/panels/dock/package/icons/weather-showers-day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-showers-night.svg b/panels/dock/package/icons/weather-showers-night.svg new file mode 100644 index 000000000..85b1dd9b8 --- /dev/null +++ b/panels/dock/package/icons/weather-showers-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-snow-night.svg b/panels/dock/package/icons/weather-snow-night.svg new file mode 100644 index 000000000..d577ca242 --- /dev/null +++ b/panels/dock/package/icons/weather-snow-night.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/panels/dock/package/icons/weather-snow-rain.svg b/panels/dock/package/icons/weather-snow-rain.svg new file mode 100644 index 000000000..518c9cddc --- /dev/null +++ b/panels/dock/package/icons/weather-snow-rain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-snow.svg b/panels/dock/package/icons/weather-snow.svg new file mode 100644 index 000000000..a27de5494 --- /dev/null +++ b/panels/dock/package/icons/weather-snow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-storm-night.svg b/panels/dock/package/icons/weather-storm-night.svg new file mode 100644 index 000000000..2231272f3 --- /dev/null +++ b/panels/dock/package/icons/weather-storm-night.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/panels/dock/package/icons/weather-storm-tornado.svg b/panels/dock/package/icons/weather-storm-tornado.svg new file mode 100644 index 000000000..41b7c6bc9 --- /dev/null +++ b/panels/dock/package/icons/weather-storm-tornado.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/panels/dock/package/icons/weather-storm.svg b/panels/dock/package/icons/weather-storm.svg new file mode 100644 index 000000000..b752ca579 --- /dev/null +++ b/panels/dock/package/icons/weather-storm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/panels/dock/package/icons/weather-windy.svg b/panels/dock/package/icons/weather-windy.svg new file mode 100644 index 000000000..e76d9daf9 --- /dev/null +++ b/panels/dock/package/icons/weather-windy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/panels/dock/package/main.qml b/panels/dock/package/main.qml index 5d12374f8..7e27cf79f 100644 --- a/panels/dock/package/main.qml +++ b/panels/dock/package/main.qml @@ -8,62 +8,196 @@ import QtQuick.Layouts 2.15 import QtQuick.Window 2.15 import QtQml -import Qt.labs.platform as LP +import Qt.labs.platform 1.1 as LP import org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 +import org.deepin.ds.dock.tray 1.0 as DDT import org.deepin.dtk 1.0 as D import org.deepin.dtk.style 1.0 as DStyle Window { id: dock + readonly property int resizeScreenEdgeMargin: 10 + readonly property int adaptiveFashionLeftWidth: 160 + readonly property int adaptiveFashionMaximumWidth: Math.max(0, Screen.width - resizeScreenEdgeMargin * 2) property int positionForAnimation: Panel.position property bool useColumnLayout: positionForAnimation % 2 - property int dockCenterPartCount: dockCenterPartModel.count - - property int dockRemainingSpaceForCenter: { - const otherCount = dockCenterPartCount - 1; // not include taskmanager - const spacing = useColumnLayout ? gridLayout.rowSpacing : gridLayout.columnSpacing; - - let otherOccupied = 0; - if (otherCount > 0) { - otherOccupied = otherCount * dockItemMaxSize + otherCount * spacing; + readonly property bool adaptiveFashionMode: !useColumnLayout + && Panel.viewMode === Dock.FashionMode + && (positionForAnimation === Dock.Bottom || positionForAnimation === Dock.Top) + readonly property bool adaptiveFashionTopMode: adaptiveFashionMode && positionForAnimation === Dock.Top + readonly property bool adaptiveFashionBottomMode: adaptiveFashionMode && positionForAnimation === Dock.Bottom + readonly property bool adaptiveFashionUsesDirectWindowGeometry: adaptiveFashionMode && Qt.platform.pluginName === "xcb" + readonly property bool useWindowMarginBasedHideAnimation: adaptiveFashionMode + readonly property bool useTransformBasedHideAnimation: Qt.platform.pluginName === "xcb" && !useWindowMarginBasedHideAnimation + readonly property var promotedCenterPluginIds: ["org.deepin.ds.dock.aibar", "org.deepin.ds.dock.searchitem"] + readonly property int fashionDockSpacing: Math.round(dockItemIconSize * 0.75) + readonly property real fashionPartSpacing: adaptiveFashionMode + ? dock.roundToPhysicalPixel(fashionDockSpacing) + : fashionDockSpacing + readonly property int fashionFloatingMargin: adaptiveFashionMode ? 8 : 0 + readonly property int fashionHorizontalPadding: 0 + readonly property int fashionVerticalPadding: adaptiveFashionMode ? Math.max(6, Math.round(dockSize * 0.16)) : 0 + readonly property int fashionBackgroundRadius: adaptiveFashionMode ? Math.round(dockSurfaceThickness / 4) : 0 + readonly property int fashionShadowRadius: adaptiveFashionMode ? Math.max(52, Math.round(dockSurfaceThickness * 0.8)) : 40 + readonly property int fashionShadowVerticalOffset: adaptiveFashionMode ? Math.max(6, Math.round(fashionBackgroundRadius * 0.35)) : 0 + readonly property bool useExternalFashionAutoHideSurface: adaptiveFashionMode && useTransformBasedHideAnimation + property real animatedDockWindowMargin: adaptiveFashionMode ? fashionFloatingMargin : 0 + readonly property real panelDevicePixelRatio: Panel.devicePixelRatio > 0 ? Panel.devicePixelRatio : Screen.devicePixelRatio + readonly property bool darkTheme: Panel.colorTheme === Dock.Dark + readonly property var appearanceApplet: DS.applet("org.deepin.ds.dde-appearance") + readonly property real appearanceOpacity: appearanceApplet ? appearanceApplet.opacity : -1 + readonly property real dockBackgroundOpacity: appearanceOpacity >= 0 ? appearanceOpacity : (darkTheme ? 85 / 255 : 0.6) + readonly property color dockBlurBlendColor: darkTheme + ? Qt.rgba(10 / 255, 10 / 255, 10 / 255, dockBackgroundOpacity) + : Qt.rgba(235 / 255.0, 235 / 255.0, 235 / 255.0, dockBackgroundOpacity) + readonly property color dockNoBlurBaseColor: darkTheme + ? DStyle.Style.behindWindowBlur.darkNoBlurColor + : DStyle.Style.behindWindowBlur.lightNoBlurColor + readonly property color dockNoBlurBlendColor: Qt.rgba(dockNoBlurBaseColor.r, + dockNoBlurBaseColor.g, + dockNoBlurBaseColor.b, + (appearanceOpacity >= 0 ? dockBackgroundOpacity : dockNoBlurBaseColor.a)) + readonly property color fashionShadowColor: darkTheme ? + Qt.rgba(0, 0, 0, 0.34) : + Qt.rgba(0, 0, 0, 0.2) + readonly property color fashionInnerBorderTopColor: darkTheme + ? Qt.rgba(1, 1, 1, 0.10) + : Qt.rgba(1, 1, 1, 0.30) + readonly property color fashionInnerBorderBottomColor: darkTheme + ? Qt.rgba(1, 1, 1, 0.05) + : Qt.rgba(1, 1, 1, 0.10) + readonly property bool useTopRoundedFashionBackground: false + readonly property int dockSurfaceThickness: useColumnLayout ? dockSize : (adaptiveFashionMode ? dockSize + fashionVerticalPadding * 2 : dockSize) + readonly property int windowThickness: dockSize + readonly property int exclusionZoneThickness: dock.windowThickness + (adaptiveFashionMode ? fashionFloatingMargin : 0) + readonly property real adaptiveFashionGridDisplayedWidth: adaptiveFashionMode + ? dock.ceilToPhysicalPixel(gridLayout.implicitWidth) + : 0 + readonly property real adaptiveFashionGridTargetWidth: adaptiveFashionMode + ? dock.ceilToPhysicalPixel(Math.max(0, + gridLayout.implicitWidth + - dockCenterPart.implicitWidth + + dockCenterPart.targetImplicitWidth)) + : 0 + readonly property real adaptiveFashionGridDisplayedHeight: adaptiveFashionMode + ? dock.ceilToPhysicalPixel(gridLayout.implicitHeight) + : 0 + readonly property real adaptiveFashionLeftPartWidth: adaptiveFashionMode && dockLeftPart.visible + ? dock.ceilToPhysicalPixel(dockLeftPart.implicitWidth) + : 0 + readonly property real adaptiveFashionRightPartWidth: adaptiveFashionMode && dockRightPart.visible + ? dock.ceilToPhysicalPixel(dockRightPart.targetImplicitWidth > 0 ? dockRightPart.targetImplicitWidth : dockRightPart.implicitWidth) + : 0 + readonly property real adaptiveFashionAvailableCenterWidth: adaptiveFashionMode + ? Math.max(0, + adaptiveFashionMaximumWidth + - adaptiveFashionLeftPartWidth + - adaptiveFashionRightPartWidth + - (adaptiveFashionLeftPartWidth > 0 ? fashionPartSpacing : 0) + - (adaptiveFashionRightPartWidth > 0 ? fashionPartSpacing : 0)) + : 0 + readonly property real adaptiveFashionAvailableTaskManagerWidth: adaptiveFashionMode + ? Math.max(0, + adaptiveFashionAvailableCenterWidth + - Math.max(0, dockCenterPart.implicitWidth - dockCenterPart.taskmanagerAppContainerWidth)) + : 0 + readonly property real adaptiveDockContentWidth: { + if (!adaptiveFashionMode) { + return 0 } - if (useColumnLayout) { - return Screen.height - dockLeftPart.implicitHeight - dockRightPart.implicitHeight - otherOccupied; - } else { - return Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth - otherOccupied; + let width = adaptiveFashionGridDisplayedWidth + if (dockRightPart.visible) { + if (width > 0) { + width += fashionPartSpacing + } + width += dockRightPart.implicitWidth } + return width } + readonly property real adaptiveDockContentTargetWidth: { + if (!adaptiveFashionMode) { + return 0 + } + + let width = adaptiveFashionGridTargetWidth + if (dockRightPart.visible) { + const rightWidth = dockRightPart.targetImplicitWidth + if (rightWidth > 0) { + if (width > 0) { + width += fashionPartSpacing + } + width += rightWidth + } + } - property int dockPartSpacing: gridLayout.columnSpacing + return width + } + readonly property int adaptiveFashionWidthAnimationDuration: 200 + readonly property int adaptiveFashionWidthAnimationEasingType: DDT.TraySortOrderModel.collapsed || !DDT.TraySortOrderModel.isCollapsing + ? Easing.OutQuad + : Easing.InQuad + readonly property bool adaptiveFashionWidthAnimationEnabled: adaptiveFashionMode + && dock.adaptiveFashionUsesDirectWindowGeometry + && !dock.isDragging + && !Panel.isResizing + && !DDT.TraySortOrderModel.actionsAlwaysVisible + property real adaptiveDockShellWidth: 0 + // TODO: 临时溢出逻辑,待后面修改 + property int dockLeftSpaceForCenter: adaptiveFashionMode ? 0 : (useColumnLayout ? + (Screen.height - dockLeftPart.implicitHeight - dockRightPart.implicitHeight) : + (Screen.width - dockLeftPart.implicitWidth - dockRightPart.implicitWidth)) + property int dockRemainingSpaceForCenter: adaptiveFashionMode ? 0 : (useColumnLayout ? + (Screen.height / 1.8 - dockRightPart.implicitHeight) : + (Screen.width / 1.8 - dockRightPart.implicitWidth)) + property real dockPartSpacing: adaptiveFashionMode ? fashionPartSpacing : gridLayout.columnSpacing // TODO signal dockCenterPartPosChanged() signal pressedAndDragging(bool isDragging) signal viewDeactivated() + property int dockCenterPartCount: dockCenterPartModel.count + + property int preferredDockSize: Applet.dockSize property int dockSize: Applet.dockSize property int dockItemMaxSize: dockSize property int itemIconSizeBase: 0 property int itemSpacing: 0 property bool isDragging: false + property real spotlightX: 0 + property real spotlightY: 0 + readonly property int spotlightHideGraceInterval: 180 + readonly property bool spotlightActive: Panel.containsMouse || spotlightTracker.hovered || spotlightHideGraceTimer.running property real dockItemIconSize: dockItemMaxSize * 9 / 14 + readonly property real spotlightBaseRadius: Math.max(264, dockItemMaxSize * (adaptiveFashionMode ? 8.0 : 6.8)) + readonly property real spotlightCoreRadius: Math.max(96, dockItemMaxSize * (adaptiveFashionMode ? 2.8 : 2.4)) + readonly property real spotlightHorizontalRadius: useColumnLayout + ? Math.max(dockSurfaceThickness * 2.24, spotlightBaseRadius * 1.56) + : spotlightBaseRadius + readonly property real spotlightVerticalRadius: useColumnLayout + ? spotlightBaseRadius + : Math.max(dockSurfaceThickness * 2.12, spotlightBaseRadius * 1.3) + readonly property real spotlightOpacity: darkTheme ? 0.14 : 0.2 + readonly property real spotlightCoreOpacity: darkTheme ? 0.23 : 0.32 + readonly property color dockWindowBorderColor: darkTheme ? + Qt.rgba(0, 0, 0, dock.dockBackgroundOpacity + 20 / 255) : + Qt.rgba(0, 0, 0, 0.15) // NOTE: -1 means not set its size, follow the platform size - width: positionForAnimation === Dock.Top || positionForAnimation === Dock.Bottom ? -1 : dockSize - height: positionForAnimation === Dock.Left || positionForAnimation === Dock.Right ? -1 : dockSize - color: "transparent" - flags: Qt.WindowDoesNotAcceptFocus + width: { + if (dock.adaptiveFashionUsesDirectWindowGeometry) { + return Math.max(1, dock.adaptiveDockShellWidth + dock.fashionHorizontalPadding * 2) + } - function blendColorAlpha(fallback) { - var appearance = DS.applet("org.deepin.ds.dde-appearance") - if (!appearance || appearance.opacity < 0) - return fallback - return appearance.opacity + return positionForAnimation === Dock.Top || positionForAnimation === Dock.Bottom ? -1 : dockSize } + height: positionForAnimation === Dock.Left || positionForAnimation === Dock.Right ? -1 : windowThickness + color: "transparent" + flags: Qt.WindowDoesNotAcceptFocus function requestShowDockMenu() { // maybe has popup visible, close it. @@ -73,73 +207,538 @@ Window { MenuHelper.openMenu(dockMenuLoader.item) } - DLayerShellWindow.anchors: position2Anchors(positionForAnimation) + function handleDockBackgroundClick(button) { + const lastActive = MenuHelper.activeMenu + MenuHelper.closeCurrent() + dockMenuLoader.active = true + + if (button === Qt.RightButton) { + if (lastActive !== dockMenuLoader.item) { + requestShowDockMenu() + } + return + } + + if (button === Qt.LeftButton) { + // try to close popup when clicked empty, because dock does not have focus. + Panel.requestClosePopup() + viewDeactivated() + } + } + + function containsItemPoint(item, x, y) { + if (!item || !item.visible || item.width <= 0 || item.height <= 0) { + return false + } + + const mapped = item.mapFromItem(dockContainer, x, y) + return mapped.x >= 0 && mapped.x < item.width && mapped.y >= 0 && mapped.y < item.height + } + + function isDockBlankAreaPoint(x, y) { + if (dock.adaptiveFashionMode) { + return false + } + + if (dock.containsItemPoint(dockTrailingBlankArea, x, y)) { + return true + } + + return Panel.viewMode === Dock.LeftAlignedMode + && dock.containsItemPoint(leftMarginArea, x, y) + } + + function hiddenTransformOffset() { + return (dock.positionForAnimation === Dock.Left || dock.positionForAnimation === Dock.Top) + ? -dock.windowThickness + : dock.windowThickness + } + + function syncDockHiddenGeometryForCurrentMode() { + dock.positionForAnimation = Panel.position + hideShowAnimation.stop() + dockAnimation.stop() + changeDragAreaAnchor() + + if (dock.useWindowMarginBasedHideAnimation) { + dockTransform.x = 0 + dockTransform.y = 0 + dock.animatedDockWindowMargin = dock.restingWindowMargin() + dock.visible = false + Panel.notifyDockPositionChanged(0, 0) + return + } + + if (dock.useTransformBasedHideAnimation) { + if (dock.useColumnLayout) { + dockTransform.x = dock.hiddenTransformOffset() + dockTransform.y = 0 + } else { + dockTransform.x = 0 + dockTransform.y = dock.hiddenTransformOffset() + } + + dock.visible = false + Panel.notifyDockPositionChanged(dockTransform.x, dockTransform.y) + return + } + + dockTransform.x = 0 + dockTransform.y = 0 + dock.animatedDockWindowMargin = dock.restingWindowMargin() + dock.visible = false + Panel.notifyDockPositionChanged(0, 0) + } + + function syncDockShownGeometryForCurrentMode() { + dock.positionForAnimation = Panel.position + hideShowAnimation.stop() + dockAnimation.stop() + changeDragAreaAnchor() + + dockTransform.x = 0 + dockTransform.y = 0 + dock.animatedDockWindowMargin = dock.restingWindowMargin() + dock.visible = true + Panel.notifyDockPositionChanged(0, 0) + shownContentSyncTimer.restart() + } + + function syncDockPresentationForCurrentMode() { + dock.scheduleAdaptiveFashionDockSizeSync() + + if (Panel.hideState === Dock.Hide) { + dock.syncDockHiddenGeometryForCurrentMode() + } else { + dock.syncDockShownGeometryForCurrentMode() + } + + dock.scheduleFrontendGeometrySync() + } + + function clampSpotlightPosition(value, maximum) { + return Math.max(0, Math.min(maximum, value)) + } + + function hiddenWindowMargin() { + return dock.fashionFloatingMargin + - dock.windowThickness + - dock.fashionShadowRadius + - dock.fashionShadowVerticalOffset + - 4 + } + + function restingWindowMargin() { + if (!dock.adaptiveFashionMode) { + return 0 + } + + return Panel.hideState !== Dock.Hide ? dock.fashionFloatingMargin : dock.hiddenWindowMargin() + } + + function adaptiveHorizontalMarginForContentWidth(contentWidth) { + if (!adaptiveFashionMode) { + return 0 + } + + const desiredWidth = Math.max(0, contentWidth) + fashionHorizontalPadding * 2 + return Math.max(0, Math.floor((adaptiveFashionMaximumWidth - desiredWidth) / 2)) + } + + function roundToPhysicalPixel(value) { + if (!Number.isFinite(value) || dock.panelDevicePixelRatio <= 0) { + return value + } + + return Math.round(value * dock.panelDevicePixelRatio) / dock.panelDevicePixelRatio + } + + function ceilToPhysicalPixel(value) { + if (!Number.isFinite(value) || dock.panelDevicePixelRatio <= 0) { + return value + } + + return Math.ceil(value * dock.panelDevicePixelRatio) / dock.panelDevicePixelRatio + } + + function appletPluginId(item) { + return item && item.applet ? item.applet.pluginId : "" + } + + function dockOrderInRange(item, leftDockOrder, rightDockOrder) { + const order = Number(item ? item.dockOrder : NaN) + return Number.isFinite(order) && order > leftDockOrder && order <= rightDockOrder + } + + function dockAppletVisible(item) { + return item && (item.shouldVisible === undefined || item.shouldVisible) + } + + function isPromotedCenterApplet(item) { + return adaptiveFashionMode && promotedCenterPluginIds.indexOf(appletPluginId(item)) >= 0 + } + + function acceptAdaptiveLeftDockApplet(item) { + const appletItem = item.data + return dockAppletVisible(appletItem) && dockOrderInRange(appletItem, 0, 10) && !isPromotedCenterApplet(appletItem) + } + + function acceptAdaptiveCenterDockApplet(item) { + const appletItem = item.data + return dockAppletVisible(appletItem) && (dockOrderInRange(appletItem, 10, 20) || isPromotedCenterApplet(appletItem)) + } + + function acceptAdaptiveRightDockApplet(item) { + const appletItem = item.data + return dockAppletVisible(appletItem) + && dockOrderInRange(appletItem, 20, 30) + && appletPluginId(appletItem) !== "org.deepin.ds.dock.showdesktop" + } + + function adaptiveCenterSortOrder(item) { + const appletItem = item ? item.data : null + const pluginId = appletPluginId(appletItem) + switch (pluginId) { + case "org.deepin.ds.dock.launcherapplet": + return 1200 + case "org.deepin.ds.dock.aibar": + return 1300 + case "org.deepin.ds.dock.searchitem": + return 1400 + default: { + const order = Number(appletItem ? appletItem.dockOrder : NaN) + return Number.isFinite(order) ? order * 100 : 0 + } + } + } + + function clampDockSizeByScreenLimit(proposedDockSize) { + if (useColumnLayout || !adaptiveFashionMode || dockSize <= 0) { + return proposedDockSize + } + + const maximumWidth = adaptiveFashionMaximumWidth + if (maximumWidth <= 0) { + return Dock.MIN_DOCK_SIZE + } + + const currentWidth = adaptiveDockContentTargetWidth + fashionHorizontalPadding * 2 + if (currentWidth <= 0) { + return proposedDockSize + } + + const estimatedWidth = currentWidth * proposedDockSize / dockSize + if (estimatedWidth <= maximumWidth) { + return proposedDockSize + } + + return Math.max(Dock.MIN_DOCK_SIZE, + Math.min(Dock.MAX_DOCK_SIZE, + Math.floor(proposedDockSize * maximumWidth / estimatedWidth))) + } + + function syncAdaptiveFashionDockSize() { + if (!adaptiveFashionMode || useColumnLayout) { + if (dock.dockSize !== dock.preferredDockSize) { + dock.dockSize = dock.preferredDockSize + } + return + } + + if (dock.preferredDockSize <= 0) { + return + } + + const fittedDockSize = dock.clampDockSizeByScreenLimit(dock.preferredDockSize) + if (dock.dockSize !== fittedDockSize) { + dock.dockSize = fittedDockSize + } + } + + function effectiveAdaptiveFashionShellWidth() { + if (!adaptiveFashionMode) { + return 0 + } + + if (adaptiveDockContentTargetWidth > 0) { + return dock.ceilToPhysicalPixel(adaptiveDockContentTargetWidth) + } + + return dock.ceilToPhysicalPixel(adaptiveDockContentWidth) + } + + function scheduleAdaptiveFashionDockSizeSync() { + if (dock.useColumnLayout) { + return + } + + if (dock._adaptiveFashionDockSizeSyncPending) { + return + } + + dock._adaptiveFashionDockSizeSyncPending = true + Qt.callLater(function() { + dock._adaptiveFashionDockSizeSyncPending = false + dock.syncAdaptiveFashionDockSize() + }) + } + + function scheduleFrontendGeometrySync() { + if (dock._frontendGeometrySyncPending) { + return + } + + dock._frontendGeometrySyncPending = true + Qt.callLater(function() { + dock._frontendGeometrySyncPending = false + Panel.notifyDockPositionChanged(0, 0) + }) + } + + function refreshAdaptiveFashionGeometry() { + dock.syncDockPresentationForCurrentMode() + } + + function setDockViewMode(mode) { + Applet.viewMode = mode + } + + Path { + id: topRoundedFashionWindowClipPath + startX: 0 + startY: dock.height + + PathLine { + x: 0 + y: dock.fashionBackgroundRadius + } + PathArc { + x: dock.fashionBackgroundRadius + y: 0 + radiusX: dock.fashionBackgroundRadius + radiusY: dock.fashionBackgroundRadius + direction: PathArc.Clockwise + } + PathLine { + x: dock.width - dock.fashionBackgroundRadius + y: 0 + } + PathArc { + x: dock.width + y: dock.fashionBackgroundRadius + radiusX: dock.fashionBackgroundRadius + radiusY: dock.fashionBackgroundRadius + direction: PathArc.Clockwise + } + PathLine { + x: dock.width + y: dock.height + } + PathLine { + x: 0 + y: dock.height + } + } + + DLayerShellWindow.anchors: dock.adaptiveFashionUsesDirectWindowGeometry + ? (dock.adaptiveFashionTopMode ? DLayerShellWindow.AnchorTop : DLayerShellWindow.AnchorBottom) + : position2Anchors(positionForAnimation) DLayerShellWindow.layer: DLayerShellWindow.LayerTop - DLayerShellWindow.exclusionZone: Panel.hideMode === Dock.KeepShowing ? Applet.dockSize : 0 + DLayerShellWindow.exclusionZone: Panel.hideMode === Dock.KeepShowing ? dock.exclusionZoneThickness : 0 + DLayerShellWindow.leftMargin: dock.adaptiveFashionUsesDirectWindowGeometry + ? 0 + : (adaptiveFashionMode ? dock.adaptiveHorizontalMarginForContentWidth(dock.adaptiveDockShellWidth) : 0) + DLayerShellWindow.rightMargin: dock.adaptiveFashionUsesDirectWindowGeometry + ? 0 + : (adaptiveFashionMode ? dock.adaptiveHorizontalMarginForContentWidth(dock.adaptiveDockShellWidth) : 0) + DLayerShellWindow.topMargin: dock.adaptiveFashionTopMode + ? (dock.useWindowMarginBasedHideAnimation + ? dock.animatedDockWindowMargin + : dock.restingWindowMargin()) + : 0 + DLayerShellWindow.bottomMargin: dock.adaptiveFashionBottomMode + ? (dock.useWindowMarginBasedHideAnimation + ? dock.animatedDockWindowMargin + : dock.restingWindowMargin()) + : 0 DLayerShellWindow.scope: "dde-shell/dock" DLayerShellWindow.keyboardInteractivity: DLayerShellWindow.KeyboardInteractivityOnDemand D.DWindow.enabled: true - D.DWindow.windowRadius: 0 + D.DWindow.windowRadius: useTopRoundedFashionBackground ? 0 : fashionBackgroundRadius + D.DWindow.clipPath: useTopRoundedFashionBackground ? topRoundedFashionWindowClipPath : null //TODO:由于windoweffect处理有BUG,导致动画结束后一致保持无阴影,无borderwidth状态。 无法恢复到最初的阴影和边框 //D.DWindow.windowEffect: hideShowAnimation.running ? D.PlatformHandle.EffectNoShadow | D.PlatformHandle.EffectNoBorder : 0 // 目前直接处理shadowColor(透明和默认值的切换)和borderWidth(0和1的切换),来控制阴影和边框 // 参数默认值见: https://github.com/linuxdeepin/qt5platform-plugins/blob/master/xcb/dframewindow.h#L122 // 需要注意,shadowRadius不能直接套用于“扩散”参数,拿到不透明度100%的设计图确定radius更合适一些。 - D.DWindow.shadowColor: (hideShowAnimation.running || dockAnimation.running) ? Qt.rgba(0, 0, 0, 0) : Qt.rgba(0, 0, 0, 0.1) - D.DWindow.shadowOffset: Qt.point(0, 0) - D.DWindow.shadowRadius: 40 - D.DWindow.borderWidth: (hideShowAnimation.running || dockAnimation.running) ? 0 : 1 + D.DWindow.shadowColor: { + if (dockAnimation.running) { + return Qt.rgba(0, 0, 0, 0) + } + + if (dock.adaptiveFashionMode) { + return dock.fashionShadowColor + } + + if (hideShowAnimation.running) { + return Qt.rgba(0, 0, 0, 0) + } + + return Qt.rgba(0, 0, 0, 0.1) + } + D.DWindow.shadowOffset: dock.adaptiveFashionMode ? Qt.point(0, dock.fashionShadowVerticalOffset) : Qt.point(0, 0) + D.DWindow.shadowRadius: dock.adaptiveFashionMode ? dock.fashionShadowRadius : 40 + D.DWindow.borderWidth: dock.adaptiveFashionMode ? 1 : ((hideShowAnimation.running || dockAnimation.running) ? 0 : 1) D.DWindow.enableBlurWindow: Qt.platform.pluginName !== "xcb" D.DWindow.themeType: Panel.colorTheme - D.DWindow.borderColor: D.DTK.themeType === D.ApplicationHelper.DarkType ? Qt.rgba(0, 0, 0, dock.blendColorAlpha(0.6) + 20 / 255) : Qt.rgba(0, 0, 0, 0.15) + D.DWindow.borderColor: dock.dockWindowBorderColor D.ColorSelector.family: D.Palette.CrystalColor onDockSizeChanged: { - if (dock.dockSize === Dock.MIN_DOCK_SIZE) { + if (dock.adaptiveFashionMode) { + Panel.indicatorStyle = Dock.Fashion + } else if (dock.dockSize === Dock.MIN_DOCK_SIZE) { Panel.indicatorStyle = Dock.Efficient } else { Panel.indicatorStyle = Dock.Fashion } } + onAdaptiveFashionModeChanged: { + updateAppItems() + dock.refreshAdaptiveFashionGeometry() + } + + onAdaptiveDockContentWidthChanged: { + if (dock.adaptiveFashionMode) { + Panel.notifyDockPositionChanged(0, 0) + } + } + onAdaptiveDockContentTargetWidthChanged: { + dock.scheduleAdaptiveFashionDockSizeSync() + } + onAdaptiveFashionMaximumWidthChanged: dock.scheduleAdaptiveFashionDockSizeSync() + onWidthChanged: { + if (dock.adaptiveFashionMode) { + dock.scheduleAdaptiveFashionDockSizeSync() + } + } + + property bool _adaptiveFashionDockSizeSyncPending: false + property bool _frontendGeometrySyncPending: false + + Binding { + target: dock + property: "adaptiveDockShellWidth" + value: dock.effectiveAdaptiveFashionShellWidth() + restoreMode: Binding.RestoreNone + } + + Behavior on adaptiveDockShellWidth { + enabled: dock.adaptiveFashionWidthAnimationEnabled + NumberAnimation { + duration: dock.adaptiveFashionWidthAnimationDuration + easing.type: dock.adaptiveFashionWidthAnimationEasingType + } + } + Binding on itemIconSizeBase { when: !isDragging value: dockItemMaxSize restoreMode: Binding.RestoreNone } - PropertyAnimation { + QtObject { 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" - target: useTransformBasedAnimation ? dockTransform : dock; - property: { - if (useTransformBasedAnimation) return dock.useColumnLayout ? "x" : "y"; - return dock.useColumnLayout ? "width" : "height"; + readonly property bool useWindowMarginBasedAnimation: dock.useWindowMarginBasedHideAnimation + readonly property bool useTransformBasedAnimation: dock.useTransformBasedHideAnimation + readonly property int showDuration: useWindowMarginBasedAnimation ? 120 : 160 + readonly property int hideDuration: useWindowMarginBasedAnimation ? 180 : 220 + readonly property bool running: hideShowMarginAnimation.running + || hideShowTransformAnimation.running + || hideShowSizeAnimation.running + + function stop() { + hideShowMarginAnimation.stop() + hideShowTransformAnimation.stop() + hideShowSizeAnimation.stop() } - to: { - if (useTransformBasedAnimation) return Panel.hideState !== Dock.Hide ? 0 : ((dock.positionForAnimation === Dock.Left || dock.positionForAnimation === Dock.Top) ? -Panel.dockSize : Panel.dockSize); - return Panel.hideState !== Dock.Hide ? Panel.dockSize : 1; + + function finalize() { + dock.visible = Panel.hideState !== Dock.Hide + if (dock.visible) { + shownContentSyncTimer.restart() + } } - duration: 500 - easing.type: Easing.OutCubic - onStarted: { + + function start() { dock.visible = true - } - onStopped: { + + if (useWindowMarginBasedAnimation) { + hideShowMarginAnimation.stop() + hideShowMarginAnimation.from = dock.animatedDockWindowMargin + hideShowMarginAnimation.to = dock.restingWindowMargin() + hideShowMarginAnimation.duration = Panel.hideState !== Dock.Hide ? showDuration : hideDuration + hideShowMarginAnimation.start() + return + } + if (useTransformBasedAnimation) { - dock.visible = ((dock.useColumnLayout ? dockTransform.x : dockTransform.y) === 0) - } else { - dock.visible = ((dock.useColumnLayout ? dock.width : dock.height) !== 1) + const transformProperty = dock.useColumnLayout ? "x" : "y" + hideShowTransformAnimation.stop() + hideShowTransformAnimation.property = transformProperty + hideShowTransformAnimation.from = dock.useColumnLayout ? dockTransform.x : dockTransform.y + hideShowTransformAnimation.to = Panel.hideState !== Dock.Hide ? 0 : dock.hiddenTransformOffset() + hideShowTransformAnimation.duration = Panel.hideState !== Dock.Hide ? showDuration : hideDuration + hideShowTransformAnimation.start() + return } + + const sizeProperty = dock.useColumnLayout ? "width" : "height" + hideShowSizeAnimation.stop() + hideShowSizeAnimation.property = sizeProperty + hideShowSizeAnimation.from = dock.useColumnLayout ? dock.width : dock.height + hideShowSizeAnimation.to = Panel.hideState !== Dock.Hide ? dock.windowThickness : 1 + hideShowSizeAnimation.duration = Panel.hideState !== Dock.Hide ? showDuration : hideDuration + hideShowSizeAnimation.start() + } + + function restart() { + stop() + start() } } + PropertyAnimation { + id: hideShowMarginAnimation + target: dock + property: "animatedDockWindowMargin" + easing.type: Easing.OutCubic + onStopped: hideShowAnimation.finalize() + } + + PropertyAnimation { + id: hideShowTransformAnimation + target: dockTransform + property: "x" + easing.type: Easing.OutCubic + onStopped: hideShowAnimation.finalize() + } + + PropertyAnimation { + id: hideShowSizeAnimation + target: dock + property: "height" + easing.type: Easing.OutCubic + onStopped: hideShowAnimation.finalize() + } + Connections { target: dockTransform - enabled: Qt.platform.pluginName === "xcb" && hideShowAnimation.running + enabled: hideShowAnimation.useTransformBasedAnimation && hideShowAnimation.running function onXChanged() { if (dock.useColumnLayout) { @@ -156,7 +755,18 @@ Window { Connections { target: dock - enabled: Qt.platform.pluginName !== "xcb" && hideShowAnimation.running + enabled: hideShowAnimation.useWindowMarginBasedAnimation && hideShowAnimation.running + + function onAnimatedBottomMarginChanged() { + Panel.notifyDockPositionChanged(0, 0) + } + } + + Connections { + target: dock + enabled: !hideShowAnimation.useTransformBasedAnimation + && !hideShowAnimation.useWindowMarginBasedAnimation + && hideShowAnimation.running function onWidthChanged() { if (dock.useColumnLayout) { @@ -173,7 +783,7 @@ Window { Timer { id: hideTimer - interval: 500 + interval: 1 running: false repeat: false onTriggered: { @@ -182,6 +792,22 @@ Window { } } + Timer { + id: shownContentSyncTimer + interval: 0 + running: false + repeat: false + onTriggered: { + if (!dock.visible || Panel.hideState === Dock.Hide) { + return + } + + updateAppItems() + dock.scheduleAdaptiveFashionDockSizeSync() + dock.scheduleFrontendGeometrySync() + } + } + SequentialAnimation { id: dockAnimation property bool useTransformBasedAnimation: Qt.platform.pluginName === "xcb" @@ -200,7 +826,7 @@ Window { function setTransformToHiddenPosition() { if (useTransformBasedAnimation) { - var hideOffset = (Panel.position === Dock.Left || Panel.position === Dock.Top) ? -Panel.dockSize : Panel.dockSize; + var hideOffset = (Panel.position === Dock.Left || Panel.position === Dock.Top) ? -dock.windowThickness : dock.windowThickness; if (dock.useColumnLayout) { dockTransform.x = hideOffset; dockTransform.y = 0; @@ -220,18 +846,18 @@ Window { from: { if (dockAnimation.isShowing) { if (dockAnimation.useTransformBasedAnimation) { - return (dock.positionForAnimation === Dock.Left || dock.positionForAnimation === Dock.Top) ? -Panel.dockSize : Panel.dockSize; + return (dock.positionForAnimation === Dock.Left || dock.positionForAnimation === Dock.Top) ? -dock.windowThickness : dock.windowThickness; } return 1; } - return dockAnimation.useTransformBasedAnimation ? 0 : Panel.dockSize; + return dockAnimation.useTransformBasedAnimation ? 0 : dock.windowThickness; } to: { if (dockAnimation.isShowing) { - return dockAnimation.useTransformBasedAnimation ? 0 : Panel.dockSize; + return dockAnimation.useTransformBasedAnimation ? 0 : dock.windowThickness; } else { if (dockAnimation.useTransformBasedAnimation) { - return (dock.positionForAnimation === Dock.Left || dock.positionForAnimation === Dock.Top) ? -Panel.dockSize : Panel.dockSize; + return (dock.positionForAnimation === Dock.Left || dock.positionForAnimation === Dock.Top) ? -dock.windowThickness : dock.windowThickness; } return 1; } @@ -251,6 +877,10 @@ Window { dock.visible = ((dock.useColumnLayout ? dock.width : dock.height) !== 1); } + if (dock.visible) { + shownContentSyncTimer.restart() + } + dock.positionForAnimation = Panel.position; changeDragAreaAnchor() // If this was a hide animation during position change, prepare for show animation @@ -298,6 +928,10 @@ Window { function updateAppItems() { + if (!dockLeftPartModel || !dockCenterPartModel || !dockRightPartModel) { + return + } + dockLeftPartModel.update() dockCenterPartModel.update() dockRightPartModel.update() @@ -325,14 +959,19 @@ Window { MutuallyExclusiveMenu { title: qsTr("Mode") EnumPropertyMenuItem { - name: qsTr("Classic Mode") - prop: "itemAlignment" - value: Dock.LeftAlignment + name: qsTr("Left-aligned Mode") + prop: "viewMode" + value: Dock.LeftAlignedMode } EnumPropertyMenuItem { name: qsTr("Centered Mode") - prop: "itemAlignment" - value: Dock.CenterAlignment + prop: "viewMode" + value: Dock.CenteredMode + } + EnumPropertyMenuItem { + name: qsTr("Fashion Mode") + prop: "viewMode" + value: Dock.FashionMode } } MutuallyExclusiveMenu { @@ -392,10 +1031,42 @@ Window { } } + Item { + id: fashionAutoHideSurfaceLayer + visible: dock.useExternalFashionAutoHideSurface + width: dock.useColumnLayout ? dock.dockSize : dock.width + height: dock.useColumnLayout ? dock.height : dock.windowThickness + x: dock.useColumnLayout ? dockTransform.x : 0 + y: dock.useColumnLayout ? 0 : dockTransform.y + z: 0 + + D.StyledBehindWindowBlur { + control: fashionAutoHideSurfaceLayer + anchors.fill: parent + cornerRadius: dock.fashionBackgroundRadius + blendColor: { + if (valid) { + return dock.dockBlurBlendColor + } + return dock.dockNoBlurBlendColor + } + } + + FashionBackgroundInnerBorder { + anchors.fill: parent + visible: dock.adaptiveFashionMode + cornerRadius: dock.fashionBackgroundRadius + devicePixelRatio: dock.panelDevicePixelRatio + topColor: dock.fashionInnerBorderTopColor + bottomColor: dock.fashionInnerBorderBottomColor + } + } + Item { id: dockContainer + z: 1 width: dock.useColumnLayout ? dock.dockSize : parent.width - height: dock.useColumnLayout ? parent.height : dock.dockSize + height: dock.useColumnLayout ? parent.height : dock.windowThickness anchors { left: parent.left top: parent.top @@ -407,37 +1078,105 @@ Window { // only add blendColor effect when DWindow.enableBlurWindow is true, // avoid to updating blur area frequently.-- D.StyledBehindWindowBlur { + id: dockBackgroundBlur control: parent anchors.fill: parent - cornerRadius: 0 + visible: !dock.useTopRoundedFashionBackground && !dock.useExternalFashionAutoHideSurface + cornerRadius: dock.adaptiveFashionMode ? dock.fashionBackgroundRadius : 0 blendColor: { if (valid) { - return DStyle.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 dock.dockBlurBlendColor } - return DStyle.Style.control.selectColor(undefined, - DStyle.Style.behindWindowBlur.lightNoBlurColor, - DStyle.Style.behindWindowBlur.darkNoBlurColor) + return dock.dockNoBlurBlendColor } } - TapHandler { - acceptedButtons: Qt.LeftButton | Qt.RightButton - gesturePolicy: TapHandler.WithinBounds - onTapped: function(eventPoint, button) { - let lastActive = MenuHelper.activeMenu - MenuHelper.closeCurrent() - dockMenuLoader.active = true - if (button === Qt.RightButton && lastActive !== dockMenuLoader.item) { - requestShowDockMenu() + FashionBackgroundInnerBorder { + anchors.fill: parent + visible: dock.adaptiveFashionMode + && !dock.useTopRoundedFashionBackground + && !dock.useExternalFashionAutoHideSurface + cornerRadius: dock.fashionBackgroundRadius + devicePixelRatio: dock.panelDevicePixelRatio + topColor: dock.fashionInnerBorderTopColor + bottomColor: dock.fashionInnerBorderBottomColor + } + + Item { + id: topRoundedFashionBackground + anchors.fill: parent + visible: dock.useTopRoundedFashionBackground && !dock.useExternalFashionAutoHideSurface + + Item { + id: topRoundedFashionSurface + anchors.fill: parent + + D.StyledBehindWindowBlur { + control: topRoundedFashionBackground + anchors.fill: parent + cornerRadius: 0 + blendColor: { + if (valid) { + return dock.dockBlurBlendColor + } + return dock.dockNoBlurBlendColor + } } - if (button === Qt.LeftButton) { - // try to close popup when clicked empty, because dock does not have focus. - Panel.requestClosePopup() - viewDeactivated() + + } + } + + Item { + id: spotlightLayer + anchors.fill: parent + clip: true + visible: dock.spotlightActive || opacity > 0.01 + opacity: dock.spotlightActive ? 1 : 0 + enabled: false + + Behavior on opacity { + NumberAnimation { + duration: 90 + easing.type: Easing.OutQuad } } + + Image { + id: spotlightImage + width: dock.spotlightHorizontalRadius * 2 + height: dock.spotlightVerticalRadius * 2 + x: dock.clampSpotlightPosition(dock.spotlightX, spotlightLayer.width) - width / 2 + y: dock.clampSpotlightPosition(dock.spotlightY, spotlightLayer.height) - height / 2 + source: "SpotlightGradient.png" + fillMode: Image.Stretch + smooth: true + cache: true + asynchronous: false + opacity: dock.spotlightOpacity + } + + Image { + id: spotlightCoreImage + width: dock.spotlightCoreRadius * 2 + height: dock.spotlightCoreRadius * 2 + x: dock.clampSpotlightPosition(dock.spotlightX, spotlightLayer.width) - width / 2 + y: dock.clampSpotlightPosition(dock.spotlightY, spotlightLayer.height) - height / 2 + source: "SpotlightGradient.png" + fillMode: Image.Stretch + smooth: true + cache: true + asynchronous: false + opacity: dock.spotlightCoreOpacity + } + } + + MouseArea { + id: dockBackgroundMouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: function(mouse) { + dock.handleDockBackgroundClick(mouse.button) + } } //Touch screen click @@ -463,7 +1202,57 @@ Window { } HoverHandler { + id: spotlightTracker + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus cursorShape: Qt.ArrowCursor + + onPointChanged: { + const local = point.position + dock.spotlightX = local.x + dock.spotlightY = local.y + } + + onHoveredChanged: { + if (hovered) { + spotlightHideGraceTimer.stop() + const local = point.position + dock.spotlightX = local.x + dock.spotlightY = local.y + return + } + + if (!Panel.containsMouse) { + spotlightHideGraceTimer.restart() + } + } + } + + Connections { + target: Panel + + function onCursorPositionChanged(cursorPosition) { + dock.spotlightX = cursorPosition.x + dock.spotlightY = cursorPosition.y + } + + function onContainsMouseChanged(containsMouse) { + if (containsMouse) { + spotlightHideGraceTimer.stop() + dock.spotlightX = Panel.cursorPosition.x + dock.spotlightY = Panel.cursorPosition.y + return + } + + if (!spotlightTracker.hovered) { + spotlightHideGraceTimer.restart() + } + } + } + + Timer { + id: spotlightHideGraceTimer + interval: dock.spotlightHideGraceInterval + repeat: false } // TODO missing property binding to update ProxyModel's filter and sort opearation. @@ -484,11 +1273,17 @@ Window { //此处为边距区域的点击实践特殊处理。 MouseArea { id: leftMarginArea - width: useColumnLayout ? parent.width : gridLayout.columnSpacing + width: useColumnLayout ? parent.width : (dock.adaptiveFashionMode ? 0 : gridLayout.columnSpacing) height: useColumnLayout ? gridLayout.rowSpacing : parent.height anchors.left: parent.left anchors.top: parent.top - onClicked: { + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: function(mouse) { + if (mouse.button === Qt.RightButton) { + dock.handleDockBackgroundClick(mouse.button) + return + } + let minOrder = Number.MAX_VALUE for (let i = 0; i < Applet.appletItems.rowCount(); i++) { @@ -501,46 +1296,134 @@ Window { } } // TODO: remove GridLayout and use delegatechosser manager all items + DockPartAppletModel { + id: dockRightPartModel + leftDockOrder: 20 + rightDockOrder: 30 + acceptItem: dock.adaptiveFashionMode ? dock.acceptAdaptiveRightDockApplet : null + } + + Component { + id: rightPartContent + + OverflowContainer { + useColumnLayout: dock.useColumnLayout + model: dockRightPartModel + } + } + GridLayout { id: gridLayout - anchors.fill: parent + anchors.left: parent.left + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: dock.adaptiveFashionMode ? 0 : 0 columns: 1 rows: 1 - flow: useColumnLayout ? GridLayout.LeftToRight : GridLayout.TopToBottom - property real itemMargin: Math.max((dockItemIconSize / 48 * 10)) - columnSpacing: dockLeftPartModel.count > 0 ? 10 : itemMargin + flow: dock.useColumnLayout ? GridLayout.LeftToRight : GridLayout.TopToBottom + property real itemMargin: Math.max((dock.dockItemIconSize / 48 * 10)) + columnSpacing: dock.adaptiveFashionMode ? dock.fashionPartSpacing : (dockLeftPartModel.count > 0 ? 10 : itemMargin) rowSpacing: columnSpacing + states: State { + name: "adaptiveFashion" + when: dock.adaptiveFashionMode + + AnchorChanges { + target: gridLayout + anchors.top: undefined + anchors.right: undefined + anchors.bottom: undefined + anchors.verticalCenter: parent.verticalCenter + } + } + + Binding { + target: gridLayout + property: "width" + when: dock.adaptiveFashionMode + value: dock.adaptiveFashionGridDisplayedWidth + } + + Binding { + target: gridLayout + property: "height" + when: dock.adaptiveFashionMode + value: dock.adaptiveFashionGridDisplayedHeight + } + Item { id: leftMargin + visible: !dock.adaptiveFashionMode implicitWidth: 0 implicitHeight: 0 } Item { id: dockLeftPart - visible: dockLeftPartModel.count > 0 - implicitWidth: leftLoader.implicitWidth + visible: dock.adaptiveFashionMode || dockLeftPartModel.count > 0 + readonly property real targetImplicitWidth: implicitWidth + readonly property real displayedImplicitWidth: implicitWidth + implicitWidth: dock.adaptiveFashionMode ? dock.adaptiveFashionLeftWidth : leftLoader.implicitWidth implicitHeight: leftLoader.implicitHeight - OverflowContainer { + width: implicitWidth + Layout.preferredWidth: implicitWidth + Layout.minimumWidth: implicitWidth + Layout.maximumWidth: implicitWidth + + DockPartAppletModel { + id: dockLeftPartModel + leftDockOrder: 0 + rightDockOrder: 10 + acceptItem: dock.adaptiveFashionMode ? dock.acceptAdaptiveLeftDockApplet : null + } + + Loader { id: leftLoader - anchors.fill: parent - useColumnLayout: dock.useColumnLayout - model: DockPartAppletModel { - id: dockLeftPartModel - leftDockOrder: 0 - rightDockOrder: 10 + active: dockLeftPart.visible + sourceComponent: dock.adaptiveFashionMode ? adaptiveFashionLeftPart : legacyFashionLeftPart + } + + Component { + id: legacyFashionLeftPart + + OverflowContainer { + useColumnLayout: dock.useColumnLayout + model: dockLeftPartModel + } + } + + Component { + id: adaptiveFashionLeftPart + + FashionLeftDockArea { } } } Item { id: dockCenterPart - implicitWidth: centerLoader.implicitWidth - implicitHeight: centerLoader.implicitHeight + property var taskmanagerRootObject: { + let applet = DS.applet("org.deepin.ds.dock.taskmanager") + return applet ? applet.rootObject : null + } + + readonly property real taskmanagerImplicitWidth: taskmanagerRootObject ? taskmanagerRootObject.implicitWidth : 0 + readonly property real taskmanagerImplicitHeight: taskmanagerRootObject ? taskmanagerRootObject.implicitHeight : 0 + readonly property real taskmanagerAppContainerWidth: taskmanagerRootObject ? taskmanagerRootObject.appContainerWidth : 0 + readonly property real taskmanagerAppContainerHeight: taskmanagerRootObject ? taskmanagerRootObject.appContainerHeight : 0 + readonly property real taskmanagerAppContainerTargetWidth: taskmanagerRootObject ? taskmanagerRootObject.appContainerTargetWidth : 0 + readonly property real taskmanagerAppContainerTargetHeight: taskmanagerRootObject ? taskmanagerRootObject.appContainerTargetHeight : 0 + + readonly property real targetImplicitWidth: centerLoader.targetImplicitWidth - taskmanagerImplicitWidth + taskmanagerAppContainerTargetWidth + readonly property real displayedImplicitWidth: implicitWidth + implicitWidth: centerLoader.implicitWidth - taskmanagerImplicitWidth + taskmanagerAppContainerWidth + readonly property real targetImplicitHeight: centerLoader.targetImplicitHeight - taskmanagerImplicitHeight + taskmanagerAppContainerTargetHeight + implicitHeight: centerLoader.implicitHeight - taskmanagerImplicitHeight + taskmanagerAppContainerHeight onXChanged: dockCenterPartPosChanged() onYChanged: dockCenterPartPosChanged() - Layout.leftMargin: !useColumnLayout && Panel.itemAlignment === Dock.CenterAlignment ? + Layout.leftMargin: !useColumnLayout && !dock.adaptiveFashionMode && 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.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 @@ -566,37 +1449,103 @@ Window { anchors.fill: parent useColumnLayout: dock.useColumnLayout spacing: dock.itemSpacing - model: DockPartAppletModel { - id: dockCenterPartModel - leftDockOrder: 10 - rightDockOrder: 20 - } + model: dockCenterPartModel + } + + DockPartAppletModel { + id: dockCenterPartModel + leftDockOrder: 10 + rightDockOrder: 20 + acceptItem: dock.adaptiveFashionMode ? dock.acceptAdaptiveCenterDockApplet : null + sortOrderProvider: dock.adaptiveFashionMode ? dock.adaptiveCenterSortOrder : null } } Item { - Layout.fillWidth: true - Layout.fillHeight: true + id: dockTrailingBlankArea + Layout.fillWidth: !dock.adaptiveFashionMode + Layout.fillHeight: !dock.adaptiveFashionMode + visible: !dock.adaptiveFashionMode + } + } + + MouseArea { + id: dockBlankContextMenuArea + anchors.fill: parent + z: dockRightPart.z + 2 + visible: !dock.adaptiveFashionMode + acceptedButtons: Qt.RightButton + preventStealing: true + propagateComposedEvents: true + + onPressed: function(mouse) { + if (!dock.isDockBlankAreaPoint(mouse.x, mouse.y)) { + mouse.accepted = false + } + } + + onClicked: function(mouse) { + if (!dock.isDockBlankAreaPoint(mouse.x, mouse.y)) { + mouse.accepted = false + return + } + + dock.handleDockBackgroundClick(mouse.button) } } Item { id: dockRightPart - implicitWidth: rightLoader.implicitWidth - implicitHeight: rightLoader.implicitHeight + visible: dockRightPartModel.count > 0 + // Align the right edge gap with the fashion mode's vertical inset. + // Keep it within the requested range of [vertical, vertical + 2] + // by using a centered +1px bias. + readonly property int trailingInset: dock.adaptiveFashionMode ? (dock.fashionVerticalPadding + 1) : 0 + readonly property real rightContentTargetWidth: rightLoader.item + ? (rightLoader.item.targetImplicitWidth !== undefined + ? rightLoader.item.targetImplicitWidth + : rightLoader.item.implicitWidth) + : 0 + readonly property real targetImplicitWidth: dock.adaptiveFashionMode + ? dock.ceilToPhysicalPixel(rightContentTargetWidth + trailingInset) + : (rightContentTargetWidth + trailingInset) + implicitWidth: dock.adaptiveFashionMode + ? dock.ceilToPhysicalPixel((rightLoader.item ? rightLoader.item.implicitWidth : 0) + trailingInset) + : ((rightLoader.item ? rightLoader.item.implicitWidth : 0) + trailingInset) + implicitHeight: rightLoader.item ? rightLoader.item.implicitHeight : 0 + width: implicitWidth + height: implicitHeight anchors.right: parent.right anchors.bottom: parent.bottom - OverflowContainer { + + states: State { + name: "adaptiveFashion" + when: dock.adaptiveFashionMode + + AnchorChanges { + target: dockRightPart + anchors.right: undefined + anchors.bottom: undefined + anchors.verticalCenter: parent.verticalCenter + } + } + + Binding { + target: dockRightPart + property: "x" + when: dock.adaptiveFashionMode + value: dock.roundToPhysicalPixel(gridLayout.x + dock.adaptiveFashionGridDisplayedWidth + (dock.adaptiveFashionGridDisplayedWidth > 0 ? dock.fashionPartSpacing : 0)) + } + + Loader { id: rightLoader anchors.fill: parent - useColumnLayout: dock.useColumnLayout - model: DockPartAppletModel { - id: dockRightPartModel - leftDockOrder: 20 - rightDockOrder: 30 - } + active: dockRightPart.visible + sourceComponent: rightPartContent } + } + } MouseArea { @@ -605,6 +1554,7 @@ Window { property int oldDockSize: 0 property list recentDeltas: [] property int averageCount: 5 + z: dockContainer.z + 1 hoverEnabled: true propagateComposedEvents: true enabled: !Panel.locked @@ -622,8 +1572,9 @@ Window { onPressed: function(mouse) { if (Panel.locked) return dock.isDragging = true + Panel.isResizing = true oldMousePos = mapToGlobal(mouse.x, mouse.y) - oldDockSize = dockSize + oldDockSize = dock.preferredDockSize recentDeltas = [] Panel.requestClosePopup() DS.grabMouse(Panel.rootObject, true) @@ -661,8 +1612,15 @@ Window { newDockSize = Math.min(Math.max(oldDockSize - changeAverage, Dock.MIN_DOCK_SIZE), Dock.MAX_DOCK_SIZE) } - if (newDockSize !== dockSize) { - dockSize = newDockSize + newDockSize = dock.clampDockSizeByScreenLimit(newDockSize) + + if (newDockSize !== dock.preferredDockSize) { + dock.preferredDockSize = newDockSize + if (!dock.adaptiveFashionMode || dock.useColumnLayout) { + dock.dockSize = newDockSize + } else { + dock.scheduleAdaptiveFashionDockSizeSync() + } } pressedAndDragging(true) @@ -671,8 +1629,16 @@ Window { onReleased: function(mouse) { if (Panel.locked) return dock.isDragging = false - Applet.dockSize = dockSize + Applet.dockSize = dock.preferredDockSize itemIconSizeBase = dockItemMaxSize + Panel.isResizing = false + pressedAndDragging(false) + DS.grabMouse(Panel.rootObject, false) + } + + onCanceled: { + dock.isDragging = false + Panel.isResizing = false pressedAndDragging(false) DS.grabMouse(Panel.rootObject, false) } @@ -767,8 +1733,16 @@ Window { dockAnimation.startAnimation(true); } } + function onViewModeChanged() { + updateAppItems() + dock.syncDockPresentationForCurrentMode() + } function onDockSizeChanged() { - dock.dockSize = Panel.dockSize + dock.preferredDockSize = Panel.dockSize + if (!dock.adaptiveFashionMode || dock.useColumnLayout) { + dock.dockSize = Panel.dockSize + } + dock.scheduleAdaptiveFashionDockSizeSync() } function onHideStateChanged() { @@ -831,11 +1805,15 @@ Window { } Component.onCompleted: { - Panel.toolTipWindow.D.DWindow.themeType = Qt.binding(function(){ + Panel.toolTipWindow.windowThemeType = Qt.binding(function(){ return Panel.colorTheme }) - Panel.popupWindow.D.DWindow.themeType = Qt.binding(function(){ + Panel.popupWindow.windowThemeType = Qt.binding(function(){ + return Panel.colorTheme + }) + + Panel.menuWindow.windowThemeType = Qt.binding(function(){ return Panel.colorTheme }) @@ -856,7 +1834,6 @@ Window { }) dock.itemIconSizeBase = dock.dockItemMaxSize - dock.visible = Panel.hideState !== Dock.Hide - changeDragAreaAnchor() + dock.syncDockPresentationForCurrentMode() } } diff --git a/panels/dock/package/translations/org.deepin.ds.dock_zh_CN.qm b/panels/dock/package/translations/org.deepin.ds.dock_zh_CN.qm new file mode 120000 index 000000000..ec2ac5551 --- /dev/null +++ b/panels/dock/package/translations/org.deepin.ds.dock_zh_CN.qm @@ -0,0 +1 @@ +/home/shule/src/dde-shell/build-run/panels/dock/org.deepin.ds.dock_zh_CN.qm \ No newline at end of file diff --git a/panels/dock/pluginmanagerextension.cpp b/panels/dock/pluginmanagerextension.cpp index d5e84f10d..5b458bb26 100644 --- a/panels/dock/pluginmanagerextension.cpp +++ b/panels/dock/pluginmanagerextension.cpp @@ -467,12 +467,12 @@ PluginManager::PluginManager(QWaylandCompositor *compositor) : QWaylandCompositorExtensionTemplate(compositor) { auto theme = DGuiApplicationHelper::instance()->applicationTheme(); - QObject::connect(theme, &DPlatformTheme::fontNameChanged, this, &PluginManager::onFontChanged); - QObject::connect(theme, &DPlatformTheme::fontPointSizeChanged, this, &PluginManager::onFontChanged); - QObject::connect(theme, &DPlatformTheme::activeColorChanged, this, &PluginManager::onActiveColorChanged); - QObject::connect(theme, &DPlatformTheme::darkActiveColorChanged, this, &PluginManager::onActiveColorChanged); - QObject::connect(theme, &DPlatformTheme::themeNameChanged, this, &PluginManager::onThemeChanged); - QObject::connect(theme, &DPlatformTheme::iconThemeNameChanged, this, &PluginManager::onThemeChanged); + QObject::connect(theme, &Dtk::Gui::DPlatformTheme::fontNameChanged, this, &PluginManager::onFontChanged); + QObject::connect(theme, &Dtk::Gui::DPlatformTheme::fontPointSizeChanged, this, &PluginManager::onFontChanged); + QObject::connect(theme, &Dtk::Gui::DPlatformTheme::activeColorChanged, this, &PluginManager::onActiveColorChanged); + QObject::connect(theme, &Dtk::Gui::DPlatformTheme::darkActiveColorChanged, this, &PluginManager::onActiveColorChanged); + QObject::connect(theme, &Dtk::Gui::DPlatformTheme::themeNameChanged, this, &PluginManager::onThemeChanged); + QObject::connect(theme, &Dtk::Gui::DPlatformTheme::iconThemeNameChanged, this, &PluginManager::onThemeChanged); } void PluginManager::initialize() diff --git a/panels/dock/showdesktop/package/showdesktop.qml b/panels/dock/showdesktop/package/showdesktop.qml index cd3b18932..7f2d83bca 100644 --- a/panels/dock/showdesktop/package/showdesktop.qml +++ b/panels/dock/showdesktop/package/showdesktop.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 @@ -13,11 +13,13 @@ AppletItem { id: showdesktop readonly property int showDesktopWidth: 10 property bool useColumnLayout: Panel.position % 2 + readonly property bool adaptiveFashionMode: Panel.rootObject && Panel.rootObject.adaptiveFashionMode property int dockSize: Panel.rootObject.dockItemMaxSize 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 && !adaptiveFashionMode + visible: shouldVisible + implicitWidth: shouldVisible ? (useColumnLayout ? Panel.rootObject.dockSize : showDesktopWidth) : 0 + implicitHeight: shouldVisible ? (useColumnLayout ? showDesktopWidth : Panel.rootObject.dockSize) : 0 PanelToolTip { id: toolTip @@ -32,10 +34,22 @@ AppletItem { Rectangle { property D.Palette lineColor: DockPalette.showDesktopLineColor - // Use device pixel ratio to ensure the line is always 1 physical pixel regardless of system scaling - property real devicePixelRatio: Screen.devicePixelRatio + // Keep both thickness and position aligned to physical pixels so + // fractional scales such as 1.75 do not introduce a visible seam. + property real devicePixelRatio: Panel.devicePixelRatio > 0 ? Panel.devicePixelRatio : Screen.devicePixelRatio + function snapToPhysicalPixel(value) { + if (!Number.isFinite(value) || devicePixelRatio <= 0) { + return value + } + + return Math.round(value * devicePixelRatio) / devicePixelRatio + } implicitWidth: useColumnLayout ? showdesktop.implicitWidth : (1 / devicePixelRatio) implicitHeight: useColumnLayout ? (1 / devicePixelRatio) : showdesktop.implicitHeight + width: implicitWidth + height: implicitHeight + x: useColumnLayout ? 0 : snapToPhysicalPixel((parent.width - width) / 2) + y: useColumnLayout ? snapToPhysicalPixel((parent.height - height) / 2) : 0 color: D.ColorSelector.lineColor } diff --git a/panels/dock/taskmanager/CMakeLists.txt b/panels/dock/taskmanager/CMakeLists.txt index 9fcade49d..e42b37c0f 100644 --- a/panels/dock/taskmanager/CMakeLists.txt +++ b/panels/dock/taskmanager/CMakeLists.txt @@ -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 @@ -72,10 +72,14 @@ add_library(dock-taskmanager SHARED ${DBUS_INTERFACES} dockitemmodel.h dockglobalelementmodel.cpp dockglobalelementmodel.h + dockfoldermigrationutils.cpp + dockfoldermigrationutils.h dockgroupmodel.cpp dockgroupmodel.h hoverpreviewproxymodel.cpp hoverpreviewproxymodel.h + popupsortutils.cpp + popupsortutils.h taskmanager.cpp taskmanager.h treelandwindow.cpp @@ -129,3 +133,8 @@ ds_install_package(PACKAGE org.deepin.ds.dock.taskmanager TARGET dock-taskmanage dtk_add_config_meta_files(APPID org.deepin.ds.dock FILES dconfig/org.deepin.ds.dock.taskmanager.json) # compat dtk_add_config_meta_files(APPID org.deepin.dde.shell FILES dconfig/org.deepin.ds.dock.taskmanager.json) ds_handle_package_translation(PACKAGE org.deepin.ds.dock.taskmanager) + +add_dependencies(dock-taskmanager + org.deepin.ds.dock.taskmanager_package + org.deepin.ds.dock.taskmanager_translation +) diff --git a/panels/dock/taskmanager/abstractwindowmonitor.h b/panels/dock/taskmanager/abstractwindowmonitor.h index ddf60c527..8a66bc69f 100644 --- a/panels/dock/taskmanager/abstractwindowmonitor.h +++ b/panels/dock/taskmanager/abstractwindowmonitor.h @@ -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 @@ -42,7 +42,11 @@ class AbstractWindowMonitor : public QAbstractListModel, public AbstractTaskMana void requestUpdateWindowIconGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate = nullptr) const override; virtual void - requestPreview(QAbstractItemModel *sourceModel, QWindow *relativePositionItem, int32_t previewXoffset, int32_t previewYoffset, uint32_t direction) = 0; + requestPreview(QAbstractItemModel *sourceModel, + QWindow *relativePositionItem, + int32_t previewXoffset, + int32_t previewYoffset, + uint32_t direction) = 0; void requestWindowsView(const QModelIndexList &indexes) const override; diff --git a/panels/dock/taskmanager/api/amdbus/org.desktopspec.ApplicationManager1.Application.xml b/panels/dock/taskmanager/api/amdbus/org.desktopspec.ApplicationManager1.Application.xml index 29b5723dd..644350dc4 100644 --- a/panels/dock/taskmanager/api/amdbus/org.desktopspec.ApplicationManager1.Application.xml +++ b/panels/dock/taskmanager/api/amdbus/org.desktopspec.ApplicationManager1.Application.xml @@ -1,3 +1,8 @@ + @@ -18,6 +23,12 @@ + + + + + + - \ No newline at end of file + diff --git a/panels/dock/taskmanager/appitem.cpp b/panels/dock/taskmanager/appitem.cpp index ed92adf94..916706f00 100644 --- a/panels/dock/taskmanager/appitem.cpp +++ b/panels/dock/taskmanager/appitem.cpp @@ -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 @@ -52,20 +52,21 @@ QString AppItem::type() const QString AppItem::icon() const { - if (m_currentActiveWindow.isNull() || m_currentActiveWindow->icon().isEmpty() || (m_desktopfileParser && m_desktopfileParser->isValied().first)) - return m_desktopfileParser ? m_desktopfileParser->desktopIcon() : "application-default-icon"; - else { - return m_currentActiveWindow->icon(); + if (!m_currentActiveWindow.isNull()) { + const QString windowIcon = m_currentActiveWindow->icon().trimmed(); + if (!windowIcon.isEmpty()) { + return windowIcon; + } } - // QString icon; - // if (m_currentActiveWindow) { - // icon = m_currentActiveWindow->icon(); - // } - // if (icon.isEmpty() && m_desktopfileParser && !m_desktopfileParser.isNull()) { - // icon = m_desktopfileParser->desktopIcon(); - // } - // return icon; + if (m_desktopfileParser && m_desktopfileParser->isValied().first) { + const QString desktopIcon = m_desktopfileParser->desktopIcon().trimmed(); + if (!desktopIcon.isEmpty()) { + return desktopIcon; + } + } + + return QStringLiteral("application-default-icon"); } QString AppItem::name() const diff --git a/panels/dock/taskmanager/dconfig/org.deepin.ds.dock.taskmanager.json b/panels/dock/taskmanager/dconfig/org.deepin.ds.dock.taskmanager.json index 3ac48dc91..ba0d7fa16 100644 --- a/panels/dock/taskmanager/dconfig/org.deepin.ds.dock.taskmanager.json +++ b/panels/dock/taskmanager/dconfig/org.deepin.ds.dock.taskmanager.json @@ -88,7 +88,7 @@ "visibility": "private" }, "dockedElements": { - "value": ["desktop/dde-file-manager", "desktop/deepin-app-store", "desktop/org.deepin.browser", "desktop/deepin-mail", "desktop/deepin-terminal", "desktop/dde-calendar", "desktop/deepin-music", "desktop/deepin-editor", "desktop/deepin-calculator", "desktop/org.deepin.dde.control-center"], + "value": ["desktop/dde-file-manager", "folder/$DOWNLOADS", "folder//usr/share/applications", "desktop/deepin-app-store", "desktop/org.deepin.browser", "desktop/deepin-mail", "desktop/deepin-terminal", "desktop/dde-calendar", "desktop/deepin-music", "desktop/deepin-editor", "desktop/deepin-calculator", "desktop/org.deepin.dde.control-center"], "serial": 0, "flags": [], "name": "dockedElements", @@ -96,6 +96,16 @@ "description": "The items which is docked when dock is started.", "permissions": "readwrite", "visibility": "private" + }, + "defaultDockFoldersMigrationVersion": { + "value": 0, + "serial": 0, + "flags": [], + "name": "defaultDockFoldersMigrationVersion", + "name[zh_CN]": "默认驻留文件夹迁移版本", + "description": "Internal version used to migrate the default dock folders once.", + "permissions": "readwrite", + "visibility": "private" } } } diff --git a/panels/dock/taskmanager/desktopfileamparser.cpp b/panels/dock/taskmanager/desktopfileamparser.cpp index 4e480e8c1..ea7dba82e 100644 --- a/panels/dock/taskmanager/desktopfileamparser.cpp +++ b/panels/dock/taskmanager/desktopfileamparser.cpp @@ -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 @@ -13,7 +13,10 @@ #include #include +#include +#include #include +#include Q_LOGGING_CATEGORY(amdesktopfileLog, "org.deepin.dde.shell.dock.amdesktopfile") @@ -50,17 +53,37 @@ DesktopFileAMParser::DesktopFileAMParser(QString id, QObject* parent) connect(&dbusWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](){ m_amIsAvaliable = true; + m_name.clear(); + m_icon.clear(); + m_genericName.clear(); + m_xDeepinVendor.clear(); + m_actions.clear(); + m_isValid = !m_id.isEmpty() && m_applicationInterface && (m_applicationInterface->iD() == m_id); + Q_EMIT nameChanged(); + Q_EMIT genericNameChanged(); + Q_EMIT xDeepinVendorChanged(); + Q_EMIT actionsChanged(); Q_EMIT iconChanged(); }); connect(&dbusWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](){ m_amIsAvaliable = false; + m_name.clear(); + m_icon.clear(); + m_genericName.clear(); + m_xDeepinVendor.clear(); + m_actions.clear(); + Q_EMIT nameChanged(); + Q_EMIT genericNameChanged(); + Q_EMIT xDeepinVendorChanged(); + Q_EMIT actionsChanged(); Q_EMIT iconChanged(); }); qCDebug(amdesktopfileLog()) << "create a am desktopfile object: " << m_id; m_applicationInterface.reset(new Application(AM_DBUS_PATH, id2dbusPath(id), QDBusConnection::sessionBus(), this)); m_isValid = !m_id.isEmpty() && (m_applicationInterface->iD() == m_id); + connectToAmDBusSignal(QStringLiteral("PropertiesChanged"), SLOT(onPropertyChanged(QDBusMessage))); } DesktopFileAMParser::~DesktopFileAMParser() @@ -152,22 +175,24 @@ QString DesktopFileAMParser::identifyWindow(QPointer window) if (!m_amIsAvaliable) return QString(); - auto pidfd = pidfd_open(window->pid(),0); - auto res = DDBusSender().service("org.desktopspec.ApplicationManager1") - .interface("org.desktopspec.ApplicationManager1") - .path("/org/desktopspec/ApplicationManager1") - .method("Identify") - .arg(QDBusUnixFileDescriptor(pidfd)) - .call(); - res.waitForFinished(); + const auto pidfd = pidfd_open(window->pid(), 0); + if (pidfd < 0) { + qCDebug(amdesktopfileLog) << "AM pidfd_open failed for pid:" << window->pid(); + return QString(); + } + + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.desktopspec.ApplicationManager1"), + QStringLiteral("/org/desktopspec/ApplicationManager1"), + QStringLiteral("org.desktopspec.ApplicationManager1"), + QStringLiteral("Identify")); + message << QVariant::fromValue(QDBusUnixFileDescriptor(pidfd)); + const QDBusMessage reply = QDBusConnection::sessionBus().call(message, QDBus::BlockWithGui, 300); close(pidfd); - if (res.isValid()) { - auto reply = res.reply(); - QList data = reply.arguments(); - return data.first().toString(); + if (reply.type() != QDBusMessage::ErrorMessage && !reply.arguments().isEmpty()) { + return reply.arguments().constFirst().toString(); } - qCDebug(amdesktopfileLog()) << "AM failed to identify, reason is: " << res.error().message(); + qCDebug(amdesktopfileLog()) << "AM failed to identify, reason is: " << reply.errorMessage(); return QString(); } @@ -219,17 +244,13 @@ void DesktopFileAMParser::connectToAmDBusSignal(const QString& signalName, const void DesktopFileAMParser::launchByAMTool(const QString &action) { - QProcess process; const auto path = m_applicationInterface->path(); - process.setProcessChannelMode(QProcess::MergedChannels); - process.start("dde-am", {"--by-user", path, action}); - if (!process.waitForFinished()) { - qWarning() << "Failed to launch the path:" << path << process.errorString(); - return; - } else if (process.exitCode() != 0) { - qWarning() << "Failed to launch the path:" << path << process.readAll(); + const QStringList arguments = {QStringLiteral("--by-user"), path, action}; + if (!QProcess::startDetached(QStringLiteral("dde-am"), arguments)) { + qWarning() << "Failed to launch the path:" << path; return; } + qDebug() << "Launch the application path:" << path; } @@ -301,16 +322,20 @@ void DesktopFileAMParser::onPropertyChanged(const QDBusMessage &msg) if (changedProps.contains("Name")) { updateLocalName(); Q_EMIT nameChanged(); - } else if (changedProps.contains("Actions")) { + } + if (changedProps.contains("Actions")) { updateActions(); Q_EMIT actionsChanged(); - } else if (changedProps.contains("GenericName")) { + } + if (changedProps.contains("GenericName")) { updateLocalGenericName(); Q_EMIT genericNameChanged(); - } else if (changedProps.contains("Name")) { - updateLocalName(); - Q_EMIT nameChanged(); - } else if (changedProps.contains("X_Deepin_Vendor")) { + } + if (changedProps.contains("Icons")) { + updateDesktopIcon(); + Q_EMIT iconChanged(); + } + if (changedProps.contains("X_Deepin_Vendor")) { m_xDeepinVendor = m_applicationInterface->x_Deepin_Vendor(); Q_EMIT xDeepinVendorChanged(); } diff --git a/panels/dock/taskmanager/dockfoldermigrationutils.cpp b/panels/dock/taskmanager/dockfoldermigrationutils.cpp new file mode 100644 index 000000000..aa091651c --- /dev/null +++ b/panels/dock/taskmanager/dockfoldermigrationutils.cpp @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dockfoldermigrationutils.h" + +#include +#include +#include + +namespace dock { +namespace { +static const QString DOWNLOADS_PLACEHOLDER = QStringLiteral("folder/$DOWNLOADS"); +static const QString APPLICATIONS_FOLDER_DOCK_ELEMENT = QStringLiteral("folder//usr/share/applications"); +static const QString FILE_MANAGER_DOCK_ELEMENT = QStringLiteral("desktop/dde-file-manager"); + +static QString resolveDockedElement(const QString &element) +{ + if (element != DOWNLOADS_PLACEHOLDER) { + return element; + } + + const QString downloadsPath = QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + if (downloadsPath.isEmpty()) { + return {}; + } + + return QStringLiteral("folder/%1").arg(downloadsPath); +} + +static QSet asSet(const QStringList &elements) +{ + QSet elementSet; + for (const QString &element : elements) { + elementSet.insert(element); + } + + return elementSet; +} +} + +QStringList resolveDockedElements(const QStringList &elements) +{ + QStringList resolvedElements; + + for (const QString &element : elements) { + const QString resolvedElement = resolveDockedElement(element); + if (resolvedElement.isEmpty() || resolvedElements.contains(resolvedElement)) { + continue; + } + + resolvedElements.append(resolvedElement); + } + + return resolvedElements; +} + +QStringList defaultFolderDockedElements() +{ + return resolveDockedElements({DOWNLOADS_PLACEHOLDER, APPLICATIONS_FOLDER_DOCK_ELEMENT}); +} + +bool shouldMigrateDefaultDockFolders(const QStringList &elements) +{ + if (!elements.contains(FILE_MANAGER_DOCK_ELEMENT)) { + return false; + } + + const QSet expectedFolderElements = asSet(defaultFolderDockedElements()); + bool hasDesktopElement = false; + for (const QString &element : elements) { + if (element.startsWith(QStringLiteral("desktop/"))) { + hasDesktopElement = true; + continue; + } + + // Only inject the new default folders when the existing folder entries are + // also defaults. Unknown folder/group items indicate user customization. + if (element.startsWith(QStringLiteral("folder/")) && expectedFolderElements.contains(element)) { + continue; + } + + return false; + } + + return hasDesktopElement; +} + +QStringList mergedWithDefaultDockFolders(const QStringList &elements) +{ + QStringList mergedElements = elements; + int insertIndex = mergedElements.indexOf(FILE_MANAGER_DOCK_ELEMENT); + if (insertIndex < 0) { + insertIndex = -1; + } + + for (const QString &folderElement : defaultFolderDockedElements()) { + const int existingIndex = mergedElements.indexOf(folderElement); + if (existingIndex >= 0) { + insertIndex = existingIndex; + continue; + } + + mergedElements.insert(++insertIndex, folderElement); + } + + return resolveDockedElements(mergedElements); +} + +} diff --git a/panels/dock/taskmanager/dockfoldermigrationutils.h b/panels/dock/taskmanager/dockfoldermigrationutils.h new file mode 100644 index 000000000..ce9c36051 --- /dev/null +++ b/panels/dock/taskmanager/dockfoldermigrationutils.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace dock { + +QStringList resolveDockedElements(const QStringList &elements); +QStringList defaultFolderDockedElements(); +bool shouldMigrateDefaultDockFolders(const QStringList &elements); +QStringList mergedWithDefaultDockFolders(const QStringList &elements); + +} diff --git a/panels/dock/taskmanager/dockglobalelementmodel.cpp b/panels/dock/taskmanager/dockglobalelementmodel.cpp index d7f1eb1f8..3994719b4 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.cpp +++ b/panels/dock/taskmanager/dockglobalelementmodel.cpp @@ -10,10 +10,21 @@ #include #include +#include +#include +#include #include #include #include +#include +#include +#include #include +#include +#include +#include + +#include Q_LOGGING_CATEGORY(dockGlobalElementModelLog, "org.deepin.dde.shell.dock.taskmanager.dockglobalelementmodel") @@ -22,14 +33,446 @@ Q_LOGGING_CATEGORY(dockGlobalElementModelLog, "org.deepin.dde.shell.dock.taskman namespace dock { -DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, DockCombineModel *activeAppModel, QObject *parent) +static QPair splitDockElement(const QString &dockElement) +{ + const int separatorIndex = dockElement.indexOf(QLatin1Char('/')); + if (separatorIndex <= 0) { + return {}; + } + + return {dockElement.left(separatorIndex), dockElement.mid(separatorIndex + 1)}; +} + +static QString localizedDesktopEntryText(QSettings &settings, const QString &key) +{ + if (key.isEmpty()) { + return {}; + } + + QStringList localizedKeys; + const QStringList uiLanguages = QLocale::system().uiLanguages(); + for (const QString &uiLanguage : uiLanguages) { + QString normalizedLanguage = uiLanguage.trimmed(); + if (normalizedLanguage.isEmpty()) { + continue; + } + + normalizedLanguage.replace(QLatin1Char('-'), QLatin1Char('_')); + + const QString fullKey = QStringLiteral("%1[%2]").arg(key, normalizedLanguage); + if (!localizedKeys.contains(fullKey)) { + localizedKeys.append(fullKey); + } + + const int separatorIndex = normalizedLanguage.indexOf(QLatin1Char('_')); + if (separatorIndex > 0) { + const QString baseLanguageKey = QStringLiteral("%1[%2]").arg(key, normalizedLanguage.left(separatorIndex)); + if (!localizedKeys.contains(baseLanguageKey)) { + localizedKeys.append(baseLanguageKey); + } + } + } + + for (const QString &localizedKey : localizedKeys) { + const QString localizedValue = settings.value(localizedKey).toString().trimmed(); + if (!localizedValue.isEmpty()) { + return localizedValue; + } + } + + return settings.value(key).toString().trimmed(); +} + +static QString displayNameForPath(const QString &path) +{ + const auto localizedStandardLocationName = [](QStandardPaths::StandardLocation location) { + const auto englishDefaultName = [](QStandardPaths::StandardLocation standardLocation) -> QString { + switch (standardLocation) { + case QStandardPaths::HomeLocation: + return QStringLiteral("Home"); + case QStandardPaths::DesktopLocation: + return QStringLiteral("Desktop"); + case QStandardPaths::DocumentsLocation: + return QStringLiteral("Documents"); + case QStandardPaths::DownloadLocation: + return QStringLiteral("Download"); + case QStandardPaths::MoviesLocation: + return QStringLiteral("Movies"); + case QStandardPaths::MusicLocation: + return QStringLiteral("Music"); + case QStandardPaths::PicturesLocation: + return QStringLiteral("Pictures"); + case QStandardPaths::TemplatesLocation: + return QStringLiteral("Templates"); + case QStandardPaths::PublicShareLocation: + return QStringLiteral("Public"); + case QStandardPaths::ApplicationsLocation: + return QStringLiteral("Applications"); + default: + return {}; + } + }; + + const QString localizedName = QStandardPaths::displayName(location); + const QString englishName = englishDefaultName(location); + if (!localizedName.isEmpty() + && (englishName.isEmpty() || localizedName.compare(englishName, Qt::CaseInsensitive) != 0)) { + return localizedName; + } + + if (!englishName.isEmpty()) { + const QByteArray englishNameUtf8 = englishName.toUtf8(); + const QString translatedName = QString::fromLocal8Bit(dgettext("xdg-user-dirs", englishNameUtf8.constData())).trimmed(); + if (!translatedName.isEmpty() && translatedName.compare(englishName, Qt::CaseInsensitive) != 0) { + return translatedName; + } + } + + return QString(); + }; + + QFileInfo fileInfo(path); + const QString normalizedPath = QDir::cleanPath(fileInfo.absoluteFilePath().isEmpty() ? path : fileInfo.absoluteFilePath()); + const QString directoryEntryPath = QDir(normalizedPath).filePath(QStringLiteral(".directory")); + if (QFileInfo::exists(directoryEntryPath)) { + QSettings settings(directoryEntryPath, QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + const QString localizedDirectoryName = localizedDesktopEntryText(settings, QStringLiteral("Name")); + if (!localizedDirectoryName.isEmpty()) { + return localizedDirectoryName; + } + } + + if (normalizedPath == QStringLiteral("/usr/share/applications")) { + if (QLocale().language() == QLocale::Chinese) { + return QStringLiteral("应用程序"); + } + + const QString applicationsName = QStandardPaths::displayName(QStandardPaths::ApplicationsLocation); + if (!applicationsName.isEmpty()) { + return applicationsName; + } + } + + const QList standardLocations = { + QStandardPaths::HomeLocation, + QStandardPaths::DesktopLocation, + QStandardPaths::DocumentsLocation, + QStandardPaths::DownloadLocation, + QStandardPaths::MoviesLocation, + QStandardPaths::MusicLocation, + QStandardPaths::PicturesLocation, + QStandardPaths::TemplatesLocation, + QStandardPaths::PublicShareLocation, + }; + for (const QStandardPaths::StandardLocation location : standardLocations) { + const QString locationPath = QDir::cleanPath(QStandardPaths::writableLocation(location)); + if (!locationPath.isEmpty() && locationPath == normalizedPath) { + const QString localizedName = localizedStandardLocationName(location); + if (!localizedName.isEmpty()) { + return localizedName; + } + break; + } + } + + QString name = fileInfo.fileName(); + if (name.isEmpty()) { + name = fileInfo.absoluteFilePath(); + } + if (name.isEmpty()) { + name = path; + } + return name; +} + +struct DesktopEntryPreviewMetadata +{ + QString iconName; + bool hidden = false; +}; + +struct FilePreviewInfo +{ + QString iconName; + bool hidden = false; +}; + +static bool isDesktopEntryFile(const QFileInfo &fileInfo) +{ + return fileInfo.isFile() + && fileInfo.suffix().compare(QStringLiteral("desktop"), Qt::CaseInsensitive) == 0; +} + +static DesktopEntryPreviewMetadata desktopEntryPreviewMetadataForFile(const QFileInfo &fileInfo) +{ + if (!isDesktopEntryFile(fileInfo)) { + return {}; + } + + QSettings settings(fileInfo.absoluteFilePath(), QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + + DesktopEntryPreviewMetadata metadata; + metadata.hidden = settings.value(QStringLiteral("Hidden")).toBool() + || settings.value(QStringLiteral("NoDisplay")).toBool(); + metadata.iconName = settings.value(QStringLiteral("Icon")).toString().trimmed(); + return metadata; +} + +static QString defaultFileIconName(const QFileInfo &fileInfo) +{ + if (fileInfo.isDir()) { + return QStringLiteral("folder"); + } + + static QMimeDatabase mimeDatabase; + const auto mimeType = mimeDatabase.mimeTypeForFile(fileInfo, QMimeDatabase::MatchDefault); + if (!mimeType.iconName().isEmpty()) { + return mimeType.iconName(); + } + if (!mimeType.genericIconName().isEmpty()) { + return mimeType.genericIconName(); + } + + return QStringLiteral("text-x-generic"); +} + +static FilePreviewInfo filePreviewInfo(const QFileInfo &fileInfo) +{ + FilePreviewInfo info; + + if (fileInfo.isDir()) { + info.iconName = QStringLiteral("folder"); + return info; + } + + const DesktopEntryPreviewMetadata desktopEntry = desktopEntryPreviewMetadataForFile(fileInfo); + info.hidden = desktopEntry.hidden; + if (!desktopEntry.iconName.isEmpty()) { + info.iconName = desktopEntry.iconName; + } + + if (info.iconName.isEmpty()) { + info.iconName = defaultFileIconName(fileInfo); + } + + return info; +} + +static QString fileIconName(const QFileInfo &fileInfo) +{ + return filePreviewInfo(fileInfo).iconName; +} + +static QModelIndex findIndexByRole(QAbstractItemModel *model, int role, const QString &value) +{ + if (!model || value.isEmpty()) { + return {}; + } + + return model->match(model->index(0, 0), role, value, 1, Qt::MatchExactly).value(0); +} + +static int modelRole(QAbstractItemModel *model, const QByteArray &roleName, int fallbackRole) +{ + if (!model) { + return fallbackRole; + } + + return model->roleNames().key(roleName, fallbackRole); +} + +static QModelIndex findIndexByNamedRole(QAbstractItemModel *model, + const QByteArray &roleName, + const QString &value, + int fallbackRole) +{ + return findIndexByRole(model, modelRole(model, roleName, fallbackRole), value); +} + +static bool isLauncherFolderId(const QString &launcherId) +{ + return launcherId.startsWith(QStringLiteral("internal/folder/")) || + launcherId.startsWith(QStringLiteral("internal/folders/")) || + launcherId.startsWith(QStringLiteral("internal/group/")); +} + +static QString alternateLauncherFolderId(const QString &launcherId) +{ + const QString singularPrefix = QStringLiteral("internal/folder/"); + const QString pluralPrefix = QStringLiteral("internal/folders/"); + const QString legacyPrefix = QStringLiteral("internal/group/"); + + if (launcherId.startsWith(singularPrefix)) { + return pluralPrefix + launcherId.mid(singularPrefix.size()); + } + + if (launcherId.startsWith(pluralPrefix)) { + return singularPrefix + launcherId.mid(pluralPrefix.size()); + } + + if (launcherId.startsWith(legacyPrefix)) { + return pluralPrefix + launcherId.mid(legacyPrefix.size()); + } + + return {}; +} + +static QString resolveLauncherGroupId(QAbstractItemModel *groupModel, const QString &groupId) +{ + if (!groupModel || !isLauncherFolderId(groupId)) { + return groupId; + } + + const QString suffix = groupId.section(QLatin1Char('/'), -1); + const QStringList candidates = { + groupId, + alternateLauncherFolderId(groupId), + QStringLiteral("internal/folders/%1").arg(suffix), + QStringLiteral("internal/folder/%1").arg(suffix), + QStringLiteral("internal/group/%1").arg(suffix), + suffix + }; + + for (const QString &candidate : candidates) { + if (!candidate.isEmpty() && + findIndexByNamedRole(groupModel, MODEL_DESKTOPID, candidate, TaskManager::DesktopIdRole).isValid()) { + return candidate; + } + } + + return groupId; +} + +static QStringList invokeLauncherGroupItems(QObject *groupModel, const QString &groupId) +{ + if (!groupModel || groupId.isEmpty()) { + return {}; + } + + const QString resolvedGroupId = resolveLauncherGroupId(qobject_cast(groupModel), groupId); + QStringList items; + QMetaObject::invokeMethod(groupModel, + "groupItems", + Q_RETURN_ARG(QStringList, items), + Q_ARG(QString, resolvedGroupId)); + return items; +} + +static QString invokeLauncherGroupDisplayName(QObject *groupModel, const QString &groupId) +{ + if (!groupModel || groupId.isEmpty()) { + return {}; + } + + const QString resolvedGroupId = resolveLauncherGroupId(qobject_cast(groupModel), groupId); + QString displayName; + QMetaObject::invokeMethod(groupModel, + "groupDisplayName", + Q_RETURN_ARG(QString, displayName), + Q_ARG(QString, resolvedGroupId)); + return displayName; +} + +static bool preferChineseLauncherGroupNames() +{ + return QLocale().language() == QLocale::Chinese; +} + +static QString launcherGroupFallbackName() +{ + return preferChineseLauncherGroupNames() ? QStringLiteral("应用组") : TaskManager::tr("App Group"); +} + +static QString translatedLauncherCategoryName(const QString &groupName) +{ + if (!groupName.startsWith(QStringLiteral("internal/category/"))) { + return {}; + } + + bool ok = false; + const int category = groupName.section(QLatin1Char('/'), -1).toInt(&ok); + if (!ok) { + return {}; + } + + if (preferChineseLauncherGroupNames()) { + switch (category) { + case 0: return QStringLiteral("网络"); + case 1: return QStringLiteral("社交"); + case 2: return QStringLiteral("音乐"); + case 3: return QStringLiteral("视频"); + case 4: return QStringLiteral("图形图像"); + case 5: return QStringLiteral("游戏"); + case 6: return QStringLiteral("办公"); + case 7: return QStringLiteral("阅读"); + case 8: return QStringLiteral("编程开发"); + case 9: return QStringLiteral("系统管理"); + case 10: return QStringLiteral("其他"); + default: return {}; + } + } + + switch (category) { + case 0: return TaskManager::tr("Internet"); + case 1: return TaskManager::tr("Chat"); + case 2: return TaskManager::tr("Music"); + case 3: return TaskManager::tr("Video"); + case 4: return TaskManager::tr("Graphics"); + case 5: return TaskManager::tr("Game"); + case 6: return TaskManager::tr("Office"); + case 7: return TaskManager::tr("Reading"); + case 8: return TaskManager::tr("Development"); + case 9: return TaskManager::tr("System"); + case 10: return TaskManager::tr("Others"); + default: return {}; + } +} + +static QStringList previewIconsForDirectory(const QString &path, int limit = 4) +{ + QStringList iconNames; + + QDir directory(path); + const QFileInfoList fileInfos = directory.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot, + QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); + for (const QFileInfo &fileInfo : fileInfos) { + const FilePreviewInfo preview = filePreviewInfo(fileInfo); + if (preview.hidden) { + continue; + } + + iconNames.append(preview.iconName); + if (iconNames.size() >= limit) { + break; + } + } + + return iconNames; +} + +DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, + DockCombineModel *activeAppModel, + QAbstractItemModel *groupModel, + QObject *parent) : QAbstractListModel(parent) , AbstractTaskManagerInterface(nullptr) , m_appsModel(appsModel) , m_activeAppModel(activeAppModel) + , m_groupModel(groupModel) { connect(TaskManagerSettings::instance(), &TaskManagerSettings::dockedElementsChanged, this, &DockGlobalElementModel::loadDockedElements); connect(TaskManagerSettings::instance(), &TaskManagerSettings::windowSplitChanged, this, &DockGlobalElementModel::groupItemsByApp); + if (m_groupModel) { + auto refreshDockedElements = [this]() { + QMetaObject::invokeMethod(this, &DockGlobalElementModel::loadDockedElements, Qt::QueuedConnection); + }; + connect(m_groupModel, &QAbstractItemModel::dataChanged, this, refreshDockedElements, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::modelReset, this, refreshDockedElements, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::rowsInserted, this, refreshDockedElements, Qt::QueuedConnection); + connect(m_groupModel, &QAbstractItemModel::rowsRemoved, this, refreshDockedElements, Qt::QueuedConnection); + } connect( m_appsModel, @@ -39,7 +482,7 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do Q_UNUSED(parent) for (int i = first; i <= last; ++i) { auto it = std::find_if(m_data.begin(), m_data.end(), [this, &i](auto data) { - return std::get<1>(data) == m_appsModel && std::get<2>(data) == i; + return std::get<2>(data) == m_appsModel && std::get<3>(data) == i; }); if (it != m_data.end()) { auto pos = it - m_data.begin(); @@ -49,8 +492,11 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do } } std::for_each(m_data.begin(), m_data.end(), [this, first, last](auto &data) { - if (std::get<1>(data) == m_appsModel && std::get<2>(data) >= first) { - data = std::make_tuple(std::get<0>(data), std::get<1>(data), std::get<2>(data) - ((last - first) + 1)); + if (std::get<2>(data) == m_appsModel && std::get<3>(data) >= first) { + data = std::make_tuple(std::get<0>(data), + std::get<1>(data), + std::get<2>(data), + std::get<3>(data) - ((last - first) + 1)); } }); }, @@ -66,26 +512,26 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do auto index = m_activeAppModel->index(i, 0); auto desktopId = index.data(TaskManager::DesktopIdRole).toString(); - if (desktopId.isEmpty()) + if (desktopId.isEmpty()) continue; - //将同一应用的窗口添加到一起 + // 将同一应用的窗口添加到一起 // Find the first occurrence of this app in m_data (either docked item or existing window) auto firstIt = std::find_if(m_data.begin(), m_data.end(), [&desktopId](const auto &data) { - return std::get<0>(data) == desktopId; + return std::get<0>(data) == QStringLiteral("desktop") && std::get<1>(data) == desktopId; }); if (firstIt == m_data.end()) { // No docked item or existing window yet, append to the end beginInsertRows(QModelIndex(), m_data.size(), m_data.size()); - m_data.append(std::make_tuple(desktopId, m_activeAppModel, i)); + m_data.append(std::make_tuple(QStringLiteral("desktop"), desktopId, m_activeAppModel, i)); endInsertRows(); continue; } // If the first occurrence still comes from m_appsModel, this is the first window: // reuse the docked position and turn it into a running window. - if (std::get<1>(*firstIt) == m_appsModel) { - *firstIt = std::make_tuple(desktopId, m_activeAppModel, i); + if (std::get<2>(*firstIt) == m_appsModel) { + *firstIt = std::make_tuple(QStringLiteral("desktop"), desktopId, m_activeAppModel, i); auto pIndex = this->index(firstIt - m_data.begin(), 0); Q_EMIT dataChanged(pIndex, pIndex, @@ -102,22 +548,23 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do // Search the entire list since windows may not be consecutive after drag reorder. auto lastIt = firstIt; for (auto it = firstIt + 1; it != m_data.end(); ++it) { - if (std::get<0>(*it) == desktopId) { + if (std::get<0>(*it) == QStringLiteral("desktop") && std::get<1>(*it) == desktopId) { lastIt = it; } } auto insertRow = (lastIt - m_data.begin()) + 1; beginInsertRows(QModelIndex(), insertRow, insertRow); - m_data.insert(lastIt + 1, std::make_tuple(desktopId, m_activeAppModel, i)); + m_data.insert(lastIt + 1, std::make_tuple(QStringLiteral("desktop"), desktopId, m_activeAppModel, i)); endInsertRows(); } std::for_each(m_data.begin(), m_data.end(), [this, first, last](auto &data) { - if (std::get<1>(data) == m_activeAppModel && std::get<2>(data) > first) { + if (std::get<2>(data) == m_activeAppModel && std::get<3>(data) > first) { data = std::make_tuple(std::get<0>(data), std::get<1>(data), - std::get<2>(data) + ((last - first) + 1)); + std::get<2>(data), + std::get<3>(data) + ((last - first) + 1)); } }); }, @@ -134,7 +581,7 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do for (int i = first; i <= last; ++i) { auto it = std::find_if(m_data.begin(), m_data.end(), [this, i](auto data) { - return std::get<1>(data) == m_activeAppModel && std::get<2>(data) == i; + return std::get<2>(data) == m_activeAppModel && std::get<3>(data) == i; }); if (it == m_data.end()) { @@ -143,13 +590,19 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do } auto pos = it - m_data.begin(); - auto id = std::get<0>(*it); - - auto oit = std::find_if(m_data.constBegin(), m_data.constEnd(), [this, &id, i](auto &data) { - return std::get<0>(data) == id && std::get<1>(data) == m_activeAppModel && std::get<2>(data) != i; + auto type = std::get<0>(*it); + auto id = std::get<1>(*it); + + auto oit = std::find_if(m_data.constBegin(), m_data.constEnd(), [this, &type, &id, i](auto &data) { + return std::get<0>(data) == type && + std::get<1>(data) == id && + std::get<2>(data) == m_activeAppModel && + std::get<3>(data) != i; }); - if (oit == m_data.constEnd() && m_dockedElements.contains(std::make_tuple("desktop", id))) { + if (type == QStringLiteral("desktop") && + oit == m_data.constEnd() && + m_dockedElements.contains(std::make_tuple(QStringLiteral("desktop"), id))) { auto res = m_appsModel->match(m_appsModel->index(0, 0), TaskManager::DesktopIdRole, id, 1, Qt::MatchExactly); if (res.isEmpty()) { beginRemoveRows(QModelIndex(), pos, pos); @@ -157,7 +610,7 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do endRemoveRows(); } else { auto row = res.first().row(); - *it = std::make_tuple(id, m_appsModel, row); + *it = std::make_tuple(QStringLiteral("desktop"), id, m_appsModel, row); // DEFER emitter until internal model shift is done! pendingDataChangedRows.append(pos); } @@ -170,8 +623,11 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do // Adjust remaining row mappings for the active app model BEFORE any outer access std::for_each(m_data.begin(), m_data.end(), [this, first, last](auto &data) { - if (std::get<1>(data) == m_activeAppModel && std::get<2>(data) >= first) { - data = std::make_tuple(std::get<0>(data), std::get<1>(data), std::get<2>(data) - ((last - first) + 1)); + if (std::get<2>(data) == m_activeAppModel && std::get<3>(data) >= first) { + data = std::make_tuple(std::get<0>(data), + std::get<1>(data), + std::get<2>(data), + std::get<3>(data) - ((last - first) + 1)); } }); @@ -193,7 +649,7 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do int first = topLeft.row(), last = bottomRight.row(); for (int i = first; i <= last; i++) { auto it = std::find_if(m_data.constBegin(), m_data.constEnd(), [this, i](auto data) { - return std::get<1>(data) == m_activeAppModel && std::get<2>(data) == i; + return std::get<2>(data) == m_activeAppModel && std::get<3>(data) == i; }); if (it == m_data.end()) @@ -205,6 +661,7 @@ DockGlobalElementModel::DockGlobalElementModel(QAbstractItemModel *appsModel, Do auto identifyId = roles.indexOf(TaskManager::IdentityRole); if (desktopId != -1 || identifyId != -1) { oldRoles.append(TaskManager::ItemIdRole); + oldRoles.append(TaskManager::DockElementRole); } Q_EMIT dataChanged(index(pos, 0), index(pos, 0), oldRoles); } @@ -218,6 +675,9 @@ QHash DockGlobalElementModel::roleNames() const { return { {TaskManager::ItemIdRole, MODEL_ITEMID}, + {TaskManager::DockElementRole, "dockElement"}, + {TaskManager::ItemKindRole, "itemKind"}, + {TaskManager::PreviewIconsRole, "previewIcons"}, {TaskManager::NameRole, MODEL_NAME}, {TaskManager::IconNameRole, MODEL_ICONNAME}, {TaskManager::ActiveRole, MODEL_ACTIVE}, @@ -249,6 +709,77 @@ int DockGlobalElementModel::columnCount(const QModelIndex &parent) const return 1; } +QString DockGlobalElementModel::dockElementForRow(int row) const +{ + if (row < 0 || row >= m_data.size()) { + return {}; + } + + const auto data = m_data.at(row); + return QStringLiteral("%1/%2").arg(std::get<0>(data), std::get<1>(data)); +} + +QString DockGlobalElementModel::displayNameFor(const QString &type, const QString &id) const +{ + if (type == QStringLiteral("group")) { + QString groupName = invokeLauncherGroupDisplayName(m_groupModel, id); + if (groupName.isEmpty()) { + const QString resolvedGroupId = resolveLauncherGroupId(m_groupModel, id); + const QModelIndex groupIndex = findIndexByNamedRole(m_groupModel, MODEL_DESKTOPID, resolvedGroupId, TaskManager::DesktopIdRole); + groupName = groupIndex.data(modelRole(m_groupModel, MODEL_NAME, TaskManager::NameRole)).toString(); + } + const QString categoryName = translatedLauncherCategoryName(groupName); + if (!categoryName.isEmpty()) { + return categoryName; + } + if (groupName.isEmpty()) { + groupName = launcherGroupFallbackName(); + } + return groupName; + } + + if (type == QStringLiteral("folder")) { + return displayNameForPath(id); + } + + return {}; +} + +QString DockGlobalElementModel::iconNameFor(const QString &type, const QString &id) const +{ + Q_UNUSED(id) + if (type == QStringLiteral("group") || type == QStringLiteral("folder")) { + return QStringLiteral("folder"); + } + + return QString::fromLatin1(DEFAULT_APP_ICONNAME); +} + +QStringList DockGlobalElementModel::previewIconsFor(const QString &type, const QString &id) const +{ + if (type == QStringLiteral("group")) { + QStringList iconNames; + for (const QString &appId : invokeLauncherGroupItems(m_groupModel, id)) { + const QModelIndex appIndex = findIndexByNamedRole(m_appsModel, MODEL_DESKTOPID, appId, TaskManager::DesktopIdRole); + QString iconName = appIndex.data(modelRole(m_appsModel, MODEL_ICONNAME, TaskManager::IconNameRole)).toString(); + if (iconName.isEmpty()) { + iconName = QString::fromLatin1(DEFAULT_APP_ICONNAME); + } + iconNames.append(iconName); + if (iconNames.size() >= 4) { + break; + } + } + return iconNames; + } + + if (type == QStringLiteral("folder")) { + return previewIconsForDirectory(id); + } + + return {}; +} + void DockGlobalElementModel::initDockedElements(bool unused) { Q_UNUSED(unused); @@ -259,37 +790,40 @@ void DockGlobalElementModel::loadDockedElements() { QList> newDocked; for (auto elementInfo : TaskManagerSettings::instance()->dockedElements()) { - auto pair = elementInfo.split('/'); - if (pair.size() != 2) + const auto [type, id] = splitDockElement(elementInfo); + if (type.isEmpty() || id.isEmpty()) continue; - auto type = pair[0]; - auto id = pair[1]; - auto tmp = std::make_tuple(type, id); - // check desktop is installed QAbstractItemModel *model = nullptr; - int row = 0; - if (type == "desktop") { + int row = -1; + 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); + auto res = findIndexByNamedRole(m_appsModel, MODEL_DESKTOPID, id, TaskManager::DesktopIdRole); if (!res.isValid()) continue; row = res.row(); + } else if (type == QStringLiteral("group")) { + const QString resolvedGroupId = resolveLauncherGroupId(m_groupModel, id); + auto res = findIndexByNamedRole(m_groupModel, MODEL_DESKTOPID, resolvedGroupId, TaskManager::DesktopIdRole); + if (!res.isValid()) + continue; + } else if (type != QStringLiteral("folder")) { + continue; } newDocked.append(tmp); if (m_dockedElements.contains(tmp)) continue; - auto isRunning = std::any_of(m_data.constBegin(), m_data.constEnd(), [this, &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) { + return std::get<0>(data) == type && std::get<1>(data) == id; }); if (!isRunning) { beginInsertRows(QModelIndex(), m_data.size(), m_data.size()); - m_data.append(std::make_tuple(id, model, row)); + m_data.append(std::make_tuple(type, id, model, row)); endInsertRows(); } } @@ -298,8 +832,16 @@ 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 &data) { + if (std::get<0>(data) != type || std::get<1>(data) != id) { + return false; + } + + if (type == QStringLiteral("desktop")) { + return std::get<2>(data) == m_appsModel; + } + + return std::get<2>(data) == nullptr; }); if (dataIt != m_data.end()) { auto pos = (dataIt - m_data.begin()); @@ -315,34 +857,48 @@ void DockGlobalElementModel::loadDockedElements() if (!m_data.isEmpty()) { // MenusRole should also be handled here due to it contains the copywriting of docked or undocked - Q_EMIT dataChanged(index(0, 0), index(m_data.size() - 1, 0), {TaskManager::DockedRole, TaskManager::MenusRole}); + Q_EMIT dataChanged(index(0, 0), + index(m_data.size() - 1, 0), + {TaskManager::DockedRole, + TaskManager::MenusRole, + TaskManager::NameRole, + TaskManager::IconNameRole, + TaskManager::PreviewIconsRole}); } } QString DockGlobalElementModel::getMenus(const QModelIndex &index) const { auto data = m_data.at(index.row()); - auto id = std::get<0>(data); - auto model = std::get<1>(data); - auto row = std::get<2>(data); + auto type = std::get<0>(data); + auto id = std::get<1>(data); + auto model = std::get<2>(data); + auto row = std::get<3>(data); QJsonArray menusArray; - QString appNameInMenu = tr("Open"); + QString appNameInMenu = index.data(TaskManager::NameRole).toString(); if (model == m_activeAppModel) { - appNameInMenu = index.data(TaskManager::NameRole).toString(); // In case a window does not belongs to a known application, use the window title instead if (appNameInMenu.isEmpty()) { appNameInMenu = index.data(TaskManager::WinTitleRole).toString(); } } + if (appNameInMenu.isEmpty()) { + appNameInMenu = tr("Open"); + } menusArray.append(QJsonObject{{"id", ""}, {"name", appNameInMenu}}); - auto actions = model->index(row, 0).data(TaskManager::ActionsRole).toByteArray(); - for (auto action : QJsonDocument::fromJson(actions).array()) { - menusArray.append(action); + if (model) { + auto actions = model->index(row, 0).data(TaskManager::ActionsRole).toByteArray(); + for (auto action : QJsonDocument::fromJson(actions).array()) { + menusArray.append(action); + } } - bool isDocked = (model == nullptr) || m_dockedElements.contains(std::make_tuple("desktop", id)); + bool isDocked = m_dockedElements.contains(std::make_tuple(type, id)); + if (type == QStringLiteral("folder")) { + menusArray.append(QJsonObject{{"id", DOCK_ACTION_OPEN_IN_FILEMANAGER}, {"name", tr("Open in File Manager")}}); + } menusArray.append(QJsonObject{{"id", DOCK_ACTION_DOCK}, {"name", isDocked ? tr("Undock") : tr("Dock")}}); if (model == m_activeAppModel) { @@ -370,48 +926,81 @@ QVariant DockGlobalElementModel::data(const QModelIndex &index, int role) const return {}; auto data = m_data.at(index.row()); - auto id = std::get<0>(data); - auto model = std::get<1>(data); - auto row = std::get<2>(data); + auto type = std::get<0>(data); + auto id = std::get<1>(data); + auto model = std::get<2>(data); + auto row = std::get<3>(data); + const QModelIndex sourceIndex = model ? model->index(row, 0) : QModelIndex(); switch (role) { case TaskManager::ItemIdRole: return id; + case TaskManager::DockElementRole: + return dockElementForRow(index.row()); + case TaskManager::ItemKindRole: + return type; + case TaskManager::PreviewIconsRole: + return previewIconsFor(type, id); case TaskManager::WindowsRole: { if (model == m_activeAppModel) { return QStringList{model->index(row, 0).data(TaskManager::WinIdRole).toString()}; } - // For m_appsModel data, when it's GroupModel we can directly get all window IDs for this desktop ID + if (!model) { + return QStringList{}; + } QModelIndex groupIndex = model->index(row, 0); return groupIndex.data(TaskManager::WindowsRole).toStringList(); } - case TaskManager::ActiveRole: - case TaskManager::AttentionRole: { - if (model == m_activeAppModel) { - return model->index(row, 0).data(role); - } - return false; - } - case TaskManager::MenusRole: { return getMenus(index); } + case TaskManager::NameRole: + if (!model) { + return displayNameFor(type, id); + } + return sourceIndex.data(role); + case TaskManager::IconNameRole: + if (!model) { + return iconNameFor(type, id); + } + return sourceIndex.data(role); + case TaskManager::DockedRole: + if (!model) { + return true; + } + return sourceIndex.data(role); + case TaskManager::ActiveRole: + case TaskManager::AttentionRole: + if (!model) { + return false; + } + return sourceIndex.data(role); + case TaskManager::DesktopIdRole: + if (!model) { + return id; + } + return sourceIndex.data(role); default: { if (model) { - return model->index(row, 0).data(role); + return sourceIndex.data(role); } return {}; } } + + return {}; } void DockGlobalElementModel::requestActivate(const QModelIndex &index) const { auto data = m_data.value(index.row()); - auto id = std::get<0>(data); - auto sourceModel = std::get<1>(data); - auto sourceRow = std::get<2>(data); + auto sourceModel = std::get<2>(data); + auto sourceRow = std::get<3>(data); + + if (!sourceModel) { + return; + } if (sourceModel == m_activeAppModel) { auto sourceIndex = sourceModel->index(sourceRow, 0); @@ -427,12 +1016,14 @@ void DockGlobalElementModel::requestActivate(const QModelIndex &index) const void DockGlobalElementModel::requestNewInstance(const QModelIndex &index, const QString &action) const { auto data = m_data.value(index.row()); - auto id = std::get<0>(data); - qDebug(dockGlobalElementModelLog) << "Requesting new instance for index:" << index << "with action:" << action << "id:" << id; + auto type = std::get<0>(data); + auto id = std::get<1>(data); + auto sourceModel = std::get<2>(data); + qDebug(dockGlobalElementModelLog) << "Requesting new instance for index:" << index << "with action:" << action << "type:" << type << "id:" << id; // Handle special actions first (for both active and docked apps) if (action == DOCK_ACTION_DOCK) { - TaskManagerSettings::instance()->toggleDockedElement(QStringLiteral("desktop/%1").arg(id)); + TaskManagerSettings::instance()->toggleDockedElement(dockElementForRow(index.row())); return; } else if (action == DOCK_ACTION_FORCEQUIT) { requestClose(index, true); @@ -440,18 +1031,32 @@ void DockGlobalElementModel::requestNewInstance(const QModelIndex &index, const } else if (action == DOCK_ACTION_CLOSEWINDOW || action == DOCK_ACTION_CLOSEALL) { requestClose(index, false); return; - } else { - QProcess process; - process.setProcessChannelMode(QProcess::MergedChannels); - process.start("dde-am", {"--by-user", id, action}); - process.waitForFinished(); + } + + if (!sourceModel) { + if (type == QStringLiteral("folder") && action == DOCK_ACTION_OPEN_IN_FILEMANAGER) { + QDesktopServices::openUrl(QUrl::fromLocalFile(id)); + } + return; + } + + if (sourceModel == m_activeAppModel || sourceModel == m_appsModel) { + const QStringList arguments = {QStringLiteral("--by-user"), id, action}; + if (!QProcess::startDetached(QStringLiteral("dde-am"), arguments)) { + qWarning() << "Failed to launch dde-am for:" << id << "action:" << action; + } } } void DockGlobalElementModel::requestOpenUrls(const QModelIndex &index, const QList &urls) const { auto data = m_data.value(index.row()); - auto id = std::get<0>(data); + auto id = std::get<1>(data); + auto sourceModel = std::get<2>(data); + + if (!sourceModel) { + return; + } QStringList urlStrings; for (const QUrl &url : urls) { @@ -470,9 +1075,8 @@ void DockGlobalElementModel::requestOpenUrls(const QModelIndex &index, const QLi void DockGlobalElementModel::requestClose(const QModelIndex &index, bool force) const { auto data = m_data.value(index.row()); - auto id = std::get<0>(data); - auto sourceModel = std::get<1>(data); - auto sourceRow = std::get<2>(data); + auto sourceModel = std::get<2>(data); + auto sourceRow = std::get<3>(data); if (sourceModel == m_activeAppModel) { auto sourceIndex = sourceModel->index(sourceRow, 0); @@ -486,9 +1090,8 @@ void DockGlobalElementModel::requestClose(const QModelIndex &index, bool force) void DockGlobalElementModel::requestUpdateWindowIconGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) const { auto data = m_data.value(index.row()); - auto id = std::get<0>(data); - auto sourceModel = std::get<1>(data); - auto sourceRow = std::get<2>(data); + auto sourceModel = std::get<2>(data); + auto sourceRow = std::get<3>(data); if (sourceModel == m_activeAppModel) { auto sourceIndex = sourceModel->index(sourceRow, 0); @@ -524,16 +1127,23 @@ void DockGlobalElementModel::groupItemsByApp() return; for (int i = 0; i < m_data.size(); ++i) { - const QString currentId = std::get<0>(m_data.at(i)); + const QString currentType = std::get<0>(m_data.at(i)); + const QString currentId = std::get<1>(m_data.at(i)); + + if (currentType != QStringLiteral("desktop")) { + continue; + } int insertPos = i + 1; - while (insertPos < m_data.size() && std::get<0>(m_data.at(insertPos)) == currentId) { + while (insertPos < m_data.size() && + std::get<0>(m_data.at(insertPos)) == QStringLiteral("desktop") && + std::get<1>(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<0>(m_data.at(j)) != QStringLiteral("desktop") || std::get<1>(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..31259c8b5 100644 --- a/panels/dock/taskmanager/dockglobalelementmodel.h +++ b/panels/dock/taskmanager/dockglobalelementmodel.h @@ -15,7 +15,10 @@ class DockGlobalElementModel : public QAbstractListModel, public AbstractTaskMan { Q_OBJECT public: - explicit DockGlobalElementModel(QAbstractItemModel *appsModel, DockCombineModel *activeAppModel, QObject *parent = nullptr); + explicit DockGlobalElementModel(QAbstractItemModel *appsModel, + DockCombineModel *activeAppModel, + QAbstractItemModel *groupModel, + QObject *parent = nullptr); QHash roleNames() const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; @@ -45,14 +48,19 @@ public slots: void loadDockedElements(); QString getMenus(const QModelIndex &index) const; void groupItemsByApp(); + QString dockElementForRow(int row) const; + QString displayNameFor(const QString &type, const QString &id) const; + QString iconNameFor(const QString &type, const QString &id) const; + QStringList previewIconsFor(const QString &type, const QString &id) const; private: - // id, model, and pos - QList> m_data; + // type, id, model, and row + QList> m_data; // type, id QList> m_dockedElements; QAbstractItemModel *m_appsModel; DockCombineModel *m_activeAppModel; + QAbstractItemModel *m_groupModel; }; } diff --git a/panels/dock/taskmanager/dockgroupmodel.cpp b/panels/dock/taskmanager/dockgroupmodel.cpp index 0d4513edf..b635fbf72 100644 --- a/panels/dock/taskmanager/dockgroupmodel.cpp +++ b/panels/dock/taskmanager/dockgroupmodel.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 @@ -6,11 +6,8 @@ #include "abstracttaskmanagerinterface.h" #include "rolegroupmodel.h" #include "taskmanager.h" -#include "taskmanagersettings.h" #include "globals.h" -#include - namespace dock { DockGroupModel::DockGroupModel(QAbstractItemModel *sourceModel, int role, QObject *parent) @@ -195,21 +192,28 @@ void DockGroupModel::requestWindowsView(const QModelIndexList &indexes) const void DockGroupModel::requestNewInstance(const QModelIndex &index, const QString &action) const { - if (action == DOCK_ACTION_DOCK) { - auto desktopId = index.data(TaskManager::DesktopIdRole).toString(); - TaskManagerSettings::instance()->toggleDockedElement(QStringLiteral("desktop/%1").arg(desktopId)); - } else if (action == DOCK_ACTION_FORCEQUIT) { + if (action == DOCK_ACTION_FORCEQUIT) { requestClose(index, true); - } else if (action == DOCK_ACTION_CLOSEALL) { + } else if (action == DOCK_ACTION_CLOSEALL || action == DOCK_ACTION_CLOSEWINDOW) { requestClose(index); - } else { - auto desktopId = index.data(TaskManager::DesktopIdRole).toString(); - QProcess process; - process.setProcessChannelMode(QProcess::MergedChannels); - process.start("dde-am", {"--by-user", desktopId, action}); - process.waitForFinished(); return; } + + auto interface = dynamic_cast(sourceModel()); + if (nullptr == interface) { + return; + } + + QModelIndex sourceIndex = mapToSource(index); + if (RoleGroupModel::rowCount(index) > 0) { + sourceIndex = mapToSource(RoleGroupModel::index(0, 0, index)); + } + + if (!sourceIndex.isValid()) { + return; + } + + interface->requestNewInstance(sourceIndex, action); } void DockGroupModel::resetActiveWindow(int parentRow) diff --git a/panels/dock/taskmanager/globals.h b/panels/dock/taskmanager/globals.h index 6db543a09..db26c2ce7 100644 --- a/panels/dock/taskmanager/globals.h +++ b/panels/dock/taskmanager/globals.h @@ -16,6 +16,7 @@ static inline const QString DOCK_ACTION_CLOSEALL = "dock-action-closeAll"; static inline const QString DOCK_ACTION_CLOSEWINDOW = "dock-action-closeWindow"; static inline const QString DOCK_ACTIN_LAUNCH = "dock-action-launch"; static inline const QString DOCK_ACTION_DOCK = "dock-action-dock"; +static inline const QString DOCK_ACTION_OPEN_IN_FILEMANAGER = "dock-action-openInFileManager"; // setting keys static inline const QString TASKMANAGER_ALLOWFOCEQUIT_KEY = "Allow_Force_Quit"; @@ -26,6 +27,7 @@ static inline const QString TASKMANAGER_CGROUPS_BASED_GROUPING_SKIP_APPIDS = "cg static inline const QString TASKMANAGER_CGROUPS_BASED_GROUPING_SKIP_CATEGORIES = "cgroupsBasedGroupingSkipCategories"; static inline const QString TASKMANAGER_DOCKEDITEMS_KEY = "Docked_Items"; constexpr auto TASKMANAGER_DOCKEDELEMENTS_KEY = "dockedElements"; +constexpr auto TASKMANAGER_DEFAULT_DOCK_FOLDERS_MIGRATION_VERSION_KEY = "defaultDockFoldersMigrationVersion"; // model roleNames constexpr auto MODEL_WINID = "winId"; diff --git a/panels/dock/taskmanager/package/AppItem.qml b/panels/dock/taskmanager/package/AppItem.qml index 973251aa4..3cd6238a6 100644 --- a/panels/dock/taskmanager/package/AppItem.qml +++ b/panels/dock/taskmanager/package/AppItem.qml @@ -2,23 +2,28 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +pragma ComponentBehavior: Bound + import QtQuick 2.15 import QtQuick.Controls 2.15 +import Qt.labs.platform 1.1 as LP import org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 import org.deepin.dtk 1.0 as D -import Qt.labs.platform 1.1 as LP Item { id: root required property int displayMode required property int colorTheme - required property bool active + required property bool itemActive required property bool attention required property string itemId + required property string dockElement + required property string itemKind required property string name required property string iconName + required property var previewIcons required property string menus required property list windows required property int visualIndex @@ -26,6 +31,10 @@ Item { required property string title property real blendOpacity: 1.0 + property point lastSpotlightPoint: Qt.point(0, 0) + readonly property string toolTipText: root.itemId === "dde-trash" + ? root.name + "-" + taskmanager.Applet.trashTipText + : root.name signal dropFilesOnItem(itemId: string, files: list) signal dragFinished() @@ -35,12 +44,28 @@ Item { Drag.hotSpot.x: icon.width / 2 Drag.hotSpot.y: icon.height / 2 Drag.dragType: Drag.Automatic - Drag.mimeData: { "text/x-dde-dock-dnd-appid": itemId, "text/x-dde-dock-dnd-source": "taskbar", "text/x-dde-dock-dnd-winid": windows.length > 0 ? windows[0] : ""} + Drag.mimeData: { + "text/x-dde-dock-dnd-appid": itemId, + "text/x-dde-dock-dnd-element": dockElement, + "text/x-dde-dock-dnd-itemkind": itemKind, + "text/x-dde-dock-dnd-source": "taskbar", + "text/x-dde-dock-dnd-winid": windows.length > 0 ? windows[0] : "" + } property bool useColumnLayout: Panel.rootObject.useColumnLayout + property bool compactFashionIndicator: root.displayMode === Dock.Fashion + && (Panel.position === Dock.Bottom || Panel.position === Dock.Top) + property int statusIndicatorSize: useColumnLayout ? root.width * 0.72 : root.height * 0.72 property int iconSize: Panel.rootObject.dockItemMaxSize * 9 / 14 + property int popupIconSize: Math.max(1, Math.round(iconSize * 0.92)) property bool enableTitle: false property bool titleActive: enableTitle && titleLoader.active + property int appTitleSpacing: 0 + property bool popupItem: root.itemKind === "group" || root.itemKind === "folder" + readonly property bool resizeOptimizationActive: Panel.isResizing || (Panel.rootObject && Panel.rootObject.isDragging) + readonly property bool dockPopupContext: Window.window && Panel.popupWindow && Window.window === Panel.popupWindow + property bool deferredWindowIconGeometryUpdate: false + property bool deferredPhysicalPixelFix: false property var iconGlobalPoint: { var a = icon var x = 0, y = 0 @@ -55,14 +80,119 @@ Item { implicitWidth: appItem.implicitWidth + function dockWindowGlobalPoint() { + if (!Panel.rootObject) { + return Qt.point(0, 0) + } + + return Qt.point(Panel.rootObject.x, Panel.rootObject.y) + } + + function mapPointToDockWindow(targetItem, localPoint) { + const item = targetItem || appItem + const point = localPoint || Qt.point(item.width / 2, item.height / 2) + + if (!item) { + return Qt.point(0, 0) + } + + if (!dockPopupContext) { + return item.mapToItem(null, point.x, point.y) + } + + const globalPoint = item.mapToGlobal(point.x, point.y) + const dockGlobalPoint = dockWindowGlobalPoint() + return Qt.point(globalPoint.x - dockGlobalPoint.x, globalPoint.y - dockGlobalPoint.y) + } + + function dockRelativeRect(targetItem) { + const item = targetItem || root + const topLeft = mapPointToDockWindow(item, Qt.point(0, 0)) + return Qt.rect(topLeft.x, topLeft.y, item.width, item.height) + } + + function dockPopupRect() { + if (!dockPopupContext || !Window.window || !Window.window.contentItem) { + return dockRelativeRect(root) + } + + const popupTopLeftGlobal = Window.window.contentItem.mapToGlobal(0, 0) + const dockGlobalPoint = dockWindowGlobalPoint() + return Qt.rect(popupTopLeftGlobal.x - dockGlobalPoint.x, + popupTopLeftGlobal.y - dockGlobalPoint.y, + Window.window.contentItem.width, + Window.window.contentItem.height) + } + + function mapSpotlightPoint(localPoint) { + const point = localPoint || Qt.point(appItem.width / 2, appItem.height / 2) + return mapPointToDockWindow(appItem, point) + } + + function updateSpotlight(localPoint) { + lastSpotlightPoint = mapSpotlightPoint(localPoint) + Panel.reportMousePresence(true, lastSpotlightPoint) + } + + function clearSpotlight() { + Panel.reportMousePresence(false, lastSpotlightPoint) + } + + function scheduleWindowIconGeometryUpdate() { + deferredWindowIconGeometryUpdate = true + if (resizeOptimizationActive) { + return + } + + updateWindowIconGeometryTimer.restart() + } + + function flushWindowIconGeometryUpdate() { + if (!deferredWindowIconGeometryUpdate) { + return + } + + deferredWindowIconGeometryUpdate = false + var pos = mapPointToDockWindow(icon, Qt.point(0, 0)) + taskmanager.Applet.requestUpdateWindowIconGeometry(root.modelIndex, + Qt.rect(pos.x, pos.y, icon.width, icon.height), + Panel.rootObject) + } + + function schedulePhysicalPixelFix() { + deferredPhysicalPixelFix = true + if (resizeOptimizationActive) { + icon.anchors.centerIn = icon.parent + return + } + + fixPositionTimer.restart() + } + + onResizeOptimizationActiveChanged: { + if (resizeOptimizationActive) { + fixPositionTimer.stop() + updateWindowIconGeometryTimer.stop() + icon.anchors.centerIn = icon.parent + return + } + + deferredPhysicalPixelFix = true + deferredWindowIconGeometryUpdate = true + fixPositionTimer.restart() + updateWindowIconGeometryTimer.restart() + } + // Monitor Panel position changes to update icon geometry Connections { target: Panel.rootObject function onXChanged() { - updateWindowIconGeometryTimer.start() + root.scheduleWindowIconGeometryUpdate() + root.schedulePhysicalPixelFix() } function onYChanged() { - updateWindowIconGeometryTimer.start() + root.scheduleWindowIconGeometryUpdate() + root.schedulePhysicalPixelFix() } } @@ -70,22 +200,26 @@ Item { id: itemPalette displayMode: root.displayMode colorTheme: root.colorTheme - active: root.active + itemActive: root.itemActive backgroundColor: D.DTK.palette.highlight } Control { anchors.fill: parent id: appItem - implicitWidth: root.titleActive ? (root.iconSize + hoverBackground.horizontalSpacing + titleLoader.width) : iconContainer.width + implicitWidth: root.titleActive ? (iconContainer.width + 4 + titleLoader.width + root.appTitleSpacing) : iconContainer.width + root.appTitleSpacing visible: !root.Drag.active // When in dragging, hide app item - background: AppItemBackground { + background: AppletItemBackground { id: hoverBackground - readonly property int verticalSpacing: Math.round(root.iconSize / 8) + 1 - readonly property int horizontalSpacing: Math.round(root.iconSize / 8) + readonly property int hoverInset: 4 + readonly property int verticalSpacing: hoverInset + readonly property int horizontalSpacing: hoverInset readonly property int nonSplitHeight: root.iconSize + verticalSpacing * 2 - readonly property int splitWidth: Math.round(root.iconSize + titleLoader.width + horizontalSpacing * 3) + readonly property int splitWidth: Math.round(icon.width + + titleLoader.anchors.leftMargin + + titleLoader.width + + horizontalSpacing * 2) readonly property int nonSplitWidth: Math.round(root.iconSize + horizontalSpacing * 2) enabled: false @@ -94,9 +228,12 @@ Item { height: nonSplitHeight radius: height / 5 anchors.centerIn: parent - isActive: root.active - windowCount: root.windows.length - displayMode: root.displayMode + isActive: root.itemActive + opacity: (hoverHandler.hovered || (root.itemActive && root.windows.length > 0)) ? 1.0 : 0.0 + D.ColorSelector.hovered: hoverHandler.hovered + Behavior on opacity { + NumberAnimation { duration: 150 } + } } Item { id: iconContainer @@ -113,6 +250,10 @@ Item { anchors.left: parent.left anchors.horizontalCenter: undefined } + PropertyChanges { + target: iconContainer + anchors.leftMargin: hoverBackground.horizontalSpacing + } }, State { name: "nonTitleActive" @@ -125,18 +266,20 @@ Item { } } ] - - Connections { - function onDisplayModeChanged() { - windowIndicator.updateIndicatorAnchors() - } - target: root + StatusIndicator { + id: statusIndicator + palette: itemPalette + width: root.statusIndicatorSize + height: root.statusIndicatorSize + anchors.centerIn: iconContainer + visible: root.displayMode === Dock.Efficient && root.windows.length > 0 } Connections { function onPositionChanged() { windowIndicator.updateIndicatorAnchors() - updateWindowIconGeometryTimer.start() + root.scheduleWindowIconGeometryUpdate() + root.schedulePhysicalPixelFix() } target: Panel } @@ -150,6 +293,7 @@ Item { anchors.centerIn: parent retainWhileLoading: true smooth: false + visible: !root.popupItem function mapToScene(px, py) { return parent.mapToItem(Window.window.contentItem, Qt.point(px, py)) @@ -163,6 +307,12 @@ Item { if (root.Drag.active || !parent || launchAnimation.running) { return } + + if (root.resizeOptimizationActive) { + anchors.centerIn = parent + return + } + anchors.centerIn = undefined var targetX = (parent.width - width) / 2 var targetY = (parent.height - height) / 2 @@ -173,17 +323,22 @@ Item { var physicalY = Math.round(scenePos.y * Panel.devicePixelRatio) var localPos = mapFromScene(physicalX / Panel.devicePixelRatio, physicalY / Panel.devicePixelRatio) - + + if (Math.abs(x - localPos.x) < 0.01 && Math.abs(y - localPos.y) < 0.01) { + return + } + x = localPos.x y = localPos.y } Timer { id: fixPositionTimer - interval: 100 + interval: 16 repeat: false running: false onTriggered: { + root.deferredPhysicalPixelFix = false icon.fixPosition() } } @@ -191,7 +346,7 @@ Item { Connections { target: root function onIconGlobalPointChanged() { - fixPositionTimer.start() + root.schedulePhysicalPixelFix() } } LaunchAnimation { @@ -224,15 +379,29 @@ Item { running: false } } + + PinnedItemIcon { + anchors.centerIn: parent + width: root.popupIconSize + height: root.popupIconSize + iconName: root.iconName + previewIcons: root.previewIcons + iconSize: root.popupIconSize + colorTheme: root.colorTheme + visible: root.popupItem + } } WindowIndicator { id: windowIndicator - dotWidth: root.useColumnLayout ? Math.max(iconSize / 16, 2) : Math.max(iconSize / 3, 2) - dotHeight: root.useColumnLayout ? Math.max(iconSize / 3, 2) : Math.max(iconSize / 16, 2) + dotWidth: root.compactFashionIndicator ? 4 : (root.useColumnLayout ? Math.max(iconSize / 16, 2) : Math.max(iconSize / 3, 2)) + dotHeight: root.compactFashionIndicator ? 3 : (root.useColumnLayout ? Math.max(iconSize / 3, 2) : Math.max(iconSize / 16, 2)) + multiDotWidth: root.compactFashionIndicator ? 3 : (root.useColumnLayout ? Math.max(iconSize / 16, 2) : Math.max(iconSize / 3, 2)) + multiDotHeight: root.compactFashionIndicator ? 3 : (root.useColumnLayout ? Math.max(iconSize / 3, 2) : Math.max(iconSize / 16, 2)) windows: root.windows displayMode: root.displayMode useColumnLayout: root.useColumnLayout + compactFashionIndicator: root.compactFashionIndicator palette: itemPalette visible: (root.displayMode === Dock.Efficient && root.windows.length > 1) || (root.displayMode === Dock.Fashion && root.windows.length > 0) @@ -248,30 +417,28 @@ Item { windowIndicator.anchors.horizontalCenter = undefined windowIndicator.anchors.verticalCenter = undefined - const anchorTarget = root.displayMode === Dock.Efficient ? appItem : hoverBackground - switch(Panel.position) { case Dock.Top: { windowIndicator.anchors.horizontalCenter = iconContainer.horizontalCenter - windowIndicator.anchors.top = anchorTarget.top + windowIndicator.anchors.top = root.compactFashionIndicator ? appItem.top : hoverBackground.top windowIndicator.anchors.topMargin = 1 return } case Dock.Bottom: { windowIndicator.anchors.horizontalCenter = iconContainer.horizontalCenter - windowIndicator.anchors.bottom = anchorTarget.bottom + windowIndicator.anchors.bottom = root.compactFashionIndicator ? appItem.bottom : hoverBackground.bottom windowIndicator.anchors.bottomMargin = 1 return } case Dock.Left: { windowIndicator.anchors.verticalCenter = parent.verticalCenter - windowIndicator.anchors.left = anchorTarget.left + windowIndicator.anchors.left = hoverBackground.left windowIndicator.anchors.leftMargin = 1 return } case Dock.Right:{ windowIndicator.anchors.verticalCenter = parent.verticalCenter - windowIndicator.anchors.right = anchorTarget.right + windowIndicator.anchors.right = hoverBackground.right windowIndicator.anchors.rightMargin = 1 return } @@ -281,33 +448,31 @@ Item { Component.onCompleted: { windowIndicator.updateIndicatorAnchors() } + + Connections { + target: root + function onCompactFashionIndicatorChanged() { + windowIndicator.updateIndicatorAnchors() + } + } } AppItemTitle { id: titleLoader anchors.left: iconContainer.right - anchors.leftMargin: Math.round(root.iconSize / 8) + anchors.leftMargin: 4 anchors.verticalCenter: parent.verticalCenter enabled: root.enableTitle && root.windows.length > 0 text: root.title - textColor: { - if (root.displayMode === Dock.Efficient && root.active) { - return "#000000" - } - return D.DTK.themeType === D.ApplicationHelper.DarkType ? "#FFFFFF" : "#000000" - } + colorTheme: root.colorTheme } + // TODO: value can set during debugPanel Loader { id: animationRoot anchors.fill: parent z: -1 - active: root.attention && !Panel.rootObject.isDragging && TaskManager.showAttentionAnimation - onActiveChanged: { - if (!active) { - icon.scale = 1.0 - } - } + active: root.attention && !Panel.rootObject.isDragging sourceComponent: Repeater { model: 5 Rectangle { @@ -366,37 +531,255 @@ Item { HoverHandler { id: hoverHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + + onPointChanged: { + if (hovered) { + appItemSpotlightClearTimer.stop() + root.updateSpotlight(hoverHandler.point.position) + } + } + onHoveredChanged: function () { if (hovered) { + appItemSpotlightClearTimer.stop() + root.updateSpotlight() root.onEntered() } else { + appItemSpotlightClearTimer.restart() root.onExited() } } } } + Timer { + id: appItemSpotlightClearTimer + interval: 70 + repeat: false + onTriggered: { + if (!hoverHandler.hovered) { + root.clearSpotlight() + } + } + } + Loader { id: contextMenuLoader active: false property bool trashEmpty: true sourceComponent: LP.Menu { id: contextMenu + property var menuItems: { + try { + return JSON.parse(root.menus || "[]") + } catch (error) { + console.warn("failed to parse taskmanager menu", error, root.menus) + return [] + } + } + property var popupSortInfo: ({}) + readonly property bool preferChineseText: { + const localeName = String(Qt.locale().name || "").toLowerCase() + const uiLanguage = String(Qt.uiLanguage || "").toLowerCase() + return localeName.indexOf("zh") === 0 || uiLanguage.indexOf("zh") === 0 + } + + function localizedMenuText(text) { + const sourceText = String(text || "") + if (!preferChineseText || !sourceText.length) { + return sourceText + } + + const textMap = { + "Open": "打开", + "Undock": "移除驻留", + "Dock": "驻留", + "Force Quit": "强制退出", + "Close All": "关闭所有", + "Close this window": "关闭当前窗口", + "Open in File Manager": "在文件管理器中打开", + "Move to Trash": "移动到回收站", + "Sort": "排序方式", + "Ascending": "升序", + "Descending": "降序", + "Name": "名称", + "Modified Time": "修改时间", + "Created Time": "创建时间", + "Size": "大小", + "Type": "类型", + "Sort by Name": "按名称排序", + "Sort by Modified Time": "按修改时间排序", + "Sort by Created Time": "按创建时间排序", + "Sort by Size": "按大小排序", + "Sort by Type": "按类型排序" + } + + return textMap[sourceText] !== undefined ? textMap[sourceText] : sourceText + } + + function refreshPopupSortInfo() { + popupSortInfo = root.popupItem ? TaskManager.popupSortState(root.dockElement) : ({}) + } + + function isCurrentPopupSort(fieldName) { + return popupSortInfo && popupSortInfo.sortField === fieldName + } + + function popupSortText(baseText, fieldName) { + if (!isCurrentPopupSort(fieldName)) { + return localizedMenuText(baseText) + } + + const localizedBaseText = localizedMenuText(baseText) + return localizedBaseText + (popupSortInfo.sortDescending ? + " (" + localizedMenuText(qsTr("Descending")) + ")" : + " (" + localizedMenuText(qsTr("Ascending")) + ")") + } + + function applyPopupSort(fieldName) { + TaskManager.cyclePopupSort(root.dockElement, fieldName) + refreshPopupSortInfo() + if (pinnedPopup.popupVisible) { + pinnedPopupContent.refresh(pinnedPopupContent.currentLocation(), false) + } + } + + Component.onCompleted: refreshPopupSortInfo() + onAboutToShow: refreshPopupSortInfo() Instantiator { id: menuItemInstantiator - model: JSON.parse(menus) + model: contextMenu.menuItems delegate: LP.MenuItem { - text: modelData.name - enabled: (root.itemId === "dde-trash" && modelData.id === "clean-trash") + required property var modelData + + readonly property string menuId: modelData && modelData.id !== undefined ? String(modelData.id) : "" + readonly property string menuText: modelData && modelData.name !== undefined ? String(modelData.name) : "" + + text: contextMenu.localizedMenuText(menuText) + enabled: (root.itemId === "dde-trash" && menuId === "clean-trash") ? !contextMenuLoader.trashEmpty : true onTriggered: { - TaskManager.requestNewInstance(root.modelIndex, modelData.id); + TaskManager.requestNewInstance(root.modelIndex, menuId); } } onObjectAdded: (index, object) => contextMenu.insertItem(index, object) onObjectRemoved: (index, object) => contextMenu.removeItem(object) } + Instantiator { + id: popupSortMenuInstantiator + model: root.popupItem ? 1 : 0 + delegate: LP.Menu { + title: contextMenu.localizedMenuText(qsTr("Sort")) + + LP.MenuItem { + checkable: true + text: contextMenu.popupSortText(qsTr("Name"), "name") + checked: contextMenu.isCurrentPopupSort("name") + onTriggered: contextMenu.applyPopupSort("name") + } + LP.MenuItem { + checkable: true + text: contextMenu.popupSortText(qsTr("Modified Time"), "modified") + checked: contextMenu.isCurrentPopupSort("modified") + onTriggered: contextMenu.applyPopupSort("modified") + } + LP.MenuItem { + checkable: true + text: contextMenu.popupSortText(qsTr("Created Time"), "created") + checked: contextMenu.isCurrentPopupSort("created") + onTriggered: contextMenu.applyPopupSort("created") + } + LP.MenuItem { + checkable: true + text: contextMenu.popupSortText(qsTr("Size"), "size") + checked: contextMenu.isCurrentPopupSort("size") + onTriggered: contextMenu.applyPopupSort("size") + } + LP.MenuItem { + checkable: true + text: contextMenu.popupSortText(qsTr("Type"), "type") + checked: contextMenu.isCurrentPopupSort("type") + onTriggered: contextMenu.applyPopupSort("type") + } + } + onObjectAdded: (index, object) => contextMenu.insertMenu(contextMenu.items.length, object) + onObjectRemoved: (index, object) => contextMenu.removeMenu(object) + } + } + } + + PanelPopup { + id: pinnedPopup + property point popupAnchorPoint: Qt.point(0, 0) + width: pinnedPopupContent.width + height: pinnedPopupContent.height + popupX: { + switch (Panel.position) { + case Dock.Top: + case Dock.Bottom: + return popupAnchorPoint.x - pinnedPopup.width / 2 + case Dock.Right: + return -pinnedPopup.width - 10 + case Dock.Left: + return Panel.rootObject.dockSize + 10 + } + return popupAnchorPoint.x - pinnedPopup.width / 2 + } + popupY: { + switch (Panel.position) { + case Dock.Top: + return Panel.rootObject.dockSize + 10 + case Dock.Right: + case Dock.Left: + return popupAnchorPoint.y - pinnedPopup.height / 2 + case Dock.Bottom: + return -pinnedPopup.height - 10 + } + return -pinnedPopup.height - 10 + } + + DockPinnedPopup { + id: pinnedPopupContent + applet: taskmanager.Applet + dockElement: root.dockElement + colorTheme: root.colorTheme + popupWindow: pinnedPopup.popupWindow + onCloseRequested: pinnedPopup.close() + } + + Component.onCompleted: { + if ("keyEventTarget" in pinnedPopup) { + pinnedPopup.keyEventTarget = pinnedPopupContent + } + } + } + + Connections { + target: pinnedPopup.popupWindow + + function onVisibleChanged() { + if (!pinnedPopup.popupWindow || pinnedPopup.popupWindow.visible) { + return + } + + DS.grabKeyboard(pinnedPopup.popupWindow, false) + } + } + + Timer { + id: pinnedPopupKeyboardGrabTimer + interval: 1 + repeat: false + onTriggered: { + if (!pinnedPopup.popupWindow || !pinnedPopup.popupWindow.visible) { + return + } + + pinnedPopup.popupWindow.requestActivate() + DS.grabKeyboard(pinnedPopup.popupWindow) + pinnedPopupContent.forceActiveFocus(Qt.OtherFocusReason) } } @@ -406,42 +789,104 @@ Item { running: false repeat: false onTriggered: { - var pos = icon.mapToItem(null, 0, 0) - taskmanager.Applet.requestUpdateWindowIconGeometry(root.modelIndex, Qt.rect(pos.x, pos.y, - icon.width, icon.height), Panel.rootObject) + root.flushWindowIconGeometryUpdate() } } Timer { id: previewTimer - interval: 500 + interval: 220 running: false repeat: false property int xOffset: 0 property int yOffset: 0 onTriggered: { + if (root.popupItem) { + return + } + if (root.windows.length != 0 || Qt.platform.pluginName === "wayland") { // 使用基于 modelIndex 的预览API,确保精确匹配 - taskmanager.Applet.requestPreview(root.modelIndex, Panel.rootObject, xOffset, yOffset, Panel.position); + root.requestPreviewNow(xOffset, yOffset) } } } + function togglePinnedPopup() { + if (pinnedPopup.popupVisible) { + pinnedPopup.close() + return + } + + Panel.requestClosePopup() + pinnedPopupContent.beginPopupSession() + pinnedPopupContent.refresh("", false) + if ("keyEventTarget" in pinnedPopup) { + pinnedPopup.keyEventTarget = pinnedPopupContent + } + pinnedPopup.popupAnchorPoint = root.mapToItem(null, root.width / 2, root.height / 2) + pinnedPopup.open() + pinnedPopupKeyboardGrabTimer.restart() + } + + function showToolTipNow() { + var point = mapPointToDockWindow(root, Qt.point(root.width / 2, root.height / 2)) + toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) + toolTip.open() + } + + function requestPreviewNow(xOffset, yOffset) { + taskmanager.Applet.requestPreview(root.modelIndex, + Panel.rootObject, + xOffset, + yOffset, + dockPopupContext + ? (Panel.position === Dock.Top ? Dock.Top : Dock.Bottom) + : Panel.position) + } function onEntered() { - if (windows.length === 0) { - toolTipShowTimer.start() + if (root.popupItem) { + if (toolTip.toolTipWindow && toolTip.toolTipWindow.visible) { + showToolTipNow() + } else { + toolTipShowTimer.start() + } return } - var itemPos = root.mapToItem(null, 0, 0) + if (Qt.platform.pluginName === "xcb" && windows.length === 0) { + if (toolTip.toolTipWindow && toolTip.toolTipWindow.visible) { + showToolTipNow() + } else { + toolTipShowTimer.start() + } + return + } + + const itemRect = dockRelativeRect(root) let xOffset, yOffset, interval = 10 - if (Panel.position % 2 === 0) { - xOffset = itemPos.x + (root.width / 2) - yOffset = (Panel.position == 2 ? -interval : interval + Panel.dockSize) + if (dockPopupContext) { + const popupRect = dockPopupRect() + xOffset = popupRect.x + popupRect.width / 2 + yOffset = Panel.position === Dock.Top + ? popupRect.y + popupRect.height + interval + : popupRect.y - interval + } else if (Panel.position % 2 === 0) { + xOffset = itemRect.x + itemRect.width / 2 + yOffset = Panel.position === Dock.Bottom + ? itemRect.y - interval + : itemRect.y + itemRect.height + interval } else { - xOffset = (Panel.position == 1 ? -interval : interval + Panel.dockSize) - yOffset = itemPos.y + (root.height / 2) + xOffset = Panel.position === Dock.Left + ? itemRect.x + itemRect.width + interval + : itemRect.x - interval + yOffset = itemRect.y + itemRect.height / 2 + } + if (root.windows.length > 0 && taskmanager.previewSwitchActive) { + taskmanager.endPreviewSwitch() + requestPreviewNow(xOffset, yOffset) + return } previewTimer.xOffset = xOffset previewTimer.yOffset = yOffset @@ -449,22 +894,30 @@ Item { } function onExited() { + var hadPendingPreview = previewTimer.running if (toolTipShowTimer.running) { toolTipShowTimer.stop() } - if (previewTimer.running) { + if (hadPendingPreview) { previewTimer.stop() } - if (windows.length === 0) { + if (root.popupItem || (Qt.platform.pluginName === "xcb" && windows.length === 0)) { toolTip.close() return } + if (root.windows.length > 0 && !hadPendingPreview) { + taskmanager.beginPreviewSwitch() + } closeItemPreview() } function closeItemPreview() { + if (root.popupItem) { + return + } + if (previewTimer.running) { previewTimer.stop() } else { @@ -474,16 +927,45 @@ Item { function requestAppItemMenu() { Panel.requestClosePopup() - contextMenuLoader.trashEmpty = TaskManager.isTrashEmpty() + contextMenuLoader.trashEmpty = taskmanager.Applet.trashEmpty contextMenuLoader.active = true MenuHelper.openMenu(contextMenuLoader.item) } + function openAppItemMenu() { + toolTip.close() + closeItemPreview() + requestAppItemMenu() + } + + TapHandler { + acceptedButtons: Qt.NoButton + acceptedDevices: PointerDevice.TouchScreen + onLongPressed: root.openAppItemMenu() + } + + MouseArea { + id: contextMenuMouseArea + anchors.fill: parent + acceptedButtons: Qt.RightButton + hoverEnabled: false + preventStealing: true + + onPressed: function(mouse) { + toolTip.close() + closeItemPreview() + } + + onClicked: function(mouse) { + root.openAppItemMenu() + } + } + MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: false - acceptedButtons: Qt.LeftButton | Qt.RightButton + acceptedButtons: Qt.LeftButton drag.target: root drag.onActiveChanged: { if (!drag.active) { @@ -499,34 +981,32 @@ Item { appItem.grabToImage(function(result) { root.Drag.imageSource = result.url; }) + appItemSpotlightClearTimer.stop() } toolTip.close() closeItemPreview() } - // touchscreen long press. - onPressAndHold: function (mouse) { - if (mouse.button === Qt.NoButton) { - requestAppItemMenu() - } - } onClicked: function (mouse) { let index = root.modelIndex; - if (mouse.button === Qt.RightButton) { - requestAppItemMenu() - } else { - if (root.windows.length === 0) { - launchAnimation.start(); - TaskManager.requestNewInstance(index, ""); - return; - } - TaskManager.requestActivate(index); + if (root.popupItem) { + togglePinnedPopup() + return + } + + if (root.windows.length === 0) { + launchAnimation.start(); + TaskManager.requestNewInstance(index, ""); + return; } + TaskManager.requestActivate(index); } PanelToolTip { id: toolTip + text: root.toolTipText toolTipX: DockPanelPositioner.x toolTipY: DockPanelPositioner.y + closeGraceInterval: 90 } PanelToolTip { @@ -541,10 +1021,7 @@ 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) - toolTip.open() + root.showToolTipNow() } } } @@ -565,7 +1042,7 @@ Item { if (root.itemId === "dde-trash") { dragToolTipCloseTimer.stop() if (!dragToolTip.toolTipVisible) { - var point = root.mapToItem(null, root.width / 2, root.height / 2) + var point = mapPointToDockWindow(root, Qt.point(root.width / 2, root.height / 2)) dragToolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, dragToolTip.width, dragToolTip.height) dragToolTip.open() } @@ -579,6 +1056,9 @@ Item { } onDropped: function (drop){ + if (root.popupItem) { + return + } dragToolTipCloseTimer.stop() dragToolTip.close() root.dropFilesOnItem(root.itemId, drop.urls) @@ -586,7 +1066,7 @@ Item { } onWindowsChanged: { - updateWindowIconGeometryTimer.start() + root.scheduleWindowIconGeometryUpdate() // Close tooltip when window appears if (windows.length > 0 && toolTip.toolTipVisible) { toolTip.close() @@ -594,6 +1074,7 @@ Item { } onIconGlobalPointChanged: { - updateWindowIconGeometryTimer.start() + root.scheduleWindowIconGeometryUpdate() } + } diff --git a/panels/dock/taskmanager/package/AppItemPalette.qml b/panels/dock/taskmanager/package/AppItemPalette.qml index 49e3f65e7..c1952819f 100644 --- a/panels/dock/taskmanager/package/AppItemPalette.qml +++ b/panels/dock/taskmanager/package/AppItemPalette.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 @@ -10,21 +10,21 @@ import org.deepin.dtk as D Item { required property int displayMode required property int colorTheme - required property bool active + required property bool itemActive required property color backgroundColor property color dotIndicator: { if (displayMode === Dock.Efficient) { if (colorTheme === Dock.Dark) { - return active ? Qt.rgba(1, 1, 1, 0.6) : Qt.rgba(1, 1, 1, 0.3) + return itemActive ? Qt.rgba(1, 1, 1, 0.6) : Qt.rgba(1, 1, 1, 0.3) } else { - return active ? Qt.rgba(0, 0, 0, 0.8) : Qt.rgba(0, 0, 0, 0.3) + return itemActive ? Qt.rgba(0, 0, 0, 0.8) : Qt.rgba(0, 0, 0, 0.3) } } else if (displayMode === Dock.Fashion) { if (colorTheme === Dock.Dark) { - return active ? backgroundColor : Qt.rgba(1, 1, 1) + return itemActive ? backgroundColor : Qt.rgba(1, 1, 1) } else { - return active ? backgroundColor : Qt.rgba(0, 0, 0) + return itemActive ? backgroundColor : Qt.rgba(0, 0, 0) } } else { return "#00000000" @@ -38,9 +38,9 @@ Item { property color rectIndicator: { if (colorTheme === Dock.Light) { - return active ? Qt.rgba(0, 0, 0, 0.8) : Qt.rgba(0, 0, 0, 0.3) + return itemActive ? Qt.rgba(0, 0, 0, 0.8) : Qt.rgba(0, 0, 0, 0.3) } else { - return active ? Qt.rgba(1, 1, 1, 0.6) : Qt.rgba(1, 1, 1, 0.3) + return itemActive ? Qt.rgba(1, 1, 1, 0.6) : Qt.rgba(1, 1, 1, 0.3) } } diff --git a/panels/dock/taskmanager/package/AppItemTitle.qml b/panels/dock/taskmanager/package/AppItemTitle.qml index 06c6806e9..8a278df0f 100644 --- a/panels/dock/taskmanager/package/AppItemTitle.qml +++ b/panels/dock/taskmanager/package/AppItemTitle.qml @@ -6,6 +6,7 @@ import QtQuick import QtQuick.Controls import Qt5Compat.GraphicalEffects +import org.deepin.ds.dock 1.0 import org.deepin.ds.dock.taskmanager 1.0 import org.deepin.dtk 1.0 as D @@ -14,7 +15,7 @@ Item { property bool active: titleLoader.active property string text: "" - property color textColor: D.DTK.themeType === D.ApplicationHelper.DarkType ? "#FFFFFF" : "#000000" + property int colorTheme: Dock.Dark implicitWidth: titleLoader.width implicitHeight: titleLoader.height @@ -29,7 +30,7 @@ Item { text: root.TextCalculator.elidedText - color: root.textColor + color: root.colorTheme === Dock.Dark ? "#FFFFFF" : "#000000" font: root.TextCalculator.calculator.font verticalAlignment: Text.AlignVCenter diff --git a/panels/dock/taskmanager/package/DockPinnedPopup.qml b/panels/dock/taskmanager/package/DockPinnedPopup.qml new file mode 100644 index 000000000..41ad500bb --- /dev/null +++ b/panels/dock/taskmanager/package/DockPinnedPopup.qml @@ -0,0 +1,1535 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma ComponentBehavior: Bound + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import Qt5Compat.GraphicalEffects + +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +FocusScope { + id: root + + required property var applet + required property string dockElement + property var popupWindow: null + property int colorTheme: Dock.Dark + + signal closeRequested() + + property var descriptor: ({ entries: [] }) + property var pendingDescriptor: null + property int pendingPopupHeight: 0 + property int popupHeightValue: headerHeight + headerBottomSpacing + 72 + bottomPadding + property real contentOpacity: 1.0 + property real contentOffsetX: 0.0 + property int lockedColumnCount: 0 + property string typeAheadBuffer: "" + property int keyboardCurrentIndex: -1 + property bool keyboardSelectionActive: false + + readonly property var entries: descriptor && descriptor.entries ? descriptor.entries : [] + readonly property int sidePadding: 0 + readonly property int headerHeight: 36 + readonly property int headerBottomSpacing: 0 + readonly property int bottomPadding: 8 + readonly property int gridSpacing: 8 + readonly property int itemIconSize: 48 + readonly property int cellWidth: 96 + readonly property int cellHeight: 104 + readonly property int gridWidthExtra: 10 + readonly property int itemTextMaxWidth: 84 + readonly property int itemTopPadding: 4 + readonly property int itemInnerSpacing: 6 + readonly property int itemHoverPadding: 10 + readonly property int itemHoverBottomMargin: 2 + readonly property int itemTextBottomMargin: 6 + readonly property int scrollBarWidth: 0 + readonly property int scrollBarGap: 0 + readonly property int scrollBarLaneWidth: scrollBarWidth + readonly property int thumbnailCornerRadius: 3 + readonly property color selectionFillColor: Qt.rgba(1, 1, 1, 0.21) + readonly property color selectionInsideBorderColor: Qt.rgba(1, 1, 1, 0.15) + readonly property color selectionOutsideBorderColor: Qt.rgba(0, 0, 0, 0.14) + readonly property color hoverFillColor: Qt.rgba(1, 1, 1, 0.08) + readonly property color hoverInsideBorderColor: Qt.rgba(1, 1, 1, 0.06) + readonly property color hoverOutsideBorderColor: Qt.rgba(0, 0, 0, 0.06) + readonly property color thumbnailInsideBorderColor: root.colorTheme === Dock.Dark ? + Qt.rgba(1, 1, 1, 0.14) : + Qt.rgba(1, 1, 1, 0.45) + readonly property color thumbnailOutsideBorderColor: root.colorTheme === Dock.Dark ? + Qt.rgba(0, 0, 0, 0.24) : + Qt.rgba(0, 0, 0, 0.12) + readonly property int gridWidth: columnCount * cellWidth + Math.max(0, columnCount - 1) * gridSpacing + readonly property int gridAreaWidth: gridWidth + gridWidthExtra + readonly property int columnCount: lockedColumnCount > 0 ? lockedColumnCount : preferredColumnCount(descriptor) + readonly property int totalRows: entries.length === 0 ? 0 : Math.ceil(entries.length / columnCount) + readonly property int visibleRows: Math.max(1, Math.min(3, totalRows)) + readonly property int gridContentHeight: entries.length === 0 ? + 72 : + totalRows * cellHeight + Math.max(0, totalRows - 1) * gridSpacing + readonly property int gridViewportHeight: entries.length === 0 ? + 72 : + visibleRows * cellHeight + Math.max(0, visibleRows - 1) * gridSpacing + readonly property int cornerRadius: D.DTK.platformTheme.windowRadius < 0 ? 18 : D.DTK.platformTheme.windowRadius + readonly property real contentScrollY: contentLoader.item && contentLoader.item.scrollY !== undefined ? contentLoader.item.scrollY : 0 + readonly property bool showHeaderSeparator: contentScrollY > 0.5 + readonly property string middleEllipsis: "\u2026" + readonly property bool popupOwnerActive: !!(root.parent && root.parent.visible) + + width: sidePadding * 2 + gridAreaWidth + scrollBarLaneWidth + height: popupHeightValue + focus: true + + function beginPopupSession() { + clearTypeAhead() + lockedColumnCount = 0 + if (popupWindow) { + popupWindow.opacity = 1.0 + } + } + + function preferredColumnCount(nextDescriptor) { + const nextEntries = nextDescriptor && nextDescriptor.entries ? nextDescriptor.entries.length : 0 + return nextEntries > 12 ? 5 : 4 + } + + function descriptorFor(location) { + return root.applet ? root.applet.popupDescriptor(root.dockElement, location || "") : ({ entries: [] }) + } + + function popupHeightFor(nextDescriptor) { + const nextEntries = nextDescriptor && nextDescriptor.entries ? nextDescriptor.entries.length : 0 + const nextRows = nextEntries === 0 ? 0 : Math.ceil(nextEntries / columnCount) + const nextVisibleRows = Math.max(1, Math.min(3, nextRows)) + const nextGridViewportHeight = nextEntries === 0 ? + 72 : + nextVisibleRows * cellHeight + Math.max(0, nextVisibleRows - 1) * gridSpacing + return headerHeight + headerBottomSpacing + nextGridViewportHeight + bottomPadding + } + + function commitDescriptor(nextDescriptor) { + descriptor = nextDescriptor || ({ entries: [] }) + pendingDescriptor = null + pendingPopupHeight = 0 + popupHeightValue = popupHeightFor(descriptor) + resetKeyboardNavigation() + if (popupWindow) { + popupWindow.opacity = 1.0 + } + contentOpacity = 1.0 + contentOffsetX = 0.0 + ensureKeyboardFocus() + } + + function clearDescriptor() { + descriptor = ({ entries: [] }) + pendingDescriptor = null + pendingPopupHeight = 0 + popupHeightValue = popupHeightFor(descriptor) + contentOpacity = 1.0 + contentOffsetX = 0.0 + resetKeyboardNavigation() + } + + function trimTextToWidth(metrics, text, maxWidth, fromRight) { + if (!metrics || !text || maxWidth <= 0) { + return ({ + text: "", + width: 0, + length: 0 + }) + } + + const fullWidth = metrics.advanceWidth(text) + if (fullWidth <= maxWidth) { + return ({ + text: text, + width: fullWidth, + length: text.length + }) + } + + let result = "" + let resultWidth = 0 + if (fromRight) { + for (let index = text.length - 1; index >= 0; --index) { + const candidate = text.charAt(index) + result + const candidateWidth = metrics.advanceWidth(candidate) + if (candidateWidth > maxWidth) { + break + } + result = candidate + resultWidth = candidateWidth + } + return ({ + text: result, + width: resultWidth, + length: result.length + }) + } + + for (let index = 0; index < text.length; ++index) { + const candidate = result + text.charAt(index) + const candidateWidth = metrics.advanceWidth(candidate) + if (candidateWidth > maxWidth) { + break + } + result = candidate + resultWidth = candidateWidth + } + return ({ + text: result, + width: resultWidth, + length: result.length + }) + } + + function twoLineElideCandidate(metrics, text, maxWidth, ellipsisOnFirstLine) { + const ellipsisWidth = metrics.advanceWidth(middleEllipsis) + const firstLineBudget = Math.max(0, maxWidth - (ellipsisOnFirstLine ? ellipsisWidth : 0)) + const secondLineBudget = Math.max(0, maxWidth - (ellipsisOnFirstLine ? 0 : ellipsisWidth)) + const prefix = trimTextToWidth(metrics, text, firstLineBudget, false) + const suffix = trimTextToWidth(metrics, text, secondLineBudget, true) + const hiddenLength = Math.max(0, text.length - prefix.length - suffix.length) + + if (!prefix.length || !suffix.length || hiddenLength <= 0) { + return null + } + + const visibleLength = prefix.length + suffix.length + const balancedLength = Math.min(prefix.length, suffix.length) + const suffixPenalty = suffix.length <= 1 ? 5000 : (suffix.length === 2 ? 800 : 0) + const prefixPenalty = prefix.length <= 1 ? 3000 : 0 + + return ({ + text: ellipsisOnFirstLine ? + prefix.text + middleEllipsis + "\n" + suffix.text : + prefix.text + "\n" + middleEllipsis + suffix.text, + score: visibleLength * 1000 + + balancedLength * 2000 + + prefix.width + + suffix.width - + suffixPenalty - + prefixPenalty + + (ellipsisOnFirstLine ? 100 : 0) + }) + } + + function twoLineMiddleElidedText(metrics, text, maxWidth, needsElide) { + if (!text || maxWidth <= 0) { + return "" + } + + if (!needsElide) { + return text + } + + const candidates = [ + twoLineElideCandidate(metrics, text, maxWidth, true), + twoLineElideCandidate(metrics, text, maxWidth, false) + ].filter(function(candidate) { return candidate !== null }) + + if (!candidates.length) { + return trimTextToWidth(metrics, text, maxWidth, false).text + } + + let bestCandidate = candidates[0] + for (let index = 1; index < candidates.length; ++index) { + if (candidates[index].score > bestCandidate.score) { + bestCandidate = candidates[index] + } + } + + return bestCandidate.text + } + + function clampContentY(flickable, value) { + if (!flickable) { + return 0 + } + + return Math.max(0, Math.min(value, Math.max(0, flickable.contentHeight - flickable.height))) + } + + function parentDirectoryFor(path) { + if (!path) { + return "" + } + + const normalizedPath = String(path) + const separatorIndex = normalizedPath.lastIndexOf("/") + return separatorIndex > 0 ? normalizedPath.substring(0, separatorIndex) : "" + } + + function backIconSource() { + return Qt.resolvedUrl(root.colorTheme === Dock.Dark ? + "icons/back-chevron-dark.svg" : + "icons/back-chevron-light.svg") + } + + function currentLocation() { + return descriptor && descriptor.location ? String(descriptor.location) : "" + } + + function clearTypeAhead() { + typeAheadResetTimer.stop() + typeAheadBuffer = "" + } + + function clearKeyboardSelection() { + keyboardCurrentIndex = -1 + keyboardSelectionActive = false + } + + function resetKeyboardNavigation() { + clearTypeAhead() + clearKeyboardSelection() + } + + function ensureKeyboardFocus() { + if (!root.popupOwnerActive || !root.popupWindow || !root.popupWindow.visible) { + return + } + + focusRestoreTimer.restart() + } + + function normalizedEntryName(value) { + return String(value || "").toLocaleLowerCase() + } + + function typeAheadTextForKey(key, text) { + const keyText = String(text || "") + if (keyText.length === 1 && keyText.charCodeAt(0) >= 0x20) { + return keyText + } + + if (key >= Qt.Key_A && key <= Qt.Key_Z) { + return String.fromCharCode("a".charCodeAt(0) + key - Qt.Key_A) + } + + if (key >= Qt.Key_0 && key <= Qt.Key_9) { + return String.fromCharCode("0".charCodeAt(0) + key - Qt.Key_0) + } + + return "" + } + + function findPrefixMatch(prefix) { + if (!prefix) { + return -1 + } + + const normalizedPrefix = normalizedEntryName(prefix) + for (let index = 0; index < entries.length; ++index) { + const entryName = entries[index] && entries[index].name !== undefined ? + normalizedEntryName(entries[index].name) : + "" + if (entryName.startsWith(normalizedPrefix)) { + return index + } + } + + return -1 + } + + function selectEntryIndex(index, animated) { + if (index < 0 || index >= entries.length) { + clearKeyboardSelection() + return false + } + + keyboardCurrentIndex = index + keyboardSelectionActive = true + if (contentLoader.item && contentLoader.item.scrollToEntry) { + contentLoader.item.scrollToEntry(index, animated !== false) + } + return true + } + + function moveKeyboardSelection(delta) { + if (!entries.length) { + clearKeyboardSelection() + return false + } + + if (!keyboardSelectionActive || keyboardCurrentIndex < 0 || keyboardCurrentIndex >= entries.length) { + return selectEntryIndex(0, false) + } + + const currentIndex = keyboardCurrentIndex + const nextIndex = Math.max(0, Math.min(entries.length - 1, currentIndex + delta)) + return selectEntryIndex(nextIndex, true) + } + + function moveKeyboardSelectionTo(index) { + if (!entries.length) { + clearKeyboardSelection() + return false + } + + const nextIndex = Math.max(0, Math.min(entries.length - 1, index)) + return selectEntryIndex(nextIndex, true) + } + + function handleTypeAheadInput(text) { + const addition = String(text || "") + if (!addition.length) { + return + } + + let nextBuffer = typeAheadBuffer + addition + let matchIndex = findPrefixMatch(nextBuffer) + if (matchIndex < 0) { + nextBuffer = addition + matchIndex = findPrefixMatch(nextBuffer) + if (matchIndex < 0) { + typeAheadResetTimer.restart() + return + } + } + + typeAheadBuffer = nextBuffer + typeAheadResetTimer.restart() + selectEntryIndex(matchIndex, true) + } + + function handleTypeAheadBackspace() { + if (!typeAheadBuffer.length) { + clearTypeAhead() + return + } + + typeAheadBuffer = typeAheadBuffer.slice(0, -1) + if (!typeAheadBuffer.length) { + clearTypeAhead() + return + } + + typeAheadResetTimer.restart() + selectEntryIndex(findPrefixMatch(typeAheadBuffer), true) + } + + function handlePopupKeyPress(key, text, modifiers) { + if (!root.popupOwnerActive || !root.popupWindow || !root.popupWindow.visible) { + return false + } + + if (key === Qt.Key_Escape) { + if (root.typeAheadBuffer.length || root.keyboardSelectionActive) { + root.resetKeyboardNavigation() + return true + } + return false + } + + if (key === Qt.Key_Backspace) { + root.handleTypeAheadBackspace() + return true + } + + if (key === Qt.Key_Left) { + return root.moveKeyboardSelection(-1) + } + + if (key === Qt.Key_Right) { + return root.moveKeyboardSelection(1) + } + + if (key === Qt.Key_Up) { + return root.moveKeyboardSelection(-root.columnCount) + } + + if (key === Qt.Key_Down) { + return root.moveKeyboardSelection(root.columnCount) + } + + if (key === Qt.Key_Home) { + return root.moveKeyboardSelectionTo(0) + } + + if (key === Qt.Key_End) { + return root.moveKeyboardSelectionTo(root.entries.length - 1) + } + + if (key === Qt.Key_Return || key === Qt.Key_Enter) { + if (root.keyboardSelectionActive) { + root.activateKeyboardSelection() + return true + } + return false + } + + if ((modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)) !== 0) { + return false + } + + const keyText = root.typeAheadTextForKey(key, text) + if (!keyText.length) { + return false + } + + root.handleTypeAheadInput(keyText) + return true + } + + function handleApplicationKeyEvent(key, text, modifiers) { + return handlePopupKeyPress(Number(key), String(text || ""), Number(modifiers)) + } + + function activateKeyboardSelection() { + if (!keyboardSelectionActive || keyboardCurrentIndex < 0 || keyboardCurrentIndex >= entries.length) { + return + } + + const currentEntry = entries[keyboardCurrentIndex] + if (!currentEntry) { + return + } + + if (currentEntry.directory) { + refresh(currentEntry.entryId, true) + return + } + + root.applet.activatePopupEntry(root.dockElement, currentEntry.entryId) + root.closeRequested() + } + + function refresh(location, animated) { + const nextDescriptor = descriptorFor(location) + if (lockedColumnCount <= 0) { + lockedColumnCount = preferredColumnCount(nextDescriptor) + } + + if (!animated) { + commitDescriptor(nextDescriptor) + return + } + + if (contentSwapAnimation.running) { + contentSwapAnimation.stop() + commitDescriptor(pendingDescriptor || descriptor) + } + + const currentLocation = descriptor && descriptor.location ? String(descriptor.location) : "" + const nextLocation = nextDescriptor && nextDescriptor.location ? String(nextDescriptor.location) : "" + if (currentLocation === nextLocation) { + commitDescriptor(nextDescriptor) + return + } + + pendingDescriptor = nextDescriptor + pendingPopupHeight = popupHeightFor(nextDescriptor) + const forward = currentLocation !== "" && + nextDescriptor && + nextDescriptor.parentLocation !== undefined && + String(nextDescriptor.parentLocation) === currentLocation + contentOffsetX = 0 + contentSwapOutAnimation.to = forward ? -18 : 18 + contentSwapInAnimation.from = forward ? 18 : -18 + contentSwapAnimation.restart() + } + + onDockElementChanged: { + if (root.popupWindow && root.popupWindow.visible) { + refresh("", false) + return + } + + clearDescriptor() + } + + Keys.priority: Keys.BeforeItem + Keys.onPressed: function(event) { + event.accepted = root.handlePopupKeyPress(event.key, event.text, event.modifiers) + } + + onActiveFocusChanged: { + if (root.popupOwnerActive && !root.activeFocus && root.popupWindow && root.popupWindow.visible && !root.popupWindow.active) { + root.closeRequested() + } + } + + Connections { + target: root.applet + + function onPopupEntryThumbnailChanged(entryPath) { + if (!entryPath || + !root.descriptor || + root.descriptor.kind !== "folder" || + !root.descriptor.location || + (root.popupWindow && !root.popupWindow.visible)) { + return + } + + if (root.parentDirectoryFor(entryPath) === String(root.descriptor.location)) { + thumbnailRefreshTimer.restart() + } + } + } + + Connections { + target: root.popupWindow + enabled: root.popupOwnerActive + + function onVisibleChanged() { + if (!root.popupWindow) { + return + } + + if (root.popupWindow.visible) { + root.ensureKeyboardFocus() + } else { + root.resetKeyboardNavigation() + } + } + + function onActiveChanged() { + if (!root.popupWindow) { + return + } + + if (root.popupWindow.active) { + root.ensureKeyboardFocus() + } else if (root.popupWindow.visible) { + root.closeRequested() + } + } + } + + Timer { + id: thumbnailRefreshTimer + interval: 60 + repeat: false + onTriggered: { + if (root.descriptor && + root.descriptor.kind === "folder" && + root.descriptor.location && + (!root.popupWindow || root.popupWindow.visible)) { + root.refresh(String(root.descriptor.location), false) + } + } + } + + Timer { + id: typeAheadResetTimer + interval: 900 + repeat: false + onTriggered: root.clearTypeAhead() + } + + Timer { + id: focusRestoreTimer + interval: 1 + repeat: false + onTriggered: { + if (root.popupOwnerActive && root.popupWindow && root.popupWindow.visible) { + root.forceActiveFocus(Qt.OtherFocusReason) + } + } + } + + Rectangle { + id: clippedContent + anchors.fill: parent + radius: cornerRadius + color: "transparent" + clip: true + Item { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: headerHeight + + D.ToolButton { + id: backButton + anchors.left: parent.left + anchors.leftMargin: 6 + anchors.top: parent.top + anchors.topMargin: 6 + visible: !!(root.descriptor && root.descriptor.canGoBack) + width: 24 + height: 24 + display: AbstractButton.IconOnly + flat: true + padding: 0 + implicitWidth: width + implicitHeight: height + contentItem: Item { + implicitWidth: 24 + implicitHeight: 24 + + Image { + anchors.centerIn: parent + width: 16 + height: 16 + source: root.backIconSource() + sourceSize: Qt.size(Math.round(width * (Screen.devicePixelRatio > 0 ? Screen.devicePixelRatio : 1.0)), + Math.round(height * (Screen.devicePixelRatio > 0 ? Screen.devicePixelRatio : 1.0))) + fillMode: Image.PreserveAspectFit + smooth: false + } + } + background: Item { + implicitWidth: 24 + implicitHeight: 24 + + AppletItemBackground { + anchors.fill: parent + enabled: false + radius: 10 + isActive: false + opacity: backButton.hovered || backButton.down ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { duration: 150 } + } + } + } + onClicked: root.refresh(root.descriptor.parentLocation, true) + } + + Label { + anchors.centerIn: parent + width: parent.width - 72 + text: root.descriptor && root.descriptor.title ? root.descriptor.title : "" + elide: Text.ElideMiddle + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: 13 + font.weight: Font.Normal + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.bottom: parent.bottom + height: 1 + color: root.colorTheme === Dock.Dark ? + Qt.rgba(1, 1, 1, 0.10) : + Qt.rgba(0, 0, 0, 0.10) + visible: root.showHeaderSeparator + } + } + + Item { + id: listViewport + width: root.gridAreaWidth + root.scrollBarLaneWidth + height: root.gridViewportHeight + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: headerRow.bottom + anchors.topMargin: headerBottomSpacing + opacity: root.contentOpacity + + transform: Translate { + x: root.contentOffsetX + } + + Loader { + id: contentLoader + anchors.left: parent.left + anchors.top: parent.top + width: root.gridAreaWidth + height: parent.height + sourceComponent: root.entries.length === 0 ? emptyStateComponent : gridContentComponent + } + } + } + + Component { + id: emptyStateComponent + + Item { + Label { + anchors.fill: parent + text: qsTr("No items") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: 0.7 + } + } + } + + Component { + id: gridContentComponent + + Item { + id: gridContentRoot + width: root.gridAreaWidth + height: root.gridViewportHeight + property real scrollY: gridFlickable.contentY + property Item hoverTargetButton: null + property Item selectionTargetButton: null + property bool hoverSyncPending: false + property real hoverBackgroundTargetX: 0 + property real hoverBackgroundTargetY: 0 + property bool hoverBackgroundPositionAnimationEnabled: false + + function hoverBackgroundXFor(button) { + if (!button) { + return hoverBackgroundTargetX + } + + return contentGrid.x + button.x + Math.round((button.width - button.hoverWidth) / 2) + } + + function hoverBackgroundYFor(button) { + if (!button) { + return hoverBackgroundTargetY + } + + return contentGrid.y + button.y + } + + function setHoverTarget(nextTarget, animatePosition) { + const nextTargetX = nextTarget ? hoverBackgroundXFor(nextTarget) : hoverBackgroundTargetX + const nextTargetY = nextTarget ? hoverBackgroundYFor(nextTarget) : hoverBackgroundTargetY + const targetChanged = hoverTargetButton !== nextTarget + const positionChanged = !nextTarget + ? false + : (Math.abs(hoverBackgroundTargetX - nextTargetX) > 0.5 + || Math.abs(hoverBackgroundTargetY - nextTargetY) > 0.5) + + if (!targetChanged && !positionChanged) { + return + } + + const hadTarget = hoverTargetButton !== null + hoverBackgroundPositionAnimationEnabled = !!animatePosition && hadTarget && nextTarget !== null && (targetChanged || positionChanged) + if (nextTarget) { + hoverBackgroundTargetX = nextTargetX + hoverBackgroundTargetY = nextTargetY + } + + hoverTargetButton = nextTarget + } + + function scheduleHoverSync() { + if (hoverSyncPending) { + return + } + + hoverSyncPending = true + Qt.callLater(function() { + hoverSyncPending = false + syncHoverTarget() + }) + } + + function syncHoverTarget() { + let nextTarget = null + for (let index = 0; index < gridRepeater.count; ++index) { + const candidate = gridRepeater.itemAt(index) + if (candidate && candidate.hoverActive) { + nextTarget = candidate + break + } + } + + setHoverTarget(nextTarget, true) + } + + function syncSelectionTarget() { + if (!root.keyboardSelectionActive || root.keyboardCurrentIndex < 0 || root.keyboardCurrentIndex >= gridRepeater.count) { + selectionTargetButton = null + return + } + + selectionTargetButton = gridRepeater.itemAt(root.keyboardCurrentIndex) + } + + function scrollToEntry(entryIndex, animated) { + if (entryIndex < 0 || entryIndex >= root.entries.length) { + return + } + + const row = Math.floor(entryIndex / root.columnCount) + const rowHeight = root.cellHeight + root.gridSpacing + const targetContentY = root.clampContentY(gridFlickable, + row * rowHeight - Math.max(0, Math.round((gridFlickable.height - root.cellHeight) / 2))) + + if (!animated) { + scrollToEntryAnimation.stop() + gridFlickable.contentY = targetContentY + return + } + + scrollToEntryAnimation.from = gridFlickable.contentY + scrollToEntryAnimation.to = targetContentY + scrollToEntryAnimation.restart() + } + + D.ScrollView { + id: gridScrollView + anchors.fill: parent + padding: 0 + clip: true + + Flickable { + id: gridFlickable + anchors.fill: parent + contentWidth: root.gridAreaWidth + contentHeight: root.gridContentHeight + clip: true + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.DragAndOvershootBounds + maximumFlickVelocity: 3200 + flickDeceleration: 2800 + interactive: root.totalRows > root.visibleRows + rebound: Transition { + NumberAnimation { + properties: "x,y" + duration: 180 + easing.type: Easing.OutCubic + } + } + + onContentHeightChanged: { + const clampedContentY = root.clampContentY(gridFlickable, contentY) + if (Math.abs(clampedContentY - contentY) > 0.5) { + scrollToEntryAnimation.stop() + contentY = clampedContentY + } + } + onHeightChanged: { + const clampedContentY = root.clampContentY(gridFlickable, contentY) + if (Math.abs(clampedContentY - contentY) > 0.5) { + scrollToEntryAnimation.stop() + contentY = clampedContentY + } + } + + NumberAnimation { + id: scrollToEntryAnimation + target: gridFlickable + property: "contentY" + duration: 220 + easing.type: Easing.OutCubic + } + + Item { + id: keyboardSelectionBackground + x: gridContentRoot.selectionTargetButton ? + contentGrid.x + gridContentRoot.selectionTargetButton.x + + Math.round((gridContentRoot.selectionTargetButton.width - width) / 2) : + 0 + y: gridContentRoot.selectionTargetButton ? + contentGrid.y + gridContentRoot.selectionTargetButton.y : + 0 + width: gridContentRoot.selectionTargetButton ? gridContentRoot.selectionTargetButton.hoverWidth : 0 + height: gridContentRoot.selectionTargetButton ? gridContentRoot.selectionTargetButton.hoverHeight : 0 + opacity: gridContentRoot.selectionTargetButton ? 1.0 : 0.0 + + Rectangle { + anchors.fill: parent + radius: 12 + color: root.selectionFillColor + } + + D.InsideBoxBorder { + anchors.fill: parent + radius: 12 + color: root.selectionInsideBorderColor + borderWidth: 1 / Screen.devicePixelRatio + } + + D.OutsideBoxBorder { + anchors.fill: parent + radius: 12 + color: root.selectionOutsideBorderColor + borderWidth: 1 / Screen.devicePixelRatio + } + + Behavior on x { + NumberAnimation { duration: 72; easing.type: Easing.OutQuad } + } + Behavior on y { + NumberAnimation { duration: 72; easing.type: Easing.OutQuad } + } + Behavior on opacity { + NumberAnimation { duration: 72; easing.type: Easing.OutQuad } + } + } + + Item { + id: hoverBackground + x: gridContentRoot.hoverBackgroundTargetX + y: gridContentRoot.hoverBackgroundTargetY + width: gridContentRoot.hoverTargetButton ? gridContentRoot.hoverTargetButton.hoverWidth : 0 + height: gridContentRoot.hoverTargetButton ? gridContentRoot.hoverTargetButton.hoverHeight : 0 + opacity: gridContentRoot.hoverTargetButton && gridContentRoot.hoverTargetButton !== gridContentRoot.selectionTargetButton ? 1.0 : 0.0 + + Rectangle { + anchors.fill: parent + radius: 12 + color: root.hoverFillColor + } + + D.InsideBoxBorder { + anchors.fill: parent + radius: 12 + color: root.hoverInsideBorderColor + borderWidth: 1 / Screen.devicePixelRatio + } + + D.OutsideBoxBorder { + anchors.fill: parent + radius: 12 + color: root.hoverOutsideBorderColor + borderWidth: 1 / Screen.devicePixelRatio + } + + Behavior on x { + enabled: gridContentRoot.hoverBackgroundPositionAnimationEnabled + NumberAnimation { duration: 72; easing.type: Easing.OutQuad } + } + Behavior on y { + enabled: gridContentRoot.hoverBackgroundPositionAnimationEnabled + NumberAnimation { duration: 72; easing.type: Easing.OutQuad } + } + Behavior on opacity { + NumberAnimation { duration: 72; easing.type: Easing.OutQuad } + } + } + + Grid { + id: contentGrid + x: Math.round((root.gridAreaWidth - width) / 2) + columns: root.columnCount + spacing: root.gridSpacing + + Repeater { + id: gridRepeater + model: root.entries + + delegate: D.ToolButton { + id: gridButton + required property int index + required property var modelData + readonly property string entryUrl: modelData && modelData.entryUrl ? String(modelData.entryUrl) : "" + readonly property string thumbnailUrl: modelData && modelData.thumbnailUrl ? String(modelData.thumbnailUrl) : "" + readonly property bool thumbnailAvailable: thumbnailUrl !== "" + readonly property bool thumbnailReady: thumbnailAvailable && thumbnailImage.status === Image.Ready + readonly property real thumbnailAspectRatio: thumbnailImage.implicitHeight > 0 ? + thumbnailImage.implicitWidth / thumbnailImage.implicitHeight : + 1 + readonly property int thumbnailDisplayWidth: Math.max(1, Math.round(thumbnailAspectRatio >= 1 ? + root.itemIconSize : + root.itemIconSize * thumbnailAspectRatio)) + readonly property int thumbnailDisplayHeight: Math.max(1, Math.round(thumbnailAspectRatio >= 1 ? + root.itemIconSize / thumbnailAspectRatio : + root.itemIconSize)) + readonly property bool keyboardSelected: root.keyboardSelectionActive && index === root.keyboardCurrentIndex + readonly property bool hoverActive: itemHoverHandler.hovered || down + property string dragImageSource: "" + property string dragImagePath: "" + property bool suppressClick: false + readonly property color dragTitleBackgroundColor: root.colorTheme === Dock.Dark ? + Qt.rgba(0, 0, 0, 0.58) : + Qt.rgba(1, 1, 1, 0.92) + readonly property color dragTitleBorderColor: root.colorTheme === Dock.Dark ? + Qt.rgba(1, 1, 1, 0.12) : + Qt.rgba(0, 0, 0, 0.10) + readonly property int dragTitleLineCount: Math.max(1, Math.min(titleMeasure.lineCount > 0 ? titleMeasure.lineCount : 1, 2)) + readonly property int dragTitleWidth: Math.min(root.itemTextMaxWidth, Math.max(titleMetrics.advanceWidth, 24)) + readonly property int dragTitleHeight: dragTitleLineCount * titleFontMetrics.height + readonly property int dragOverlayWidth: Math.max(root.itemIconSize + 24, dragTitleWidth + 18) + readonly property int dragOverlayHeight: root.itemIconSize + 8 + dragTitleHeight + 10 + readonly property real sourceDragHotSpotX: width / 2 + readonly property real sourceDragHotSpotY: root.itemTopPadding + root.itemIconSize / 2 + readonly property real dragOverlayHotSpotScaleY: dragOverlayHeight > 0 + ? (root.itemIconSize / 2) / dragOverlayHeight + : 0.5 + + property int hoverWidth: Math.min(root.cellWidth - 4, + Math.max(root.itemIconSize + root.itemHoverPadding * 2, + titleMetrics.advanceWidth + root.itemHoverPadding * 2)) + property int hoverHeight: Math.min(root.cellHeight - 2, + contentColumn.implicitHeight + root.itemHoverPadding + root.itemTextBottomMargin - root.itemHoverBottomMargin) + + width: root.cellWidth + height: root.cellHeight + flat: true + padding: 0 + Drag.dragType: Drag.Automatic + Drag.hotSpot.x: Qt.platform.pluginName === "xcb" ? 0 : sourceDragHotSpotX + Drag.hotSpot.y: Qt.platform.pluginName === "xcb" ? 0 : sourceDragHotSpotY + Drag.supportedActions: Qt.CopyAction | Qt.MoveAction | Qt.LinkAction + Drag.mimeData: ({ + "text/uri-list": gridButton.entryUrl + }) + DQuickDrag.hotSpotScale: Qt.size(0.5, dragOverlayHotSpotScaleY) + DQuickDrag.active: Drag.active && Qt.platform.pluginName === "xcb" + DQuickDrag.overlay: dragOverlayWindow + + function releaseDragImage() { + if (dragImagePath !== "" && root.applet && root.applet.releaseManagedTempFile) { + root.applet.releaseManagedTempFile(dragImagePath) + } + dragImagePath = "" + dragImageSource = "" + Drag.imageSource = "" + } + + background: Item {} + + HoverHandler { + id: itemHoverHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + onHoveredChanged: { + if (hovered || gridButton.down) { + gridContentRoot.setHoverTarget(gridButton, true) + } else if (gridContentRoot.hoverTargetButton === gridButton) { + gridContentRoot.scheduleHoverSync() + } + } + } + + contentItem: Item { + Column { + id: contentColumn + anchors.top: parent.top + anchors.topMargin: root.itemTopPadding + anchors.horizontalCenter: parent.horizontalCenter + width: gridButton.hoverWidth - root.itemHoverPadding * 2 + spacing: root.itemInnerSpacing + + Item { + anchors.horizontalCenter: parent.horizontalCenter + width: root.itemIconSize + height: root.itemIconSize + + Image { + id: thumbnailImage + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + source: gridButton.thumbnailUrl + sourceSize: Qt.size(Math.round(root.itemIconSize * (Screen.devicePixelRatio > 0 ? Screen.devicePixelRatio : 1.0)), + Math.round(root.itemIconSize * (Screen.devicePixelRatio > 0 ? Screen.devicePixelRatio : 1.0))) + fillMode: Image.Stretch + asynchronous: true + cache: false + smooth: true + visible: false + } + + Rectangle { + id: thumbnailMask + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + radius: root.thumbnailCornerRadius + color: "white" + visible: false + } + + OpacityMask { + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + source: thumbnailImage + maskSource: thumbnailMask + cached: false + visible: gridButton.thumbnailReady + } + + D.InsideBoxBorder { + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + radius: root.thumbnailCornerRadius + color: root.thumbnailInsideBorderColor + borderWidth: 1 / Screen.devicePixelRatio + visible: gridButton.thumbnailReady + } + + D.OutsideBoxBorder { + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + radius: root.thumbnailCornerRadius + color: root.thumbnailOutsideBorderColor + borderWidth: 1 / Screen.devicePixelRatio + visible: gridButton.thumbnailReady + } + + D.DciIcon { + anchors.centerIn: parent + width: root.itemIconSize + height: root.itemIconSize + sourceSize: Qt.size(width, height) + name: modelData.iconName + smooth: false + retainWhileLoading: true + visible: !gridButton.thumbnailReady + } + } + + Label { + id: titleLabel + width: Math.min(root.itemTextMaxWidth, parent.width) + anchors.horizontalCenter: parent.horizontalCenter + text: root.twoLineMiddleElidedText(titleFontMetrics, + titleMeasure.text, + width, + titleMeasure.lineCount > 2) + wrapMode: Text.WrapAnywhere + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideNone + color: root.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 1) : Qt.rgba(0, 0, 0, 1) + font.pixelSize: 11 + font.weight: Font.Normal + } + + Text { + id: titleMeasure + width: titleLabel.width + visible: false + text: gridButton.modelData && gridButton.modelData.name ? String(gridButton.modelData.name) : "" + wrapMode: Text.WrapAnywhere + font.pixelSize: titleLabel.font.pixelSize + font.weight: titleLabel.font.weight + } + + FontMetrics { + id: titleFontMetrics + font.pixelSize: titleLabel.font.pixelSize + font.weight: titleLabel.font.weight + } + + Item { + width: 1 + height: root.itemTextBottomMargin + } + } + } + + TextMetrics { + id: titleMetrics + font.pixelSize: 11 + text: gridButton.modelData && gridButton.modelData.name ? String(gridButton.modelData.name) : "" + } + + property Component dragOverlayWindow: QuickDragWindow { + width: gridButton.dragOverlayWidth + height: gridButton.dragOverlayHeight + + Column { + id: dragVisual + anchors.centerIn: parent + spacing: 8 + width: gridButton.dragOverlayWidth + + Item { + id: dragIconPreview + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: root.itemIconSize + implicitHeight: root.itemIconSize + + Image { + id: dragThumbnailImage + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + source: gridButton.thumbnailUrl + sourceSize: Qt.size(Math.round(root.itemIconSize * (Screen.devicePixelRatio > 0 ? Screen.devicePixelRatio : 1.0)), + Math.round(root.itemIconSize * (Screen.devicePixelRatio > 0 ? Screen.devicePixelRatio : 1.0))) + fillMode: Image.Stretch + asynchronous: true + cache: false + smooth: true + visible: false + } + + Rectangle { + id: dragThumbnailMask + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + radius: root.thumbnailCornerRadius + color: "white" + visible: false + } + + OpacityMask { + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + source: dragThumbnailImage + maskSource: dragThumbnailMask + cached: false + visible: gridButton.thumbnailAvailable && dragThumbnailImage.status === Image.Ready + } + + D.InsideBoxBorder { + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + radius: root.thumbnailCornerRadius + color: root.thumbnailInsideBorderColor + borderWidth: 1 / Screen.devicePixelRatio + visible: gridButton.thumbnailAvailable && dragThumbnailImage.status === Image.Ready + } + + D.OutsideBoxBorder { + anchors.centerIn: parent + width: gridButton.thumbnailDisplayWidth + height: gridButton.thumbnailDisplayHeight + radius: root.thumbnailCornerRadius + color: root.thumbnailOutsideBorderColor + borderWidth: 1 / Screen.devicePixelRatio + visible: gridButton.thumbnailAvailable && dragThumbnailImage.status === Image.Ready + } + + D.DciIcon { + anchors.centerIn: parent + width: root.itemIconSize + height: root.itemIconSize + sourceSize: Qt.size(width, height) + name: gridButton.modelData && gridButton.modelData.iconName ? String(gridButton.modelData.iconName) : "" + visible: !gridButton.thumbnailAvailable || dragThumbnailImage.status !== Image.Ready + smooth: false + retainWhileLoading: true + } + } + + Rectangle { + id: dragTitleBackground + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: gridButton.dragOverlayWidth + implicitHeight: gridButton.dragTitleHeight + 10 + radius: 8 + color: gridButton.dragTitleBackgroundColor + + D.InsideBoxBorder { + anchors.fill: parent + radius: parent.radius + color: gridButton.dragTitleBorderColor + borderWidth: 1 / Screen.devicePixelRatio + } + + Label { + id: dragTitleLabel + anchors.centerIn: parent + width: Math.min(root.itemTextMaxWidth, parent.width - 12) + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WrapAnywhere + maximumLineCount: 2 + elide: Text.ElideNone + text: root.twoLineMiddleElidedText(titleFontMetrics, + titleMeasure.text, + width, + titleMeasure.lineCount > 2) + color: root.colorTheme === Dock.Dark ? Qt.rgba(1, 1, 1, 1) : Qt.rgba(0, 0, 0, 1) + font.pixelSize: 11 + font.weight: Font.Normal + } + } + } + } + + Timer { + id: suppressClickTimer + interval: 120 + repeat: false + onTriggered: { + gridButton.suppressClick = false + } + } + + DragHandler { + id: fileDragHandler + target: null + enabled: gridButton.entryUrl !== "" + acceptedButtons: Qt.LeftButton + dragThreshold: 6 + onActiveChanged: { + if (active) { + gridButton.suppressClick = true + Panel.contextDragging = true + gridButton.releaseDragImage() + if (Qt.platform.pluginName !== "xcb") { + gridButton.grabToImage(function(result) { + if (!fileDragHandler.active) { + return + } + + const dragImagePath = root.applet && root.applet.createManagedTempFilePath + ? root.applet.createManagedTempFilePath("dock-popup-drag-", ".png") + : "" + if (dragImagePath === "") { + return + } + if (!result.saveToFile(dragImagePath)) { + if (root.applet && root.applet.releaseManagedTempFile) { + root.applet.releaseManagedTempFile(dragImagePath) + } + return + } + const dragImageUrl = "file://" + dragImagePath + gridButton.dragImagePath = dragImagePath + gridButton.dragImageSource = dragImageUrl + gridButton.Drag.imageSource = dragImageUrl + }) + } + } else { + Panel.contextDragging = false + gridButton.releaseDragImage() + suppressClickTimer.restart() + } + + Qt.callLater(function() { + gridButton.Drag.active = fileDragHandler.active + if (fileDragHandler.active) { + root.closeRequested() + } + }) + } + } + Component.onDestruction: releaseDragImage() + + onClicked: { + if (gridButton.suppressClick || fileDragHandler.active || gridButton.Drag.active) { + return + } + + root.selectEntryIndex(index, false) + if (modelData.directory) { + root.refresh(modelData.entryId, true) + return + } + + root.applet.activatePopupEntry(root.dockElement, modelData.entryId) + root.closeRequested() + } + + hoverEnabled: false + + onDownChanged: { + if (down) { + gridContentRoot.setHoverTarget(gridButton, true) + } else if (!itemHoverHandler.hovered && gridContentRoot.hoverTargetButton === gridButton) { + gridContentRoot.scheduleHoverSync() + } + } + onKeyboardSelectedChanged: gridContentRoot.syncSelectionTarget() + Component.onCompleted: { + gridContentRoot.scheduleHoverSync() + gridContentRoot.syncSelectionTarget() + } + } + + onItemAdded: function(index, item) { + gridContentRoot.scheduleHoverSync() + gridContentRoot.syncSelectionTarget() + } + + onItemRemoved: function(index, item) { + gridContentRoot.scheduleHoverSync() + gridContentRoot.syncSelectionTarget() + } + } + } + } + } + + Connections { + target: root + function onKeyboardCurrentIndexChanged() { + gridContentRoot.syncSelectionTarget() + } + function onKeyboardSelectionActiveChanged() { + gridContentRoot.syncSelectionTarget() + } + } + } + } + + SequentialAnimation { + id: contentSwapAnimation + + ParallelAnimation { + NumberAnimation { + target: root.popupWindow + property: "opacity" + to: 0.35 + duration: 90 + easing.type: Easing.OutQuad + } + + NumberAnimation { + target: root + property: "contentOpacity" + to: 0 + duration: 90 + easing.type: Easing.OutQuad + } + + NumberAnimation { + id: contentSwapOutAnimation + target: root + property: "contentOffsetX" + to: -18 + duration: 90 + easing.type: Easing.OutQuad + } + } + + ScriptAction { + script: { + root.descriptor = root.pendingDescriptor || ({ entries: [] }) + root.popupHeightValue = root.pendingPopupHeight > 0 ? root.pendingPopupHeight : root.popupHeightFor(root.descriptor) + root.resetKeyboardNavigation() + root.contentOpacity = 0 + root.contentOffsetX = contentSwapInAnimation.from + } + } + + ParallelAnimation { + NumberAnimation { + target: root.popupWindow + property: "opacity" + to: 1.0 + duration: 150 + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: root + property: "contentOpacity" + to: 1 + duration: 150 + easing.type: Easing.OutCubic + } + + NumberAnimation { + id: contentSwapInAnimation + target: root + property: "contentOffsetX" + from: 18 + to: 0 + duration: 150 + easing.type: Easing.OutCubic + } + } + + ScriptAction { + script: { + root.pendingDescriptor = null + root.pendingPopupHeight = 0 + } + } + } +} diff --git a/panels/dock/taskmanager/package/PinnedItemIcon.qml b/panels/dock/taskmanager/package/PinnedItemIcon.qml new file mode 100644 index 000000000..703a1492e --- /dev/null +++ b/panels/dock/taskmanager/package/PinnedItemIcon.qml @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 as D + +Item { + id: root + + property string iconName: "" + property var previewIcons: [] + property int iconSize: 32 + property int colorTheme: Dock.Dark + readonly property var visiblePreviewIcons: { + const icons = [] + if (!previewIcons) { + return icons + } + + for (let i = 0; i < previewIcons.length && icons.length < 4; ++i) { + const icon = previewIcons[i] + if (icon && icon.length > 0) { + icons.push(icon) + } + } + return icons + } + readonly property bool useCompositePreview: visiblePreviewIcons.length > 1 + readonly property int desiredCompositeOuterInset: Math.max(2, Math.round(iconSize * 0.12)) + readonly property int desiredCompositeGap: Math.max(1, Math.round(iconSize * 0.05)) + readonly property int compositeIconSize: Math.max(1, Math.floor((iconSize - desiredCompositeOuterInset * 2 - desiredCompositeGap) / 2)) + readonly property int compositeGap: Math.max(1, Math.min(desiredCompositeGap, Math.max(1, iconSize - compositeIconSize * 2))) + readonly property int compositeContentSize: compositeIconSize * 2 + compositeGap + readonly property int compositeOuterInset: Math.max(1, Math.floor((iconSize - compositeContentSize) / 2)) + + width: iconSize + height: 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 + } + + Item { + anchors.centerIn: parent + width: root.compositeContentSize + height: root.compositeContentSize + visible: root.useCompositePreview + + Grid { + anchors.centerIn: parent + columns: 2 + rows: 2 + rowSpacing: root.compositeGap + columnSpacing: root.compositeGap + + Repeater { + model: root.useCompositePreview ? root.visiblePreviewIcons : 0 + delegate: D.DciIcon { + required property string modelData + + name: modelData + width: root.compositeIconSize + height: root.compositeIconSize + sourceSize: Qt.size(width, height) + smooth: false + retainWhileLoading: true + } + } + } + } + + D.DciIcon { + anchors.centerIn: parent + width: root.iconSize + height: root.iconSize + sourceSize: Qt.size(width, height) + name: root.visiblePreviewIcons.length === 1 ? root.visiblePreviewIcons[0] : root.iconName + visible: !root.useCompositePreview + smooth: false + retainWhileLoading: true + } +} diff --git a/panels/dock/taskmanager/package/TaskManager.qml b/panels/dock/taskmanager/package/TaskManager.qml index ce7aeafed..89fa01c9d 100644 --- a/panels/dock/taskmanager/package/TaskManager.qml +++ b/panels/dock/taskmanager/package/TaskManager.qml @@ -2,8 +2,11 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +pragma ComponentBehavior: Bound + import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQml.Models 2.15 import org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 @@ -14,17 +17,70 @@ ContainmentItem { id: taskmanager property bool useColumnLayout: Panel.rootObject.useColumnLayout property int dockOrder: 16 - property real remainingSpacesForTaskManager: Panel.rootObject.dockRemainingSpaceForCenter - readonly property int appTitleSpacing: Math.max(10, Math.round(Panel.rootObject.dockItemMaxSize * 9 / 14) / 3) + property real remainingSpacesForTaskManager: Panel.rootObject.adaptiveFashionMode ? 0 : (Panel.itemAlignment === Dock.LeftAlignment ? Panel.rootObject.dockLeftSpaceForCenter : Panel.rootObject.dockRemainingSpaceForCenter) + readonly property bool centeredHorizontalFashionMode: Panel.viewMode === Dock.FashionMode + && !Panel.rootObject.adaptiveFashionMode + && !useColumnLayout + readonly property bool resizeOptimizationActive: Panel.isResizing || (Panel.rootObject && Panel.rootObject.isDragging) + readonly property int targetDockItemMaxSize: { + if (Panel.rootObject.adaptiveFashionMode) { + return Panel.rootObject.dockSize + } + + const slotCount = Panel.rootObject.dockCenterPartCount - 1 + visualModel.count + if (slotCount <= 0) { + return Panel.rootObject.dockSize + } - implicitWidth: { - let maxW = Panel.itemAlignment === Dock.LeftAlignment ? Math.max(remainingSpacesForTaskManager, appContainer.implicitWidth) : Math.min(remainingSpacesForTaskManager, appContainer.implicitWidth) - return useColumnLayout ? Panel.rootObject.dockSize : maxW + return Math.min(Panel.rootObject.dockSize, + Panel.rootObject.dockLeftSpaceForCenter * 1.2 / slotCount - 2) } - implicitHeight: { - let maxH = Panel.itemAlignment === Dock.LeftAlignment ? Math.max(remainingSpacesForTaskManager, appContainer.implicitHeight) : Math.min(remainingSpacesForTaskManager, appContainer.implicitHeight) - return useColumnLayout ? maxH : Panel.rootObject.dockSize + + readonly property int appTitleSpacing: Math.max(10, Math.round(Panel.rootObject.dockItemMaxSize * 9 / 14) / 3) + readonly property bool adaptiveFashionMinimumReached: Panel.rootObject.adaptiveFashionMode + && !useColumnLayout + && (Panel.rootObject.preferredDockSize <= Dock.MIN_DOCK_SIZE + || Panel.rootObject.dockSize <= Dock.MIN_DOCK_SIZE) + readonly property real adaptiveFashionOverflowHysteresis: 8 + readonly property real adaptiveFashionItemWidth: Math.max(1, Math.round(Panel.rootObject.dockItemMaxSize * 9 / 14 + appTitleSpacing)) + readonly property real adaptiveFashionOverflowButtonWidth: adaptiveFashionItemWidth + readonly property real adaptiveFashionFullItemsWidth: { + const totalCount = visualModel.items.count + if (totalCount <= 0) { + return 0 + } + + let width = 0 + for (let index = 0; index < totalCount; ++index) { + if (index > 0) { + width += appContainer.spacing + } + width += adaptiveFashionItemWidthForIndex(index) + } + return width } + property bool adaptiveFashionOverflowLatched: false + readonly property bool adaptiveFashionOverflowEnabled: Panel.rootObject.adaptiveFashionMode + && !useColumnLayout + && adaptiveFashionOverflowLatched + property int adaptiveFashionVisibleItemCount: 0 + property var adaptiveFashionOverflowItems: [] + property var adaptiveFashionMeasuredItemWidths: ({}) + readonly property int adaptiveFashionOverflowCount: adaptiveFashionOverflowItems.length + property bool adaptiveFashionOverflowSyncPending: false + property real remainingSpacesForSplitWindow: Panel.rootObject.adaptiveFashionMode ? 0 : Math.max(0, Panel.rootObject.dockLeftSpaceForCenter - ( + (Panel.rootObject.dockCenterPartCount - 1) * (visualModel.cellWidth + appTitleSpacing) + (Panel.rootObject.dockCenterPartCount) * Panel.rootObject.dockPartSpacing)) + // 用于居中计算的实际应用区域尺寸 + property int appContainerWidth: useColumnLayout ? Panel.rootObject.dockSize : appContainer.implicitWidth + property int appContainerHeight: useColumnLayout ? appContainer.implicitHeight : Panel.rootObject.dockSize + property int appContainerTargetWidth: useColumnLayout ? Panel.rootObject.dockSize : appContainer.targetImplicitWidth + property int appContainerTargetHeight: useColumnLayout ? appContainer.targetImplicitHeight : Panel.rootObject.dockSize + + implicitWidth: useColumnLayout + ? Panel.rootObject.dockSize + : (centeredHorizontalFashionMode ? appContainer.implicitWidth : Math.max(remainingSpacesForTaskManager, appContainer.implicitWidth)) + implicitHeight: useColumnLayout ? Math.max(remainingSpacesForTaskManager, appContainer.implicitHeight) : Panel.rootObject.dockSize + // Helper function to find the current index of an app by its appId in the visualModel function findAppIndex(appId) { for (let i = 0; i < visualModel.items.count; i++) { @@ -48,13 +104,234 @@ ContainmentItem { return -1 } + function findDockElementIndex(dockElement) { + for (let i = 0; i < visualModel.items.count; i++) { + const item = visualModel.items.get(i) + if (item.model.dockElement === dockElement) { + return item.itemsIndex + } + } + return -1 + } + function blendColorAlpha(fallback) { var appearance = DS.applet("org.deepin.ds.dde-appearance") if (!appearance || appearance.opacity < 0) return fallback return appearance.opacity } - property real blendOpacity: blendColorAlpha(D.DTK.themeType === D.ApplicationHelper.DarkType ? 0.25 : 1.0) + property real blendOpacity: blendColorAlpha(Panel.colorTheme === Dock.Dark ? 0.25 : 1.0) + readonly property bool previewSwitchActive: previewSwitchGraceTimer.running + + function overflowItemDataAt(sourceIndex) { + if (sourceIndex < 0 || sourceIndex >= visualModel.items.count) { + return null + } + + const item = visualModel.items.get(sourceIndex) + if (!item || !item.model) { + return null + } + + const model = item.model + return { + active: model.active, + attention: model.attention, + itemId: model.itemId, + dockElement: model.dockElement, + itemKind: model.itemKind, + name: model.name, + title: model.title, + iconName: model.iconName, + previewIcons: model.previewIcons, + menus: model.menus, + windows: model.windows, + visualIndex: sourceIndex, + modelIndex: visualModel.modelIndex(sourceIndex) + } + } + + function scheduleAdaptiveFashionOverflowSync() { + if (adaptiveFashionOverflowSyncPending) { + return + } + + adaptiveFashionOverflowSyncPending = true + Qt.callLater(function() { + adaptiveFashionOverflowSyncPending = false + taskmanager.syncAdaptiveFashionOverflow() + }) + } + + function adaptiveFashionWidthKeyAt(sourceIndex) { + if (sourceIndex < 0 || sourceIndex >= visualModel.items.count) { + return "" + } + + const item = visualModel.items.get(sourceIndex) + if (!item || !item.model) { + return "" + } + + const model = item.model + if (model.dockElement && model.dockElement.length > 0) { + return model.dockElement + } + + if (model.itemId && model.itemId.length > 0) { + return model.itemId + } + + return String(sourceIndex) + } + + function registerAdaptiveFashionItemWidth(key, width) { + if (!key || !Number.isFinite(width) || width <= 0) { + return + } + + const currentWidth = adaptiveFashionMeasuredItemWidths[key] + if (currentWidth === width) { + return + } + + adaptiveFashionMeasuredItemWidths = Object.assign({}, adaptiveFashionMeasuredItemWidths, {[key]: width}) + scheduleAdaptiveFashionOverflowSync() + } + + function unregisterAdaptiveFashionItemWidth(key) { + if (!key || adaptiveFashionMeasuredItemWidths[key] === undefined) { + return + } + + let nextWidths = Object.assign({}, adaptiveFashionMeasuredItemWidths) + delete nextWidths[key] + adaptiveFashionMeasuredItemWidths = nextWidths + scheduleAdaptiveFashionOverflowSync() + } + + function adaptiveFashionItemWidthForIndex(sourceIndex) { + const widthKey = adaptiveFashionWidthKeyAt(sourceIndex) + const measuredWidth = widthKey ? adaptiveFashionMeasuredItemWidths[widthKey] : undefined + return (Number.isFinite(measuredWidth) && measuredWidth > 0) ? measuredWidth : adaptiveFashionItemWidth + } + + function syncAdaptiveFashionOverflowEnabledState() { + if (!Panel.rootObject.adaptiveFashionMode || useColumnLayout) { + adaptiveFashionOverflowLatched = false + return false + } + + const totalCount = visualModel.items.count + const availableWidth = Math.max(0, Panel.rootObject.adaptiveFashionAvailableTaskManagerWidth) + if (totalCount <= 0 || !Number.isFinite(availableWidth) || availableWidth <= 0) { + adaptiveFashionOverflowLatched = false + return false + } + + const startThreshold = availableWidth + adaptiveFashionOverflowHysteresis + const stopThreshold = Math.max(0, availableWidth - adaptiveFashionOverflowHysteresis) + const totalItemsWidth = adaptiveFashionFullItemsWidth + const nextLatched = adaptiveFashionOverflowLatched + ? (totalItemsWidth > stopThreshold) + : (adaptiveFashionMinimumReached && totalItemsWidth > startThreshold) + + if (adaptiveFashionOverflowLatched !== nextLatched) { + adaptiveFashionOverflowLatched = nextLatched + } + + return nextLatched + } + + function syncAdaptiveFashionOverflow() { + const totalCount = visualModel.items.count + const overflowEnabled = syncAdaptiveFashionOverflowEnabledState() + if (!overflowEnabled || totalCount <= 0) { + adaptiveFashionVisibleItemCount = totalCount + adaptiveFashionOverflowItems = [] + return + } + + const availableWidth = Math.max(0, Panel.rootObject.adaptiveFashionAvailableTaskManagerWidth) + if (!Number.isFinite(availableWidth) || availableWidth <= 0) { + adaptiveFashionVisibleItemCount = totalCount + adaptiveFashionOverflowItems = [] + return + } + + const overflowWidth = adaptiveFashionOverflowButtonWidth + const spacing = appContainer.spacing + + let usedWidth = 0 + let visibleCount = 0 + for (let index = 0; index < totalCount; ++index) { + const itemWidth = adaptiveFashionItemWidthForIndex(index) + const leadingSpacing = visibleCount > 0 ? spacing : 0 + const remainingCount = totalCount - (visibleCount + 1) + const reservedOverflowWidth = remainingCount > 0 ? spacing + overflowWidth : 0 + if (usedWidth + leadingSpacing + itemWidth + reservedOverflowWidth > availableWidth) { + break + } + + usedWidth += leadingSpacing + itemWidth + visibleCount++ + } + + if (visibleCount >= totalCount) { + adaptiveFashionVisibleItemCount = totalCount + adaptiveFashionOverflowItems = [] + return + } + + let overflowItems = [] + for (let index = visibleCount; index < totalCount; ++index) { + const itemData = overflowItemDataAt(index) + if (itemData) { + overflowItems.push(itemData) + } + } + + adaptiveFashionVisibleItemCount = visibleCount + adaptiveFashionOverflowItems = overflowItems + } + + function beginPreviewSwitch() { + previewSwitchGraceTimer.restart() + } + + function endPreviewSwitch() { + previewSwitchGraceTimer.stop() + } + + function applyDockItemMaxSize() { + if (Panel.rootObject.dockItemMaxSize !== targetDockItemMaxSize) { + Panel.rootObject.dockItemMaxSize = targetDockItemMaxSize + } + } + + onTargetDockItemMaxSizeChanged: { + if (resizeOptimizationActive) { + dockItemSizeSyncTimer.restart() + return + } + + applyDockItemMaxSize() + } + + onResizeOptimizationActiveChanged: { + if (resizeOptimizationActive) { + return + } + + dockItemSizeSyncTimer.stop() + applyDockItemMaxSize() + } + + onAdaptiveFashionOverflowEnabledChanged: scheduleAdaptiveFashionOverflowSync() + onAdaptiveFashionMinimumReachedChanged: scheduleAdaptiveFashionOverflowSync() + onAdaptiveFashionFullItemsWidthChanged: scheduleAdaptiveFashionOverflowSync() + onAdaptiveFashionItemWidthChanged: scheduleAdaptiveFashionOverflowSync() + onUseColumnLayoutChanged: scheduleAdaptiveFashionOverflowSync() TextCalculator { id: textCalculator @@ -63,17 +340,283 @@ ContainmentItem { iconSize: Panel.rootObject.dockItemMaxSize * 9 / 14 spacing: appContainer.spacing cellSize: visualModel.cellWidth - itemPadding: taskmanager.appTitleSpacing - remainingSpace: taskmanager.remainingSpacesForTaskManager + itemPadding: 4 + remainingSpace: taskmanager.remainingSpacesForSplitWindow font.family: D.DTK.fontManager.t6.family font.pixelSize: Math.max(10, Math.min(20, Math.round(textCalculator.iconSize * 0.35))) } + Timer { + id: previewSwitchGraceTimer + interval: 96 + repeat: false + } + + Timer { + id: dockItemSizeSyncTimer + interval: 16 + repeat: false + onTriggered: { + taskmanager.applyDockItemMaxSize() + } + } + + Connections { + target: Panel.rootObject + + function onAdaptiveFashionAvailableTaskManagerWidthChanged() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + + function onDockItemMaxSizeChanged() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + + function onPreferredDockSizeChanged() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + } + + Connections { + target: taskmanager.Applet.dataModel + + function onDataChanged() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + + function onRowsInserted() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + + function onRowsMoved() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + + function onRowsRemoved() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + + function onModelReset() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + + function onLayoutChanged() { + taskmanager.scheduleAdaptiveFashionOverflowSync() + } + } + + Component { + id: overflowFooterComponent + + Item { + id: overflowFooter + readonly property int previewCount: Math.min(taskmanager.adaptiveFashionOverflowItems.length, 3) + readonly property bool singleOverflowItem: taskmanager.adaptiveFashionOverflowCount === 1 + readonly property int previewStartIndex: Math.max(0, taskmanager.adaptiveFashionOverflowItems.length - previewCount) + readonly property var primaryOverflowItem: taskmanager.adaptiveFashionOverflowItems.length > 0 + ? taskmanager.adaptiveFashionOverflowItems[0] + : null + readonly property real stackedIconSize: Math.max(12, Math.round(Panel.rootObject.dockItemIconSize * 0.78)) + readonly property real stackXOffset: Math.max(2, Math.round(stackedIconSize * 0.24)) + readonly property real hoverHeight: Math.round(Panel.rootObject.dockItemIconSize + 8) + readonly property int popupColumns: Math.min(6, Math.max(1, taskmanager.adaptiveFashionOverflowCount)) + readonly property int popupInnerMargin: 10 + property point popupAnchorPoint: Qt.point(0, 0) + + implicitWidth: taskmanager.adaptiveFashionOverflowButtonWidth + implicitHeight: taskmanager.implicitHeight + width: implicitWidth + height: implicitHeight + + function togglePopup() { + if (taskmanager.adaptiveFashionOverflowCount <= 0) { + return + } + + if (overflowPopup.popupVisible) { + overflowPopup.close() + return + } + + Panel.requestClosePopup() + popupAnchorPoint = overflowFooter.mapToItem(null, overflowFooter.width / 2, overflowFooter.height / 2) + overflowPopup.open() + } + + AppletItemBackground { + width: overflowFooter.width + height: overflowFooter.hoverHeight + anchors.centerIn: parent + enabled: false + opacity: overflowMouseArea.containsMouse + || overflowPopup.popupVisible + || (overflowFooter.singleOverflowItem + && overflowFooter.primaryOverflowItem + && overflowFooter.primaryOverflowItem.active + && overflowFooter.primaryOverflowItem.windows.length > 0) + ? 1.0 + : 0.0 + + Behavior on opacity { + NumberAnimation { duration: 150 } + } + } + + AppItem { + anchors.fill: parent + visible: overflowFooter.singleOverflowItem && overflowFooter.primaryOverflowItem + enabled: false + displayMode: Panel.indicatorStyle + colorTheme: Panel.colorTheme + itemActive: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.active : false + attention: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.attention : false + itemId: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.itemId : "" + dockElement: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.dockElement : "" + itemKind: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.itemKind : "" + name: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.name : "" + iconName: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.iconName : "" + previewIcons: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.previewIcons : [] + menus: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.menus : "" + windows: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.windows : [] + visualIndex: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.visualIndex : -1 + modelIndex: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.modelIndex : null + blendOpacity: taskmanager.blendOpacity + title: overflowFooter.primaryOverflowItem ? overflowFooter.primaryOverflowItem.title : "" + enableTitle: false + appTitleSpacing: taskmanager.appTitleSpacing + } + + Item { + anchors.centerIn: parent + visible: !overflowFooter.singleOverflowItem + width: overflowFooter.stackedIconSize + + overflowFooter.stackXOffset * Math.max(0, overflowFooter.previewCount - 1) + height: overflowFooter.stackedIconSize + + Repeater { + model: overflowFooter.previewCount + + D.DciIcon { + required property int index + + readonly property int previewIndex: overflowFooter.previewStartIndex + index + readonly property var previewItem: taskmanager.adaptiveFashionOverflowItems[previewIndex] + + name: previewItem && previewItem.iconName ? previewItem.iconName : "" + width: overflowFooter.stackedIconSize + height: overflowFooter.stackedIconSize + sourceSize: Qt.size(width, height) + smooth: false + retainWhileLoading: true + x: index * overflowFooter.stackXOffset + y: 0 + opacity: 0.7 + index * 0.15 + } + } + } + + MouseArea { + id: overflowMouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + onClicked: overflowFooter.togglePopup() + } + + PanelPopup { + id: overflowPopup + width: overflowPopupContent.width + height: overflowPopupContent.height + popupX: { + switch (Panel.position) { + case Dock.Top: + case Dock.Bottom: + return overflowFooter.popupAnchorPoint.x - overflowPopup.width / 2 + case Dock.Right: + return -overflowPopup.width - overflowFooter.popupInnerMargin + case Dock.Left: + return Panel.rootObject.dockSize + overflowFooter.popupInnerMargin + } + + return overflowFooter.popupAnchorPoint.x - overflowPopup.width / 2 + } + popupY: { + switch (Panel.position) { + case Dock.Top: + return Panel.rootObject.dockSize + overflowFooter.popupInnerMargin + case Dock.Right: + case Dock.Left: + return overflowFooter.popupAnchorPoint.y - overflowPopup.height / 2 + case Dock.Bottom: + return -overflowPopup.height - overflowFooter.popupInnerMargin + } + + return -overflowPopup.height - overflowFooter.popupInnerMargin + } + + Control { + id: overflowPopupContent + padding: 10 + implicitWidth: overflowGrid.implicitWidth + leftPadding + rightPadding + implicitHeight: overflowGrid.implicitHeight + topPadding + bottomPadding + width: implicitWidth + height: implicitHeight + + background: Item {} + + contentItem: Grid { + id: overflowGrid + columns: overflowFooter.popupColumns + rowSpacing: Math.max(6, Math.round(taskmanager.adaptiveFashionOverflowButtonWidth * 0.12)) + columnSpacing: rowSpacing + + Repeater { + model: taskmanager.adaptiveFashionOverflowItems + + delegate: Item { + required property var modelData + + width: taskmanager.adaptiveFashionOverflowButtonWidth + height: taskmanager.implicitHeight + + AppItem { + anchors.fill: parent + displayMode: Panel.indicatorStyle + colorTheme: Panel.colorTheme + itemActive: modelData.active + attention: modelData.attention + itemId: modelData.itemId + dockElement: modelData.dockElement + itemKind: modelData.itemKind + name: modelData.name + iconName: modelData.iconName + previewIcons: modelData.previewIcons + menus: modelData.menus + windows: modelData.windows + visualIndex: modelData.visualIndex + modelIndex: modelData.modelIndex + blendOpacity: taskmanager.blendOpacity + title: modelData.title + enableTitle: false + appTitleSpacing: taskmanager.appTitleSpacing + + Component.onCompleted: { + dropFilesOnItem.connect(taskmanager.Applet.dropFilesOnItem) + } + } + } + } + } + } + } + } + } + OverflowContainer { id: appContainer anchors.fill: parent useColumnLayout: taskmanager.useColumnLayout - spacing: taskmanager.appTitleSpacing + spacing: 0 remove: Transition { NumberAnimation { properties: "scale,opacity" @@ -93,20 +636,41 @@ ContainmentItem { required property bool active required property bool attention required property string itemId + required property string dockElement + required property string itemKind required property string name required property string title // winTitle required property string iconName required property string icon // winIconName + required property var previewIcons required property string menus required property list windows z: attention ? -1 : 0 - property bool visibility: { + readonly property bool overflowProxyItem: taskmanager.adaptiveFashionOverflowEnabled + && taskmanager.adaptiveFashionOverflowCount > 0 + && DelegateModel.itemsIndex === taskmanager.adaptiveFashionVisibleItemCount + readonly property bool hiddenByOverflow: taskmanager.adaptiveFashionOverflowEnabled + && DelegateModel.itemsIndex > taskmanager.adaptiveFashionVisibleItemCount + readonly property bool hiddenByDrag: { let draggedAppId = taskmanager.Applet.desktopIdToAppId(launcherDndDropArea.launcherDndDesktopId) if (itemId !== draggedAppId) { - return true + return false } - return windows.length > 0 && launcherDndDropArea.launcherDndWinId !== windows[0] + + if (windows.length === 0) { + return true + } + + return launcherDndDropArea.launcherDndWinId !== windows[0] } + property bool visibility: !hiddenByOverflow && !hiddenByDrag + readonly property real layoutImplicitWidth: useColumnLayout ? taskmanager.implicitWidth : appItem.implicitWidth + readonly property real layoutImplicitHeight: useColumnLayout ? visualModel.cellWidth : taskmanager.implicitHeight + readonly property real targetImplicitWidth: hiddenByOverflow ? 0 : layoutImplicitWidth + readonly property real targetImplicitHeight: hiddenByOverflow ? 0 : layoutImplicitHeight + readonly property string adaptiveFashionWidthKey: dockElement && dockElement.length > 0 + ? dockElement + : (itemId && itemId.length > 0 ? itemId : String(DelegateModel.itemsIndex)) ListView.onAdd: NumberAnimation { target: delegateRoot @@ -132,34 +696,37 @@ ContainmentItem { Behavior on opacity { NumberAnimation { duration: 200 } } Behavior on scale { NumberAnimation { duration: 200 } } - implicitWidth: { - let targetW = useColumnLayout ? taskmanager.implicitWidth : appItem.implicitWidth; - if (useColumnLayout || visualModel.count <= 0) return targetW; - - let totalSpacing = Math.max(0, visualModel.count - 1) * taskmanager.appTitleSpacing; - let availableW = taskmanager.remainingSpacesForTaskManager - totalSpacing; - let maxW = availableW / visualModel.count; - return Math.min(targetW, Math.max(1, maxW)); - } - implicitHeight: { - let targetH = useColumnLayout ? visualModel.cellWidth : taskmanager.implicitHeight; - if (!useColumnLayout || visualModel.count <= 0) return targetH; - - let totalSpacing = Math.max(0, visualModel.count - 1) * taskmanager.appTitleSpacing; - let availableH = taskmanager.remainingSpacesForTaskManager - totalSpacing; - let maxH = availableH / visualModel.count; - return Math.min(targetH, Math.max(1, maxH)); + implicitWidth: targetImplicitWidth + implicitHeight: targetImplicitHeight + width: targetImplicitWidth + height: targetImplicitHeight + visible: !hiddenByOverflow + + function syncAdaptiveFashionMeasuredWidth() { + taskmanager.registerAdaptiveFashionItemWidth(adaptiveFashionWidthKey, layoutImplicitWidth) } property int visualIndex: DelegateModel.itemsIndex property var modelIndex: visualModel.modelIndex(index) + onLayoutImplicitWidthChanged: syncAdaptiveFashionMeasuredWidth() + onAdaptiveFashionWidthKeyChanged: syncAdaptiveFashionMeasuredWidth() + + Component.onCompleted: { + syncAdaptiveFashionMeasuredWidth() + } + + Component.onDestruction: { + taskmanager.unregisterAdaptiveFashionItemWidth(adaptiveFashionWidthKey) + } + Rectangle { // kept for debug purpose // border.color: "red" // border.width: 1 id: appItemRect color: "transparent" + visible: !delegateRoot.overflowProxyItem && !delegateRoot.hiddenByOverflow parent: appContainer x: delegateRoot.x y: delegateRoot.y @@ -194,11 +761,14 @@ ContainmentItem { displayMode: Panel.indicatorStyle colorTheme: Panel.colorTheme - active: delegateRoot.active + itemActive: delegateRoot.active attention: delegateRoot.attention itemId: delegateRoot.itemId + dockElement: delegateRoot.dockElement + itemKind: delegateRoot.itemKind name: delegateRoot.name iconName: delegateRoot.iconName + previewIcons: delegateRoot.previewIcons menus: delegateRoot.menus windows: delegateRoot.windows visualIndex: delegateRoot.visualIndex @@ -206,6 +776,7 @@ ContainmentItem { blendOpacity: taskmanager.blendOpacity title: delegateRoot.title enableTitle: textCalculator.enabled + appTitleSpacing: taskmanager.appTitleSpacing ListView.delayRemove: Drag.active Component.onCompleted: { dropFilesOnItem.connect(taskmanager.Applet.dropFilesOnItem) @@ -215,6 +786,20 @@ ContainmentItem { } } } + + Loader { + id: overflowProxyLoader + active: delegateRoot.overflowProxyItem + visible: delegateRoot.overflowProxyItem + parent: appContainer + x: delegateRoot.x + y: delegateRoot.y + width: delegateRoot.width + height: delegateRoot.height + scale: delegateRoot.scale + opacity: delegateRoot.opacity + sourceComponent: overflowFooterComponent + } } } @@ -222,74 +807,245 @@ ContainmentItem { id: launcherDndDropArea anchors.fill: parent z: 3 - keys: ["text/x-dde-dock-dnd-appid"] property string launcherDndDesktopId: "" property string launcherDndDragSource: "" property string launcherDndWinId: "" + property string pendingDockElement: "" + property string pendingFolderUrl: "" function resetDndState() { launcherDndDesktopId = "" launcherDndDragSource = "" launcherDndWinId = "" + pendingDockElement = "" + pendingFolderUrl = "" + } + + function dragString(drag, key) { + if (!drag || drag.getDataAsString === undefined || drag.getDataAsString === null) { + return "" + } + + const value = drag.getDataAsString(key) + if (value === undefined || value === null) { + return "" + } + return String(value) + } + + function urlsFromText(rawText) { + const text = rawText ? String(rawText).trim() : "" + if (text === "") { + return [] + } + + return text.split(/[\r\n]+/).filter(function(entry) { + return entry !== "" + }) + } + + function dragUrls(drag) { + const urls = drag.urls || [] + if (urls.length > 0) { + let result = [] + for (let i = 0; i < urls.length; ++i) { + result.push(String(urls[i])) + } + return result + } + + const treeUrls = urlsFromText(dragString(drag, "dfm_tree_urls_for_drag")) + if (treeUrls.length > 0) { + return treeUrls + } + + return urlsFromText(dragString(drag, "text/uri-list")) + } + + function launcherDesktopIdFromDrag(drag) { + let desktopId = dragString(drag, "text/x-dde-dock-dnd-appid") + if (desktopId !== "") { + return desktopId + } + + desktopId = dragString(drag, "text/x-dde-launcher-dnd-desktopId") + if (desktopId !== "") { + return desktopId + } + + if (!drag.source) { + return "" + } + + if (drag.source.desktopId !== undefined && drag.source.desktopId !== null && drag.source.desktopId !== "") { + return String(drag.source.desktopId) + } + + if (drag.source.itemId !== undefined && drag.source.itemId !== null && drag.source.itemId !== "") { + return String(drag.source.itemId) + } + + if (drag.source.appId !== undefined && drag.source.appId !== null && drag.source.appId !== "") { + return String(drag.source.appId) + } + + return "" + } + + function candidateFolderUrl(drag) { + const urls = dragUrls(drag) + if (urls.length !== 1) { + return "" + } + + const candidate = urls[0] + if (candidate.indexOf("file://") !== 0) { + return "" + } + + return candidate + } + + function currentDragIndex() { + if (pendingDockElement === "") { + return -1 + } + + if (taskmanager.Applet.windowSplit && pendingDockElement.indexOf("desktop/") === 0) { + let appId = taskmanager.Applet.desktopIdToAppId(launcherDndDesktopId) + if (launcherDndDragSource === "taskbar" && launcherDndWinId !== "") { + return taskmanager.findAppIndexByWindow(appId, launcherDndWinId) + } + return taskmanager.findAppIndex(appId) + } + + return taskmanager.findDockElementIndex(pendingDockElement) + } + + function logDrag(prefix, drag, extra) { + console.warn(prefix, + "source=", launcherDndDragSource, + "desktopId=", launcherDndDesktopId, + "dockElement=", pendingDockElement, + "folderUrl=", pendingFolderUrl, + "dockAppId=", dragString(drag, "text/x-dde-dock-dnd-appid"), + "launcherDesktopId=", dragString(drag, "text/x-dde-launcher-dnd-desktopId"), + "appType=", dragString(drag, "dfm_app_type_for_drag"), + "treeUrls=", dragString(drag, "dfm_tree_urls_for_drag"), + "uriList=", dragString(drag, "text/uri-list"), + "urls=", JSON.stringify(dragUrls(drag)), + extra || "") } onEntered: function(drag) { - let desktopId = drag.getDataAsString("text/x-dde-dock-dnd-appid") - launcherDndDragSource = drag.getDataAsString("text/x-dde-dock-dnd-source") - launcherDndWinId = drag.getDataAsString("text/x-dde-dock-dnd-winid") - launcherDndDesktopId = desktopId - if (launcherDndDragSource !== "taskbar" && taskmanager.Applet.requestDockByDesktopId(desktopId) === false) { - resetDndState() + launcherDndDragSource = dragString(drag, "text/x-dde-dock-dnd-source") + launcherDndWinId = dragString(drag, "text/x-dde-dock-dnd-winid") + launcherDndDesktopId = launcherDesktopIdFromDrag(drag) + pendingDockElement = dragString(drag, "text/x-dde-dock-dnd-element") + pendingFolderUrl = "" + + if (launcherDndDragSource === "" && launcherDndDesktopId !== "") { + launcherDndDragSource = "launcher" } + + logDrag("taskmanager drag entered", drag) + + if (launcherDndDragSource === "taskbar") { + if (pendingDockElement === "" && launcherDndDesktopId !== "") { + pendingDockElement = taskmanager.Applet.dockElementFromLauncherId(launcherDndDesktopId) + } + if (pendingDockElement === "") { + drag.accepted = false + resetDndState() + } else { + drag.accepted = true + } + return + } + + if (launcherDndDesktopId !== "") { + pendingDockElement = taskmanager.Applet.dockElementFromLauncherId(launcherDndDesktopId) + if (pendingDockElement === "" || taskmanager.Applet.requestDockByDesktopId(launcherDndDesktopId) === false) { + logDrag("taskmanager launcher drag rejected", drag) + drag.accepted = false + resetDndState() + } else { + drag.accepted = true + } + return + } + + const folderUrl = candidateFolderUrl(drag) + if (folderUrl !== "") { + pendingFolderUrl = folderUrl + pendingDockElement = taskmanager.Applet.folderUrlToElementId(pendingFolderUrl) + if (pendingDockElement === "" || taskmanager.Applet.requestDockByFolderUrl(pendingFolderUrl) === false) { + logDrag("taskmanager folder drag rejected", drag) + drag.accepted = false + resetDndState() + } else { + drag.accepted = true + } + return + } + + logDrag("taskmanager drag unsupported", drag) + drag.accepted = false + resetDndState() } onPositionChanged: function(drag) { - if (launcherDndDesktopId === "") return + if (pendingDockElement === "") 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) + let currentIndex = currentDragIndex() if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { if (taskmanager.Applet.windowSplit) { taskmanager.Applet.moveItem(currentIndex, targetIndex) } else { visualModel.items.move(currentIndex, targetIndex) + taskmanager.scheduleAdaptiveFashionOverflowSync() } } } onDropped: function(drop) { + logDrag("taskmanager drag dropped", drop) Panel.contextDragging = false - if (launcherDndDesktopId === "") return + if (pendingDockElement === "") return + drop.accepted = true 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 currentIndex = currentDragIndex() if (currentIndex !== -1 && targetIndex !== -1 && currentIndex !== targetIndex) { if (taskmanager.Applet.windowSplit) { taskmanager.Applet.moveItem(currentIndex, targetIndex) } else { visualModel.items.move(currentIndex, targetIndex) + taskmanager.scheduleAdaptiveFashionOverflowSync() } } - let appIds = [] + let dockElements = [] for (let i = 0; i < visualModel.items.count; i++) { - appIds.push(visualModel.items.get(i).model.itemId) + dockElements.push(visualModel.items.get(i).model.dockElement) } - taskmanager.Applet.saveDockElementsOrder(appIds) + taskmanager.Applet.saveDockElementsOrder(dockElements) resetDndState() } - onExited: function() { + onExited: function(drag) { + logDrag("taskmanager drag exited", drag) if (launcherDndDesktopId !== "" && launcherDndDragSource !== "taskbar") { taskmanager.Applet.requestUndockByDesktopId(launcherDndDesktopId) } + if (pendingFolderUrl !== "" && launcherDndDragSource !== "taskbar") { + taskmanager.Applet.requestUndockByFolderUrl(pendingFolderUrl) + } resetDndState() } } } Component.onCompleted: { - Panel.rootObject.dockItemMaxSize = Qt.binding(function(){ - return Math.min(Panel.rootObject.dockSize, Panel.rootObject.dockRemainingSpaceForCenter * 1.2 / (Panel.rootObject.dockCenterPartCount - 1 + visualModel.count) - 2) - }) + applyDockItemMaxSize() + scheduleAdaptiveFashionOverflowSync() } } diff --git a/panels/dock/taskmanager/package/WindowIndicator.qml b/panels/dock/taskmanager/package/WindowIndicator.qml index a4c867c11..ce4239707 100644 --- a/panels/dock/taskmanager/package/WindowIndicator.qml +++ b/panels/dock/taskmanager/package/WindowIndicator.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 @@ -13,11 +13,24 @@ Item { required property list windows required property int displayMode required property bool useColumnLayout + required property bool compactFashionIndicator required property AppItemPalette palette required property int dotWidth required property int dotHeight + required property int multiDotWidth + required property int multiDotHeight - property int borderWidth: (displayMode === Dock.Fashion) ? 1 : 0 + property real borderWidth: compactFashionIndicator ? 1 : ((displayMode === Dock.Fashion) ? 1 : 0) + readonly property real compactFashionOuterRadius: 2 + readonly property real compactFashionInnerRadius: Math.max(0, compactFashionOuterRadius - borderWidth) + readonly property real singleIndicatorWidth: compactFashionIndicator + ? dotWidth + 2 * borderWidth + : ((root.displayMode === Dock.Fashion || useColumnLayout) ? dotWidth : dotWidth / 2 - 1) + 2 * borderWidth + readonly property real singleIndicatorHeight: compactFashionIndicator + ? dotHeight + 2 * borderWidth + : ((root.displayMode === Dock.Fashion || !useColumnLayout) ? dotHeight : dotHeight / 2 - 1) + 2 * borderWidth + readonly property real multiIndicatorWidth: compactFashionIndicator ? multiDotWidth + 2 * borderWidth : singleIndicatorWidth + readonly property real multiIndicatorHeight: compactFashionIndicator ? multiDotHeight + 2 * borderWidth : singleIndicatorHeight width: indicatorLoader.width height: indicatorLoader.height @@ -30,13 +43,24 @@ Item { Component { id: dot - Rectangle { - border.width: borderWidth - border.color: palette.dotIndicatorBorder - width: ((root.displayMode === Dock.Fashion || useColumnLayout) ? dotWidth : dotWidth / 2 - 1) + 2 * borderWidth - height: ((root.displayMode === Dock.Fashion || !useColumnLayout) ? dotHeight : dotHeight / 2 - 1) + 2 * borderWidth - color: palette.dotIndicator - radius: (root.displayMode === Dock.Fashion || useColumnLayout) ? width / 2 : height / 2 + Item { + width: root.singleIndicatorWidth + height: root.singleIndicatorHeight + + Rectangle { + anchors.fill: parent + color: palette.dotIndicatorBorder + antialiasing: compactFashionIndicator + radius: compactFashionIndicator ? compactFashionOuterRadius : ((root.displayMode === Dock.Fashion || useColumnLayout) ? width / 2 : height / 2) + } + + Rectangle { + anchors.fill: parent + anchors.margins: borderWidth + color: palette.dotIndicator + antialiasing: compactFashionIndicator + radius: compactFashionIndicator ? compactFashionInnerRadius : ((root.displayMode === Dock.Fashion || useColumnLayout) ? width / 2 : height / 2) + } } } @@ -47,8 +71,44 @@ Item { rows: useColumnLayout ? 2 : 1 flow: useColumnLayout ? GridLayout.LeftToRight : GridLayout.TopToBottom spacing: 2 - Loader { sourceComponent: dot } - Loader { sourceComponent: dot } + Item { + width: root.multiIndicatorWidth + height: root.multiIndicatorHeight + + Rectangle { + anchors.fill: parent + color: palette.dotIndicatorBorder + antialiasing: compactFashionIndicator + radius: compactFashionIndicator ? compactFashionOuterRadius : ((root.displayMode === Dock.Fashion || useColumnLayout) ? width / 2 : height / 2) + } + + Rectangle { + anchors.fill: parent + anchors.margins: borderWidth + color: palette.dotIndicator + antialiasing: compactFashionIndicator + radius: compactFashionIndicator ? compactFashionInnerRadius : ((root.displayMode === Dock.Fashion || useColumnLayout) ? width / 2 : height / 2) + } + } + Item { + width: root.multiIndicatorWidth + height: root.multiIndicatorHeight + + Rectangle { + anchors.fill: parent + color: palette.dotIndicatorBorder + antialiasing: compactFashionIndicator + radius: compactFashionIndicator ? compactFashionOuterRadius : ((root.displayMode === Dock.Fashion || useColumnLayout) ? width / 2 : height / 2) + } + + Rectangle { + anchors.fill: parent + anchors.margins: borderWidth + color: palette.dotIndicator + antialiasing: compactFashionIndicator + radius: compactFashionIndicator ? compactFashionInnerRadius : ((root.displayMode === Dock.Fashion || useColumnLayout) ? width / 2 : height / 2) + } + } } } diff --git a/panels/dock/taskmanager/package/icons/back-chevron-dark.svg b/panels/dock/taskmanager/package/icons/back-chevron-dark.svg new file mode 100644 index 000000000..47fe2ed61 --- /dev/null +++ b/panels/dock/taskmanager/package/icons/back-chevron-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/panels/dock/taskmanager/package/icons/back-chevron-light.svg b/panels/dock/taskmanager/package/icons/back-chevron-light.svg new file mode 100644 index 000000000..1812a5648 --- /dev/null +++ b/panels/dock/taskmanager/package/icons/back-chevron-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/panels/dock/taskmanager/package/translations/org.deepin.ds.dock.taskmanager_zh_CN.qm b/panels/dock/taskmanager/package/translations/org.deepin.ds.dock.taskmanager_zh_CN.qm new file mode 120000 index 000000000..07d71909c --- /dev/null +++ b/panels/dock/taskmanager/package/translations/org.deepin.ds.dock.taskmanager_zh_CN.qm @@ -0,0 +1 @@ +/home/shule/src/dde-shell/build-run/panels/dock/taskmanager/org.deepin.ds.dock.taskmanager_zh_CN.qm \ No newline at end of file diff --git a/panels/dock/taskmanager/popupsortutils.cpp b/panels/dock/taskmanager/popupsortutils.cpp new file mode 100644 index 000000000..c5a9e2309 --- /dev/null +++ b/panels/dock/taskmanager/popupsortutils.cpp @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "popupsortutils.h" + +#include + +#include + +namespace dock { +namespace { + +int compareText(const QString &left, const QString &right) +{ + static thread_local QCollator collator; + collator.setCaseSensitivity(Qt::CaseInsensitive); + collator.setNumericMode(true); + collator.setIgnorePunctuation(false); + return collator.compare(left, right); +} + +int compareNumbers(qint64 left, qint64 right) +{ + if (left < right) { + return -1; + } + if (left > right) { + return 1; + } + return 0; +} + +int compareEntries(const PopupSortableEntry &left, const PopupSortableEntry &right, PopupSortField field) +{ + switch (field) { + case PopupSortField::ModifiedTime: + return compareNumbers(left.modifiedTime, right.modifiedTime); + case PopupSortField::CreatedTime: + return compareNumbers(left.createdTime, right.createdTime); + case PopupSortField::Size: + return compareNumbers(left.size, right.size); + case PopupSortField::Type: { + const int typeCompare = compareText(left.typeText, right.typeText); + if (typeCompare != 0) { + return typeCompare; + } + return compareText(left.name, right.name); + } + case PopupSortField::Name: + default: + return compareText(left.name, right.name); + } +} + +} + +PopupSortField popupSortFieldFromString(const QString &fieldName) +{ + if (fieldName == QStringLiteral("modified")) { + return PopupSortField::ModifiedTime; + } + if (fieldName == QStringLiteral("created")) { + return PopupSortField::CreatedTime; + } + if (fieldName == QStringLiteral("size")) { + return PopupSortField::Size; + } + if (fieldName == QStringLiteral("type")) { + return PopupSortField::Type; + } + return PopupSortField::Name; +} + +QString popupSortFieldToString(PopupSortField field) +{ + switch (field) { + case PopupSortField::ModifiedTime: + return QStringLiteral("modified"); + case PopupSortField::CreatedTime: + return QStringLiteral("created"); + case PopupSortField::Size: + return QStringLiteral("size"); + case PopupSortField::Type: + return QStringLiteral("type"); + case PopupSortField::Name: + default: + return QStringLiteral("name"); + } +} + +PopupSortState cyclePopupSortState(const PopupSortState ¤tState, PopupSortField selectedField) +{ + PopupSortState nextState = currentState; + if (nextState.field == selectedField) { + nextState.order = nextState.order == Qt::AscendingOrder ? + Qt::DescendingOrder : + Qt::AscendingOrder; + return nextState; + } + + nextState.field = selectedField; + nextState.order = Qt::AscendingOrder; + return nextState; +} + +void sortPopupEntries(QList *entries, const PopupSortState &state, bool keepDirectoriesFirst) +{ + if (!entries || entries->size() < 2) { + return; + } + + std::stable_sort(entries->begin(), entries->end(), [state, keepDirectoriesFirst](const PopupSortableEntry &left, + const PopupSortableEntry &right) { + if (keepDirectoriesFirst && left.directory != right.directory) { + return left.directory && !right.directory; + } + + int result = compareEntries(left, right, state.field); + if (result == 0 && state.field != PopupSortField::Name) { + result = compareText(left.name, right.name); + } + + if (result == 0) { + return false; + } + + return state.order == Qt::AscendingOrder ? result < 0 : result > 0; + }); +} + +} diff --git a/panels/dock/taskmanager/popupsortutils.h b/panels/dock/taskmanager/popupsortutils.h new file mode 100644 index 000000000..4fb04769d --- /dev/null +++ b/panels/dock/taskmanager/popupsortutils.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +namespace dock { + +enum class PopupSortField { + Name, + ModifiedTime, + CreatedTime, + Size, + Type, +}; + +struct PopupSortState { + PopupSortField field = PopupSortField::Name; + Qt::SortOrder order = Qt::AscendingOrder; +}; + +struct PopupSortableEntry { + QVariantMap entryData; + QString name; + QString typeText; + qint64 modifiedTime = 0; + qint64 createdTime = 0; + qint64 size = 0; + bool directory = false; +}; + +PopupSortField popupSortFieldFromString(const QString &fieldName); +QString popupSortFieldToString(PopupSortField field); +PopupSortState cyclePopupSortState(const PopupSortState ¤tState, PopupSortField selectedField); +void sortPopupEntries(QList *entries, const PopupSortState &state, bool keepDirectoriesFirst); + +} diff --git a/panels/dock/taskmanager/rolecombinemodel.cpp b/panels/dock/taskmanager/rolecombinemodel.cpp index 23b59eebf..d66f4943f 100644 --- a/panels/dock/taskmanager/rolecombinemodel.cpp +++ b/panels/dock/taskmanager/rolecombinemodel.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 @@ -11,15 +11,69 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel { setSourceModel(major); m_minor = minor; + auto resolveMinorIndex = [this, majorRoles, func](int row, int column) -> QPair { + if (!sourceModel()) { + return qMakePair(-1, -1); + } + + const QModelIndex majorIndex = sourceModel()->index(row, column); + if (!majorIndex.isValid()) { + return qMakePair(-1, -1); + } + + const QModelIndex minorIndex = func(majorIndex.data(majorRoles), m_minor); + if (!minorIndex.isValid()) { + return qMakePair(-1, -1); + } + + return qMakePair(minorIndex.row(), minorIndex.column()); + }; + + auto rebuildAllMappings = [this, resolveMinorIndex]() { + QMap, QPair> newIndexMap; + if (!sourceModel()) { + m_indexMap = newIndexMap; + return; + } + + const int rowCount = sourceModel()->rowCount(); + const int columnCount = sourceModel()->columnCount(); + for (int i = 0; i < rowCount; ++i) { + for (int j = 0; j < columnCount; ++j) { + const QPair minorPos = resolveMinorIndex(i, j); + if (minorPos.first != -1 && minorPos.second != -1) { + newIndexMap.insert(qMakePair(i, j), minorPos); + } + } + } + + m_indexMap = newIndexMap; + }; + + auto emitMinorDataChangedForAllRows = [this]() { + if (!sourceModel() || m_minorRolesMap.isEmpty()) { + return; + } + + const int rowCount = sourceModel()->rowCount(); + const int columnCount = sourceModel()->columnCount(); + if (rowCount <= 0 || columnCount <= 0) { + return; + } + + Q_EMIT dataChanged(index(0, 0), + index(rowCount - 1, columnCount - 1), + m_minorRolesMap.keys()); + }; + // create minor row & column map int rowCount = major->rowCount(); int columnCount = major->columnCount(); for (int i = 0; i < rowCount; i++) { for (int j = 0; j < columnCount; j++) { - QModelIndex majorIndex = major->index(i, j); - QModelIndex minorIndex = func(majorIndex.data(majorRoles), m_minor); - if (majorIndex.isValid() && minorIndex.isValid()) - m_indexMap[qMakePair(i, j)] = qMakePair(minorIndex.row(), minorIndex.column()); + const QPair minorPos = resolveMinorIndex(i, j); + if (minorPos.first != -1 && minorPos.second != -1) + m_indexMap[qMakePair(i, j)] = minorPos; } } @@ -173,7 +227,7 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel Q_EMIT dataChanged(index(topLeft.row(), topLeft.column()), index(bottomRight.row(), bottomRight.column()), - roles + (roles.contains(majorRoles) ? m_minorRolesMap.values() : QList()) + roles + (roles.contains(majorRoles) ? m_minorRolesMap.keys() : QList()) ); }); @@ -181,26 +235,38 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel connect(m_minor, &QAbstractItemModel::dataChanged, this, [this, majorRoles, func](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles){ Q_UNUSED(roles) - for (int i = topLeft.row(); i <= bottomRight.row(); i++) { - for (int j = topLeft.column(); j <= bottomRight.column(); j++) { - auto majorPos = m_indexMap.key(qMakePair(i, j), qMakePair(-1, -1)); - if (-1 == majorPos.first && -1 == majorPos.second) - continue; + QList> affectedMajorPositions; + for (auto it = m_indexMap.constBegin(); it != m_indexMap.constEnd(); ++it) { + const auto &minorPos = it.value(); + if (minorPos.first < topLeft.row() || minorPos.first > bottomRight.row() + || minorPos.second < topLeft.column() || minorPos.second > bottomRight.column()) { + continue; + } - auto majorIndex = sourceModel()->index(majorPos.first, majorPos.second); - if (!majorIndex.isValid()) - continue; + affectedMajorPositions.append(it.key()); + } - auto minorIndex = func(majorIndex.data(majorRoles), m_minor); - if (!minorIndex.isValid()) - continue; + for (const auto &majorPos : affectedMajorPositions) { + auto majorIndex = sourceModel()->index(majorPos.first, majorPos.second); + if (!majorIndex.isValid()) + continue; + auto minorIndex = func(majorIndex.data(majorRoles), m_minor); + if (minorIndex.isValid()) { m_indexMap[majorPos] = qMakePair(minorIndex.row(), minorIndex.column()); - Q_EMIT dataChanged(majorIndex, majorIndex, m_minorRolesMap.values()); + } else { + m_indexMap.remove(majorPos); } + + Q_EMIT dataChanged(majorIndex, majorIndex, m_minorRolesMap.keys()); } }); + connect(m_minor, &QAbstractItemModel::modelReset, this, [rebuildAllMappings, emitMinorDataChangedForAllRows]() { + rebuildAllMappings(); + emitMinorDataChangedForAllRows(); + }); + // 添加对minor模型删除操作的处理 connect(m_minor, &QAbstractItemModel::rowsRemoved, this, [this, majorRoles, func](const QModelIndex &parent, int first, int last) { Q_UNUSED(parent) @@ -243,7 +309,7 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel // 对受影响的major索引发送数据变化信号 for (const auto &majorIndex : affectedMajorIndexes) { - Q_EMIT dataChanged(majorIndex, majorIndex, m_minorRolesMap.values()); + Q_EMIT dataChanged(majorIndex, majorIndex, m_minorRolesMap.keys()); } }); @@ -262,24 +328,31 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel }); connect(m_minor, &QAbstractItemModel::rowsInserted, this, - [this, majorRoles, func](const QModelIndex &parent, int first, int last){ + [this, resolveMinorIndex, emitMinorDataChangedForAllRows](const QModelIndex &parent, int firstRow, int lastRow){ Q_UNUSED(parent) - Q_UNUSED(first) - Q_UNUSED(last) - auto rowCount = sourceModel()->rowCount(); - auto columnCount = sourceModel()->columnCount(); - for (int i = 0; i < rowCount; i++) { - for (int j = 0; j < columnCount; j++) { - // already bind, pass this - if (m_indexMap.contains(qMakePair(i ,j))) - continue; + const int offset = lastRow - firstRow + 1; + for (auto it = m_indexMap.begin(); it != m_indexMap.end(); ++it) { + if (it.value().first >= firstRow) { + it.value().first += offset; + } + } - QModelIndex majorIndex = sourceModel()->index(i, j); - QModelIndex minorIndex = func(majorIndex.data(majorRoles), m_minor); - if (majorIndex.isValid() && minorIndex.isValid()) - m_indexMap[qMakePair(i, j)] = qMakePair(minorIndex.row(), minorIndex.column()); + auto rowCount = sourceModel()->rowCount(); + auto columnCount = sourceModel()->columnCount(); + for (int i = 0; i < rowCount; i++) { + for (int j = 0; j < columnCount; j++) { + const auto key = qMakePair(i, j); + if (m_indexMap.contains(key)) + continue; + + const auto minorPos = resolveMinorIndex(i, j); + if (minorPos.first != -1 && minorPos.second != -1) { + m_indexMap[key] = minorPos; + } + } } - } + + emitMinorDataChangedForAllRows(); }); // TODO: support columsInserted diff --git a/panels/dock/taskmanager/taskmanager.cpp b/panels/dock/taskmanager/taskmanager.cpp index 68d104fe3..2dc383558 100644 --- a/panels/dock/taskmanager/taskmanager.cpp +++ b/panels/dock/taskmanager/taskmanager.cpp @@ -16,19 +16,34 @@ #include "hoverpreviewproxymodel.h" #include "itemmodel.h" #include "pluginfactory.h" +#include "popupsortutils.h" #include "taskmanager.h" #include "taskmanageradaptor.h" #include "taskmanagersettings.h" #include "textcalculator.h" #include "treelandwindowmonitor.h" +#include +#include +#include +#include #include +#include +#include +#include #include +#include #include #include +#include #include +#include #include +#include + +#include + #include #include @@ -48,67 +63,912 @@ Q_LOGGING_CATEGORY(taskManagerLog, "org.deepin.dde.shell.dock.taskmanager", QtDe namespace dock { -// 通过AM(Application Manager)匹配应用程序的辅助函数 -static QString getDesktopIdByPid(const QStringList &identifies) +static QStringList mergedDockedElementsOrder(const QStringList ¤tDockedElements, const QStringList &orderedDockElements) +{ + QStringList mergedDockedElements; + for (const QString &dockElement : orderedDockElements) { + if (currentDockedElements.contains(dockElement) && !mergedDockedElements.contains(dockElement)) { + mergedDockedElements.append(dockElement); + } + } + + for (int i = 0; i < currentDockedElements.size(); ++i) { + const QString &dockElement = currentDockedElements.at(i); + if (mergedDockedElements.contains(dockElement)) { + continue; + } + + int insertIndex = mergedDockedElements.size(); + for (int j = i - 1; j >= 0; --j) { + const QString &previousElement = currentDockedElements.at(j); + const int previousIndex = mergedDockedElements.indexOf(previousElement); + if (previousIndex >= 0) { + insertIndex = previousIndex + 1; + break; + } + } + + if (insertIndex == mergedDockedElements.size()) { + for (int j = i + 1; j < currentDockedElements.size(); ++j) { + const QString &nextElement = currentDockedElements.at(j); + const int nextIndex = mergedDockedElements.indexOf(nextElement); + if (nextIndex >= 0) { + insertIndex = nextIndex; + break; + } + } + } + + mergedDockedElements.insert(insertIndex, dockElement); + } + + return mergedDockedElements; +} + +// 通过AM(Application Manager)匹配应用程序的辅助函数 +static QString getDesktopIdByPid(const QStringList &identifies) +{ + if (identifies.isEmpty()) + return {}; + + pid_t windowPid = identifies.last().toInt(); + if (windowPid <= 0) + return {}; + + auto appId = DSGApplication::getId(windowPid); + if (appId.isEmpty()) { + qCDebug(taskManagerLog) << "appId is empty, AM failed to identify window with pid:" << windowPid; + return {}; + } + + return QString::fromUtf8(appId); +} + +// 尝试通过 AM(Application Manager) 匹配应用程序 +static QModelIndex tryMatchByApplicationManager(const QStringList &identifies, + QAbstractItemModel *model, + const QHash &roleNames) +{ + Q_ASSERT(model); + + if (!Settings->cgroupsBasedGrouping()) { + return QModelIndex(); + } + + auto desktopId = getDesktopIdByPid(identifies); + if (desktopId.isEmpty() || Settings->cgroupsBasedGroupingSkipIds().contains(desktopId)) { + return QModelIndex(); + } + + // 先在 model 中查找 desktopId 对应的 item + auto res = model->match(model->index(0, 0), roleNames.key(MODEL_DESKTOPID), + desktopId, 1, Qt::MatchFixedString | Qt::MatchWrap).value(0); + + if (!res.isValid()) { + return QModelIndex(); + } + + // 检查应用的 Categories 是否在跳过列表中 + auto skipCategories = Settings->cgroupsBasedGroupingSkipCategories(); + if (!skipCategories.isEmpty()) { + QStringList categories = res.data(TaskManager::CategoriesRole).toStringList(); + if (!categories.isEmpty()) { + // 检查是否有任何 skipCategory 在应用的 categories 中 + for (const QString &skipCategory : skipCategories) { + if (categories.contains(skipCategory)) { + qCDebug(taskManagerLog) << "Skipping cgroups grouping for" << desktopId + << "due to category:" << skipCategory; + return QModelIndex(); + } + } + } + } + + qCDebug(taskManagerLog) << "matched by AM desktop ID:" << desktopId << res; + return res; +} + +static QStringList filteredIdentityCandidates(const QStringList &identifies, bool allowNumeric) +{ + QStringList filtered; + for (const QString &identity : identifies) { + if (identity.isEmpty() || filtered.contains(identity)) { + continue; + } + + bool isNumeric = false; + identity.toLongLong(&isNumeric); + if (!allowNumeric && isNumeric) { + continue; + } + + filtered.append(identity); + } + + return filtered; +} + +static QModelIndex matchLauncherAppByRoles(const QStringList &identifies, + QAbstractItemModel *model, + const QHash &roleNames, + const QList &roleOrder) +{ + if (!model || identifies.isEmpty()) { + return {}; + } + + const QModelIndex startIndex = model->index(0, 0); + for (const QByteArray &roleName : roleOrder) { + const int role = roleNames.key(roleName, -1); + if (role < 0) { + continue; + } + + for (const QString &identity : identifies) { + const QModelIndex result = model->match(startIndex, + role, + identity, + 1, + Qt::MatchFixedString | Qt::MatchWrap).value(0); + if (result.isValid()) { + qCDebug(taskManagerLog) << "matched by role:" << roleName << identity << result; + return result; + } + } + } + + return {}; +} + +static QModelIndex matchLauncherApplication(const QStringList &identifies, + QAbstractItemModel *model, + const QHash &roleNames) +{ + const QStringList exactCandidates = filteredIdentityCandidates(identifies, true); + const QModelIndex exactResult = matchLauncherAppByRoles(exactCandidates, + model, + roleNames, + {MODEL_DESKTOPID, MODEL_STARTUPWMCLASS}); + if (exactResult.isValid()) { + return exactResult; + } + + const QModelIndex amMatchResult = tryMatchByApplicationManager(identifies, model, roleNames); + if (amMatchResult.isValid()) { + return amMatchResult; + } + + const QStringList textCandidates = filteredIdentityCandidates(identifies, false); + const QModelIndex fallbackResult = matchLauncherAppByRoles(textCandidates, + model, + roleNames, + {MODEL_NAME, MODEL_ICONNAME}); + if (fallbackResult.isValid()) { + return fallbackResult; + } + + const int desktopIdRole = roleNames.key(MODEL_DESKTOPID, -1); + if (desktopIdRole < 0) { + return {}; + } + + const QString fallbackIdentity = !textCandidates.isEmpty() ? textCandidates.constFirst() + : exactCandidates.value(0); + if (fallbackIdentity.isEmpty()) { + return {}; + } + + const QModelIndex result = model->match(model->index(0, 0), + desktopIdRole, + fallbackIdentity, + 1, + Qt::MatchEndsWith).value(0); + qCDebug(taskManagerLog) << "matched by desktopId suffix:" << fallbackIdentity << result; + return result; +} + +static QPair splitDockElement(const QString &dockElement) +{ + const int separatorIndex = dockElement.indexOf(QLatin1Char('/')); + if (separatorIndex <= 0) { + return {}; + } + + return {dockElement.left(separatorIndex), dockElement.mid(separatorIndex + 1)}; +} + +static QString normalizedFolderPath(const QString &folderUrlOrPath) +{ + QUrl url(folderUrlOrPath); + QString folderPath = url.isLocalFile() ? url.toLocalFile() : folderUrlOrPath; + if (folderPath.isEmpty()) { + return {}; + } + + QFileInfo fileInfo(folderPath); + const QString canonicalPath = fileInfo.canonicalFilePath(); + if (!canonicalPath.isEmpty()) { + return canonicalPath; + } + + if (fileInfo.exists()) { + return fileInfo.absoluteFilePath(); + } + + if (folderPath.startsWith(QLatin1Char('/'))) { + return QDir::cleanPath(folderPath); + } + + return {}; +} + +static bool isWithinBasePath(const QString &basePath, const QString &candidatePath) +{ + if (basePath.isEmpty() || candidatePath.isEmpty()) { + return false; + } + + if (basePath == candidatePath) { + return true; + } + + const QString prefix = basePath.endsWith(QLatin1Char('/')) ? basePath : basePath + QLatin1Char('/'); + return candidatePath.startsWith(prefix); +} + +static QString localizedDesktopEntryText(QSettings &settings, const QString &key); + +static QString displayNameForPath(const QString &path) +{ + const auto localizedStandardLocationName = [](QStandardPaths::StandardLocation location) { + const auto englishDefaultName = [](QStandardPaths::StandardLocation standardLocation) -> QString { + switch (standardLocation) { + case QStandardPaths::HomeLocation: + return QStringLiteral("Home"); + case QStandardPaths::DesktopLocation: + return QStringLiteral("Desktop"); + case QStandardPaths::DocumentsLocation: + return QStringLiteral("Documents"); + case QStandardPaths::DownloadLocation: + return QStringLiteral("Download"); + case QStandardPaths::MoviesLocation: + return QStringLiteral("Movies"); + case QStandardPaths::MusicLocation: + return QStringLiteral("Music"); + case QStandardPaths::PicturesLocation: + return QStringLiteral("Pictures"); + case QStandardPaths::TemplatesLocation: + return QStringLiteral("Templates"); + case QStandardPaths::PublicShareLocation: + return QStringLiteral("Public"); + case QStandardPaths::ApplicationsLocation: + return QStringLiteral("Applications"); + default: + return {}; + } + }; + + const QString localizedName = QStandardPaths::displayName(location); + const QString englishName = englishDefaultName(location); + if (!localizedName.isEmpty() + && (englishName.isEmpty() || localizedName.compare(englishName, Qt::CaseInsensitive) != 0)) { + return localizedName; + } + + if (!englishName.isEmpty()) { + const QByteArray englishNameUtf8 = englishName.toUtf8(); + const QString translatedName = QString::fromLocal8Bit(dgettext("xdg-user-dirs", englishNameUtf8.constData())).trimmed(); + if (!translatedName.isEmpty() && translatedName.compare(englishName, Qt::CaseInsensitive) != 0) { + return translatedName; + } + } + + return QString(); + }; + + QFileInfo fileInfo(path); + const QString normalizedPath = QDir::cleanPath(fileInfo.absoluteFilePath().isEmpty() ? path : fileInfo.absoluteFilePath()); + const QString directoryEntryPath = QDir(normalizedPath).filePath(QStringLiteral(".directory")); + if (QFileInfo::exists(directoryEntryPath)) { + QSettings settings(directoryEntryPath, QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + const QString localizedDirectoryName = localizedDesktopEntryText(settings, QStringLiteral("Name")); + if (!localizedDirectoryName.isEmpty()) { + return localizedDirectoryName; + } + } + + if (normalizedPath == QStringLiteral("/usr/share/applications")) { + if (QLocale().language() == QLocale::Chinese) { + return QStringLiteral("应用程序"); + } + + const QString applicationsName = QStandardPaths::displayName(QStandardPaths::ApplicationsLocation); + if (!applicationsName.isEmpty()) { + return applicationsName; + } + } + + const QList standardLocations = { + QStandardPaths::HomeLocation, + QStandardPaths::DesktopLocation, + QStandardPaths::DocumentsLocation, + QStandardPaths::DownloadLocation, + QStandardPaths::MoviesLocation, + QStandardPaths::MusicLocation, + QStandardPaths::PicturesLocation, + QStandardPaths::TemplatesLocation, + QStandardPaths::PublicShareLocation, + }; + for (const QStandardPaths::StandardLocation location : standardLocations) { + const QString locationPath = QDir::cleanPath(QStandardPaths::writableLocation(location)); + if (!locationPath.isEmpty() && locationPath == normalizedPath) { + const QString localizedName = localizedStandardLocationName(location); + if (!localizedName.isEmpty()) { + return localizedName; + } + break; + } + } + + QString name = fileInfo.fileName(); + if (name.isEmpty()) { + name = fileInfo.absoluteFilePath(); + } + if (name.isEmpty()) { + name = path; + } + return name; +} + +struct DesktopEntryMetadata +{ + QString displayName; + QString iconName; + bool hidden = false; +}; + +struct FilePresentationInfo +{ + QString displayName; + QString iconName; + bool hidden = false; +}; + +static bool isDesktopEntryFile(const QFileInfo &fileInfo) +{ + return fileInfo.isFile() + && fileInfo.suffix().compare(QStringLiteral("desktop"), Qt::CaseInsensitive) == 0; +} + +static QString localizedDesktopEntryText(QSettings &settings, const QString &key) +{ + if (key.isEmpty()) { + return {}; + } + + QStringList localizedKeys; + const QStringList uiLanguages = QLocale::system().uiLanguages(); + for (const QString &uiLanguage : uiLanguages) { + QString normalizedLanguage = uiLanguage.trimmed(); + if (normalizedLanguage.isEmpty()) { + continue; + } + + normalizedLanguage.replace(QLatin1Char('-'), QLatin1Char('_')); + + const QString fullKey = QStringLiteral("%1[%2]").arg(key, normalizedLanguage); + if (!localizedKeys.contains(fullKey)) { + localizedKeys.append(fullKey); + } + + const int separatorIndex = normalizedLanguage.indexOf(QLatin1Char('_')); + if (separatorIndex > 0) { + const QString baseLanguageKey = QStringLiteral("%1[%2]").arg(key, normalizedLanguage.left(separatorIndex)); + if (!localizedKeys.contains(baseLanguageKey)) { + localizedKeys.append(baseLanguageKey); + } + } + } + + for (const QString &localizedKey : localizedKeys) { + const QString localizedValue = settings.value(localizedKey).toString().trimmed(); + if (!localizedValue.isEmpty()) { + return localizedValue; + } + } + + return settings.value(key).toString().trimmed(); +} + +static DesktopEntryMetadata desktopEntryMetadataForFile(const QFileInfo &fileInfo) +{ + if (!isDesktopEntryFile(fileInfo)) { + return {}; + } + + QSettings settings(fileInfo.absoluteFilePath(), QSettings::IniFormat); + settings.beginGroup(QStringLiteral("Desktop Entry")); + + DesktopEntryMetadata metadata; + metadata.hidden = settings.value(QStringLiteral("Hidden")).toBool() + || settings.value(QStringLiteral("NoDisplay")).toBool(); + metadata.displayName = localizedDesktopEntryText(settings, QStringLiteral("Name")); + metadata.iconName = settings.value(QStringLiteral("Icon")).toString().trimmed(); + return metadata; +} + +static QString defaultFileIconName(const QFileInfo &fileInfo) +{ + if (fileInfo.isDir()) { + return QStringLiteral("folder"); + } + + static QMimeDatabase mimeDatabase; + const auto mimeType = mimeDatabase.mimeTypeForFile(fileInfo, QMimeDatabase::MatchDefault); + if (!mimeType.iconName().isEmpty()) { + return mimeType.iconName(); + } + if (!mimeType.genericIconName().isEmpty()) { + return mimeType.genericIconName(); + } + + return QStringLiteral("text-x-generic"); +} + +static FilePresentationInfo filePresentationInfo(const QFileInfo &fileInfo) +{ + FilePresentationInfo info; + info.displayName = fileInfo.fileName(); + + if (info.displayName.isEmpty()) { + info.displayName = fileInfo.absoluteFilePath(); + } + if (info.displayName.isEmpty()) { + info.displayName = fileInfo.filePath(); + } + + if (fileInfo.isDir()) { + info.iconName = QStringLiteral("folder"); + return info; + } + + const DesktopEntryMetadata desktopEntry = desktopEntryMetadataForFile(fileInfo); + info.hidden = desktopEntry.hidden; + if (!desktopEntry.displayName.isEmpty()) { + info.displayName = desktopEntry.displayName; + } + if (!desktopEntry.iconName.isEmpty()) { + info.iconName = desktopEntry.iconName; + } + + if (info.iconName.isEmpty()) { + info.iconName = defaultFileIconName(fileInfo); + } + + return info; +} + +static QString fileIconName(const QFileInfo &fileInfo) +{ + return filePresentationInfo(fileInfo).iconName; +} + +static QString thumbnailUrlForFile(const QFileInfo &fileInfo) +{ + if (fileInfo.isDir() || isDesktopEntryFile(fileInfo)) { + return {}; + } + + auto *provider = Dtk::Gui::DThumbnailProvider::instance(); + if (!provider || !provider->hasThumbnail(fileInfo)) { + return {}; + } + + const QString thumbnailPath = provider->thumbnailFilePath(fileInfo, Dtk::Gui::DThumbnailProvider::Small); + if (!thumbnailPath.isEmpty() && QFileInfo::exists(thumbnailPath)) { + return QUrl::fromLocalFile(thumbnailPath).toString(); + } + + provider->appendToProduceQueue(fileInfo, Dtk::Gui::DThumbnailProvider::Small); + return {}; +} + +static bool launchDesktopEntryFile(const QFileInfo &fileInfo) +{ + if (!isDesktopEntryFile(fileInfo)) { + return false; + } + + const QString desktopFilePath = fileInfo.absoluteFilePath(); + if (desktopFilePath.isEmpty()) { + return false; + } + + return QProcess::startDetached(QStringLiteral("gio"), + {QStringLiteral("launch"), desktopFilePath}); +} + +static QVariantMap popupEntry(const QString &entryId, + const QString &name, + const QString &iconName, + bool directory, + const QString &entryUrl = {}, + const QString &thumbnailUrl = {}) +{ + return { + {QStringLiteral("entryId"), entryId}, + {QStringLiteral("name"), name}, + {QStringLiteral("iconName"), iconName}, + {QStringLiteral("directory"), directory}, + {QStringLiteral("entryUrl"), entryUrl}, + {QStringLiteral("thumbnailUrl"), thumbnailUrl}, + }; +} + +static qint64 dateTimeToSortValue(const QDateTime &dateTime) +{ + return dateTime.isValid() ? dateTime.toMSecsSinceEpoch() : 0; +} + +static qint64 fileModifiedTimeForSort(const QFileInfo &fileInfo) +{ + return dateTimeToSortValue(fileInfo.lastModified()); +} + +static qint64 fileCreatedTimeForSort(const QFileInfo &fileInfo) +{ + const qint64 birthTime = dateTimeToSortValue(fileInfo.birthTime()); + if (birthTime > 0) { + return birthTime; + } + + const qint64 metadataTime = dateTimeToSortValue(fileInfo.metadataChangeTime()); + if (metadataTime > 0) { + return metadataTime; + } + + return fileModifiedTimeForSort(fileInfo); +} + +static qint64 fileSizeForSort(const QFileInfo &fileInfo) +{ + return fileInfo.isDir() ? 0 : fileInfo.size(); +} + +static QString fileTypeSortText(const QFileInfo &fileInfo) +{ + if (fileInfo.isDir()) { + return QStringLiteral("inode/directory"); + } + + static QMimeDatabase mimeDatabase; + const auto mimeType = mimeDatabase.mimeTypeForFile(fileInfo, QMimeDatabase::MatchDefault); + if (!mimeType.name().isEmpty()) { + return mimeType.name(); + } + + return QStringLiteral("application/octet-stream"); +} + +static QVariantList directoryEntriesForPath(const QString &path, const PopupSortState &sortState) +{ + QList entries; + + QDir directory(path); + const QFileInfoList fileInfos = directory.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot, + QDir::NoSort); + for (const QFileInfo &fileInfo : fileInfos) { + const QString entryPath = fileInfo.absoluteFilePath(); + const FilePresentationInfo presentation = filePresentationInfo(fileInfo); + if (presentation.hidden) { + continue; + } + + PopupSortableEntry entry; + entry.entryData = popupEntry(entryPath, + presentation.displayName, + presentation.iconName, + fileInfo.isDir(), + QUrl::fromLocalFile(entryPath).toString(), + thumbnailUrlForFile(fileInfo)); + entry.name = presentation.displayName; + entry.typeText = fileTypeSortText(fileInfo); + entry.modifiedTime = fileModifiedTimeForSort(fileInfo); + entry.createdTime = fileCreatedTimeForSort(fileInfo); + entry.size = fileSizeForSort(fileInfo); + entry.directory = fileInfo.isDir(); + entries.append(entry); + } + + sortPopupEntries(&entries, sortState, true); + + QVariantList result; + for (const PopupSortableEntry &entry : std::as_const(entries)) { + result.append(entry.entryData); + } + + return result; +} + +static QStringList previewIconsForDirectory(const QString &path, int limit = 4) +{ + QStringList iconNames; + + QDir directory(path); + const QFileInfoList fileInfos = directory.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot, + QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); + for (const QFileInfo &fileInfo : fileInfos) { + const FilePresentationInfo presentation = filePresentationInfo(fileInfo); + if (presentation.hidden) { + continue; + } + + iconNames.append(presentation.iconName); + if (iconNames.size() >= limit) { + break; + } + } + + return iconNames; +} + +static QModelIndex findIndexByRole(QAbstractItemModel *model, int role, const QString &value) +{ + if (!model || value.isEmpty()) { + return {}; + } + + return model->match(model->index(0, 0), role, value, 1, Qt::MatchExactly).value(0); +} + +static int modelRole(QAbstractItemModel *model, const QByteArray &roleName, int fallbackRole) +{ + if (!model) { + return fallbackRole; + } + + return model->roleNames().key(roleName, fallbackRole); +} + +static QModelIndex findIndexByNamedRole(QAbstractItemModel *model, + const QByteArray &roleName, + const QString &value, + int fallbackRole) +{ + return findIndexByRole(model, modelRole(model, roleName, fallbackRole), value); +} + +static bool isLauncherFolderId(const QString &launcherId) +{ + return launcherId.startsWith(QStringLiteral("internal/folder/")) || + launcherId.startsWith(QStringLiteral("internal/folders/")) || + launcherId.startsWith(QStringLiteral("internal/group/")); +} + +static int launcherGroupNumber(const QString &launcherId) +{ + if (launcherId.isEmpty()) { + return -1; + } + + if (isLauncherFolderId(launcherId)) { + return launcherId.section(QLatin1Char('/'), -1).toInt(); + } + + bool ok = false; + const int groupId = launcherId.toInt(&ok); + return ok ? groupId : -1; +} + +static bool isLauncherRootFolderId(const QString &launcherId) +{ + return launcherGroupNumber(launcherId) == 0; +} + +static QString alternateLauncherFolderId(const QString &launcherId) +{ + const QString singularPrefix = QStringLiteral("internal/folder/"); + const QString pluralPrefix = QStringLiteral("internal/folders/"); + const QString legacyPrefix = QStringLiteral("internal/group/"); + + if (launcherId.startsWith(singularPrefix)) { + return pluralPrefix + launcherId.mid(singularPrefix.size()); + } + + if (launcherId.startsWith(pluralPrefix)) { + return singularPrefix + launcherId.mid(pluralPrefix.size()); + } + + if (launcherId.startsWith(legacyPrefix)) { + return pluralPrefix + launcherId.mid(legacyPrefix.size()); + } + + return {}; +} + +static QString resolveLauncherGroupId(QAbstractItemModel *groupModel, const QString &groupId) +{ + if (!groupModel || !isLauncherFolderId(groupId)) { + return groupId; + } + + const QString suffix = groupId.section(QLatin1Char('/'), -1); + const QStringList candidates = { + groupId, + alternateLauncherFolderId(groupId), + QStringLiteral("internal/folders/%1").arg(suffix), + QStringLiteral("internal/folder/%1").arg(suffix), + QStringLiteral("internal/group/%1").arg(suffix), + suffix + }; + + for (const QString &candidate : candidates) { + if (!candidate.isEmpty() && + findIndexByNamedRole(groupModel, MODEL_DESKTOPID, candidate, TaskManager::DesktopIdRole).isValid()) { + return candidate; + } + } + + return groupId; +} + +static QStringList invokeLauncherGroupItems(QObject *groupModel, const QString &groupId) +{ + if (!groupModel || groupId.isEmpty()) { + return {}; + } + + const QString resolvedGroupId = resolveLauncherGroupId(qobject_cast(groupModel), groupId); + QStringList items; + QMetaObject::invokeMethod(groupModel, + "groupItems", + Q_RETURN_ARG(QStringList, items), + Q_ARG(QString, resolvedGroupId)); + return items; +} + +static QString invokeLauncherGroupDisplayName(QObject *groupModel, const QString &groupId) +{ + if (!groupModel || groupId.isEmpty()) { + return {}; + } + + const QString resolvedGroupId = resolveLauncherGroupId(qobject_cast(groupModel), groupId); + QString displayName; + QMetaObject::invokeMethod(groupModel, + "groupDisplayName", + Q_RETURN_ARG(QString, displayName), + Q_ARG(QString, resolvedGroupId)); + return displayName; +} + +static bool preferChineseLauncherGroupNames() +{ + return QLocale().language() == QLocale::Chinese; +} + +static QString launcherGroupFallbackName() +{ + return preferChineseLauncherGroupNames() ? QStringLiteral("应用组") : TaskManager::tr("App Group"); +} + +static QString translatedLauncherCategoryName(int category) +{ + if (preferChineseLauncherGroupNames()) { + switch (category) { + case 0: return QStringLiteral("网络"); + case 1: return QStringLiteral("社交"); + case 2: return QStringLiteral("音乐"); + case 3: return QStringLiteral("视频"); + case 4: return QStringLiteral("图形图像"); + case 5: return QStringLiteral("游戏"); + case 6: return QStringLiteral("办公"); + case 7: return QStringLiteral("阅读"); + case 8: return QStringLiteral("编程开发"); + case 9: return QStringLiteral("系统管理"); + case 10: return QStringLiteral("其他"); + default: return {}; + } + } + + switch (category) { + case 0: return TaskManager::tr("Internet"); + case 1: return TaskManager::tr("Chat"); + case 2: return TaskManager::tr("Music"); + case 3: return TaskManager::tr("Video"); + case 4: return TaskManager::tr("Graphics"); + case 5: return TaskManager::tr("Game"); + case 6: return TaskManager::tr("Office"); + case 7: return TaskManager::tr("Reading"); + case 8: return TaskManager::tr("Development"); + case 9: return TaskManager::tr("System"); + case 10: return TaskManager::tr("Others"); + default: return {}; + } +} + +static QString translatedLauncherCategoryName(const QString &groupName) { - if (identifies.isEmpty()) - return {}; - - pid_t windowPid = identifies.last().toInt(); - if (windowPid <= 0) + if (!groupName.startsWith(QStringLiteral("internal/category/"))) { return {}; + } - auto appId = DSGApplication::getId(windowPid); - if (appId.isEmpty()) { - qCDebug(taskManagerLog) << "appId is empty, AM failed to identify window with pid:" << windowPid; + bool ok = false; + const int category = groupName.section(QLatin1Char('/'), -1).toInt(&ok); + if (!ok) { return {}; } - return QString::fromUtf8(appId); + return translatedLauncherCategoryName(category); } -// 尝试通过 AM(Application Manager) 匹配应用程序 -static QModelIndex tryMatchByApplicationManager(const QStringList &identifies, - QAbstractItemModel *model, - const QHash &roleNames) +static qint64 launcherInstalledTimeForSort(const QModelIndex &appIndex) { - Q_ASSERT(model); + const qint64 installedTime = appIndex.data(TaskManager::InstalledTimeRole).toLongLong(); + if (installedTime > 0) { + return installedTime; + } - if (!Settings->cgroupsBasedGrouping()) { - return QModelIndex(); + const QFileInfo fileInfo(appIndex.data(TaskManager::DesktopSourcePathRole).toString()); + if (!fileInfo.exists()) { + return 0; } - auto desktopId = getDesktopIdByPid(identifies); - if (desktopId.isEmpty() || Settings->cgroupsBasedGroupingSkipIds().contains(desktopId)) { - return QModelIndex(); + return fileCreatedTimeForSort(fileInfo); +} + +static qint64 launcherModifiedTimeForSort(const QModelIndex &appIndex) +{ + const QFileInfo fileInfo(appIndex.data(TaskManager::DesktopSourcePathRole).toString()); + if (!fileInfo.exists()) { + return 0; } - // 先在 model 中查找 desktopId 对应的 item - auto res = model->match(model->index(0, 0), roleNames.key(MODEL_DESKTOPID), - desktopId, 1, Qt::MatchFixedString | Qt::MatchWrap).value(0); + return fileModifiedTimeForSort(fileInfo); +} - if (!res.isValid()) { - return QModelIndex(); +static qint64 launcherSizeForSort(const QModelIndex &appIndex) +{ + const QFileInfo fileInfo(appIndex.data(TaskManager::DesktopSourcePathRole).toString()); + if (!fileInfo.exists()) { + return 0; } - // 检查应用的 Categories 是否在跳过列表中 - auto skipCategories = Settings->cgroupsBasedGroupingSkipCategories(); - if (!skipCategories.isEmpty()) { - QStringList categories = res.data(TaskManager::CategoriesRole).toStringList(); - if (!categories.isEmpty()) { - // 检查是否有任何 skipCategory 在应用的 categories 中 - for (const QString &skipCategory : skipCategories) { - if (categories.contains(skipCategory)) { - qCDebug(taskManagerLog) << "Skipping cgroups grouping for" << desktopId - << "due to category:" << skipCategory; - return QModelIndex(); - } - } - } + return fileInfo.size(); +} + +static QString launcherEntryTypeText(const QModelIndex &appIndex) +{ + const QString categoryName = translatedLauncherCategoryName(appIndex.data(TaskManager::DDECategoryRole).toInt()); + if (!categoryName.isEmpty()) { + return categoryName; } - qCDebug(taskManagerLog) << "matched by AM desktop ID:" << desktopId << res; - return res; + const QStringList categories = appIndex.data(TaskManager::CategoriesRole).toStringList(); + if (!categories.isEmpty()) { + return categories.constFirst(); + } + + return QStringLiteral("application"); +} + +static QString launcherGroupDisplayName(QAbstractItemModel *groupModel, const QString &groupId) +{ + QString groupName = invokeLauncherGroupDisplayName(groupModel, groupId); + if (groupName.isEmpty()) { + const QString resolvedGroupId = resolveLauncherGroupId(groupModel, groupId); + const QModelIndex groupIndex = findIndexByNamedRole(groupModel, MODEL_DESKTOPID, resolvedGroupId, TaskManager::DesktopIdRole); + groupName = groupIndex.data(modelRole(groupModel, MODEL_NAME, TaskManager::NameRole)).toString(); + } + const QString categoryName = translatedLauncherCategoryName(groupName); + if (!categoryName.isEmpty()) { + return categoryName; + } + if (groupName.isEmpty()) { + groupName = launcherGroupFallbackName(); + } + return groupName; } class BoolFilterModel : public QSortFilterProxyModel, public AbstractTaskManagerInterface @@ -149,6 +1009,52 @@ TaskManager::TaskManager(QObject *parent) connect(Settings, &TaskManagerSettings::allowedForceQuitChanged, this, &TaskManager::allowedForceQuitChanged); connect(Settings, &TaskManagerSettings::showAttentionAnimationChanged, this, &TaskManager::showAttentionAnimationChanged); connect(Settings, &TaskManagerSettings::windowSplitChanged, this, &TaskManager::windowSplitChanged); + + if (auto *thumbnailProvider = Dtk::Gui::DThumbnailProvider::instance()) { + const auto notifyThumbnailChanged = [this](const QString &sourceFilePath, const QString &) { + if (!sourceFilePath.isEmpty()) { + Q_EMIT popupEntryThumbnailChanged(QDir::cleanPath(sourceFilePath)); + } + }; + connect(thumbnailProvider, &Dtk::Gui::DThumbnailProvider::thumbnailChanged, this, notifyThumbnailChanged); + connect(thumbnailProvider, &Dtk::Gui::DThumbnailProvider::createThumbnailFinished, this, notifyThumbnailChanged); + } + + m_trashCountProcess = new QProcess(this); + m_trashCountProcess->setProgram(QStringLiteral("gio")); + m_trashCountProcess->setArguments({QStringLiteral("trash"), QStringLiteral("--list")}); + connect(m_trashCountProcess, + QOverload::of(&QProcess::finished), + this, + [this](int exitCode, QProcess::ExitStatus exitStatus) { + const int nextTrashCount = (exitStatus == QProcess::NormalExit && exitCode == 0) + ? trashCountFromOutput(m_trashCountProcess->readAllStandardOutput()) + : m_cachedTrashCount; + const bool stateChanged = !m_trashStateInitialized || m_cachedTrashCount != nextTrashCount; + m_cachedTrashCount = nextTrashCount; + m_trashStateInitialized = true; + if (m_trashCountRefreshTimer.isValid()) { + m_trashCountRefreshTimer.restart(); + } else { + m_trashCountRefreshTimer.start(); + } + if (stateChanged) { + emit trashStateChanged(); + } + }); + connect(m_trashCountProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) { + if (!m_trashStateInitialized) { + m_trashStateInitialized = true; + if (m_trashCountRefreshTimer.isValid()) { + m_trashCountRefreshTimer.restart(); + } else { + m_trashCountRefreshTimer.start(); + } + emit trashStateChanged(); + } + }); + + refreshTrashCount(true); } bool TaskManager::load() @@ -181,38 +1087,27 @@ bool TaskManager::init() BoolFilterModel *leftModel = new BoolFilterModel(m_windowMonitor.data(), m_windowMonitor->roleNames().key("shouldSkip"), this); if (auto applet = bridge.applet()) { auto model = applet->property("appModel").value(); + auto groupModel = applet->property("appGroupModel").value(); Q_ASSERT(model); - m_activeAppModel = new DockCombineModel(leftModel, model, TaskManager::IdentityRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { - auto roleNames = model->roleNames(); - QList identifiedOrders = {MODEL_DESKTOPID, MODEL_STARTUPWMCLASS, MODEL_NAME, MODEL_ICONNAME}; - - auto identifies = data.toStringList(); - for (auto id : identifies) { - if (id.isEmpty()) { - continue; - } - - for (auto identifiedOrder : identifiedOrders) { - auto res = model->match(model->index(0, 0), roleNames.key(identifiedOrder), id, 1, Qt::MatchFixedString | Qt::MatchWrap).value(0); - if (res.isValid()) { - qCDebug(taskManagerLog) << "matched" << res; - return res; - } - } - } - - // 尝试通过AM(Application Manager)匹配应用程序 - auto amMatchResult = tryMatchByApplicationManager(identifies, model, roleNames); - if (amMatchResult.isValid()) { - return amMatchResult; + m_launcherAppModel = model; + m_launcherGroupModel = groupModel; + if (m_launcherGroupModel) { + const int desktopIdRole = modelRole(m_launcherGroupModel, MODEL_DESKTOPID, DesktopIdRole); + const int nameRole = modelRole(m_launcherGroupModel, MODEL_NAME, NameRole); + qCWarning(taskManagerLog) << "launcher group model rows" << m_launcherGroupModel->rowCount(); + for (int i = 0; i < m_launcherGroupModel->rowCount(); ++i) { + const QModelIndex groupIndex = m_launcherGroupModel->index(i, 0); + qCWarning(taskManagerLog) << "launcher group model item" + << i + << groupIndex.data(desktopIdRole).toString() + << groupIndex.data(nameRole).toString(); } - - auto res = model->match(model->index(0, 0), roleNames.key(MODEL_DESKTOPID), identifies.value(0), 1, Qt::MatchEndsWith); - qCDebug(taskManagerLog) << "matched" << res.value(0); - return res.value(0); + } + m_activeAppModel = new DockCombineModel(leftModel, model, TaskManager::IdentityRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + return matchLauncherApplication(data.toStringList(), model, model->roleNames()); }); - m_dockGlobalElementModel = new DockGlobalElementModel(model, m_activeAppModel, this); + m_dockGlobalElementModel = new DockGlobalElementModel(model, m_activeAppModel, groupModel, this); m_itemModel = new DockItemModel(m_dockGlobalElementModel, this); // 初始化预览代理模型,基于合并后的数据 @@ -284,7 +1179,11 @@ void TaskManager::requestUpdateWindowIconGeometry(const QModelIndex &index, cons dataModel()->requestUpdateWindowIconGeometry(index, geometry, delegate); } -void TaskManager::requestPreview(const QModelIndex &index, QObject *relativePositionItem, int32_t previewXoffset, int32_t previewYoffset, uint32_t direction) +void TaskManager::requestPreview(const QModelIndex &index, + QObject *relativePositionItem, + int32_t previewXoffset, + int32_t previewYoffset, + uint32_t direction) { if (!m_hoverPreviewModel) { qCWarning(taskManagerLog) << "TaskManager::requestPreview: m_hoverPreviewModel is null"; @@ -307,7 +1206,11 @@ void TaskManager::requestPreview(const QModelIndex &index, QObject *relativePosi return; } - m_windowMonitor->requestPreview(m_hoverPreviewModel, qobject_cast(relativePositionItem), previewXoffset, previewYoffset, direction); + m_windowMonitor->requestPreview(m_hoverPreviewModel, + qobject_cast(relativePositionItem), + previewXoffset, + previewYoffset, + direction); } void TaskManager::requestWindowsView(const QModelIndexList &indexes) const @@ -342,6 +1245,15 @@ void TaskManager::handleWindowAdded(QPointer window) qCDebug(taskManagerLog()) << "identify by Fallback:" << desktopId; } + if (desktopfile.isNull()) { + desktopfile = DesktopfileParserFactory::createByWindow(window); + } + + if (desktopfile.isNull()) { + qCWarning(taskManagerLog()) << "Unable to create desktop file parser for window:" << window->id(); + return; + } + auto appitem = desktopfile->getAppItem(); if (appitem.isNull() || (appitem->hasWindow() && windowSplit())) { @@ -385,26 +1297,325 @@ bool TaskManager::showAttentionAnimation() return Settings->showAttentionAnimation(); } -QString TaskManager::desktopIdToAppId(const QString& desktopId) +QString TaskManager::desktopIdToAppId(const QString& desktopId) const { return Q_LIKELY(desktopId.endsWith(".desktop")) ? desktopId.chopped(8) : desktopId; } +QString TaskManager::dockElementFromLauncherId(const QString &launcherId) const +{ + if (launcherId.isEmpty()) { + return {}; + } + + if (isLauncherFolderId(launcherId)) { + const QString resolvedGroupId = resolveLauncherGroupId(m_launcherGroupModel, launcherId); + if (isLauncherRootFolderId(resolvedGroupId)) { + return {}; + } + return QStringLiteral("group/%1").arg(resolvedGroupId); + } + + if (launcherId.startsWith(QStringLiteral("internal/"))) { + return {}; + } + + return QStringLiteral("desktop/%1").arg(desktopIdToAppId(launcherId)); +} + +QString TaskManager::displayNameForDockElement(const QString &dockElement) const +{ + const auto [type, id] = splitDockElement(dockElement); + if (type.isEmpty() || id.isEmpty()) { + return {}; + } + + if (type == QStringLiteral("folder")) { + return displayNameForPath(id); + } + + if (type == QStringLiteral("group")) { + QString groupName = invokeLauncherGroupDisplayName(m_launcherGroupModel, id); + if (groupName.isEmpty()) { + const QString resolvedGroupId = resolveLauncherGroupId(m_launcherGroupModel, id); + const QModelIndex groupIndex = findIndexByNamedRole(m_launcherGroupModel, + MODEL_DESKTOPID, + resolvedGroupId, + DesktopIdRole); + if (groupIndex.isValid()) { + groupName = groupIndex.data(modelRole(m_launcherGroupModel, MODEL_NAME, NameRole)).toString(); + } + } + const QString categoryName = translatedLauncherCategoryName(groupName); + if (!categoryName.isEmpty()) { + return categoryName; + } + return groupName; + } + + return {}; +} + +QString TaskManager::folderUrlToElementId(const QString &folderUrl) const +{ + const QString folderPath = normalizedFolderPath(folderUrl); + if (folderPath.isEmpty()) { + return {}; + } + + return QStringLiteral("folder/%1").arg(folderPath); +} + bool TaskManager::requestDockByDesktopId(const QString& desktopID) { + qCWarning(taskManagerLog) << "requestDockByDesktopId" << desktopID; + if (isLauncherFolderId(desktopID)) { + if (!m_launcherGroupModel || isLauncherRootFolderId(desktopID)) { + qCWarning(taskManagerLog) << "reject launcher group dock request due to missing model or root group" << desktopID; + return false; + } + + const QString resolvedGroupId = resolveLauncherGroupId(m_launcherGroupModel, desktopID); + const QString dockElement = dockElementFromLauncherId(desktopID); + if (dockElement.isEmpty() || Settings->isDocked(dockElement)) { + qCWarning(taskManagerLog) << "reject launcher group dock request due to empty/already docked element" + << desktopID << resolvedGroupId << dockElement; + return false; + } + + const QModelIndex groupIndex = findIndexByNamedRole(m_launcherGroupModel, MODEL_DESKTOPID, resolvedGroupId, DesktopIdRole); + if (!groupIndex.isValid()) { + qCWarning(taskManagerLog) << "reject launcher group dock request due to invalid group index" + << desktopID << resolvedGroupId; + return false; + } + + Settings->appendDockedElement(dockElement); + qCWarning(taskManagerLog) << "accepted launcher group dock request" << desktopID << resolvedGroupId << dockElement; + return true; + } + if (desktopID.startsWith("internal/")) return false; QString appId = desktopIdToAppId(desktopID); // 检查应用是否已经在任务栏中,如果是则返回 false - if (IsDocked(appId)) + if (IsDocked(appId)) { + qCWarning(taskManagerLog) << "reject app dock request because already docked" << desktopID << appId; return false; + } - return RequestDock(appId); + const bool ok = RequestDock(appId); + qCWarning(taskManagerLog) << "app dock request result" << desktopID << appId << ok; + return ok; } bool TaskManager::requestUndockByDesktopId(const QString& desktopID) { + if (isLauncherFolderId(desktopID)) { + const QString dockElement = dockElementFromLauncherId(desktopID); + if (dockElement.isEmpty()) { + qCWarning(taskManagerLog) << "reject launcher group undock request due to empty element" << desktopID; + return false; + } + + Settings->removeDockedElement(dockElement); + qCWarning(taskManagerLog) << "accepted launcher group undock request" << desktopID << dockElement; + return true; + } + if (desktopID.startsWith("internal/")) return false; - return RequestUndock(desktopIdToAppId(desktopID)); + const QString appId = desktopIdToAppId(desktopID); + const bool ok = RequestUndock(appId); + qCWarning(taskManagerLog) << "app undock request result" << desktopID << appId << ok; + return ok; +} + +bool TaskManager::requestDockByFolderUrl(const QString &folderUrl) +{ + qCWarning(taskManagerLog) << "requestDockByFolderUrl" << folderUrl; + const QString folderPath = normalizedFolderPath(folderUrl); + if (folderPath.isEmpty()) { + qCWarning(taskManagerLog) << "reject folder dock request due to empty normalized path" << folderUrl; + return false; + } + + QFileInfo folderInfo(folderPath); + if (!folderInfo.exists() || !folderInfo.isDir()) { + qCWarning(taskManagerLog) << "reject folder dock request due to invalid folder" << folderUrl << folderPath; + return false; + } + + const QString dockElement = QStringLiteral("folder/%1").arg(folderPath); + if (Settings->isDocked(dockElement)) { + qCWarning(taskManagerLog) << "reject folder dock request because already docked" << folderUrl << dockElement; + return false; + } + + Settings->appendDockedElement(dockElement); + qCWarning(taskManagerLog) << "accepted folder dock request" << folderUrl << dockElement; + return true; +} + +bool TaskManager::requestUndockByFolderUrl(const QString &folderUrl) +{ + const QString folderPath = normalizedFolderPath(folderUrl); + if (folderPath.isEmpty()) { + qCWarning(taskManagerLog) << "reject folder undock request due to empty normalized path" << folderUrl; + return false; + } + + const QString dockElement = QStringLiteral("folder/%1").arg(folderPath); + Settings->removeDockedElement(dockElement); + qCWarning(taskManagerLog) << "accepted folder undock request" << folderUrl << dockElement; + return true; +} + +QVariantMap TaskManager::popupSortState(const QString &dockElement) const +{ + const auto [type, id] = splitDockElement(dockElement); + Q_UNUSED(id) + + if (type == QStringLiteral("group") && !m_popupSortStates.contains(dockElement)) { + return { + {QStringLiteral("sortField"), QString()}, + {QStringLiteral("sortDescending"), false}, + }; + } + + const PopupSortState state = m_popupSortStates.value(dockElement, PopupSortState{}); + return { + {QStringLiteral("sortField"), popupSortFieldToString(state.field)}, + {QStringLiteral("sortDescending"), state.order == Qt::DescendingOrder}, + }; +} + +QVariantMap TaskManager::cyclePopupSort(const QString &dockElement, const QString &fieldName) +{ + if (dockElement.isEmpty()) { + return popupSortState(dockElement); + } + + const auto [type, id] = splitDockElement(dockElement); + Q_UNUSED(id) + + const PopupSortField selectedField = popupSortFieldFromString(fieldName); + if (type == QStringLiteral("group") && !m_popupSortStates.contains(dockElement)) { + PopupSortState nextState; + nextState.field = selectedField; + nextState.order = Qt::AscendingOrder; + m_popupSortStates.insert(dockElement, nextState); + return popupSortState(dockElement); + } + + const PopupSortState currentState = m_popupSortStates.value(dockElement, PopupSortState{}); + const PopupSortState nextState = cyclePopupSortState(currentState, selectedField); + m_popupSortStates.insert(dockElement, nextState); + return popupSortState(dockElement); +} + +QVariantMap TaskManager::popupDescriptor(const QString &dockElement, const QString &location) const +{ + const auto [type, id] = splitDockElement(dockElement); + if (type.isEmpty() || id.isEmpty()) { + return {}; + } + + if (type == QStringLiteral("group")) { + QList entries; + for (const QString &appId : invokeLauncherGroupItems(m_launcherGroupModel, id)) { + const QModelIndex appIndex = findIndexByNamedRole(m_launcherAppModel, MODEL_DESKTOPID, appId, DesktopIdRole); + const QString iconName = appIndex.data(IconNameRole).toString().isEmpty() ? + QString::fromLatin1(DEFAULT_APP_ICONNAME) : + appIndex.data(IconNameRole).toString(); + const QString appName = appIndex.data(NameRole).toString().isEmpty() ? + appId : + appIndex.data(NameRole).toString(); + PopupSortableEntry entry; + entry.entryData = popupEntry(appId, appName, iconName, false); + entry.name = appName; + entry.typeText = launcherEntryTypeText(appIndex); + entry.modifiedTime = launcherModifiedTimeForSort(appIndex); + entry.createdTime = launcherInstalledTimeForSort(appIndex); + entry.size = launcherSizeForSort(appIndex); + entries.append(entry); + } + + const bool hasCustomSort = m_popupSortStates.contains(dockElement); + const PopupSortState state = hasCustomSort ? m_popupSortStates.value(dockElement) : PopupSortState{}; + if (hasCustomSort) { + sortPopupEntries(&entries, state, false); + } + + QVariantList entryData; + for (const PopupSortableEntry &entry : std::as_const(entries)) { + entryData.append(entry.entryData); + } + + return { + {QStringLiteral("kind"), type}, + {QStringLiteral("title"), launcherGroupDisplayName(m_launcherGroupModel, id)}, + {QStringLiteral("location"), QString()}, + {QStringLiteral("parentLocation"), QString()}, + {QStringLiteral("canGoBack"), false}, + {QStringLiteral("entries"), entryData}, + {QStringLiteral("sortField"), hasCustomSort ? popupSortFieldToString(state.field) : QString()}, + {QStringLiteral("sortDescending"), hasCustomSort && state.order == Qt::DescendingOrder}, + }; + } + + if (type == QStringLiteral("folder")) { + const QString rootLocation = normalizedFolderPath(id); + if (rootLocation.isEmpty()) { + return {}; + } + + QString currentLocation = location.isEmpty() ? rootLocation : normalizedFolderPath(location); + if (currentLocation.isEmpty() || !isWithinBasePath(rootLocation, currentLocation) || !QFileInfo(currentLocation).isDir()) { + currentLocation = rootLocation; + } + + QString parentLocation = QFileInfo(currentLocation).dir().absolutePath(); + if (!isWithinBasePath(rootLocation, parentLocation)) { + parentLocation = rootLocation; + } + + const PopupSortState state = m_popupSortStates.value(dockElement, PopupSortState{}); + + return { + {QStringLiteral("kind"), type}, + {QStringLiteral("title"), displayNameForPath(currentLocation)}, + {QStringLiteral("location"), currentLocation}, + {QStringLiteral("parentLocation"), parentLocation}, + {QStringLiteral("canGoBack"), currentLocation != rootLocation}, + {QStringLiteral("entries"), directoryEntriesForPath(currentLocation, state)}, + {QStringLiteral("sortField"), popupSortFieldToString(state.field)}, + {QStringLiteral("sortDescending"), state.order == Qt::DescendingOrder}, + }; + } + + return {}; +} + +void TaskManager::activatePopupEntry(const QString &dockElement, const QString &entryId) const +{ + const auto [type, id] = splitDockElement(dockElement); + Q_UNUSED(id) + + if (type == QStringLiteral("group")) { + auto desktopfileParser = DESKTOPFILEFACTORY::createById(entryId, "amAPP"); + if (desktopfileParser && desktopfileParser->isValied().first) { + desktopfileParser->launch(); + } + return; + } + + if (type == QStringLiteral("folder")) { + const QFileInfo fileInfo(entryId); + if (launchDesktopEntryFile(fileInfo)) { + return; + } + + QDesktopServices::openUrl(QUrl::fromLocalFile(entryId)); + } } bool TaskManager::RequestDock(QString appID) @@ -477,16 +1688,10 @@ void TaskManager::activateWindow(uint32_t windowID) #endif } -void TaskManager::saveDockElementsOrder(const QStringList &appIds) +void TaskManager::saveDockElementsOrder(const QStringList &dockElements) { 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); - } - } + const QStringList newDockedElements = mergedDockedElementsOrder(dockedElements, dockElements); TaskManagerSettings::instance()->setDockedElements(newDockedElements); } @@ -498,32 +1703,130 @@ void TaskManager::moveItem(int from, int to) QString TaskManager::getTrashTipText() { - const auto count = queryTrashCount(); - return tr("%1 files").arg(count); + refreshTrashCount(); + return tr("%1 files").arg(m_cachedTrashCount); } bool TaskManager::isTrashEmpty() const { - return queryTrashCount() == 0; + refreshTrashCount(); + return m_cachedTrashCount == 0; } -int TaskManager::queryTrashCount() const +void TaskManager::refreshTrashCount(bool force) const { - int count = 0; + if (!m_trashCountProcess || m_trashCountProcess->state() != QProcess::NotRunning) { + return; + } + + if (!force + && m_trashStateInitialized + && m_trashCountRefreshTimer.isValid() + && m_trashCountRefreshTimer.elapsed() < 5000) { + return; + } + + m_trashCountProcess->start(); +} - QProcess gio; - gio.start("gio", QStringList() << "trash" << "--list"); - if (gio.waitForFinished(1000) && gio.exitStatus() == QProcess::NormalExit && gio.exitCode() == 0) { - const QByteArray &out = gio.readAllStandardOutput(); - const QList lines = out.split('\n'); - for (const QByteArray &l : lines) { - if (!l.trimmed().isEmpty()) count++; +int TaskManager::trashCountFromOutput(const QByteArray &output) +{ + int count = 0; + const QList lines = output.split('\n'); + for (const QByteArray &line : lines) { + if (!line.trimmed().isEmpty()) { + ++count; } - return count; } return count; } +QString TaskManager::managedTempDirectoryPath() +{ + QString basePath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + if (basePath.isEmpty()) { + basePath = QDir::tempPath(); + } + + return QDir(basePath).filePath(QStringLiteral("dde-shell/taskmanager")); +} + +QString TaskManager::normalizedManagedTempFilePath(const QString &pathOrUrl) +{ + if (pathOrUrl.trimmed().isEmpty()) { + return {}; + } + + const QUrl url(pathOrUrl); + const QString rawPath = url.isValid() && url.isLocalFile() ? url.toLocalFile() : pathOrUrl; + return rawPath.isEmpty() ? QString() : QDir::cleanPath(rawPath); +} + +void TaskManager::pruneManagedTempFiles() const +{ + const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); + if (m_lastManagedTempPruneMs > 0 && nowMs - m_lastManagedTempPruneMs < 10LL * 60LL * 1000LL) { + return; + } + + m_lastManagedTempPruneMs = nowMs; + const QDir directory(managedTempDirectoryPath()); + if (!directory.exists()) { + return; + } + + for (const QFileInfo &fileInfo : directory.entryInfoList(QDir::Files | QDir::NoDotAndDotDot)) { + const QString cleanPath = QDir::cleanPath(fileInfo.absoluteFilePath()); + if (m_managedTempFiles.contains(cleanPath)) { + continue; + } + + if (fileInfo.lastModified().msecsTo(QDateTime::currentDateTime()) >= 6LL * 60LL * 60LL * 1000LL) { + QFile::remove(cleanPath); + } + } +} + +QString TaskManager::createManagedTempFilePath(const QString &prefix, const QString &suffix) const +{ + pruneManagedTempFiles(); + + QDir directory(managedTempDirectoryPath()); + if (!directory.exists() && !directory.mkpath(QStringLiteral("."))) { + return {}; + } + + const QString safePrefix = prefix.trimmed().isEmpty() ? QStringLiteral("dde-shell-") : prefix.trimmed(); + const QString safeSuffix = suffix.trimmed(); + QTemporaryFile temporaryFile(directory.filePath(QStringLiteral("%1XXXXXX%2").arg(safePrefix, safeSuffix))); + temporaryFile.setAutoRemove(false); + if (!temporaryFile.open()) { + return {}; + } + + const QString path = QDir::cleanPath(temporaryFile.fileName()); + temporaryFile.close(); + m_managedTempFiles.insert(path); + return path; +} + +void TaskManager::releaseManagedTempFile(const QString &pathOrUrl) const +{ + const QString path = normalizedManagedTempFilePath(pathOrUrl); + if (path.isEmpty()) { + return; + } + + const QString managedDirectory = QDir(managedTempDirectoryPath()).absolutePath(); + const QString fileDirectory = QFileInfo(path).absolutePath(); + if (fileDirectory != managedDirectory && !fileDirectory.startsWith(managedDirectory + QLatin1Char('/'))) { + return; + } + + m_managedTempFiles.remove(path); + QFile::remove(path); +} + void TaskManager::modifyOpacityChanged() { DS_NAMESPACE::DAppletBridge appearanceBridge("org.deepin.ds.dde-appearance"); diff --git a/panels/dock/taskmanager/taskmanager.h b/panels/dock/taskmanager/taskmanager.h index 517bd0da3..db1282625 100644 --- a/panels/dock/taskmanager/taskmanager.h +++ b/panels/dock/taskmanager/taskmanager.h @@ -11,8 +11,15 @@ #include "dockglobalelementmodel.h" #include "dockitemmodel.h" #include "hoverpreviewproxymodel.h" +#include "popupsortutils.h" +#include +#include +#include #include +#include +#include +#include namespace dock { class AppItem; @@ -26,6 +33,8 @@ class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManage Q_PROPERTY(bool windowFullscreen READ windowFullscreen NOTIFY windowFullscreenChanged) Q_PROPERTY(bool allowForceQuit READ allowForceQuit NOTIFY allowedForceQuitChanged) Q_PROPERTY(bool showAttentionAnimation READ showAttentionAnimation NOTIFY showAttentionAnimationChanged) + Q_PROPERTY(QString trashTipText READ getTrashTipText NOTIFY trashStateChanged) + Q_PROPERTY(bool trashEmpty READ isTrashEmpty NOTIFY trashStateChanged) public: enum Roles { @@ -43,6 +52,9 @@ class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManage ItemIdRole, MenusRole, WindowsRole, + DockElementRole, + ItemKindRole, + PreviewIconsRole, // from dde-apps DesktopIdRole = 0x1000, @@ -88,12 +100,25 @@ class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManage Q_INVOKABLE void requestClose(const QModelIndex &index, bool force = false) const override; Q_INVOKABLE void requestUpdateWindowIconGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate = nullptr) const override; Q_INVOKABLE void - requestPreview(const QModelIndex &index, QObject *relativePositionItem, int32_t previewXoffset, int32_t previewYoffset, uint32_t direction); + requestPreview(const QModelIndex &index, + QObject *relativePositionItem, + int32_t previewXoffset, + int32_t previewYoffset, + uint32_t direction); Q_INVOKABLE void requestWindowsView(const QModelIndexList &indexes) const override; - Q_INVOKABLE QString desktopIdToAppId(const QString& desktopId); + Q_INVOKABLE QString desktopIdToAppId(const QString& desktopId) const; + Q_INVOKABLE QString dockElementFromLauncherId(const QString &launcherId) const; + Q_INVOKABLE QString displayNameForDockElement(const QString &dockElement) const; + Q_INVOKABLE QString folderUrlToElementId(const QString &folderUrl) const; Q_INVOKABLE bool requestDockByDesktopId(const QString& desktopID); Q_INVOKABLE bool requestUndockByDesktopId(const QString& desktopID); + Q_INVOKABLE bool requestDockByFolderUrl(const QString &folderUrl); + Q_INVOKABLE bool requestUndockByFolderUrl(const QString &folderUrl); + Q_INVOKABLE QVariantMap popupDescriptor(const QString &dockElement, const QString &location = QString()) const; + Q_INVOKABLE QVariantMap popupSortState(const QString &dockElement) const; + Q_INVOKABLE QVariantMap cyclePopupSort(const QString &dockElement, const QString &fieldName); + Q_INVOKABLE void activatePopupEntry(const QString &dockElement, const QString &entryId) const; Q_INVOKABLE bool RequestDock(QString appID); Q_INVOKABLE bool IsDocked(QString appID); Q_INVOKABLE bool RequestUndock(QString appID); @@ -107,26 +132,44 @@ class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManage Q_INVOKABLE QString getTrashTipText(); Q_INVOKABLE bool isTrashEmpty() const; + Q_INVOKABLE QString createManagedTempFilePath(const QString &prefix = QString(), + const QString &suffix = QString()) const; + Q_INVOKABLE void releaseManagedTempFile(const QString &pathOrUrl) const; Q_SIGNALS: void dataModelChanged(); void windowSplitChanged(); void windowFullscreenChanged(bool); void allowedForceQuitChanged(); void showAttentionAnimationChanged(); + void popupEntryThumbnailChanged(const QString &entryPath); + void trashStateChanged(); private Q_SLOTS: void handleWindowAdded(QPointer window); void modifyOpacityChanged(); private: + void refreshTrashCount(bool force = false) const; + static int trashCountFromOutput(const QByteArray &output); + static QString managedTempDirectoryPath(); + static QString normalizedManagedTempFilePath(const QString &pathOrUrl); + void pruneManagedTempFiles() const; + QScopedPointer m_windowMonitor; bool m_windowFullscreen; DockCombineModel *m_activeAppModel = nullptr; DockGlobalElementModel *m_dockGlobalElementModel = nullptr; DockItemModel *m_itemModel = nullptr; HoverPreviewProxyModel *m_hoverPreviewModel = nullptr; - int queryTrashCount() const; + QAbstractItemModel *m_launcherAppModel = nullptr; + QAbstractItemModel *m_launcherGroupModel = nullptr; + QHash m_popupSortStates; + mutable QProcess *m_trashCountProcess = nullptr; + mutable int m_cachedTrashCount = 0; + mutable bool m_trashStateInitialized = false; + mutable QElapsedTimer m_trashCountRefreshTimer; + mutable QSet m_managedTempFiles; + mutable qint64 m_lastManagedTempPruneMs = 0; }; } - diff --git a/panels/dock/taskmanager/taskmanagersettings.cpp b/panels/dock/taskmanager/taskmanagersettings.cpp index 9d1f05663..5ca0f4e8f 100644 --- a/panels/dock/taskmanager/taskmanagersettings.cpp +++ b/panels/dock/taskmanager/taskmanagersettings.cpp @@ -3,16 +3,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "globals.h" +#include "dockfoldermigrationutils.h" #include "taskmanagersettings.h" -#include #include +#include #include #include namespace dock { +namespace { +// Version 2 reruns the migration on upgraded systems where the stricter v1 +// check skipped layouts that still came from legacy desktop-only pins. +static constexpr int DEFAULT_DOCK_FOLDERS_MIGRATION_VERSION = 2; +} + static inline QString bool2EnableStr(bool enable) { return enable ? QStringLiteral("enabled") : QStringLiteral("disabled"); @@ -47,7 +54,7 @@ TaskManagerSettings::TaskManagerSettings(QObject *parent) m_windowSplit = m_taskManagerDconfig->value(TASKMANAGER_WINDOWSPLIT_KEY).toBool(); Q_EMIT windowSplitChanged(); } else if (TASKMANAGER_DOCKEDELEMENTS_KEY == key) { - m_dockedElements = m_taskManagerDconfig->value(TASKMANAGER_DOCKEDELEMENTS_KEY, {}).toStringList(); + m_dockedElements = resolveDockedElements(m_taskManagerDconfig->value(TASKMANAGER_DOCKEDELEMENTS_KEY, {}).toStringList()); Q_EMIT dockedElementsChanged(); } }); @@ -56,10 +63,11 @@ TaskManagerSettings::TaskManagerSettings(QObject *parent) m_showAttentionAnimation = m_taskManagerDconfig->value(TASKMANAGER_SHOW_ATTENTION_ANIMATION_KEY, true).toBool(); m_windowSplit = m_taskManagerDconfig->value(TASKMANAGER_WINDOWSPLIT_KEY).toBool(); m_cgroupsBasedGrouping = m_taskManagerDconfig->value(TASKMANAGER_CGROUPS_BASED_GROUPING_KEY, true).toBool(); - m_dockedElements = m_taskManagerDconfig->value(TASKMANAGER_DOCKEDELEMENTS_KEY, {}).toStringList(); + m_dockedElements = resolveDockedElements(m_taskManagerDconfig->value(TASKMANAGER_DOCKEDELEMENTS_KEY, {}).toStringList()); m_cgroupsBasedGroupingSkipAppIds = m_taskManagerDconfig->value(TASKMANAGER_CGROUPS_BASED_GROUPING_SKIP_APPIDS, {"deepin-terminal"}).toStringList(); m_cgroupsBasedGroupingSkipCategories = m_taskManagerDconfig->value(TASKMANAGER_CGROUPS_BASED_GROUPING_SKIP_CATEGORIES, {"TerminalEmulator"}).toStringList(); migrateFromDockedItems(); + migrateDefaultDockFolders(); } bool TaskManagerSettings::isAllowedForceQuit() @@ -146,15 +154,51 @@ void TaskManagerSettings::migrateFromDockedItems() legacyDockedItems.append(dockedItem); } + QStringList migratedDockedElements; for (auto dockedDesktopFile : std::as_const(legacyDockedItems)) { if (!dockedDesktopFile.isObject()) { continue; } auto dockedDesktopFileObj = dockedDesktopFile.toObject(); if (dockedDesktopFileObj.contains(QStringLiteral("id")) && dockedDesktopFileObj.contains(QStringLiteral("type"))) { - m_dockedElements.append(QStringLiteral("desktop/%1").arg(dockedDesktopFileObj[QStringLiteral("id")].toString())); + migratedDockedElements.append(QStringLiteral("desktop/%1").arg(dockedDesktopFileObj[QStringLiteral("id")].toString())); } } + + QStringList mergedDockedElements = m_dockedElements; + for (const QString &element : std::as_const(migratedDockedElements)) { + if (!mergedDockedElements.contains(element)) { + mergedDockedElements.append(element); + } + } + + mergedDockedElements = resolveDockedElements(mergedDockedElements); + if (mergedDockedElements != m_dockedElements) { + m_dockedElements = mergedDockedElements; + saveDockedElements(); + } +} + +void TaskManagerSettings::migrateDefaultDockFolders() +{ + const int migrationVersion = m_taskManagerDconfig->value(TASKMANAGER_DEFAULT_DOCK_FOLDERS_MIGRATION_VERSION_KEY, 0).toInt(); + if (migrationVersion >= DEFAULT_DOCK_FOLDERS_MIGRATION_VERSION) { + return; + } + + QStringList migratedDockedElements = m_dockedElements; + if (shouldMigrateDefaultDockFolders(migratedDockedElements)) { + migratedDockedElements = mergedWithDefaultDockFolders(migratedDockedElements); + } + + const bool dockedElementsChanged = migratedDockedElements != m_dockedElements; + if (dockedElementsChanged) { + m_dockedElements = migratedDockedElements; + saveDockedElements(); + } + + m_taskManagerDconfig->setValue(TASKMANAGER_DEFAULT_DOCK_FOLDERS_MIGRATION_VERSION_KEY, + DEFAULT_DOCK_FOLDERS_MIGRATION_VERSION); } void TaskManagerSettings::saveDockedElements() @@ -180,6 +224,10 @@ void TaskManagerSettings::toggleDockedElement(const QString &element) void TaskManagerSettings::appendDockedElement(const QString &element) { + if (m_dockedElements.contains(element)) { + return; + } + m_dockedElements.append(element); Q_EMIT dockedElementsChanged(); saveDockedElements(); diff --git a/panels/dock/taskmanager/taskmanagersettings.h b/panels/dock/taskmanager/taskmanagersettings.h index 31b360ece..d29261670 100644 --- a/panels/dock/taskmanager/taskmanagersettings.h +++ b/panels/dock/taskmanager/taskmanagersettings.h @@ -45,6 +45,7 @@ class TaskManagerSettings : public QObject private: explicit TaskManagerSettings(QObject *parent = nullptr); inline void migrateFromDockedItems(); + inline void migrateDefaultDockFolders(); inline void saveDockedElements(); Q_SIGNALS: diff --git a/panels/dock/taskmanager/textcalculator.cpp b/panels/dock/taskmanager/textcalculator.cpp index 0151311e9..badb59477 100644 --- a/panels/dock/taskmanager/textcalculator.cpp +++ b/panels/dock/taskmanager/textcalculator.cpp @@ -25,7 +25,11 @@ TextCalculator::TextCalculator(QObject *parent) , m_dataModel(nullptr) , m_remainingSpace(0) , m_enabled(false) + , m_calculationPending(false) { + m_calculationTimer.setSingleShot(true); + m_calculationTimer.setInterval(16); + connect(&m_calculationTimer, &QTimer::timeout, this, &TextCalculator::performScheduledCalculation); } TextCalculator::~TextCalculator() @@ -113,6 +117,8 @@ void TextCalculator::setEnabled(bool enabled) if (m_enabled) { scheduleCalculation(); } else { + m_calculationTimer.stop(); + m_calculationPending = false; m_optimalSingleTextWidth = 0.0; m_totalWidth = 0; emit optimalSingleTextWidthChanged(); @@ -165,6 +171,20 @@ void TextCalculator::scheduleCalculation() if (!m_enabled || !m_dataModel) { return; } + + m_calculationPending = true; + if (!m_calculationTimer.isActive()) { + m_calculationTimer.start(); + } +} + +void TextCalculator::performScheduledCalculation() +{ + if (!m_calculationPending || !m_enabled || !m_dataModel) { + return; + } + + m_calculationPending = false; calculateOptimalTextWidth(); } diff --git a/panels/dock/taskmanager/textcalculator.h b/panels/dock/taskmanager/textcalculator.h index 58a9e0323..c45133be9 100644 --- a/panels/dock/taskmanager/textcalculator.h +++ b/panels/dock/taskmanager/textcalculator.h @@ -6,6 +6,7 @@ #include #include +#include #include namespace dock @@ -160,6 +161,7 @@ class TextCalculator : public QObject, public QQmlParserStatus private slots: void onDataModelChanged(); void calculateOptimalTextWidth(); + void performScheduledCalculation(); private: void connectDataModelSignals(); @@ -181,6 +183,8 @@ private slots: QAbstractItemModel *m_dataModel; qreal m_remainingSpace; bool m_enabled; + bool m_calculationPending; + QTimer m_calculationTimer; QHash m_baselineWidthCache; // Cache for baseline widths of different character counts }; diff --git a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_CN.ts b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_CN.ts index ae47b81d7..f3fc6a209 100644 --- a/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_CN.ts +++ b/panels/dock/taskmanager/translations/org.deepin.ds.dock.taskmanager_zh_CN.ts @@ -5,6 +5,58 @@ Move to Trash 移动到回收站 + + Sort + 排序方式 + + + Name + 名称 + + + Modified Time + 修改时间 + + + Created Time + 创建时间 + + + Size + 大小 + + + Type + 类型 + + + Descending + 降序 + + + Ascending + 升序 + + + Sort by Name + 按名称排序 + + + Sort by Modified Time + 按修改时间排序 + + + Sort by Created Time + 按创建时间排序 + + + Sort by Size + 按大小排序 + + + Sort by Type + 按类型排序 + dock::AppItem @@ -55,6 +107,17 @@ Close this window 关闭当前窗口 + + Open in File Manager + 在文件管理器中打开 + + + + DockPinnedPopup + + No items + 无项目 + dock::TaskManager @@ -62,5 +125,53 @@ %1 files %1个文件 + + Internet + 网络 + + + Chat + 社交 + + + Music + 音乐 + + + Video + 视频 + + + Graphics + 图形图像 + + + Game + 游戏 + + + Office + 办公 + + + Reading + 阅读 + + + Development + 编程开发 + + + System + 系统管理 + + + Others + 其他 + + + App Group + 应用组 + - \ No newline at end of file + diff --git a/panels/dock/taskmanager/treelandwindowmonitor.cpp b/panels/dock/taskmanager/treelandwindowmonitor.cpp index 0e0a1e8d9..c0256f561 100644 --- a/panels/dock/taskmanager/treelandwindowmonitor.cpp +++ b/panels/dock/taskmanager/treelandwindowmonitor.cpp @@ -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 @@ -9,6 +9,7 @@ #include "taskmanager.h" #include "treelandwindow.h" +#include #include #include #include @@ -18,6 +19,11 @@ #include namespace dock { +namespace { +constexpr int kPreviewMotionBaseDuration = 112; +constexpr int kPreviewMotionMaxDuration = 208; +} + ForeignToplevelManager::ForeignToplevelManager(TreeLandWindowMonitor* monitor) : QWaylandClientExtensionTemplate(1) , m_monitor(monitor) @@ -35,14 +41,34 @@ TreeLandDockPreviewContext::TreeLandDockPreviewContext(struct ::treeland_dock_pr , m_isPreviewEntered(false) , m_isDockMouseAreaEnter(false) , m_hideTimer(new QTimer(this)) + , m_positionAnimation(new QVariantAnimation(this)) + , m_currentPreviewXoffset(0) + , m_currentPreviewYoffset(0) + , m_currentDirection(0) + , m_positionInitialized(false) { init(context); m_hideTimer->setSingleShot(true); m_hideTimer->setInterval(800); + m_positionAnimation->setDuration(kPreviewMotionBaseDuration); + m_positionAnimation->setEasingCurve(QEasingCurve::OutCubic); + connect(m_positionAnimation, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) { + if (m_currentWindowsId.isEmpty()) { + return; + } + + const QPoint point = value.toPoint(); + m_currentPreviewXoffset = point.x(); + m_currentPreviewYoffset = point.y(); + show(m_currentWindowsId, point.x(), point.y(), m_currentDirection); + }); connect(m_hideTimer, &QTimer::timeout, this, [this](){ if (!m_isDockMouseAreaEnter && !m_isPreviewEntered) { + m_positionAnimation->stop(); + m_positionInitialized = false; + m_currentWindowsId.clear(); emit closed(); close(); } @@ -57,7 +83,33 @@ TreeLandDockPreviewContext::~TreeLandDockPreviewContext() void TreeLandDockPreviewContext::showWindowsPreview(QByteArray windowsId, int32_t previewXoffset, int32_t previewYoffset, uint32_t direction) { m_isDockMouseAreaEnter = true; - show(windowsId, previewXoffset, previewYoffset, direction); + m_hideTimer->stop(); + m_currentWindowsId = windowsId; + m_currentDirection = direction; + + const QPoint targetPosition(previewXoffset, previewYoffset); + if (!m_positionInitialized) { + m_positionAnimation->stop(); + m_currentPreviewXoffset = previewXoffset; + m_currentPreviewYoffset = previewYoffset; + m_positionInitialized = true; + show(m_currentWindowsId, previewXoffset, previewYoffset, direction); + return; + } + + const QPoint currentPosition(m_currentPreviewXoffset, m_currentPreviewYoffset); + if (currentPosition == targetPosition) { + show(m_currentWindowsId, previewXoffset, previewYoffset, direction); + return; + } + + const int distance = (currentPosition - targetPosition).manhattanLength(); + m_positionAnimation->setDuration(std::min(kPreviewMotionMaxDuration, + kPreviewMotionBaseDuration + distance / 4)); + m_positionAnimation->stop(); + m_positionAnimation->setStartValue(currentPosition); + m_positionAnimation->setEndValue(targetPosition); + m_positionAnimation->start(); } void TreeLandDockPreviewContext::hideWindowsPreview() @@ -239,4 +291,3 @@ void TreeLandWindowMonitor::handleForeignToplevelHandleRemoved() } } } - diff --git a/panels/dock/taskmanager/treelandwindowmonitor.h b/panels/dock/taskmanager/treelandwindowmonitor.h index 3e9356f63..fb1f20a5d 100644 --- a/panels/dock/taskmanager/treelandwindowmonitor.h +++ b/panels/dock/taskmanager/treelandwindowmonitor.h @@ -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 @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -56,6 +57,12 @@ class TreeLandDockPreviewContext : public QWaylandClientExtensionTemplate #include #include #include @@ -59,6 +60,48 @@ Q_LOGGING_CATEGORY(x11WindowPreview, "org.deepin.dde.shell.dock.taskmanager.x11W DGUI_USE_NAMESPACE namespace dock { +namespace { +constexpr auto kAppearanceService = "org.deepin.dde.Appearance1"; +constexpr auto kAppearancePath = "/org/deepin/dde/Appearance1"; +constexpr auto kAppearanceInterface = "org.deepin.dde.Appearance1"; +constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"; +constexpr int kPreviewMotionBaseDuration = 88; +constexpr int kPreviewMotionMaxDuration = 156; + +DGuiApplicationHelper::ColorType previewThemeType() +{ + const auto fallbackTheme = DGuiApplicationHelper::instance()->themeType(); + QDBusInterface appearanceProperties(QString::fromLatin1(kAppearanceService), + QString::fromLatin1(kAppearancePath), + QString::fromLatin1(kPropertiesInterface), + QDBusConnection::sessionBus()); + if (!appearanceProperties.isValid()) { + return fallbackTheme; + } + + const QDBusReply reply = appearanceProperties.call(QStringLiteral("Get"), + QString::fromLatin1(kAppearanceInterface), + QStringLiteral("GlobalTheme")); + if (!reply.isValid()) { + return fallbackTheme; + } + + const QString themeName = reply.value().variant().toString(); + if (themeName.endsWith(QStringLiteral(".dark"), Qt::CaseInsensitive)) { + return DGuiApplicationHelper::DarkType; + } + if (themeName.endsWith(QStringLiteral(".light"), Qt::CaseInsensitive)) { + return DGuiApplicationHelper::LightType; + } + + return fallbackTheme; +} + +QColor previewTextColor(const DGuiApplicationHelper::ColorType themeType) +{ + return themeType == DGuiApplicationHelper::DarkType ? QColor(Qt::white) : QColor(Qt::black); +} +} static QHash s_windowPreviewCache; @@ -224,7 +267,7 @@ class AppItemWindowDeletegate : public QAbstractItemDelegate void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { - auto themeType = DGuiApplicationHelper::instance()->themeType(); + auto themeType = previewThemeType(); QRect hoverRect = option.rect; @@ -346,7 +389,7 @@ class AppItemWindowDeletegate : public QAbstractItemDelegate // 给一点时间让窗口关闭事件传播 QTimer::singleShot(100, this, [this]() { if (m_listView->model()->rowCount() == 0) { - m_parent->hide(); + m_parent->dismissPreview(); } // 大小更新现在由模型变化信号自动处理 }); @@ -378,11 +421,20 @@ X11WindowPreviewContainer::X11WindowPreviewContainer(X11WindowMonitor *monitor, , m_monitor(monitor) , m_sourceModel(nullptr) , m_titleWidget(new QWidget()) + , m_previewIcon(nullptr) + , m_previewTitle(nullptr) + , m_closeAllButton(nullptr) + , m_hideTimer(nullptr) + , m_positionAnimation(new QPropertyAnimation(this, "pos")) , m_direction(0) + , m_positionInitialized(false) + , m_previewOpacity(0.4) { m_hideTimer = new QTimer(this); m_hideTimer->setSingleShot(true); m_hideTimer->setInterval(500); + m_positionAnimation->setDuration(kPreviewMotionBaseDuration); + m_positionAnimation->setEasingCurve(QEasingCurve::OutQuad); setWindowFlags(Qt::ToolTip | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus | Qt::FramelessWindowHint); setMouseTracking(true); @@ -410,7 +462,7 @@ X11WindowPreviewContainer::X11WindowPreviewContainer(X11WindowMonitor *monitor, X11Utils::instance()->closeWindow(windowId); } - hide(); + dismissPreview(); }); connect(m_view, &QListView::entered, this, [this](const QModelIndex &enter) { @@ -429,6 +481,12 @@ X11WindowPreviewContainer::X11WindowPreviewContainer(X11WindowMonitor *monitor, }); } +void X11WindowPreviewContainer::setPreviewOpacity(double opacity) +{ + m_previewOpacity = opacity; + applyTheme(); +} + void X11WindowPreviewContainer::showPreviewWithModel(QAbstractItemModel *sourceModel, const QPointer &window, int32_t previewXoffset, @@ -469,7 +527,7 @@ void X11WindowPreviewContainer::showPreviewWithModel(QAbstractItemModel *sourceM updateSize(m_sourceModel->rowCount()); } else { // 如果没有窗口了,隐藏预览容器 - hide(); + dismissPreview(); } } }); @@ -498,14 +556,20 @@ void X11WindowPreviewContainer::showPreviewWithModel(QAbstractItemModel *sourceM } updatePreviewIconFromString(iconData.toString()); updatePreviewTitle(firstIndex.data(TaskManager::WinTitleRole).toString()); - updateSize(sourceModel->rowCount()); + updateOrientation(); } else { updateSize(0); } if (isHidden()) { - show(); + m_positionAnimation->stop(); + setGeometry(previewGeometry()); + m_positionInitialized = true; + DBlurEffectWidget::show(); + return; } + + updatePosition(); } void X11WindowPreviewContainer::updateOrientation() @@ -525,7 +589,7 @@ void X11WindowPreviewContainer::callHide() if (m_isPreviewEntered) return; if (m_isDockPreviewCount > 0) return; - hide(); + dismissPreview(); s_windowPreviewCache.clear(); } @@ -552,17 +616,21 @@ void X11WindowPreviewContainer::leaveEvent(QEvent* event) void X11WindowPreviewContainer::showEvent(QShowEvent *event) { - updateOrientation(); m_closeAllButton->setVisible(false); return DBlurEffectWidget::showEvent(event); } void X11WindowPreviewContainer::hideEvent(QHideEvent*) { + m_positionAnimation->stop(); + m_positionInitialized = false; // 只通知监视器清空预览状态,让 TaskManager 统一管理模型清理 // 不要在这里断开模型连接,因为 clearPreviewState 信号会触发 TaskManager 的 clearFilter // QPointer 会自动处理对象销毁的情况 if (m_monitor) { + if (WM_HELPER->hasComposite()) { + m_monitor->cancelPreviewWindow(); + } m_monitor->clearPreviewState(); } } @@ -573,8 +641,12 @@ void X11WindowPreviewContainer::resizeEvent(QResizeEvent *event) updatePosition(); } -void X11WindowPreviewContainer::updatePosition() +QRect X11WindowPreviewContainer::previewGeometry() const { + if (!m_baseWindow || !m_baseWindow->screen()) { + return geometry(); + } + auto screenRect = m_baseWindow->screen()->geometry(); auto dockWindowPosition = m_baseWindow->position(); int xPosition = dockWindowPosition.x() + m_previewXoffset; @@ -610,7 +682,42 @@ void X11WindowPreviewContainer::updatePosition() yPosition = std::max(yPosition, screenRect.y() + 10); yPosition = std::min(yPosition, screenRect.y() + screenRect.height() - height() - 10); - move(xPosition, yPosition); + return QRect(xPosition, yPosition, width(), height()); +} + +void X11WindowPreviewContainer::dismissPreview() +{ + if (isHidden()) { + return; + } + + m_hideTimer->stop(); + m_positionAnimation->stop(); + DBlurEffectWidget::hide(); +} + +void X11WindowPreviewContainer::updatePosition() +{ + const QPoint targetPosition = previewGeometry().topLeft(); + + if (!isVisible() || !m_positionInitialized) { + m_positionAnimation->stop(); + move(targetPosition); + m_positionInitialized = true; + return; + } + + if (pos() == targetPosition) { + return; + } + + const int distance = (pos() - targetPosition).manhattanLength(); + m_positionAnimation->setDuration(std::min(kPreviewMotionMaxDuration, + kPreviewMotionBaseDuration + distance / 5)); + m_positionAnimation->stop(); + m_positionAnimation->setStartValue(pos()); + m_positionAnimation->setEndValue(targetPosition); + m_positionAnimation->start(); } void X11WindowPreviewContainer::updatePreviewTitle(const QString& title) @@ -619,6 +726,34 @@ void X11WindowPreviewContainer::updatePreviewTitle(const QString& title) m_previewTitle->setText(m_previewTitleStr); } +void X11WindowPreviewContainer::applyTheme() +{ + const auto themeType = previewThemeType(); + const QColor textColor = previewTextColor(themeType); + QPalette titlePalette = m_previewTitle->palette(); + titlePalette.setColor(QPalette::WindowText, textColor); + titlePalette.setColor(QPalette::Text, textColor); + titlePalette.setColor(QPalette::ButtonText, textColor); + m_previewTitle->setPalette(titlePalette); + + QPalette viewPalette = m_view->palette(); + viewPalette.setColor(QPalette::Base, Qt::transparent); + m_view->setPalette(viewPalette); + + QPalette blurPalette = DGuiApplicationHelper::instance()->applicationPalette(themeType); + QColor blurColor = blurPalette.color(QPalette::Window); + if (!blurColor.isValid()) { + blurColor = themeType == DGuiApplicationHelper::DarkType + ? QColor(32, 32, 32) + : QColor(248, 248, 248); + } + setMaskColor(blurColor); + setMaskAlpha(qBound(0, qRound(m_previewOpacity * 255.0), 255)); + + update(); + m_view->viewport()->update(); +} + void X11WindowPreviewContainer::initUI() { m_view = new PreviewsListView(this); @@ -646,17 +781,10 @@ void X11WindowPreviewContainer::initUI() m_previewIcon->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_previewTitle->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_previewTitle->setElideMode(Qt::ElideRight); - - auto updateWindowTitleColorType = [this](){ - QPalette pa = palette(); - auto type = DGuiApplicationHelper::instance()->themeType(); - pa.setColor(QPalette::WindowText, type == DGuiApplicationHelper::ColorType::LightType ? Qt::black : Qt::white); - m_previewTitle->setPalette(pa); - }; - - updateWindowTitleColorType(); - - connect(DGuiApplicationHelper::instance(), & DGuiApplicationHelper::themeTypeChanged, this , updateWindowTitleColorType); + connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::themeTypeChanged, + this, &X11WindowPreviewContainer::applyTheme); + connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::applicationPaletteChanged, + this, &X11WindowPreviewContainer::applyTheme); titleLayout->addWidget(m_previewIcon); titleLayout->setSpacing(0); @@ -702,13 +830,15 @@ void X11WindowPreviewContainer::initUI() handler.setShadowRadius(12 * qApp->devicePixelRatio()); handler.setShadowColor(QColor(0, 0, 0, 0.6 * 255)); handler.setShadowOffset(QPoint(0, 4 * qApp->devicePixelRatio())); + + applyTheme(); } void X11WindowPreviewContainer::updateSize(int windowCount) { if (windowCount != -1) { if (windowCount == 0) { - DBlurEffectWidget::hide(); + dismissPreview(); return; } } @@ -750,14 +880,19 @@ void X11WindowPreviewContainer::updateSize(int windowCount) return resWidth; }; - setFixedSize(calFixWidth(), calFixHeight()); - + QSize targetSize(calFixWidth(), calFixHeight()); if (m_view->width() + this->contentsMargins().left() * 2 <= PREVIEW_MINI_WIDTH) { - setMaximumWidth(PREVIEW_MINI_WIDTH); + targetSize.setWidth(std::max(targetSize.width(), PREVIEW_MINI_WIDTH)); } + setMinimumSize(1, 1); + setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); + resize(targetSize); + m_titleWidget->setFixedWidth(m_view->width()); - QTimer::singleShot(0, this, &X11WindowPreviewContainer::adjustSize); + if (layout()) { + layout()->activate(); + } } bool X11WindowPreviewContainer::eventFilter(QObject *watched, QEvent *event) @@ -788,7 +923,7 @@ bool X11WindowPreviewContainer::eventFilter(QObject *watched, QEvent *event) if (index.isValid()) { X11Utils::instance()->setActiveWindow(index.data(TaskManager::WinIdRole).toUInt()); } - DBlurEffectWidget::hide(); + dismissPreview(); break; } default: {} diff --git a/panels/dock/taskmanager/x11preview.h b/panels/dock/taskmanager/x11preview.h index 334078ff5..6f7790221 100644 --- a/panels/dock/taskmanager/x11preview.h +++ b/panels/dock/taskmanager/x11preview.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,8 @@ #include #include #include +#include +#include #include #include #include @@ -67,9 +69,14 @@ class X11WindowPreviewContainer: public DBlurEffectWidget explicit X11WindowPreviewContainer(X11WindowMonitor* monitor, QWidget *parent = nullptr); void - showPreviewWithModel(QAbstractItemModel *sourceModel, const QPointer &window, int32_t previewXoffset, int32_t previewYoffset, uint32_t direction); + showPreviewWithModel(QAbstractItemModel *sourceModel, + const QPointer &window, + int32_t previewXoffset, + int32_t previewYoffset, + uint32_t direction); void hidePreView(); + void setPreviewOpacity(double opacity); protected: bool eventFilter(QObject *watched, QEvent *event) override; @@ -80,12 +87,15 @@ class X11WindowPreviewContainer: public DBlurEffectWidget void resizeEvent(QResizeEvent *event) override; private: + void applyTheme(); inline void updatePreviewTitle(const QString& title); inline void initUI(); inline void updateSize(int windowCount = -1); void updatePreviewIconFromString(const QString &stringData); + QRect previewGeometry() const; public Q_SLOTS: + void dismissPreview(); void updatePosition(); private Q_SLOTS: @@ -106,10 +116,13 @@ private Q_SLOTS: DIconButton* m_closeAllButton; QTimer* m_hideTimer; + QPropertyAnimation* m_positionAnimation; int32_t m_previewXoffset; int32_t m_previewYoffset; uint32_t m_direction; + bool m_positionInitialized; + double m_previewOpacity; QPointer m_baseWindow; diff --git a/panels/dock/taskmanager/x11windowmonitor.cpp b/panels/dock/taskmanager/x11windowmonitor.cpp index 7eca4f72a..933e0210f 100644 --- a/panels/dock/taskmanager/x11windowmonitor.cpp +++ b/panels/dock/taskmanager/x11windowmonitor.cpp @@ -15,6 +15,8 @@ #include +#include +#include #include #include #include @@ -102,12 +104,12 @@ QPointer X11WindowMonitor::getWindowByWindowId(ulong windowId) void X11WindowMonitor::presentWindows(QList windows) { - DDBusSender().interface("com.deepin.wm") - .path("/com/deepin/wm") - .service("com.deepin.wm") - .method("PresentWindows") - .arg(windows) - .call().waitForFinished(); + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("com.deepin.wm"), + QStringLiteral("/com/deepin/wm"), + QStringLiteral("com.deepin.wm"), + QStringLiteral("PresentWindows")); + message << QVariant::fromValue(windows); + QDBusConnection::sessionBus().asyncCall(message); } void X11WindowMonitor::hideItemPreview() @@ -119,28 +121,28 @@ void X11WindowMonitor::hideItemPreview() void X11WindowMonitor::previewWindow(uint32_t winId) { - DDBusSender().interface("com.deepin.wm") - .path("/com/deepin/wm") - .service("com.deepin.wm") - .method("PreviewWindow") - .arg(winId) - .call().waitForFinished(); + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("com.deepin.wm"), + QStringLiteral("/com/deepin/wm"), + QStringLiteral("com.deepin.wm"), + QStringLiteral("PreviewWindow")); + message << QVariant::fromValue(winId); + QDBusConnection::sessionBus().asyncCall(message); } void X11WindowMonitor::cancelPreviewWindow() { - DDBusSender().interface("com.deepin.wm") - .path("/com/deepin/wm") - .service("com.deepin.wm") - .method("CancelPreviewWindow") - .call().waitForFinished(); + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("com.deepin.wm"), + QStringLiteral("/com/deepin/wm"), + QStringLiteral("com.deepin.wm"), + QStringLiteral("CancelPreviewWindow")); + QDBusConnection::sessionBus().asyncCall(message); } void X11WindowMonitor::setPreviewOpacity(double opacity) { m_opacity = opacity; if (m_windowPreview) { - m_windowPreview->setMaskAlpha(static_cast(m_opacity * 255)); + m_windowPreview->setPreviewOpacity(m_opacity); } } @@ -152,12 +154,15 @@ void X11WindowMonitor::requestPreview(QAbstractItemModel *sourceModel, { if (!m_windowPreview) { m_windowPreview.reset(new X11WindowPreviewContainer(this)); - m_windowPreview->setMaskAlpha(static_cast(m_opacity * 255)); + m_windowPreview->setPreviewOpacity(m_opacity); m_windowPreview->windowHandle()->setTransientParent(relativePositionItem); } - m_windowPreview->showPreviewWithModel(sourceModel, relativePositionItem, previewXoffset, previewYoffset, direction); - m_windowPreview->updatePosition(); + m_windowPreview->showPreviewWithModel(sourceModel, + relativePositionItem, + previewXoffset, + previewYoffset, + direction); } void X11WindowMonitor::requestUpdateWindowIconGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) const diff --git a/panels/dock/taskmanager/x11windowmonitor.h b/panels/dock/taskmanager/x11windowmonitor.h index 6fec48c67..14cb6aa3d 100644 --- a/panels/dock/taskmanager/x11windowmonitor.h +++ b/panels/dock/taskmanager/x11windowmonitor.h @@ -45,7 +45,11 @@ class X11WindowMonitor : public AbstractWindowMonitor void clearPreviewState(); void - requestPreview(QAbstractItemModel *sourceModel, QWindow *relativePositionItem, int32_t previewXoffset, int32_t previewYoffset, uint32_t direction) override; + requestPreview(QAbstractItemModel *sourceModel, + QWindow *relativePositionItem, + int32_t previewXoffset, + int32_t previewYoffset, + uint32_t direction) override; void requestUpdateWindowIconGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate = nullptr) const override; Q_SIGNALS: diff --git a/panels/dock/translations/org.deepin.ds.dock_zh_CN.ts b/panels/dock/translations/org.deepin.ds.dock_zh_CN.ts index a15c7e9ff..83738c133 100644 --- a/panels/dock/translations/org.deepin.ds.dock_zh_CN.ts +++ b/panels/dock/translations/org.deepin.ds.dock_zh_CN.ts @@ -14,8 +14,8 @@ 高效模式 - Classic Mode - 经典模式 + Left-aligned Mode + 左对齐模式 Centered Mode @@ -70,4 +70,4 @@ 禁用自由调节 - \ No newline at end of file + diff --git a/panels/dock/tray/ShellSurfaceItemProxy.qml b/panels/dock/tray/ShellSurfaceItemProxy.qml index 57dc8cb8b..23d19841b 100644 --- a/panels/dock/tray/ShellSurfaceItemProxy.qml +++ b/panels/dock/tray/ShellSurfaceItemProxy.qml @@ -87,9 +87,13 @@ Item { function fixPosition() { // See QTBUG: https://bugreports.qt.io/browse/QTBUG-135833 - // TODO: should get the devicePixelRatio from the Window - x = mapFromScene(Math.ceil(mapToScene(0, 0).x * Panel.devicePixelRatio) / Panel.devicePixelRatio, 0).x - y = mapFromScene(0, Math.ceil(mapToScene(0, 0).y * Panel.devicePixelRatio) / Panel.devicePixelRatio).y + // Snap to the nearest physical pixel to avoid one-pixel seams between + // QML-painted items and shell-surface items in the dock. + const scenePoint = mapToScene(0, 0) + const snappedX = Math.round(scenePoint.x * Panel.devicePixelRatio) / Panel.devicePixelRatio + const snappedY = Math.round(scenePoint.y * Panel.devicePixelRatio) / Panel.devicePixelRatio + x = mapFromScene(snappedX, scenePoint.y).x + y = mapFromScene(scenePoint.x, snappedY).y } Timer { diff --git a/panels/dock/tray/package/ActionLegacyTrayPluginDelegate.qml b/panels/dock/tray/package/ActionLegacyTrayPluginDelegate.qml index 2a7fe3919..80dc18f11 100644 --- a/panels/dock/tray/package/ActionLegacyTrayPluginDelegate.qml +++ b/panels/dock/tray/package/ActionLegacyTrayPluginDelegate.qml @@ -32,6 +32,14 @@ AppletItemButton { visible: !Drag.active && itemVisible hoverEnabled: inputEventsEnabled + function reportSpotlight(point) { + root.updateSpotlight(point || Qt.point(width / 2, height / 2)) + } + + function scheduleSpotlightClear() { + spotlightClearTimer.restart() + } + function updatePluginMargins() { pluginItem.plugin.margins = itemPadding @@ -43,39 +51,58 @@ AppletItemButton { implicitHeight: plugin ? plugin.height : 0 implicitWidth: plugin ? plugin.width : 0 - property var itemGlobalPoint: { - var a = pluginItem - var x = 0, y = 0 - while(a.parent) { - x += a.x - y += a.y - a = a.parent + function localItemPoint() { + let current = pluginItem + let x = 0 + let y = 0 + while (current.parent) { + x += current.x + y += current.y + current = current.parent } return Qt.point(x, y) } - - property var itemGlobalPos: { - var a = pluginItem - var x = 0, y = 0 - - if (a.Window.window && surfaceItem.visible) { - while (a.parent) { - x += a.x - y += a.y - a = a.parent - } - x += pluginItem.Window.window.x - y += pluginItem.Window.window.y + property var itemScenePoint: { + Panel.frontendWindowRect + pluginItem.localItemPoint() + return pluginItem.mapToItem(null, 0, 0) + } + + property var itemGlobalPos: { + if (!pluginItem.Window.window || !surfaceItem.visible) { + return Qt.point(0, 0) } - return Qt.point(x, y) + Panel.frontendWindowRect + pluginItem.localItemPoint() + return pluginItem.mapToGlobal(0, 0) } HoverHandler { id: hoverHandler parent: surfaceItem.shellSurfaceItem + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + + onPointChanged: { + if (hovered) { + spotlightClearTimer.stop() + root.reportSpotlight(hoverHandler.point.position) + } + } + + onHoveredChanged: { + if (hovered) { + spotlightClearTimer.stop() + root.reportSpotlight() + return + } + + if (!root.hovered) { + root.scheduleSpotlightClear() + } + } } TapHandler { id: tapHandler @@ -86,14 +113,21 @@ AppletItemButton { id: surfaceItem anchors.fill: parent shellSurface: pluginItem.plugin + + onWidthChanged: updatePluginItemGeometryTimer.start() + onHeightChanged: updatePluginItemGeometryTimer.start() } Component.onCompleted: { if (!pluginItem.plugin || !itemVisible) return updatePluginMargins() - pluginItem.plugin.updatePluginGeometry(Qt.rect(pluginItem.itemGlobalPoint.x, pluginItem.itemGlobalPoint.y, 0, 0)) - pluginItem.plugin.setGlobalPos(pluginItem.itemGlobalPos) + pluginItem.plugin.updatePluginGeometry(Qt.rect(Math.round(pluginItem.itemScenePoint.x), + Math.round(pluginItem.itemScenePoint.y), + Math.round(surfaceItem.width), + Math.round(surfaceItem.height))) + pluginItem.plugin.setGlobalPos(Qt.point(Math.round(pluginItem.itemGlobalPos.x), + Math.round(pluginItem.itemGlobalPos.y))) } Timer { @@ -105,8 +139,11 @@ AppletItemButton { if (!pluginItem.plugin || !itemVisible) return updatePluginMargins() - if (pluginItem.itemGlobalPoint.x >= 0 && pluginItem.itemGlobalPoint.y >= 0) { - pluginItem.plugin.updatePluginGeometry(Qt.rect(pluginItem.itemGlobalPoint.x, pluginItem.itemGlobalPoint.y, 0, 0)) + if (pluginItem.itemScenePoint.x >= 0 && pluginItem.itemScenePoint.y >= 0) { + pluginItem.plugin.updatePluginGeometry(Qt.rect(Math.round(pluginItem.itemScenePoint.x), + Math.round(pluginItem.itemScenePoint.y), + Math.round(surfaceItem.width), + Math.round(surfaceItem.height))) } } } @@ -119,11 +156,12 @@ AppletItemButton { onTriggered: { if (!pluginItem.plugin || !itemVisible) return - pluginItem.plugin.setGlobalPos(pluginItem.itemGlobalPos) + pluginItem.plugin.setGlobalPos(Qt.point(Math.round(pluginItem.itemGlobalPos.x), + Math.round(pluginItem.itemGlobalPos.y))) } } - onItemGlobalPointChanged: { + onItemScenePointChanged: { updatePluginItemGeometryTimer.start() } @@ -136,11 +174,12 @@ AppletItemButton { if (!pluginItem.plugin || !itemVisible) return updatePluginMargins() - pluginItem.plugin.setGlobalPos(pluginItem.itemGlobalPos) + pluginItem.plugin.setGlobalPos(Qt.point(Math.round(pluginItem.itemGlobalPos.x), + Math.round(pluginItem.itemGlobalPos.y))) } } - D.ColorSelector.hovered: root.inputEventsEnabled && (pluginItem.plugin && pluginItem.plugin.isItemActive || hoverHandler.hovered) + D.ColorSelector.hovered: root.inputEventsEnabled && (pluginItem.plugin && pluginItem.plugin.isItemActive || hoverHandler.hovered || root.hovered) D.ColorSelector.pressed: tapHandler.pressed property Component overlayWindow: QuickDragWindow { @@ -196,6 +235,29 @@ AppletItemButton { } } + onHoveredChanged: { + if (hovered) { + spotlightClearTimer.stop() + reportSpotlight(Qt.point(width / 2, height / 2)) + return + } + + if (!hoverHandler.hovered) { + scheduleSpotlightClear() + } + } + + Timer { + id: spotlightClearTimer + interval: 70 + repeat: false + onTriggered: { + if (!root.hovered && !hoverHandler.hovered && !surfaceItem.hovered) { + root.clearSpotlight() + } + } + } + DragHandler { id: dragHandler enabled: dragable diff --git a/panels/dock/tray/package/ActionShowStashDelegate.qml b/panels/dock/tray/package/ActionShowStashDelegate.qml index d70a90c0f..4aa3674cd 100644 --- a/panels/dock/tray/package/ActionShowStashDelegate.qml +++ b/panels/dock/tray/package/ActionShowStashDelegate.qml @@ -27,16 +27,23 @@ AppletItemButton { D.ColorSelector.hovered: (isDropHover && DDT.TraySortOrderModel.actionsAlwaysVisible) || hovered || stashedPopup.popupVisible - property var itemGlobalPoint: { - var a = root - var x = 0, y = 0 - while(a.parent) { - x += a.x - y += a.y - a = a.parent + function localItemPoint() { + let current = root + let x = 0 + let y = 0 + while (current.parent) { + x += current.x + y += current.y + current = current.parent } - return Qt.point(x + width / 2, y + height / 2) + return Qt.point(x, y) + } + + property var itemGlobalPoint: { + Panel.frontendWindowRect + root.localItemPoint() + return root.mapToItem(null, root.width / 2, root.height / 2) } Connections { target: stashedPopup @@ -111,14 +118,16 @@ AppletItemButton { toolTipX: DockPanelPositioner.x toolTipY: DockPanelPositioner.y } + + function showToolTipNow() { + 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() + } Timer { id: toolTipShowTimer interval: 200 - 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() - } + onTriggered: root.showToolTipNow() } onHoveredChanged: { @@ -127,7 +136,11 @@ AppletItemButton { } if (hovered) { - toolTipShowTimer.start() + if (toolTip.toolTipWindow && toolTip.toolTipWindow.visible) { + root.showToolTipNow() + } else { + toolTipShowTimer.start() + } } else { if (toolTipShowTimer.running) { toolTipShowTimer.stop() diff --git a/panels/dock/tray/package/ActionToggleCollapseDelegate.qml b/panels/dock/tray/package/ActionToggleCollapseDelegate.qml index 7e3a1a783..f632eaa65 100644 --- a/panels/dock/tray/package/ActionToggleCollapseDelegate.qml +++ b/panels/dock/tray/package/ActionToggleCollapseDelegate.qml @@ -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 @@ -55,20 +55,26 @@ AppletItemButton { toolTipX: DockPanelPositioner.x toolTipY: DockPanelPositioner.y } + + function showToolTipNow() { + 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() + } Timer { id: toolTipShowTimer interval: 200 - 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() - } + onTriggered: root.showToolTipNow() } onHoveredChanged: { if (hovered) { - toolTipShowTimer.start() + if (toolTip.toolTipWindow && toolTip.toolTipWindow.visible) { + root.showToolTipNow() + } else { + toolTipShowTimer.start() + } } else { if (toolTipShowTimer.running) { toolTipShowTimer.stop() diff --git a/panels/dock/tray/package/TrayContainer.qml b/panels/dock/tray/package/TrayContainer.qml index 7ca315ca1..961c18173 100644 --- a/panels/dock/tray/package/TrayContainer.qml +++ b/panels/dock/tray/package/TrayContainer.qml @@ -77,6 +77,7 @@ Item { property string color: "red" property bool collapsed: false + property bool targetCollapsed: collapsed property bool isHorizontal: true readonly property int itemVisualSize: DDT.TrayItemPositionManager.itemVisualSize.width @@ -85,6 +86,74 @@ Item { property int trayHeight: 50 property size containerSize: DDT.TrayItemPositionManager.visualSize + readonly property int targetVisualItemCount: { + if (!model) { + return 0 + } + + let count = 0 + for (let row = 0; row < model.rowCount(); ++row) { + const index = model.index(row, 0) + if (!index.valid) { + continue + } + + const visibility = model.data(index, DDT.TraySortOrderModel.VisibilityRole) + const dockVisible = model.data(index, DDT.TraySortOrderModel.DockVisibleRole) + const sectionType = model.data(index, DDT.TraySortOrderModel.SectionTypeRole) + if (!visibility || !dockVisible || sectionType === "stashed") { + continue + } + + if (sectionType === "collapsable" && targetCollapsed) { + continue + } + + count++ + } + + return count + } + readonly property size targetContainerSize: { + if (targetVisualItemCount <= 0) { + return isHorizontal ? Qt.size(1, trayHeight) : Qt.size(trayHeight, 1) + } + + let extent = 0 + let visibleCount = 0 + for (let row = 0; row < model.rowCount(); ++row) { + const index = model.index(row, 0) + if (!index.valid) { + continue + } + + const visibility = model.data(index, DDT.TraySortOrderModel.VisibilityRole) + const dockVisible = model.data(index, DDT.TraySortOrderModel.DockVisibleRole) + const sectionType = model.data(index, DDT.TraySortOrderModel.SectionTypeRole) + if (!visibility || !dockVisible || sectionType === "stashed") { + continue + } + + if (sectionType === "collapsable" && targetCollapsed) { + continue + } + + const item = trayRepeater.itemAt(row) + const itemExtent = item ? (isHorizontal ? item.width : item.height) : itemVisualSize + if (visibleCount > 0) { + extent += itemSpacing + } + + extent += itemExtent + visibleCount++ + } + + if (visibleCount <= 0) { + return isHorizontal ? Qt.size(1, trayHeight) : Qt.size(trayHeight, 1) + } + + return isHorizontal ? Qt.size(extent, trayHeight) : Qt.size(trayHeight, extent) + } property bool isDragging: DDT.TraySortOrderModel.actionsAlwaysVisible property bool animationEnable: true // visiualIndex default value is -1 @@ -252,6 +321,7 @@ Item { // Tray items Repeater { + id: trayRepeater anchors.fill: parent model: root.model delegate: trayItemDelegateChooser diff --git a/panels/dock/tray/package/TrayItemDelegateChooser.qml b/panels/dock/tray/package/TrayItemDelegateChooser.qml index ce0212295..c77aeb73d 100644 --- a/panels/dock/tray/package/TrayItemDelegateChooser.qml +++ b/panels/dock/tray/package/TrayItemDelegateChooser.qml @@ -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 @@ -64,6 +64,7 @@ LQM.DelegateChooser { roleValue: "action-toggle-collapse" TrayItemPositioner { contentItem: ActionToggleCollapseDelegate { + collapsed: root.collapsed isHorizontal: root.isHorizontal inputEventsEnabled: !disableInputEvents } diff --git a/panels/dock/tray/package/tray.qml b/panels/dock/tray/package/tray.qml index e6c9a0303..6b3f2a186 100644 --- a/panels/dock/tray/package/tray.qml +++ b/panels/dock/tray/package/tray.qml @@ -16,14 +16,20 @@ import org.deepin.ds.dock.tray 1.0 as DDT AppletItem { id: tray - readonly property int nextAppletSpacing: 6 + readonly property bool adaptiveFashionMode: Panel.rootObject + && Panel.rootObject.adaptiveFashionMode + readonly property int nextAppletSpacing: adaptiveFashionMode ? 0 : 6 + readonly property int targetImplicitWidth: useColumnLayout ? Panel.rootObject.dockSize : trayContainter.targetContainerSize.width + nextAppletSpacing + readonly property int targetImplicitHeight: useColumnLayout ? trayContainter.targetContainerSize.height + nextAppletSpacing : Panel.rootObject.dockSize + readonly property bool targetCollapsed: DDT.TraySortOrderModel.collapsed property bool useColumnLayout: Panel.rootObject.positionForAnimation % 2 property int dockOrder: 25 readonly property string quickpanelTrayItemPluginId: "sound" readonly property var filterTrayPlugins: [quickpanelTrayItemPluginId] implicitWidth: useColumnLayout ? Panel.rootObject.dockSize : trayContainter.implicitWidth + nextAppletSpacing - implicitHeight: useColumnLayout ? trayContainter.implicitHeight + nextAppletSpacing: Panel.rootObject.dockSize + implicitHeight: useColumnLayout ? trayContainter.implicitHeight + nextAppletSpacing : Panel.rootObject.dockSize + Component.onCompleted: { Applet.trayPluginModel = Qt.binding(function () { return DockCompositor.trayPluginSurfaces @@ -106,7 +112,8 @@ AppletItem { id: trayContainter isHorizontal: !tray.useColumnLayout model: DDT.TraySortOrderModel - collapsed: DDT.TraySortOrderModel.collapsed + collapsed: tray.targetCollapsed + targetCollapsed: tray.targetCollapsed trayHeight: Panel.rootObject.dockSize surfaceAcceptor: isTrayPluginPopup color: "transparent" diff --git a/panels/dock/tray/quickpanel/PanelTrayItem.qml b/panels/dock/tray/quickpanel/PanelTrayItem.qml index 5e4ccd113..5d5525ac1 100644 --- a/panels/dock/tray/quickpanel/PanelTrayItem.qml +++ b/panels/dock/tray/quickpanel/PanelTrayItem.qml @@ -38,10 +38,16 @@ Control { toolTipY: DockPanelPositioner.y } - contentItem: GridLayout { - flow: root.useColumnLayout ? GridLayout.TopToBottom : GridLayout.LeftToRight - rowSpacing: 0 - columnSpacing: 0 + function showToolTipNow() { + var point = quickpanelPlaceholder.mapToItem(null, quickpanelPlaceholder.width / 2, quickpanelPlaceholder.height / 2) + toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) + toolTip.open() + } + + contentItem: Grid { + rows: root.useColumnLayout ? 2 : 1 + spacing: 0 + padding: 0 Loader { id: placeholder @@ -70,9 +76,7 @@ Control { onHoveredChanged: function () { root.contentHovered = hovered if (hovered && !root.isOpened) { - var point = quickpanelPlaceholder.mapToItem(null, quickpanelPlaceholder.width / 2, quickpanelPlaceholder.height / 2) - toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) - toolTip.open() + root.showToolTipNow() } else { toolTip.close() } @@ -92,23 +96,52 @@ Control { ShellSurfaceItemProxy { id: surfaceLayer - property var itemGlobalPoint: { - var a = surfaceLayer - var x = 0, y = 0 - while(a.parent) { - x += a.x - y += a.y - a = a.parent + onWidthChanged: { + if (!shellSurface || !(shellSurface.updatePluginGeometry)) + return + shellSurface.margins = root.itemMargins + shellSurface.updatePluginGeometry(Qt.rect(Math.round(itemScenePoint.x), + Math.round(itemScenePoint.y), + Math.round(width), + Math.round(height))) + } + onHeightChanged: { + if (!shellSurface || !(shellSurface.updatePluginGeometry)) + return + shellSurface.margins = root.itemMargins + shellSurface.updatePluginGeometry(Qt.rect(Math.round(itemScenePoint.x), + Math.round(itemScenePoint.y), + Math.round(width), + Math.round(height))) + } + + function localItemPoint() { + let current = surfaceLayer + let x = 0 + let y = 0 + while (current.parent) { + x += current.x + y += current.y + current = current.parent } return Qt.point(x, y) } - onItemGlobalPointChanged: { + property var itemScenePoint: { + Panel.frontendWindowRect + surfaceLayer.localItemPoint() + return surfaceLayer.mapToItem(null, 0, 0) + } + + onItemScenePointChanged: { if (!shellSurface || !(shellSurface.updatePluginGeometry)) return shellSurface.margins = root.itemMargins - shellSurface.updatePluginGeometry(Qt.rect(itemGlobalPoint.x, itemGlobalPoint.y, 0, 0)) + shellSurface.updatePluginGeometry(Qt.rect(Math.round(itemScenePoint.x), + Math.round(itemScenePoint.y), + Math.round(width), + Math.round(height))) } } } diff --git a/panels/dock/tray/trayitempositionmanager.cpp b/panels/dock/tray/trayitempositionmanager.cpp index 346f8b1ad..8d601c942 100644 --- a/panels/dock/tray/trayitempositionmanager.cpp +++ b/panels/dock/tray/trayitempositionmanager.cpp @@ -15,17 +15,115 @@ static const int itemPadding = 4; static const int itemSpacing = 2; static const QSize itemVisualSize = QSize(itemSize + itemPadding * 2, itemSize + itemPadding * 2); -void TrayItemPositionManager::registerVisualItemSize(int index, const QSize &size) +void TrayItemPositionManager::beginLayoutSync() { - while (m_registeredItemsSize.count() < (index + 1)) { - m_registeredItemsSize.append(itemVisualSize); + m_layoutSyncActive = true; + m_visualItemSizeChangedPending = false; +} + +void TrayItemPositionManager::endLayoutSync() +{ + m_layoutSyncActive = false; + if (m_visualItemSizeChangedPending) { + m_visualItemSizeChangedPending = false; + emit visualItemSizeChanged(); + } +} + +void TrayItemPositionManager::registerSurfaceSize(const QString &surfaceId, const QSize &size) +{ + if (surfaceId.isEmpty() || size.isEmpty()) { + return; + } + + const QSize normalizedSize = size.isValid() ? size : itemVisualSize; + bool changed = m_registeredSurfaceSizes.value(surfaceId) != normalizedSize; + m_registeredSurfaceSizes.insert(surfaceId, normalizedSize); + + for (int index = 0; index < m_registeredItemSurfaceIds.count(); ++index) { + if (m_registeredItemSurfaceIds.at(index) != surfaceId) { + continue; + } + + if (m_registeredItemsSize.at(index) == normalizedSize) { + continue; + } + + m_registeredItemsSize[index] = normalizedSize; + changed = true; + } + + if (changed) { + notifyVisualItemSizeChanged(); + } +} + +void TrayItemPositionManager::registerVisualItem(const QString &surfaceId, int index) +{ + if (surfaceId.isEmpty() || index < 0) { + return; + } + + ensureRegisteredItemCapacity(index + 1); + + bool changed = false; + for (int currentIndex = 0; currentIndex < m_registeredItemSurfaceIds.count(); ++currentIndex) { + if (currentIndex == index || m_registeredItemSurfaceIds.at(currentIndex) != surfaceId) { + continue; + } + + m_registeredItemSurfaceIds[currentIndex].clear(); + if (m_registeredItemsSize.at(currentIndex) != itemVisualSize) { + m_registeredItemsSize[currentIndex] = itemVisualSize; + changed = true; + } + } + + const QSize size = m_registeredSurfaceSizes.value(surfaceId, itemVisualSize); + if (m_registeredItemSurfaceIds.at(index) != surfaceId || m_registeredItemsSize.at(index) != size) { + m_registeredItemSurfaceIds[index] = surfaceId; + m_registeredItemsSize[index] = size; + changed = true; } + + if (changed) { + notifyVisualItemSizeChanged(); + } +} + +void TrayItemPositionManager::unregisterVisualItem(const QString &surfaceId) +{ + if (surfaceId.isEmpty()) { + return; + } + + bool changed = false; + for (int index = 0; index < m_registeredItemSurfaceIds.count(); ++index) { + if (m_registeredItemSurfaceIds.at(index) != surfaceId) { + continue; + } + + m_registeredItemSurfaceIds[index].clear(); + if (m_registeredItemsSize.at(index) != itemVisualSize) { + m_registeredItemsSize[index] = itemVisualSize; + changed = true; + } + } + + if (changed) { + notifyVisualItemSizeChanged(); + } +} + +void TrayItemPositionManager::registerVisualItemSize(int index, const QSize &size) +{ + ensureRegisteredItemCapacity(index + 1); QSize oldSize = m_registeredItemsSize[index]; m_registeredItemsSize[index] = size; // The registered itemsize may change, and the layout needs to be updated when it does. if (oldSize != size) { - emit visualItemSizeChanged(); + notifyVisualItemSizeChanged(); } } @@ -121,13 +219,15 @@ void TrayItemPositionManager::layoutHealthCheck(int delayMs) void TrayItemPositionManager::clearRegisteredSizes() { - // Avoid emitting signal if there's nothing to clear - if (m_registeredItemsSize.isEmpty()) { + // Clear visual-index mapping only. Keep surface size cache so a layout rebuild + // can reuse the last known size immediately instead of falling back to defaults. + if (m_registeredItemsSize.isEmpty() && m_registeredItemSurfaceIds.isEmpty()) { return; } - + m_registeredItemsSize.clear(); - emit visualItemSizeChanged(); + m_registeredItemSurfaceIds.clear(); + notifyVisualItemSizeChanged(); } TrayItemPositionManager::TrayItemPositionManager(QObject *parent) @@ -155,4 +255,22 @@ void TrayItemPositionManager::updateVisualSize() setProperty("visualSize", result); } +void TrayItemPositionManager::ensureRegisteredItemCapacity(int count) +{ + while (m_registeredItemsSize.count() < count) { + m_registeredItemsSize.append(itemVisualSize); + m_registeredItemSurfaceIds.append(QString()); + } +} + +void TrayItemPositionManager::notifyVisualItemSizeChanged() +{ + if (m_layoutSyncActive) { + m_visualItemSizeChangedPending = true; + return; + } + + emit visualItemSizeChanged(); +} + } diff --git a/panels/dock/tray/trayitempositionmanager.h b/panels/dock/tray/trayitempositionmanager.h index da1c89cd0..58ce5e3fb 100644 --- a/panels/dock/tray/trayitempositionmanager.h +++ b/panels/dock/tray/trayitempositionmanager.h @@ -4,9 +4,11 @@ #pragma once +#include #include #include #include +#include namespace docktray { @@ -51,6 +53,11 @@ class TrayItemPositionManager : public QObject return &instance(); } + void beginLayoutSync(); + void endLayoutSync(); + void registerSurfaceSize(const QString &surfaceId, const QSize &size); + void registerVisualItem(const QString &surfaceId, int index); + void unregisterVisualItem(const QString &surfaceId); void registerVisualItemSize(int index, const QSize & size); QSize visualItemSize(int index) const; QSize visualSize(int index, bool includeLastSpacing = true) const; @@ -70,6 +77,8 @@ class TrayItemPositionManager : public QObject private: explicit TrayItemPositionManager(QObject *parent = nullptr); + void ensureRegisteredItemCapacity(int count); + void notifyVisualItemSizeChanged(); void updateVisualSize(); Qt::Orientation m_orientation; @@ -77,9 +86,13 @@ class TrayItemPositionManager : public QObject int m_dockHeight; int m_visualItemCount; QList m_registeredItemsSize; + QStringList m_registeredItemSurfaceIds; + QHash m_registeredSurfaceSizes; QSize m_itemVisualSize; int m_itemSpacing; int m_itemPadding; + bool m_layoutSyncActive = false; + bool m_visualItemSizeChangedPending = false; }; } diff --git a/panels/dock/tray/trayitempositionregister.cpp b/panels/dock/tray/trayitempositionregister.cpp index 0f1dd0ad2..6a7c7647f 100644 --- a/panels/dock/tray/trayitempositionregister.cpp +++ b/panels/dock/tray/trayitempositionregister.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 @@ -12,6 +12,11 @@ TrayItemPositionRegisterAttachedType::TrayItemPositionRegisterAttachedType(QObje : QObject(parent) { // register / update visual position + connect(this, &TrayItemPositionRegisterAttachedType::surfaceIdChanged, + this, [this](){ + registerVisualSize(); + emit visualPositionChanged(); + }); connect(this, &TrayItemPositionRegisterAttachedType::visualIndexChanged, this, [this](){ registerVisualSize(); @@ -35,6 +40,10 @@ TrayItemPositionRegisterAttachedType::TrayItemPositionRegisterAttachedType(QObje QPoint TrayItemPositionRegisterAttachedType::visualPosition() const { + if (m_visualIndex < 0 || m_visualSize.isEmpty()) { + return QPoint(); + } + TrayItemPositionManager & pm = TrayItemPositionManager::instance(); if (pm.orientation() == Qt::Horizontal) { int width = m_visualIndex == 0 ? 0 : pm.visualSize(m_visualIndex - 1).width(); @@ -47,8 +56,21 @@ QPoint TrayItemPositionRegisterAttachedType::visualPosition() const void TrayItemPositionRegisterAttachedType::registerVisualSize() { - if (m_visualIndex == -1 || m_visualSize.isEmpty()) return; - TrayItemPositionManager::instance().registerVisualItemSize(m_visualIndex, m_visualSize); + if (m_surfaceId.isEmpty()) { + return; + } + + auto &positionManager = TrayItemPositionManager::instance(); + if (!m_visualSize.isEmpty()) { + positionManager.registerSurfaceSize(m_surfaceId, m_visualSize); + } + + if (m_visualIndex < 0 || m_visualSize.isEmpty()) { + positionManager.unregisterVisualItem(m_surfaceId); + return; + } + + positionManager.registerVisualItem(m_surfaceId, m_visualIndex); } } diff --git a/panels/dock/tray/traysortordermodel.cpp b/panels/dock/tray/traysortordermodel.cpp index 5d2a7b06a..c52d3141c 100644 --- a/panels/dock/tray/traysortordermodel.cpp +++ b/panels/dock/tray/traysortordermodel.cpp @@ -373,11 +373,21 @@ void TraySortOrderModel::updateVisualIndexes() { m_isUpdating = true; emit isUpdatingChanged(true); - - // Clear registered sizes before re-assigning visual indexes - // This ensures that when items are repositioned, the empty placeholder - // will use the default item size instead of retaining the previous item's size - TrayItemPositionManager::instance().clearRegisteredSizes(); + + auto &positionManager = TrayItemPositionManager::instance(); + positionManager.beginLayoutSync(); + // Reset visual-index mapping before rebuilding it so reserved staged-drop slots + // fall back to the default item size instead of keeping the previous occupant's size. + positionManager.clearRegisteredSizes(); + + const auto assignDockVisualIndex = [&positionManager](QStandardItem *trayItem, int visualIndex) { + if (!trayItem || visualIndex < 0) { + return; + } + + trayItem->setData(visualIndex, TraySortOrderModel::VisualIndexRole); + positionManager.registerVisualItem(trayItem->data(TraySortOrderModel::SurfaceIdRole).toString(), visualIndex); + }; for (int i = 0; i < rowCount(); i++) { item(i)->setData(-1, TraySortOrderModel::VisualIndexRole); @@ -420,7 +430,7 @@ void TraySortOrderModel::updateVisualIndexes() Q_ASSERT(!results.isEmpty()); results[0]->setData(showStashActionVisible, TraySortOrderModel::VisibilityRole); if (showStashActionVisible) { - results[0]->setData(currentVisualIndex, TraySortOrderModel::VisualIndexRole); + assignDockVisualIndex(results[0], currentVisualIndex); currentVisualIndex++; } @@ -440,7 +450,8 @@ void TraySortOrderModel::updateVisualIndexes() toogleCollapseActionVisible = true; if (!m_collapsed) { reserveStagedDropSpace(currentVisualIndex); - results[0]->setData(currentVisualIndex++, TraySortOrderModel::VisualIndexRole); + assignDockVisualIndex(results[0], currentVisualIndex); + currentVisualIndex++; } else { // When collapsed, collapsable items should be hidden (visualIndex = -1) results[0]->setData(-1, TraySortOrderModel::VisualIndexRole); @@ -454,7 +465,7 @@ void TraySortOrderModel::updateVisualIndexes() results[0]->setData(toogleCollapseActionVisible, TraySortOrderModel::VisibilityRole); if (toogleCollapseActionVisible) { reserveStagedDropSpace(currentVisualIndex); - results[0]->setData(currentVisualIndex, TraySortOrderModel::VisualIndexRole); + assignDockVisualIndex(results[0], currentVisualIndex); currentVisualIndex++; } @@ -471,7 +482,7 @@ void TraySortOrderModel::updateVisualIndexes() results[0]->setData(dockVisible, TraySortOrderModel::DockVisibleRole); if (itemVisible && dockVisible) { reserveStagedDropSpace(currentVisualIndex); - results[0]->setData(currentVisualIndex, TraySortOrderModel::VisualIndexRole); + assignDockVisualIndex(results[0], currentVisualIndex); currentVisualIndex++; } } @@ -481,7 +492,7 @@ void TraySortOrderModel::updateVisualIndexes() Q_ASSERT(!results.isEmpty()); results[0]->setData(SECTION_FIXED, TraySortOrderModel::SectionTypeRole); reserveStagedDropSpace(currentVisualIndex); - results[0]->setData(currentVisualIndex, TraySortOrderModel::VisualIndexRole); + assignDockVisualIndex(results[0], currentVisualIndex); currentVisualIndex++; // fixed (not actually 'fixed' since it's just a section next to pinned) @@ -499,13 +510,14 @@ void TraySortOrderModel::updateVisualIndexes() results[0]->setData(itemVisible, TraySortOrderModel::VisibilityRole); results[0]->setData(dockVisible, TraySortOrderModel::DockVisibleRole); if (itemVisible && dockVisible) { - results[0]->setData(currentVisualIndex, TraySortOrderModel::VisualIndexRole); + assignDockVisualIndex(results[0], currentVisualIndex); currentVisualIndex++; } } // update visible item count property setProperty("visualItemCount", currentVisualIndex); + positionManager.endLayoutSync(); m_isUpdating = false; emit isUpdatingChanged(false); diff --git a/panels/dock/tray_loader_font_sync.cpp b/panels/dock/tray_loader_font_sync.cpp new file mode 100644 index 000000000..451629eca --- /dev/null +++ b/panels/dock/tray_loader_font_sync.cpp @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +QString loadDataFontFamily() +{ + static QString dataFontFamily; + static bool initialized = false; + + if (initialized) { + return dataFontFamily; + } + + initialized = true; + const int fontId = QFontDatabase::addApplicationFont(QStringLiteral(":/tray_loader_font_sync/fonts/ElmsSans-Regular.ttf")); + if (fontId < 0) { + return dataFontFamily; + } + + const QStringList families = QFontDatabase::applicationFontFamilies(fontId); + if (!families.isEmpty()) { + dataFontFamily = families.constFirst(); + } + + return dataFontFamily; +} + +bool isDatetimeRootWidget(const QWidget *widget) +{ + if (!widget) { + return false; + } + + const QByteArray className = widget->metaObject()->className(); + return className == "DatetimeWidget" || className == "SidebarCalendarWidget"; +} + +QWidget *datetimeRootWidget(QWidget *widget) +{ + QWidget *current = widget; + while (current) { + if (isDatetimeRootWidget(current)) { + return current; + } + + current = current->parentWidget(); + } + + return nullptr; +} + +void applyFontFamilyRecursively(QWidget *widget, const QString &family) +{ + if (!widget || family.isEmpty()) { + return; + } + + QFont font = widget->font(); + if (font.family() != family) { + font.setFamily(family); + widget->setFont(font); + } + + const auto children = widget->findChildren(QString(), Qt::FindDirectChildrenOnly); + for (QWidget *child : children) { + applyFontFamilyRecursively(child, family); + } +} + +class TrayLoaderFontSync final : public QObject +{ +public: + explicit TrayLoaderFontSync(QObject *parent = nullptr) + : QObject(parent) + { + if (!qApp) { + return; + } + + qApp->installEventFilter(this); + QTimer::singleShot(0, this, [this] { + refreshDatetimeWidgets(); + }); + } + +protected: + bool eventFilter(QObject *watched, QEvent *event) override + { + auto *widget = qobject_cast(watched); + if (!widget) { + return QObject::eventFilter(watched, event); + } + + switch (event->type()) { + case QEvent::Show: + case QEvent::Polish: + case QEvent::ApplicationFontChange: + case QEvent::FontChange: + scheduleRefresh(widget); + break; + default: + break; + } + + return QObject::eventFilter(watched, event); + } + +private: + void refreshDatetimeWidgets() + { + const QString family = loadDataFontFamily(); + if (family.isEmpty()) { + return; + } + + const auto widgets = QApplication::allWidgets(); + for (QWidget *widget : widgets) { + if (isDatetimeRootWidget(widget)) { + applyFontFamilyRecursively(widget, family); + } + } + } + + void scheduleRefresh(QWidget *widget) + { + QWidget *root = datetimeRootWidget(widget); + if (!root) { + return; + } + + QPointer rootGuard(root); + QTimer::singleShot(0, this, [rootGuard] { + if (!rootGuard) { + return; + } + + const QString family = loadDataFontFamily(); + if (family.isEmpty()) { + return; + } + + applyFontFamilyRecursively(rootGuard, family); + }); + } +}; + +void initTrayLoaderFontSync() +{ + if (!qApp) { + return; + } + + static auto *fontSync = new TrayLoaderFontSync(qApp); + Q_UNUSED(fontSync) +} + +} // namespace + +Q_COREAPP_STARTUP_FUNCTION(initTrayLoaderFontSync) diff --git a/panels/dock/tray_loader_font_sync.qrc b/panels/dock/tray_loader_font_sync.qrc new file mode 100644 index 000000000..4ed82d0a5 --- /dev/null +++ b/panels/dock/tray_loader_font_sync.qrc @@ -0,0 +1,5 @@ + + + ../../shell/fonts/ElmsSans-Regular.ttf + + diff --git a/panels/dock/waylanddockhelper.cpp b/panels/dock/waylanddockhelper.cpp index 76bc0f768..86f7e7913 100644 --- a/panels/dock/waylanddockhelper.cpp +++ b/panels/dock/waylanddockhelper.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 @@ -11,10 +11,11 @@ #include "qwayland-treeland-dde-shell-v1.h" #include "wayland-treeland-dde-shell-v1-client-protocol.h" +#include + #include #include #include - namespace dock { WaylandDockHelper::WaylandDockHelper(DockPanel *panel) : DockHelper(panel) @@ -141,7 +142,8 @@ bool WaylandDockHelper::isWindowOverlap() void WaylandDockHelper::setDockColorTheme(const ColorTheme &theme) { - m_panel->setColorTheme(theme); + Q_UNUSED(theme) + m_panel->setColorTheme(static_cast(Dtk::Gui::DGuiApplicationHelper::instance()->themeType())); } WallpaperColorManager::WallpaperColorManager(WaylandDockHelper *helper) diff --git a/panels/dock/x11dockhelper.cpp b/panels/dock/x11dockhelper.cpp index 3ae6ce0aa..e3182bf7f 100644 --- a/panels/dock/x11dockhelper.cpp +++ b/panels/dock/x11dockhelper.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 @@ -25,6 +25,58 @@ Q_LOGGING_CATEGORY(dockX11Log, "org.deepin.dde.shell.dock.x11") const uint16_t monitorSize = 15; const uint32_t allWorkspace = 0xffffffff; +QRect anchoredDockGeometry(DockPanel *panel) +{ + if (!panel) { + return {}; + } + + QRect rect = panel->geometry(); + QScreen *screen = panel->dockScreen(); + if (!screen && panel->window()) { + screen = panel->window()->screen(); + } + if (!screen) { + return rect; + } + + const QRect screenRect = screen->geometry(); + switch (panel->position()) { + case Top: + rect.moveLeft(screenRect.left() + (screenRect.width() - rect.width()) / 2); + rect.moveTop(screenRect.top()); + break; + case Bottom: + rect.moveLeft(screenRect.left() + (screenRect.width() - rect.width()) / 2); + rect.moveTop(screenRect.top() + screenRect.height() - rect.height()); + break; + case Left: + rect.moveLeft(screenRect.left()); + rect.moveTop(screenRect.top() + (screenRect.height() - rect.height()) / 2); + break; + case Right: + rect.moveLeft(screenRect.left() + screenRect.width() - rect.width()); + rect.moveTop(screenRect.top() + (screenRect.height() - rect.height()) / 2); + break; + } + + return rect; +} + +QRect frontendDockGeometry(DockPanel *panel) +{ + if (!panel) { + return {}; + } + + const QRect rect = panel->frontendWindowRect(); + if (!rect.isValid() || rect.isEmpty()) { + return {}; + } + + return rect; +} + // TODO: use taskmanager window data struct WindowData { @@ -333,8 +385,14 @@ X11DockHelper::X11DockHelper(DockPanel *panel) connect(panel, &DockPanel::positionChanged, m_updateDockAreaTimer, static_cast(&QTimer::start)); connect(panel, &DockPanel::dockSizeChanged, m_updateDockAreaTimer, static_cast(&QTimer::start)); connect(panel, &DockPanel::geometryChanged, m_updateDockAreaTimer, static_cast(&QTimer::start)); + connect(panel, &DockPanel::frontendWindowRectChanged, this, [this](const QRect &) { + m_updateDockAreaTimer->start(); + }); connect(panel, &DockPanel::showInPrimaryChanged, m_updateDockAreaTimer, static_cast(&QTimer::start)); connect(panel, &DockPanel::dockScreenChanged, m_updateDockAreaTimer, static_cast(&QTimer::start)); + connect(panel, &DockPanel::viewModeChanged, this, [this](ViewMode) { + m_updateDockAreaTimer->start(); + }); qGuiApp->installNativeEventFilter(m_xcbHelper); setupKWinDBusConnection(); @@ -479,8 +537,24 @@ void X11DockHelper::updateWindowHideState(xcb_window_t window) void X11DockHelper::updateDockArea() { - QRect rect = parent()->geometry(); - uint size = parent()->dockSize(); + const bool forceAnchoredDockArea = parent()->hideMode() == SmartHide; + QRect rect; + bool usingFrontendRect = false; + + if (!forceAnchoredDockArea) { + rect = frontendDockGeometry(parent()); + usingFrontendRect = rect.isValid() && !rect.isEmpty(); + } + + if (!usingFrontendRect) { + rect = anchoredDockGeometry(parent()); + } + + int size = static_cast(parent()->dockSize()); + if (usingFrontendRect) { + size = qMax(1, qRound(size * parent()->devicePixelRatio())); + } + switch (parent()->position()) { case Top: rect.setHeight(size); @@ -504,15 +578,17 @@ void X11DockHelper::updateDockArea() break; } - // Since the position of other windows are obtained through the xcb interface without scaling - // the rect of the dock needs to be changed to the original size of the original xcb. - QScreen *screen = parent()->dockScreen(); - if (screen != nullptr) { - auto screenRect = screen->geometry(); - rect.setSize(rect.size() * parent()->devicePixelRatio()); - auto x = (rect.x() - screenRect.x()) * parent()->devicePixelRatio() + screenRect.x(); - auto y = (rect.y() - screenRect.y()) * parent()->devicePixelRatio() + screenRect.y(); - rect.moveTo(x, y); + if (!usingFrontendRect) { + // Since the position of other windows are obtained through the xcb interface without scaling + // the rect of the dock needs to be changed to the original size of the original xcb. + QScreen *screen = parent()->dockScreen(); + if (screen != nullptr) { + auto screenRect = screen->geometry(); + rect.setSize(rect.size() * parent()->devicePixelRatio()); + auto x = (rect.x() - screenRect.x()) * parent()->devicePixelRatio() + screenRect.x(); + auto y = (rect.y() - screenRect.y()) * parent()->devicePixelRatio() + screenRect.y(); + rect.moveTo(x, y); + } } if (m_dockArea != rect) { @@ -560,6 +636,10 @@ X11DockWakeUpArea::~X11DockWakeUpArea() void X11DockWakeUpArea::open() { + if (m_isOpen) { + return; + } + uint32_t values_list[] = {1}; xcb_create_window(m_connection, XCB_COPY_FROM_PARENT, @@ -577,15 +657,27 @@ void X11DockWakeUpArea::open() uint32_t values[] = {XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW}; xcb_change_window_attributes(m_connection, m_triggerWindow, XCB_CW_EVENT_MASK, values); xcb_map_window(m_connection, m_triggerWindow); + xcb_flush(m_connection); + m_isOpen = true; } void X11DockWakeUpArea::close() { + if (!m_isOpen) { + return; + } + xcb_destroy_window(m_connection, m_triggerWindow); + xcb_flush(m_connection); + m_isOpen = false; } void X11DockWakeUpArea::updateDockWakeArea(Position pos) { + if (!m_isOpen) { + return; + } + QRect triggerArea; auto rect = screen()->geometry(); diff --git a/panels/dock/x11dockhelper.h b/panels/dock/x11dockhelper.h index 1e694b685..9fb51cb76 100644 --- a/panels/dock/x11dockhelper.h +++ b/panels/dock/x11dockhelper.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 #pragma once @@ -113,6 +113,6 @@ class X11DockWakeUpArea : public QObject, public DockWakeUpArea xcb_window_t m_triggerWindow; xcb_window_t m_rootWindow; xcb_connection_t *m_connection; + bool m_isOpen = false; }; } - diff --git a/panels/notification/CMakeLists.txt b/panels/notification/CMakeLists.txt index dc7bc1b2c..26a2d5991 100644 --- a/panels/notification/CMakeLists.txt +++ b/panels/notification/CMakeLists.txt @@ -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 @@ -30,14 +30,15 @@ set_target_properties(ds-notification-shared PROPERTIES ) target_include_directories(ds-notification-shared PUBLIC ${CMAKE_SOURCE_DIR}/panels/notification/common + ${ICU_INCLUDE_DIR} ) target_link_libraries(ds-notification-shared PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Sql Dtk${DTK_VERSION_MAJOR}::Core - ICU::uc - ICU::i18n - ICU::io + ${DS_ICU_UC_LIBRARY} + ${DS_ICU_I18N_LIBRARY} + ${DS_ICU_IO_LIBRARY} ) install(TARGETS ds-notification-shared DESTINATION "${LIB_INSTALL_DIR}") diff --git a/shell/dde-shell.qrc b/shell/dde-shell.qrc index ade340751..1e051d0ef 100644 --- a/shell/dde-shell.qrc +++ b/shell/dde-shell.qrc @@ -1,5 +1,6 @@ SceneWindow.qml + fonts/ElmsSans-Regular.ttf diff --git a/shell/fonts/ElmsSans-Regular.ttf b/shell/fonts/ElmsSans-Regular.ttf new file mode 100644 index 000000000..25a1ac2e0 Binary files /dev/null and b/shell/fonts/ElmsSans-Regular.ttf differ diff --git a/shell/main.cpp b/shell/main.cpp index 94ecb508d..7e4ccb5c1 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -1,15 +1,24 @@ -// 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 #include #include #include +#include +#include +#include +#include +#include +#include #include +#include +#include #include #include #include +#include #include #include @@ -46,6 +55,385 @@ static void disableLogOutput() QLoggingCategory::setFilterRules("*.debug=false"); } +static QString readAppearanceStringProperty(const QString &propertyName) +{ + static constexpr auto kAppearanceService = "org.deepin.dde.Appearance1"; + static constexpr auto kAppearancePath = "/org/deepin/dde/Appearance1"; + static constexpr auto kAppearanceInterface = "org.deepin.dde.Appearance1"; + static constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"; + + QDBusInterface appearanceProperties(QString::fromLatin1(kAppearanceService), + QString::fromLatin1(kAppearancePath), + QString::fromLatin1(kPropertiesInterface), + QDBusConnection::sessionBus()); + if (!appearanceProperties.isValid()) { + return QString(); + } + + const QDBusReply reply = appearanceProperties.call(QStringLiteral("Get"), + QString::fromLatin1(kAppearanceInterface), + propertyName); + return reply.isValid() ? reply.value().variant().toString() : QString(); +} + +static double readAppearanceDoubleProperty(const QString &propertyName) +{ + static constexpr auto kAppearanceService = "org.deepin.dde.Appearance1"; + static constexpr auto kAppearancePath = "/org/deepin/dde/Appearance1"; + static constexpr auto kAppearanceInterface = "org.deepin.dde.Appearance1"; + static constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"; + + QDBusInterface appearanceProperties(QString::fromLatin1(kAppearanceService), + QString::fromLatin1(kAppearancePath), + QString::fromLatin1(kPropertiesInterface), + QDBusConnection::sessionBus()); + if (!appearanceProperties.isValid()) { + return 0.0; + } + + const QDBusReply reply = appearanceProperties.call(QStringLiteral("Get"), + QString::fromLatin1(kAppearanceInterface), + propertyName); + return reply.isValid() ? reply.value().variant().toDouble() : 0.0; +} + +static DGuiApplicationHelper::ColorType explicitThemeTypeFromName(const QString &themeName) +{ + const QString normalizedThemeName = themeName.trimmed().toLower(); + if (normalizedThemeName.isEmpty()) { + return DGuiApplicationHelper::UnknownType; + } + + if (normalizedThemeName == QStringLiteral("dark") + || normalizedThemeName.endsWith(QStringLiteral(".dark")) + || normalizedThemeName.endsWith(QStringLiteral("-dark")) + || normalizedThemeName.endsWith(QStringLiteral("_dark"))) { + return DGuiApplicationHelper::DarkType; + } + + if (normalizedThemeName == QStringLiteral("light") + || normalizedThemeName.endsWith(QStringLiteral(".light")) + || normalizedThemeName.endsWith(QStringLiteral("-light")) + || normalizedThemeName.endsWith(QStringLiteral("_light"))) { + return DGuiApplicationHelper::LightType; + } + + return DGuiApplicationHelper::UnknownType; +} + +static DGuiApplicationHelper::ColorType currentEffectiveThemeType(DGuiApplicationHelper *guiHelper) +{ + if (!guiHelper) { + return DGuiApplicationHelper::LightType; + } + + const auto currentThemeType = guiHelper->themeType(); + if (currentThemeType != DGuiApplicationHelper::UnknownType) { + return currentThemeType; + } + + const auto currentPaletteType = guiHelper->paletteType(); + if (currentPaletteType != DGuiApplicationHelper::UnknownType) { + return currentPaletteType; + } + + return DGuiApplicationHelper::LightType; +} + +static DGuiApplicationHelper::ColorType effectiveThemeTypeFromAppearance(const QString &globalThemeName, + const QString >kThemeName, + const QString &iconThemeName, + DGuiApplicationHelper *guiHelper) +{ + const auto globalThemeType = explicitThemeTypeFromName(globalThemeName); + if (globalThemeType != DGuiApplicationHelper::UnknownType) { + return globalThemeType; + } + + const auto gtkThemeType = explicitThemeTypeFromName(gtkThemeName); + if (gtkThemeType != DGuiApplicationHelper::UnknownType) { + return gtkThemeType; + } + + const auto iconThemeType = explicitThemeTypeFromName(iconThemeName); + if (iconThemeType != DGuiApplicationHelper::UnknownType) { + return iconThemeType; + } + + return currentEffectiveThemeType(guiHelper); +} + +static void syncApplicationThemeFromAppearance() +{ + auto *guiHelper = DGuiApplicationHelper::instance(); + if (!guiHelper) { + return; + } + + const QString globalThemeName = readAppearanceStringProperty(QStringLiteral("GlobalTheme")); + const QString gtkThemeName = readAppearanceStringProperty(QStringLiteral("GtkTheme")); + const QString iconThemeName = readAppearanceStringProperty(QStringLiteral("IconTheme")); + const QString standardFontName = readAppearanceStringProperty(QStringLiteral("StandardFont")); + const double fontSize = readAppearanceDoubleProperty(QStringLiteral("FontSize")); + const auto explicitThemeType = explicitThemeTypeFromName(globalThemeName); + const bool followSystemTheme = explicitThemeType == DGuiApplicationHelper::UnknownType; + const auto targetPaletteType = followSystemTheme ? DGuiApplicationHelper::UnknownType : explicitThemeType; + + if (auto *applicationTheme = guiHelper->applicationTheme()) { + const QByteArray gtkThemeNameBytes = gtkThemeName.toUtf8(); + if (!gtkThemeNameBytes.isEmpty() && applicationTheme->themeName() != gtkThemeNameBytes) { + applicationTheme->setThemeName(gtkThemeNameBytes); + } + + const QByteArray iconThemeNameBytes = iconThemeName.toUtf8(); + if (!iconThemeNameBytes.isEmpty() && applicationTheme->iconThemeName() != iconThemeNameBytes) { + applicationTheme->setIconThemeName(iconThemeNameBytes); + } + + const QByteArray standardFontNameBytes = standardFontName.toUtf8(); + if (!standardFontNameBytes.isEmpty() && applicationTheme->fontName() != standardFontNameBytes) { + applicationTheme->setFontName(standardFontNameBytes); + } + + if (fontSize > 0.0 && !qFuzzyCompare(applicationTheme->fontPointSize() + 1.0, fontSize + 1.0)) { + applicationTheme->setFontPointSize(fontSize); + } + } + + if (guiHelper->paletteType() != targetPaletteType) { + guiHelper->setPaletteType(targetPaletteType); + } + + const auto targetThemeType = effectiveThemeTypeFromAppearance(globalThemeName, gtkThemeName, iconThemeName, guiHelper); + const QPalette targetPalette = guiHelper->applicationPalette(targetThemeType); + if (guiHelper->applicationPalette() != targetPalette) { + guiHelper->setApplicationPalette(targetPalette); + } + + if (!iconThemeName.isEmpty() && QIcon::themeName() != iconThemeName) { + QIcon::setThemeName(iconThemeName); + } + + if (qApp) { + QFont appFont = qApp->font(); + bool fontChanged = false; + + if (!standardFontName.isEmpty() && appFont.family() != standardFontName) { + appFont.setFamily(standardFontName); + fontChanged = true; + } + + if (fontSize > 0.0 && !qFuzzyCompare(appFont.pointSizeF() + 1.0, fontSize + 1.0)) { + appFont.setPointSizeF(fontSize); + fontChanged = true; + } + + if (fontChanged) { + qApp->setFont(appFont); + } + } +} + +class NativeMenuThemeSync final : public QObject +{ +public: + explicit NativeMenuThemeSync(QObject *parent = nullptr) + : QObject(parent) + { + } + + void install() + { + qApp->installEventFilter(this); + + auto *helper = DGuiApplicationHelper::instance(); + if (!helper) { + return; + } + + QObject::connect(helper, &DGuiApplicationHelper::applicationPaletteChanged, + this, &NativeMenuThemeSync::syncAllMenus); + QObject::connect(helper, &DGuiApplicationHelper::paletteTypeChanged, + this, &NativeMenuThemeSync::syncAllMenus); + if (auto *platformTheme = helper->applicationTheme()) { + QObject::connect(platformTheme, &DPlatformTheme::themeNameChanged, + this, &NativeMenuThemeSync::syncAllMenus); + QObject::connect(platformTheme, &DPlatformTheme::fontNameChanged, + this, &NativeMenuThemeSync::syncAllMenus); + QObject::connect(platformTheme, &DPlatformTheme::fontPointSizeChanged, + this, &NativeMenuThemeSync::syncAllMenus); + } + } + + void syncAllMenus() + { + const auto widgets = QApplication::allWidgets(); + for (QWidget *widget : widgets) { + if (auto *menu = qobject_cast(widget)) { + syncMenu(menu); + } + } + } + +protected: + bool eventFilter(QObject *watched, QEvent *event) override + { + auto *menu = qobject_cast(watched); + if (!menu) { + return QObject::eventFilter(watched, event); + } + + switch (event->type()) { + case QEvent::Polish: + case QEvent::PolishRequest: + case QEvent::PaletteChange: + case QEvent::ApplicationFontChange: + case QEvent::FontChange: + case QEvent::Show: + syncMenu(menu); + break; + default: + break; + } + + return QObject::eventFilter(watched, event); + } + +private: + void syncMenu(QMenu *menu) + { + auto *helper = DGuiApplicationHelper::instance(); + if (!helper || !menu) { + return; + } + + auto paletteType = helper->paletteType(); + if (paletteType == DGuiApplicationHelper::UnknownType) { + paletteType = helper->themeType(); + } + + if (qApp && menu->font() != qApp->font()) { + menu->setFont(qApp->font()); + } + menu->setPalette(helper->applicationPalette(paletteType)); + menu->ensurePolished(); + if (auto *style = menu->style()) { + style->polish(menu); + } + menu->update(); + } +}; + +class AppearanceThemeSync final : public QObject +{ + Q_OBJECT + +public: + explicit AppearanceThemeSync(NativeMenuThemeSync *menuThemeSync, QObject *parent = nullptr) + : QObject(parent) + , m_menuThemeSync(menuThemeSync) + { + m_syncTimer.setSingleShot(true); + m_syncTimer.setInterval(80); + QObject::connect(&m_syncTimer, &QTimer::timeout, + this, &AppearanceThemeSync::refreshAppearanceState); + + m_pollTimer.setInterval(500); + QObject::connect(&m_pollTimer, &QTimer::timeout, + this, &AppearanceThemeSync::refreshAppearanceState); + } + + void install() + { + auto bus = QDBusConnection::sessionBus(); + bus.connect(QStringLiteral("org.deepin.dde.Appearance1"), + QStringLiteral("/org/deepin/dde/Appearance1"), + QStringLiteral("org.deepin.dde.Appearance1"), + QStringLiteral("Changed"), + this, + SLOT(onAppearanceChanged(QString,QString))); + bus.connect(QStringLiteral("org.deepin.dde.Appearance1"), + QStringLiteral("/org/deepin/dde/Appearance1"), + QStringLiteral("org.deepin.dde.Appearance1"), + QStringLiteral("Refreshed"), + this, + SLOT(onAppearanceRefreshed(QString))); + bus.connect(QStringLiteral("org.deepin.dde.Appearance1"), + QStringLiteral("/org/deepin/dde/Appearance1"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged"), + QStringLiteral("sa{sv}as"), + this, + SLOT(onAppearancePropertiesChanged(QString,QVariantMap,QStringList))); + + refreshAppearanceState(); + m_pollTimer.start(); + } + +private Q_SLOTS: + void onAppearanceChanged(const QString &, const QString &) + { + scheduleSync(); + } + + void onAppearanceRefreshed(const QString &) + { + scheduleSync(); + } + + void onAppearancePropertiesChanged(const QString &interfaceName, + const QVariantMap &, + const QStringList &) + { + if (interfaceName != QStringLiteral("org.deepin.dde.Appearance1")) { + return; + } + + scheduleSync(); + } + + void refreshAppearanceState() + { + const QString globalThemeName = readAppearanceStringProperty(QStringLiteral("GlobalTheme")); + const QString gtkThemeName = readAppearanceStringProperty(QStringLiteral("GtkTheme")); + const QString iconThemeName = readAppearanceStringProperty(QStringLiteral("IconTheme")); + const QString activeColor = readAppearanceStringProperty(QStringLiteral("QtActiveColor")); + + if (globalThemeName.isEmpty() && gtkThemeName.isEmpty() && iconThemeName.isEmpty()) { + return; + } + + syncApplicationThemeFromAppearance(); + + if (globalThemeName != m_lastGlobalThemeName + || gtkThemeName != m_lastGtkThemeName + || iconThemeName != m_lastIconThemeName + || activeColor != m_lastActiveColor) { + m_lastGlobalThemeName = globalThemeName; + m_lastGtkThemeName = gtkThemeName; + m_lastIconThemeName = iconThemeName; + m_lastActiveColor = activeColor; + if (m_menuThemeSync) { + m_menuThemeSync->syncAllMenus(); + } + } + } + +private: + void scheduleSync() + { + m_syncTimer.start(); + } + + QPointer m_menuThemeSync; + QTimer m_syncTimer; + QTimer m_pollTimer; + QString m_lastGlobalThemeName; + QString m_lastGtkThemeName; + QString m_lastIconThemeName; + QString m_lastActiveColor; +}; + class AppletManager { public: @@ -98,6 +486,12 @@ int main(int argc, char *argv[]) setenv("DSG_APP_ID", "org.deepin.dde.shell", 0); DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::UseInactiveColorGroup, false); DApplication a(argc, argv); + syncApplicationThemeFromAppearance(); + auto *nativeMenuThemeSync = new NativeMenuThemeSync(&a); + nativeMenuThemeSync->install(); + nativeMenuThemeSync->syncAllMenus(); + auto *appearanceThemeSync = new AppearanceThemeSync(nativeMenuThemeSync, &a); + appearanceThemeSync->install(); // Don't apply to plugins qunsetenv("QT_SCALE_FACTOR"); // dde-shell contains UI controls based on QML and Widget technologies. @@ -219,3 +613,5 @@ int main(int argc, char *argv[]) return a.exec(); } + +#include "main.moc" diff --git a/tests/panels/dock/taskmanager/CMakeLists.txt b/tests/panels/dock/taskmanager/CMakeLists.txt index 57a5bfbd0..8f41d598a 100644 --- a/tests/panels/dock/taskmanager/CMakeLists.txt +++ b/tests/panels/dock/taskmanager/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +# SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd. # # SPDX-License-Identifier: CC0-1.0 @@ -49,3 +49,40 @@ target_include_directories(rolegroupmodel_tests PRIVATE ) gtest_discover_tests(rolegroupmodel_tests) + +add_executable(popupsortutils_tests + ${CMAKE_SOURCE_DIR}/panels/dock/taskmanager/popupsortutils.h + ${CMAKE_SOURCE_DIR}/panels/dock/taskmanager/popupsortutils.cpp + popupsortutilstests.cpp +) + +target_link_libraries(popupsortutils_tests + GTest::GTest + GTest::Main + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Test +) +target_include_directories(popupsortutils_tests PRIVATE + ${CMAKE_SOURCE_DIR}/panels/dock/taskmanager/ +) + +gtest_discover_tests(popupsortutils_tests) + +add_executable(dockfoldermigrationutils_tests + ${CMAKE_SOURCE_DIR}/panels/dock/taskmanager/dockfoldermigrationutils.h + ${CMAKE_SOURCE_DIR}/panels/dock/taskmanager/dockfoldermigrationutils.cpp + dockfoldermigrationutilstests.cpp +) + +target_link_libraries(dockfoldermigrationutils_tests + GTest::GTest + GTest::Main + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Test +) +target_include_directories(dockfoldermigrationutils_tests PRIVATE + ${CMAKE_SOURCE_DIR}/panels/dock/taskmanager/ +) + +gtest_discover_tests(dockfoldermigrationutils_tests) diff --git a/tests/panels/dock/taskmanager/combinemodelb.cpp b/tests/panels/dock/taskmanager/combinemodelb.cpp index b0cbd66b0..4b158fab4 100644 --- a/tests/panels/dock/taskmanager/combinemodelb.cpp +++ b/tests/panels/dock/taskmanager/combinemodelb.cpp @@ -4,6 +4,8 @@ #include "combinemodelb.h" +#include + DataB::DataB(int id, TestModelB* parent) : m_id(id) { @@ -83,6 +85,14 @@ void TestModelB::addData(DataB *data) endInsertRows(); } +void TestModelB::insertData(int row, DataB *data) +{ + const int boundedRow = std::clamp(row, 0, static_cast(m_list.size())); + beginInsertRows(QModelIndex(), boundedRow, boundedRow); + m_list.insert(boundedRow, data); + endInsertRows(); +} + void TestModelB::removeData(DataB *data) { auto pos = m_list.indexOf(data); diff --git a/tests/panels/dock/taskmanager/combinemodelb.h b/tests/panels/dock/taskmanager/combinemodelb.h index c836daa0d..5e35d7c65 100644 --- a/tests/panels/dock/taskmanager/combinemodelb.h +++ b/tests/panels/dock/taskmanager/combinemodelb.h @@ -39,6 +39,7 @@ class TestModelB : public QAbstractListModel QVariant data(const QModelIndex &index, int role) const override; void addData(DataB *data); + void insertData(int row, DataB *data); void removeData(DataB *data); diff --git a/tests/panels/dock/taskmanager/dockfoldermigrationutilstests.cpp b/tests/panels/dock/taskmanager/dockfoldermigrationutilstests.cpp new file mode 100644 index 000000000..c5089f775 --- /dev/null +++ b/tests/panels/dock/taskmanager/dockfoldermigrationutilstests.cpp @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dockfoldermigrationutils.h" + +#include + +#include +#include + +namespace dock { +namespace { + +TEST(DockFolderMigrationUtils, ResolvesDownloadsPlaceholderAndDeduplicates) +{ + const QString downloadsPath = QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + ASSERT_FALSE(downloadsPath.isEmpty()); + + const QStringList resolved = resolveDockedElements({ + QStringLiteral("desktop/dde-file-manager"), + QStringLiteral("folder/$DOWNLOADS"), + QStringLiteral("folder/$DOWNLOADS") + }); + + EXPECT_EQ(resolved, + QStringList({ + QStringLiteral("desktop/dde-file-manager"), + QStringLiteral("folder/%1").arg(downloadsPath) + })); +} + +TEST(DockFolderMigrationUtils, MigratesLegacyDesktopOnlyLayoutWithExtraApps) +{ + const QStringList legacyLayout = { + QStringLiteral("desktop/dde-file-manager"), + QStringLiteral("desktop/custom-app"), + QStringLiteral("desktop/org.deepin.browser"), + }; + + EXPECT_TRUE(shouldMigrateDefaultDockFolders(legacyLayout)); + + const QStringList merged = mergedWithDefaultDockFolders(legacyLayout); + const QStringList defaultFolders = defaultFolderDockedElements(); + ASSERT_EQ(defaultFolders.size(), 2); + + EXPECT_EQ(merged.value(0), QStringLiteral("desktop/dde-file-manager")); + EXPECT_EQ(merged.value(1), defaultFolders.at(0)); + EXPECT_EQ(merged.value(2), defaultFolders.at(1)); + EXPECT_EQ(merged.value(3), QStringLiteral("desktop/custom-app")); + EXPECT_EQ(merged.value(4), QStringLiteral("desktop/org.deepin.browser")); +} + +TEST(DockFolderMigrationUtils, SkipsLayoutsWithCustomFolderPins) +{ + const QStringList customizedLayout = { + QStringLiteral("desktop/dde-file-manager"), + QStringLiteral("folder//tmp"), + QStringLiteral("desktop/org.deepin.browser"), + }; + + EXPECT_FALSE(shouldMigrateDefaultDockFolders(customizedLayout)); +} + +TEST(DockFolderMigrationUtils, SkipsLayoutsWithoutFileManagerAnchor) +{ + const QStringList customizedLayout = { + QStringLiteral("desktop/org.deepin.browser"), + QStringLiteral("desktop/custom-app"), + }; + + EXPECT_FALSE(shouldMigrateDefaultDockFolders(customizedLayout)); +} + +} +} diff --git a/tests/panels/dock/taskmanager/popupsortutilstests.cpp b/tests/panels/dock/taskmanager/popupsortutilstests.cpp new file mode 100644 index 000000000..764cd2446 --- /dev/null +++ b/tests/panels/dock/taskmanager/popupsortutilstests.cpp @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "popupsortutils.h" + +#include + +namespace dock { +namespace { + +PopupSortableEntry entry(const QString &name, + const QString &typeText = QString(), + qint64 size = 0, + qint64 modifiedTime = 0, + qint64 createdTime = 0, + bool directory = false) +{ + PopupSortableEntry value; + value.name = name; + value.typeText = typeText; + value.size = size; + value.modifiedTime = modifiedTime; + value.createdTime = createdTime; + value.directory = directory; + return value; +} + +TEST(PopupSortUtils, CycleStateTogglesCurrentFieldAndResetsNewField) +{ + PopupSortState state; + + state = cyclePopupSortState(state, PopupSortField::Name); + EXPECT_EQ(state.field, PopupSortField::Name); + EXPECT_EQ(state.order, Qt::DescendingOrder); + + state = cyclePopupSortState(state, PopupSortField::Type); + EXPECT_EQ(state.field, PopupSortField::Type); + EXPECT_EQ(state.order, Qt::AscendingOrder); +} + +TEST(PopupSortUtils, SortsByNameAscending) +{ + QList entries{ + entry(QStringLiteral("Zoo"), QStringLiteral("z"), 2), + entry(QStringLiteral("alpha"), QStringLiteral("a"), 1), + entry(QStringLiteral("Beta"), QStringLiteral("b"), 3), + }; + + sortPopupEntries(&entries, PopupSortState{}, false); + + ASSERT_EQ(entries.size(), 3); + EXPECT_EQ(entries.at(0).name, QStringLiteral("alpha")); + EXPECT_EQ(entries.at(1).name, QStringLiteral("Beta")); + EXPECT_EQ(entries.at(2).name, QStringLiteral("Zoo")); +} + +TEST(PopupSortUtils, SortsBySizeDescending) +{ + QList entries{ + entry(QStringLiteral("first"), QString(), 4), + entry(QStringLiteral("second"), QString(), 12), + entry(QStringLiteral("third"), QString(), 8), + }; + + PopupSortState state; + state.field = PopupSortField::Size; + state.order = Qt::DescendingOrder; + sortPopupEntries(&entries, state, false); + + ASSERT_EQ(entries.size(), 3); + EXPECT_EQ(entries.at(0).name, QStringLiteral("second")); + EXPECT_EQ(entries.at(1).name, QStringLiteral("third")); + EXPECT_EQ(entries.at(2).name, QStringLiteral("first")); +} + +TEST(PopupSortUtils, KeepsDirectoriesFirstWhenRequested) +{ + QList entries{ + entry(QStringLiteral("video.mp4"), QString(), 100, 0, 0, false), + entry(QStringLiteral("Documents"), QString(), 0, 0, 0, true), + entry(QStringLiteral("Music"), QString(), 0, 0, 0, true), + entry(QStringLiteral("notes.txt"), QString(), 5, 0, 0, false), + }; + + PopupSortState state; + state.field = PopupSortField::Size; + state.order = Qt::DescendingOrder; + sortPopupEntries(&entries, state, true); + + ASSERT_EQ(entries.size(), 4); + EXPECT_TRUE(entries.at(0).directory); + EXPECT_TRUE(entries.at(1).directory); + EXPECT_FALSE(entries.at(2).directory); + EXPECT_FALSE(entries.at(3).directory); +} + +TEST(PopupSortUtils, SortsByTypeThenName) +{ + QList entries{ + entry(QStringLiteral("Painter"), QStringLiteral("Graphics")), + entry(QStringLiteral("Browser"), QStringLiteral("Internet")), + entry(QStringLiteral("Mail"), QStringLiteral("Internet")), + }; + + PopupSortState state; + state.field = PopupSortField::Type; + sortPopupEntries(&entries, state, false); + + ASSERT_EQ(entries.size(), 3); + EXPECT_EQ(entries.at(0).name, QStringLiteral("Painter")); + EXPECT_EQ(entries.at(1).name, QStringLiteral("Browser")); + EXPECT_EQ(entries.at(2).name, QStringLiteral("Mail")); +} + +} +} diff --git a/tests/panels/dock/taskmanager/rolecombinemodeltests.cpp b/tests/panels/dock/taskmanager/rolecombinemodeltests.cpp index e14630150..33fcf208e 100644 --- a/tests/panels/dock/taskmanager/rolecombinemodeltests.cpp +++ b/tests/panels/dock/taskmanager/rolecombinemodeltests.cpp @@ -259,6 +259,41 @@ TEST(RoleCombineModel, MinorModelChangesHandlingBug) }); } +TEST(RoleCombineModel, MinorRowInsertBeforeMappedRowKeepsCorrectMapping) +{ + TestModelA modelA; + TestModelB modelB; + + RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, [](QVariant data, QAbstractItemModel *model) -> QModelIndex { + auto matches = model->match(model->index(0, 0), TestModelB::idRole, data); + return matches.isEmpty() ? QModelIndex() : matches.first(); + }); + + modelA.addData(new DataA(1, "dataA1", &modelA)); + modelA.addData(new DataA(2, "dataA2", &modelA)); + + modelB.addData(new DataB(1, "dataB1", &modelB)); + modelB.addData(new DataB(2, "dataB2", &modelB)); + + auto combinedRoleNames = model.roleNames(); + int minorDataRole = -1; + for (auto it = combinedRoleNames.constBegin(); it != combinedRoleNames.constEnd(); ++it) { + if (it.value() == "bData") { + minorDataRole = it.key(); + break; + } + } + + ASSERT_NE(minorDataRole, -1); + EXPECT_EQ(model.index(0, 0).data(minorDataRole).toString(), "dataB1"); + EXPECT_EQ(model.index(1, 0).data(minorDataRole).toString(), "dataB2"); + + modelB.insertData(0, new DataB(0, "dataB0", &modelB)); + + EXPECT_EQ(model.index(0, 0).data(minorDataRole).toString(), "dataB1"); + EXPECT_EQ(model.index(1, 0).data(minorDataRole).toString(), "dataB2"); +} + // 新增测试用例:验证角色映射修复 TEST(RoleCombineModel, RoleMappingFix) {