From 937379deb1238226075178e021e4340c341b33b7 Mon Sep 17 00:00:00 2001 From: wjyrich Date: Wed, 5 Aug 2026 16:50:41 +0800 Subject: [PATCH] refactor: migrate to shared dde-shell app model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Replace direct AppMgr/AppItem usage with shared dde-shell application model 2. Add LauncherItem::load() to bridge with org.deepin.ds.dde-apps applet 3. Refactor AppsModel to adapt source model roles instead of owning app data 4. Remove direct D-Bus interface generation for ApplicationManager1 5. Fix QML role references from AppItem to AppsModel namespace 6. Set apps applet as data source for DesktopIntegration operations 7. Update QML to use new AppsModel.ddeCategories for category names 8. Fix category sorting signal ordering to avoid duplicate layout changes Log: Unified application data source with dde-shell shared model Influence: 1. Verify launcher still displays all installed applications correctly 2. Test application launch through launcher and dock 3. Test app category browsing and filtering 4. Test sending/removing apps from desktop 5. Test autostart toggle functionality 6. Test disable scale functionality for legacy apps 7. Test search functionality with pinyin, jianpin, and Chinese 8. Test recently installed and frequently used sections 9. Verify icon loading and caching behavior refactor: 迁移到 dde-shell 共享应用模型 1. 用共享的 dde-shell 应用模型替换直接的 AppMgr/AppItem 使用 2. 添加 LauncherItem::load() 与 org.deepin.ds.dde-apps applet 桥接 3. 重构 AppsModel 适配源模型角色而非拥有应用数据 4. 移除 ApplicationManager1 的直接 D-Bus 接口生成 5. 修复 QML 中角色引用从 AppItem 到 AppsModel 命名空间的变更 6. 设置应用 qml 作为 DesktopIntegration 操作的数据源 7. 更新 QML 使用新的 AppsModel.ddeCategories 获取分类名称 8. 修复分类排序信号顺序避免重复的布局变更 Log: 应用数据源统一为 dde-shell 共享模型 Influence: 1. 验证启动器仍能正确显示所有已安装应用 2. 测试通过启动器和任务栏启动应用 3. 测试应用分类浏览和过滤功能 4. 测试发送/移除应用到桌面 5. 测试开机自启动开关功能 6. 测试旧应用的禁用缩放功能 7. 测试拼音、简拼和中文搜索功能 8. 测试最近安装和常用应用区域 9. 验证图标加载和缓存行为 PMS: TASK-393709 --- CMakeLists.txt | 2 +- debian/control | 2 +- desktopintegration.cpp | 10 +- qml/windowed/AnalysisView.qml | 10 +- qml/windowed/AppListView.qml | 11 +- shell-launcher-applet/launcheritem.cpp | 31 +- shell-launcher-applet/launcheritem.h | 3 +- .../package/launcheritem.qml | 20 +- src/ddeintegration/CMakeLists.txt | 24 +- src/ddeintegration/appmgr.cpp | 668 ++++-------------- src/ddeintegration/appmgr.h | 86 +-- src/ddeintegration/types/amglobaltypes.h | 46 -- src/ddeintegration/types/objectmap.h | 7 - src/ddeintegration/types/propmap.h | 7 - src/ddeintegration/types/qstringmap.h | 7 - ...opspec.ApplicationManager1.Application.xml | 164 ----- .../xml/org.desktopspec.ObjectManager1.xml | 20 - src/models/CMakeLists.txt | 4 +- src/models/appitem.cpp | 172 ----- src/models/appitem.h | 82 --- src/models/appsmodel.cpp | 536 +++++++++----- src/models/appsmodel.h | 84 ++- src/models/categorizedsortproxymodel.cpp | 46 +- src/models/favoritedproxymodel.cpp | 9 +- src/models/itemarrangementproxymodel.cpp | 44 +- src/models/itemarrangementproxymodel.h | 1 + src/models/recentlyinstalledproxymodel.cpp | 4 +- src/models/searchfilterproxymodel.cpp | 12 +- tests/searchfilterproxymodeltest.cpp | 195 +++-- 29 files changed, 869 insertions(+), 1438 deletions(-) delete mode 100644 src/ddeintegration/types/amglobaltypes.h delete mode 100644 src/ddeintegration/types/objectmap.h delete mode 100644 src/ddeintegration/types/propmap.h delete mode 100644 src/ddeintegration/types/qstringmap.h delete mode 100644 src/ddeintegration/xml/org.desktopspec.ApplicationManager1.Application.xml delete mode 100644 src/ddeintegration/xml/org.desktopspec.ObjectManager1.xml delete mode 100644 src/models/appitem.cpp delete mode 100644 src/models/appitem.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b2387dd..07393160 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ set(DTK_NS Dtk6) set(ASQT_NS AppStreamQt) find_package(QT NAMES ${QT_NS} REQUIRED COMPONENTS Core) -find_package(${QT_NS} REQUIRED COMPONENTS Core Gui Concurrent Qml Svg Quick QuickControls2 LinguistTools) +find_package(${QT_NS} REQUIRED COMPONENTS Core Gui Concurrent DBus Qml Svg Quick QuickControls2 LinguistTools) find_package(${DTK_NS} REQUIRED COMPONENTS Core Gui) find_package(${ASQT_NS} 1.0 REQUIRED) find_package(DDEShell REQUIRED) diff --git a/debian/control b/debian/control index f2fc7210..6e95bf3f 100644 --- a/debian/control +++ b/debian/control @@ -25,7 +25,7 @@ Build-Depends: libdtk6gui-dev (>= 6.0.19), libdtk6declarative-dev (>> 6.7.33), dde-api-dev (>> 6.0.39), - libdde-shell-dev (>= 2.0.45), + libdde-shell-dev (>= 2.0.53), libappstreamqt-dev (>= 1.0.0) Standards-Version: 4.6.0 Rules-Requires-Root: no diff --git a/desktopintegration.cpp b/desktopintegration.cpp index 51675433..58512494 100644 --- a/desktopintegration.cpp +++ b/desktopintegration.cpp @@ -3,17 +3,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "desktopintegration.h" +#include "appmgr.h" #include -#include #include #include #include +#include +#include #include #include #include #include -#include #include @@ -25,6 +26,7 @@ DCORE_USE_NAMESPACE namespace { Q_LOGGING_CATEGORY(logDesktopIntegration, "org.deepin.dde.launchpad.desktop") + } QString DesktopIntegration::currentDE() @@ -69,7 +71,7 @@ double DesktopIntegration::disableScale(const QString &desktopId) void DesktopIntegration::setDisableScale(const QString &desktopId, double disableScale) { - return AppMgr::setDisableScale(desktopId, disableScale); + AppMgr::setDisableScale(desktopId, disableScale); } void DesktopIntegration::showFolder(QStandardPaths::StandardLocation location) @@ -188,7 +190,7 @@ bool DesktopIntegration::isAutoStart(const QString &desktopId) const // only affect the one in XDG_CONFIG_HOME, don't care about the system one (even if there is one). void DesktopIntegration::setAutoStart(const QString &desktopId, bool on) { - return AppMgr::setAutoStart(desktopId, on); + AppMgr::setAutoStart(desktopId, on); } bool DesktopIntegration::shouldSkipConfirmUninstallDialog(const QString &desktopId) const diff --git a/qml/windowed/AnalysisView.qml b/qml/windowed/AnalysisView.qml index 6be0f1c7..89978b0f 100644 --- a/qml/windowed/AnalysisView.qml +++ b/qml/windowed/AnalysisView.qml @@ -27,9 +27,9 @@ Control { FrequentlyUsedProxyModel { id: freqUsedModel sourceModel: AppsModel - desktopIdRole: AppItem.DesktopIdRole - launchedTimesRole: AppItem.LaunchedTimesRole - lastLaunchedTimeRole: AppItem.LastLaunchedTimeRole + desktopIdRole: AppsModel.DesktopIdRole + launchedTimesRole: AppsModel.LaunchedTimesRole + lastLaunchedTimeRole: AppsModel.LastLaunchedTimeRole } FrequentlyUsedView { @@ -44,8 +44,8 @@ Control { id: recentlyInstalledView model: RecentlyInstalledProxyModel { sourceModel: AppsModel - installedTimeRole: AppItem.InstalledTimeRole - lastLaunchedTimeRole: AppItem.LastLaunchedTimeRole + installedTimeRole: AppsModel.InstalledTimeRole + lastLaunchedTimeRole: AppsModel.LastLaunchedTimeRole } visible: count > 0 Layout.topMargin: -(Helper.frequentlyUsed.cellPaddingRows / 2) diff --git a/qml/windowed/AppListView.qml b/qml/windowed/AppListView.qml index c4dcef0a..d9159f41 100644 --- a/qml/windowed/AppListView.qml +++ b/qml/windowed/AppListView.qml @@ -34,7 +34,10 @@ FocusScope { function scrollToAlphabetCategory(character) { for (let i = 0; i < model.count; i++) { - let transliterated1st = model.model.data(model.modelIndex(i), AppsModel.TransliteratedRole)[0].toUpperCase() + let transliterated = model.model.data(model.modelIndex(i), AppsModel.TransliteratedRole) + if (!transliterated || transliterated.length === 0) + continue + let transliterated1st = transliterated[0].toUpperCase() if (character === transliterated1st) { listView.currentIndex = i scrollToIndex(listView.currentIndex, 35) // the height of a section heading @@ -45,7 +48,7 @@ FocusScope { function scrollToDDECategory(category) { for (let i = 0; i < model.count; i++) { - let value = model.model.data(model.modelIndex(i), AppItem.DDECategoryRole) + let value = model.model.data(model.modelIndex(i), AppsModel.DDECategoryRole) if (category === value) { listView.currentIndex = i scrollToIndex(listView.currentIndex, 35) // the height of a section heading @@ -81,6 +84,8 @@ FocusScope { } else if (CategorizedSortProxyModel.categoryType === CategorizedSortProxyModel.DDECategory) { sections = CategorizedSortProxyModel.DDECategorySections() } + if (!sections || sections.length === 0) + return false if (String(sections[0]) === section.toUpperCase()) return false else @@ -373,7 +378,7 @@ FocusScope { Connections { target: CategorizedSortProxyModel function onCategoryTypeChanged() { - listView.currentIndex = 0 + root.resetViewState() } } diff --git a/shell-launcher-applet/launcheritem.cpp b/shell-launcher-applet/launcheritem.cpp index 45186803..b1291038 100644 --- a/shell-launcher-applet/launcheritem.cpp +++ b/shell-launcher-applet/launcheritem.cpp @@ -1,16 +1,18 @@ -// 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 "launcheritem.h" #include "pluginfactory.h" #include "../launchercontroller.h" +#include #include -#include #include +#include #include +#include #include DS_USE_NAMESPACE @@ -27,6 +29,31 @@ LauncherItem::LauncherItem(QObject *parent) qCDebug(logApplet) << "Initializing LauncherItem applet"; } +bool LauncherItem::load() +{ + DAppletBridge bridge(QStringLiteral("org.deepin.ds.dde-apps")); + auto applet = bridge.applet(); + if (!applet) { + qCWarning(logApplet) << "Unable to load launchpad without the dde-apps applet"; + return false; + } + + auto model = applet->property("appModel").value(); + if (!model) { + qCWarning(logApplet) << "dde-apps did not provide an application model"; + return false; + } + + auto &appsModel = AppsModel::instance(); + appsModel.setSourceModel(model); + appsModel.setReady(applet->property("appModelReady").toBool()); + appsModel.setDdeCategories(applet->property("ddeCategories").toMap()); + connect(applet, SIGNAL(appModelReadyChanged(bool)), &appsModel, SLOT(setReady(bool))); + + qCInfo(logApplet) << "Using dde-shell shared application model"; + return DApplet::load(); +} + bool LauncherItem::init() { DApplet::init(); diff --git a/shell-launcher-applet/launcheritem.h b/shell-launcher-applet/launcheritem.h index 229258ed..5a74e738 100644 --- a/shell-launcher-applet/launcheritem.h +++ b/shell-launcher-applet/launcheritem.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 @@ -15,6 +15,7 @@ class LauncherItem : public DS_NAMESPACE::DApplet Q_PROPERTY(QString iconName MEMBER m_iconName NOTIFY iconNameChanged FINAL) public: explicit LauncherItem(QObject *parent = nullptr); + bool load() override; virtual bool init() override; Q_SIGNALS: diff --git a/shell-launcher-applet/package/launcheritem.qml b/shell-launcher-applet/package/launcheritem.qml index 17201c99..b14ba723 100644 --- a/shell-launcher-applet/package/launcheritem.qml +++ b/shell-launcher-applet/package/launcheritem.qml @@ -126,25 +126,25 @@ AppletItem { function getCategoryName(section) { switch (Number(section)) { - case AppItem.Internet: + case AppsModel.ddeCategories.Internet: return qsTr("Internet"); - case AppItem.Chat: + case AppsModel.ddeCategories.Chat: return qsTr("Chat"); - case AppItem.Music: + case AppsModel.ddeCategories.Music: return qsTr("Music"); - case AppItem.Video: + case AppsModel.ddeCategories.Video: return qsTr("Video"); - case AppItem.Graphics: + case AppsModel.ddeCategories.Graphics: return qsTr("Graphics"); - case AppItem.Game: + case AppsModel.ddeCategories.Game: return qsTr("Games"); - case AppItem.Office: + case AppsModel.ddeCategories.Office: return qsTr("Office"); - case AppItem.Reading: + case AppsModel.ddeCategories.Reading: return qsTr("Reading"); - case AppItem.Development: + case AppsModel.ddeCategories.Development: return qsTr("Development"); - case AppItem.System: + case AppsModel.ddeCategories.System: return qsTr("System"); default: return qsTr("Others"); diff --git a/src/ddeintegration/CMakeLists.txt b/src/ddeintegration/CMakeLists.txt index d49f67c3..d24c9cd8 100644 --- a/src/ddeintegration/CMakeLists.txt +++ b/src/ddeintegration/CMakeLists.txt @@ -86,22 +86,6 @@ PROPERTIES NO_NAMESPACE ON ) -set_source_files_properties( - ${CMAKE_CURRENT_LIST_DIR}/xml/org.desktopspec.ApplicationManager1.Application.xml -PROPERTIES - CLASSNAME AppManager1Application - INCLUDE types/amglobaltypes.h - NO_NAMESPACE ON -) - -set_source_files_properties( - ${CMAKE_CURRENT_LIST_DIR}/xml/org.desktopspec.ObjectManager1.xml -PROPERTIES - CLASSNAME AppManager1ApplicationObjectManager - INCLUDE types/amglobaltypes.h - NO_NAMESPACE ON -) - set(CMAKE_INCLUDE_CURRENT_DIR ON) set(DBusCPP_SRC "") @@ -109,16 +93,11 @@ set(DBusCPP_SRC "") dtk_add_dbus_interface(DBusCPP_SRC ${CMAKE_CURRENT_LIST_DIR}/xml/org.deepin.dde.daemon.Dock1.xml DaemonDock1) dtk_add_dbus_interface(DBusCPP_SRC ${CMAKE_CURRENT_LIST_DIR}/xml/org.deepin.dde.daemon.Launcher1.xml DaemonLauncher1) dtk_add_dbus_interface(DBusCPP_SRC ${CMAKE_CURRENT_LIST_DIR}/xml/org.deepin.dde.Appearance1.xml Appearance1) -dtk_add_dbus_interface(DBusCPP_SRC ${CMAKE_CURRENT_LIST_DIR}/xml/org.desktopspec.ApplicationManager1.Application.xml AppManager1Application) -dtk_add_dbus_interface(DBusCPP_SRC ${CMAKE_CURRENT_LIST_DIR}/xml/org.desktopspec.ObjectManager1.xml AppManager1ApplicationObjectManager) - message(INFO ${DBusCPP_SRC}) add_library(dde-integration-dbus OBJECT ${DBusCPP_SRC} types/qrect.h - types/objectmap.h - types/amglobaltypes.h appwiz.cpp ddedock.cpp @@ -142,11 +121,10 @@ PUBLIC ) target_link_libraries(dde-integration-dbus PUBLIC - Qt::Concurrent + Qt::Concurrent Qt::DBus PRIVATE Qt::Core Qt::Gui ${DTK_NS}::Core ${ASQT_NS} launcher-utils) if (HAVE_DDE_API_EVENTLOGGER) target_compile_definitions(dde-integration-dbus PRIVATE HAVE_DDE_API_EVENTLOGGER) - target_link_libraries(dde-integration-dbus PRIVATE DDEAPI::EventLogger) endif() diff --git a/src/ddeintegration/appmgr.cpp b/src/ddeintegration/appmgr.cpp index 5c852328..6e8833ba 100644 --- a/src/ddeintegration/appmgr.cpp +++ b/src/ddeintegration/appmgr.cpp @@ -4,222 +4,100 @@ #include "appmgr.h" -#include "AppManager1Application.h" -#include "AppManager1ApplicationObjectManager.h" - -#include -#include +#include +#include +#include +#include +#include +#include #include +#include +#include -Q_DECLARE_LOGGING_CATEGORY(logDdeIntegration) - -DCORE_USE_NAMESPACE +#include -using AppManager1Application = __AppManager1Application; -using AppManager1ApplicationObjectManager = __AppManager1ApplicationObjectManager; -Q_CONSTRUCTOR_FUNCTION(registerComplexDbusType); +Q_LOGGING_CATEGORY(logAppMgr, "org.deepin.dde.launchpad.appmgr") -static QString parseDisplayName(const QStringMap &source) +namespace { - static QString key = QLocale::system().name(); - const QString & defaultValue = source.value(u8"default"); - return source.value(key, key.contains('_') ? source.value(key.split('_')[0], defaultValue) : defaultValue); -} +const QString ApplicationManagerService = QStringLiteral("org.desktopspec.ApplicationManager1"); +const QString ApplicationInterface = QStringLiteral("org.desktopspec.ApplicationManager1.Application"); +const QString PropertiesInterface = QStringLiteral("org.freedesktop.DBus.Properties"); +const QString ApplicationObjectPathPrefix = QStringLiteral("/org/desktopspec/ApplicationManager1/"); -static QString parseName(const QStringMap &source) +const QStringList &disabledScaleEnvironments() { - return source.value(u8"default"); + static const QStringList environments { + QStringLiteral("DEEPIN_WINE_SCALE=1"), + QStringLiteral("QT_SCALE_FACTOR=1"), + QStringLiteral("GDK_SCALE=1"), + QStringLiteral("GDK_DPI_SCALE=1"), + QStringLiteral("D_DXCB_DISABLE_OVERRIDE_HIDPI=1"), + }; + return environments; } -static QString parseIcon(const QStringMap &source) +QString applicationObjectPath(QString desktopId) { - return source.value(u8"Desktop Entry"); -} - -template -static DExpected parseDBusField(const QVariantMap &map, const QString &key) -{ - if (!map.contains(key)) + if (desktopId.endsWith(QLatin1String(".desktop"))) + desktopId.chop(8); + if (desktopId.isEmpty() || desktopId.startsWith(QLatin1String("internal/"))) return {}; - const auto value = map.value(key); - return DExpected{qdbus_cast(value)}; + return ApplicationObjectPathPrefix + DUtil::escapeToObjectPath(desktopId); } -static QString getDisplayName(const bool isDeepin, const QStringMap &name, const QStringMap &genericName) +bool isScaleDisabled(const QString &environment) { - if (isDeepin) { - const auto tmp = parseDisplayName(genericName); - if (!tmp.isEmpty()) - return tmp; - } - - return parseDisplayName(name); + const QStringList values = environment.split(';', Qt::SkipEmptyParts); + return std::any_of(values.cbegin(), values.cend(), [](const QString &value) { + return disabledScaleEnvironments().contains(value); + }); } -static AppMgr::AppItem *parseDBus2AppItem(const ObjectInterfaceMap &source) +QString scaleEnvironment(const QString &environment, bool disabled) { - const QVariantMap appInfo = source.value("org.desktopspec.ApplicationManager1.Application"); - if (appInfo.isEmpty()) - return nullptr; - - const auto nodisplay = parseDBusField(appInfo, u8"NoDisplay"); - if (!nodisplay || nodisplay.value()) { - return nullptr; - } - AppMgr::AppItem *item = nullptr; - if (auto value = parseDBusField(appInfo, u8"ID")) { - item = new AppMgr::AppItem(); - item->id = value.value() + ".desktop"; - item->appId = value.value(); - } - - if (!item) { - qCWarning(logDdeIntegration) << "Failed to create AppItem"; - return nullptr; - } - - if (auto value = parseDBusField(appInfo, u8"Categories")) { - item->categories = value.value(); - } - - // fallback to Name if GenericName is empty, only for X_Deepin_Vendor equals to "deepin". - const auto deepinVendor = parseDBusField(appInfo, u8"X_Deepin_Vendor"); - item->vendor = deepinVendor ? deepinVendor.value() : QString(); - - // Get GenericName field - const auto genericNameMap = parseDBusField(appInfo, u8"GenericName"); - item->genericName = genericNameMap ? parseName(genericNameMap.value()) : QString(); - - item->displayName = getDisplayName(deepinVendor && deepinVendor.value() == QStringLiteral("deepin"), - parseDBusField(appInfo, u8"Name").value(), - parseDBusField(appInfo, u8"GenericName").value()); - - // just in case the entry is ill-formed, doesn't have a valid display name, fallback to use its desktop-id instead. - if (item->displayName.isEmpty()) { - item->displayName = item->id; - } - - if (auto value = parseDBusField(appInfo, u8"Name")) { - item->name = parseName(value.value()); - } - - if (auto value = parseDBusField(appInfo, u8"Icons")) { - item->iconName = parseIcon(value.value()); - } - - if (auto value = parseDBusField(appInfo, u8"InstalledTime")) { - item->installedTime = value.value(); - } - - if (auto value = parseDBusField(appInfo, u8"LastLaunchedTime")) { - item->lastLaunchedTime = value.value(); - } - - if (auto value = parseDBusField(appInfo, u8"AutoStart")) { - item->isAutoStart = value.value(); + QStringList values = environment.split(';', Qt::SkipEmptyParts); + if (disabled) { + values.removeIf([](const QString &value) { + const QString variable = value.section('=', 0, 0); + return std::any_of(disabledScaleEnvironments().cbegin(), + disabledScaleEnvironments().cend(), + [&variable](const QString &override) { + return override.section('=', 0, 0) == variable; + }); + }); + values.append(disabledScaleEnvironments()); + } else { + for (const QString &value : disabledScaleEnvironments()) + values.removeAll(value); } - - return item; + return values.join(';'); +} } AppMgr::AppMgr(QObject *parent) : QObject(parent) - , m_objectManager(new AppManager1ApplicationObjectManager("org.desktopspec.ApplicationManager1", - "/org/desktopspec/ApplicationManager1", - QDBusConnection::sessionBus(), this)) , m_checkTimer(new QTimer(this)) - , m_checkCount(0) { - m_checkTimer->setInterval(3000); // 3 second interval + m_checkTimer->setInterval(3000); connect(m_checkTimer, &QTimer::timeout, this, &AppMgr::checkPendingAppItems); - - connect(m_objectManager, &AppManager1ApplicationObjectManager::InterfacesAdded, this, - [this](const QDBusObjectPath &objPath, ObjectInterfaceMap interfacesAndProperties) { - const QString key(objPath.path()); - qCDebug(logDdeIntegration) << "InterfacesAdded by AM, path:" << key; - if (m_appItems.contains(objPath.path())) { - qWarning() << "App already exists for the path:" << key; - return; - } - m_checkCount = 0; - if (auto appItem = parseDBus2AppItem(interfacesAndProperties)) { - qCDebug(logDdeIntegration) << "App item added, desktopId" << appItem->id; - watchingAppItemAdded(key, appItem); - } - }); - connect(m_objectManager, &AppManager1ApplicationObjectManager::InterfacesRemoved, this, - [this](const QDBusObjectPath &objPath, const QStringList &interfaces) { - Q_UNUSED(interfaces) - const QString key(objPath.path()); - qCDebug(logDdeIntegration) << "InterfacesRemoved by AM, path:" << key; - watchingAppItemRemoved(key); - }); - - if (isValid()) { - qCInfo(logDdeIntegration) << "AppManager1 service already ready, fetching app items right away"; - fetchAppItems(); - loadAppsLaunchedTimes(); - } else { - auto serviceWatcher = new QDBusServiceWatcher(QStringLiteral("org.desktopspec.ApplicationManager1"), - QDBusConnection::sessionBus(), - QDBusServiceWatcher::WatchForRegistration, - this); - connect(serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this, serviceWatcher]() { - qCInfo(logDdeIntegration) << "AppManager1 service registered on bus, fetching app items"; - fetchAppItems(); - loadAppsLaunchedTimes(); - serviceWatcher->disconnect(); - }); - } } -AppMgr::~AppMgr() +AppMgr *AppMgr::instance() { - for (auto item : std::as_const(m_appItems)) { - if (auto handler = item->handler) { - qCDebug(logDdeIntegration) << "Deleting handler for app:" << item->id; - handler->deleteLater(); - } - } - qDeleteAll(m_appItems); -} - -AppManager1Application * createAM1AppIfaceByPath(const QString &dbusPath) -{ - AppManager1Application * amAppIface = new AppManager1Application(QLatin1String("org.desktopspec.ApplicationManager1"), - dbusPath, - QDBusConnection::sessionBus()); - if (!amAppIface->isValid()) { - qCWarning(logDdeIntegration) << "D-Bus interface not exist or failed to connect to" << dbusPath; - return nullptr; - } - - return amAppIface; -} - -AppManager1Application * createAM1AppIface(const QString &desktopId) -{ - auto appItem = AppMgr::instance()->appItem(desktopId); - if (!appItem) { - qCWarning(logDdeIntegration) << "Can't find appItem for the desktopId" << desktopId; - return nullptr; - } - qCDebug(logDdeIntegration) << "Get app interface for the desktopId" << desktopId; - return appItem->handler; + static AppMgr appMgr; + return &appMgr; } -// if return false, it means the launch is not even started. -// if return true, it means we attempted to launch it via AM, but not sure if it's succeed. bool AppMgr::launchApp(const QString &desktopId, const QString &activationToken) { - qCInfo(logDdeIntegration) << "Launching app:" << desktopId; - AppManager1Application * amAppIface = createAM1AppIface(desktopId); - if (!amAppIface) { - qCWarning(logDdeIntegration) << "Failed to get interface for desktop ID:" << desktopId; + qCInfo(logAppMgr) << "Launching app:" << desktopId; + const QString path = applicationObjectPath(desktopId); + if (path.isEmpty()) { + qCWarning(logAppMgr) << "Failed to get path for desktop ID:" << desktopId; return false; } - const auto path = amAppIface->path(); QProcess process; process.setProcessChannelMode(QProcess::MergedChannels); @@ -229,418 +107,180 @@ bool AppMgr::launchApp(const QString &desktopId, const QString &activationToken) #endif if (!activationToken.isEmpty()) { - qCDebug(logDdeIntegration) << "Passing XDG_ACTIVATION_TOKEN to dde-am for:" << desktopId; + qCDebug(logAppMgr) << "Passing XDG_ACTIVATION_TOKEN to dde-am for:" << desktopId; args << "--env" << (QLatin1String("XDG_ACTIVATION_TOKEN=") + activationToken); } process.start("dde-am", args); if (!process.waitForFinished()) { - qCWarning(logDdeIntegration) << "Failed to launch the desktopId:" << desktopId << process.errorString(); + qCWarning(logAppMgr) << "Failed to launch the desktopId:" << desktopId << process.errorString(); return false; } else if (process.exitCode() != 0) { - qCWarning(logDdeIntegration) << "Failed to launch the desktopId:" << desktopId << process.readAll(); + qCWarning(logAppMgr) << "Failed to launch the desktopId:" << desktopId << process.readAll(); return false; } - qCInfo(logDdeIntegration) << "Successfully launched desktop ID:" << desktopId; + + qCInfo(logAppMgr) << "Successfully launched desktop ID:" << desktopId; return true; } bool AppMgr::autoStart(const QString &desktopId) { - AppManager1Application * amAppIface = createAM1AppIface(desktopId); - if (!amAppIface) { - qCWarning(logDdeIntegration) << "Failed to get interface for desktop ID:" << desktopId; - return false; - } - - return amAppIface->autoStart(); + const QVariant value = instance()->readProperty(desktopId, QStringLiteral("AutoStart")); + return value.isValid() && value.toBool(); } -void AppMgr::setAutoStart(const QString &desktopId, bool autoStart) +void AppMgr::setAutoStart(const QString &desktopId, bool enabled) { - AppManager1Application * amAppIface = createAM1AppIface(desktopId); - if (!amAppIface) { - qCWarning(logDdeIntegration) << "Failed to get interface for desktop ID:" << desktopId; - return; - } - - amAppIface->setAutoStart(autoStart); - qCDebug(logDdeIntegration) << "Successfully set autoStart for" << desktopId << "to:" << autoStart; + instance()->setProperty(desktopId, QStringLiteral("AutoStart"), enabled); } -static const QStringList DisabledScaleEnvironments { - "DEEPIN_WINE_SCALE=1", - "QT_SCALE_FACTOR=1", - "GDK_SCALE=1", - "GDK_DPI_SCALE=1", - "D_DXCB_DISABLE_OVERRIDE_HIDPI=1" -}; - bool AppMgr::disableScale(const QString &desktopId) { - AppManager1Application * amAppIface = createAM1AppIface(desktopId); - if (!amAppIface) { - qCWarning(logDdeIntegration) << "Failed to get interface for desktop ID:" << desktopId; - return false; - } - - const auto environ = amAppIface->environ(); - const QStringList envs(environ.split(';')); - // return true if envs contains any one of DisabledScaleEnvironments. - auto iter = std::find_if(envs.begin(), envs.end(), [] (const QString &env) { - return DisabledScaleEnvironments.contains(env); - }); - return iter != envs.end(); + const QVariant environment = instance()->readProperty(desktopId, QStringLiteral("Environ")); + return environment.isValid() && isScaleDisabled(environment.toString()); } -void AppMgr::setDisableScale(const QString &desktopId, bool disableScale) +void AppMgr::setDisableScale(const QString &desktopId, bool disabled) { - AppManager1Application * amAppIface = createAM1AppIface(desktopId); - if (!amAppIface) { - qCWarning(logDdeIntegration) << "Failed to get interface for desktop ID:" << desktopId; + const QVariant environmentValue = instance()->readProperty(desktopId, QStringLiteral("Environ")); + if (!environmentValue.isValid()) return; - } - - QString environ = amAppIface->environ(); - QStringList envs(environ.split(';', Qt::SkipEmptyParts)); - - if (disableScale) { - // remove all ScaleEnvironments, avoid other caller has set it manually. - envs.removeIf([] (const QString &env) { - auto iter = std::find_if(DisabledScaleEnvironments.begin(), DisabledScaleEnvironments.end(), - [env] (const QString &item) { - const auto left = item.split('='); - const auto right = env.split('='); - return !right.isEmpty() && left.at(0) == right.at(0); - }); - return iter != DisabledScaleEnvironments.end(); - }); - envs << DisabledScaleEnvironments; - } else { - // remove all DisabledScaleEnvironments. - envs.removeIf([] (const QString &env) { - return DisabledScaleEnvironments.contains(env); - }); - } - environ = envs.join(';'); - qCDebug(logDdeIntegration) << "Update environ for the desktopId" << desktopId << ", env:" << environ; - amAppIface->setEnviron(environ); + const QString environment = environmentValue.toString(); + const QString updatedEnvironment = scaleEnvironment(environment, disabled); + if (updatedEnvironment != environment) + instance()->setProperty(desktopId, QStringLiteral("Environ"), updatedEnvironment); } -bool AppMgr::isOnDesktop(const QString &desktopId) +QVariant AppMgr::readProperty(const QString &desktopId, const QString &property) const { - AppManager1Application * amAppIface = createAM1AppIface(desktopId); - if (!amAppIface) { - qCWarning(logDdeIntegration) << "Failed to get interface for desktop ID:" << desktopId; - return false; + const QString path = applicationObjectPath(desktopId); + if (path.isEmpty()) { + qCWarning(logAppMgr) << "Invalid desktop ID for property" << property << ":" << desktopId; + return {}; } - return amAppIface->isOnDesktop(); + QDBusMessage message = QDBusMessage::createMethodCall( + ApplicationManagerService, path, PropertiesInterface, QStringLiteral("Get")); + message.setArguments({ApplicationInterface, property}); + const QDBusReply reply = QDBusConnection::sessionBus().call(message); + if (!reply.isValid()) { + qCWarning(logAppMgr) << "Failed to read property" << property << "for" << desktopId << reply.error(); + return {}; + } + return reply.value().variant(); } -bool AppMgr::sendToDesktop(const QString &desktopId) +bool AppMgr::setProperty(const QString &desktopId, const QString &property, const QVariant &value) { - AppManager1Application * amAppIface = createAM1AppIface(desktopId); - if (!amAppIface) { - qCWarning(logDdeIntegration) << "Failed to get interface for desktop ID:" << desktopId; + const QString path = applicationObjectPath(desktopId); + if (path.isEmpty()) { + qCWarning(logAppMgr) << "Invalid desktop ID for property" << property << ":" << desktopId; return false; } - QDBusPendingReply reply = amAppIface->SendToDesktop(); - reply.waitForFinished(); - - if (reply.isError()) { - qCWarning(logDdeIntegration) << "SendToDesktop failed:" << reply.error(); + QDBusMessage message = QDBusMessage::createMethodCall( + ApplicationManagerService, path, PropertiesInterface, QStringLiteral("Set")); + message.setArguments({ApplicationInterface, property, QVariant::fromValue(QDBusVariant(value))}); + const QDBusMessage reply = QDBusConnection::sessionBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + qCWarning(logAppMgr) << "Failed to set property" << property << "for" << desktopId + << reply.errorName() << reply.errorMessage(); return false; } - - const bool result = reply.value(); - qCInfo(logDdeIntegration) << "SendToDesktop result for" << desktopId << ":" << result; - return result; + return true; } -bool AppMgr::removeFromDesktop(const QString &desktopId) +bool AppMgr::callBoolMethod(const QString &desktopId, const QString &method) const { - AppManager1Application * amAppIface = createAM1AppIface(desktopId); - if (!amAppIface) { - qCWarning(logDdeIntegration) << "Failed to get interface for desktop ID:" << desktopId; + const QString path = applicationObjectPath(desktopId); + if (path.isEmpty()) { + qCWarning(logAppMgr) << "Invalid desktop ID for" << method << ":" << desktopId; return false; } - QDBusPendingReply reply = amAppIface->RemoveFromDesktop(); - reply.waitForFinished(); - - if (reply.isError()) { - qCWarning(logDdeIntegration) << "RemoveFromDesktop failed:" << reply.error(); + QDBusMessage message = QDBusMessage::createMethodCall(ApplicationManagerService, path, ApplicationInterface, method); + const QDBusReply reply = QDBusConnection::sessionBus().call(message); + if (!reply.isValid()) { + qCWarning(logAppMgr) << method << "failed for" << desktopId << reply.error(); return false; } - return reply.value(); } -bool AppMgr::isValid() const +bool AppMgr::sendToDesktop(const QString &desktopId) { - return m_objectManager->isValid(); + return instance()->callBoolMethod(desktopId, QStringLiteral("SendToDesktop")); } -QList AppMgr::allAppInfosShouldBeShown() const +bool AppMgr::removeFromDesktop(const QString &desktopId) { - return m_appItems.values(); + return instance()->callBoolMethod(desktopId, QStringLiteral("RemoveFromDesktop")); } -AppMgr::AppItem *AppMgr::appItem(const QString &id) const + // For: bug-347859 +bool AppMgr::waitForIcon(const QString &desktopId, const QString &iconName) { - const auto items = m_appItems.values(); - auto iter = std::find_if(items.begin(), items.end(), [id](AppItem *item) { - return item->id == id; - }); - return iter != items.end() ? *iter : nullptr; -} + if (desktopId.isEmpty() || !QFileInfo(iconName).isAbsolute() || QFileInfo::exists(iconName)) { + cancelPendingAppItem(desktopId); + return false; + } -void AppMgr::watchingAppItemPropertyChanged(const QString &key, AppMgr::AppItem *appItem) -{ - AppManager1Application * amAppIface = createAM1AppIfaceByPath(key); - if (!amAppIface) - return; + if (!m_pendingAppItems.contains(desktopId)) + m_checkCount = 0; + m_pendingAppItems.insert(desktopId, iconName); - Q_ASSERT(appItem->handler == nullptr); - appItem->handler = amAppIface; - connect(amAppIface, &AppManager1Application::CategoriesChanged, this, [this, appItem](const QStringList & value) { - qCDebug(logDdeIntegration) << "CategoriesChanged by AM, desktopId" << appItem->id; - appItem->categories = value; - Q_EMIT itemDataChanged(appItem->id); - }); - connect(amAppIface, &AppManager1Application::IconsChanged, this, [this, appItem](const QStringMap & value) { - qCDebug(logDdeIntegration) << "IconsChanged by AM, desktopId" << appItem->id; - appItem->iconName = parseIcon(value); - Q_EMIT itemDataChanged(appItem->id); - }); - connect(amAppIface, &AppManager1Application::X_Deepin_VendorChanged, this, [this, appItem, amAppIface](const QString & value) { - qCDebug(logDdeIntegration) << "X_Deepin_VendorChanged by AM, desktopId" << appItem->id; - appItem->displayName = getDisplayName(!value.isEmpty(), amAppIface->name(), amAppIface->genericName()); - Q_EMIT itemDataChanged(appItem->id); - }); - connect(amAppIface, &AppManager1Application::GenericNameChanged, this, [this, appItem, amAppIface](const QStringMap & value) { - qCDebug(logDdeIntegration) << "GenericNameChanged by AM, desktopId" << appItem->id; - appItem->displayName = getDisplayName(!amAppIface->x_Deepin_Vendor().isEmpty(), amAppIface->name(), value); - Q_EMIT itemDataChanged(appItem->id); - }); - connect(amAppIface, &AppManager1Application::NameChanged, this, [this, appItem, amAppIface](const QStringMap & value) { - qCDebug(logDdeIntegration) << "NameChanged by AM, desktopId" << appItem->id; - appItem->name = parseName(value); - appItem->displayName = getDisplayName(!amAppIface->x_Deepin_Vendor().isEmpty(), value, amAppIface->genericName()); - Q_EMIT itemDataChanged(appItem->id); - }); - connect(amAppIface, &AppManager1Application::InstalledTimeChanged, this, [this, appItem](const qint64 & value) { - qCDebug(logDdeIntegration) << "InstalledTimeChanged by AM, desktopId" << appItem->id; - appItem->installedTime = value; - Q_EMIT itemDataChanged(appItem->id); - }); - connect(amAppIface, &AppManager1Application::LastLaunchedTimeChanged, this, [this, appItem](const qint64 & value) { - qCDebug(logDdeIntegration) << "LastLaunchedTimeChanged by AM, desktopId" << appItem->id; - appItem->lastLaunchedTime = value; - Q_EMIT itemDataChanged(appItem->id); - }); - connect(amAppIface, &AppManager1Application::AutoStartChanged, this, [this, appItem](bool value) { - qCDebug(logDdeIntegration) << "AutoStartChanged by AM, desktopId" << appItem->id; - appItem->isAutoStart = value; - Q_EMIT itemDataChanged(appItem->id); - }); + if (!m_checkTimer->isActive()) + m_checkTimer->start(); + return true; } -void AppMgr::updateAppsLaunchedTimes(const QVariantMap &appsLaunchedTimes) +void AppMgr::cancelPendingAppItem(const QString &desktopId) { - // need to update times for removed and updated. - const auto &appItems = m_appItems.values(); - for (const auto item : std::as_const(appItems)) { - auto iter = appsLaunchedTimes.find(item->appId); - qint64 times = 0; - if (iter != appsLaunchedTimes.cend()) - times = iter->toLongLong(); - - // including reset and increase times. - if (item->launchedTimes != times) { - qCDebug(logDdeIntegration) << "LaunchedTimesChanged by DConfig, desktopId" << item->id << "from" << item->launchedTimes << "to" << times; - item->launchedTimes = times; - Q_EMIT itemDataChanged(item->id); - } + m_pendingAppItems.remove(desktopId); + if (m_pendingAppItems.isEmpty()) { + m_checkTimer->stop(); + m_checkCount = 0; } } -void AppMgr::loadAppsLaunchedTimes() +void AppMgr::clearPendingAppItems() { - DConfig *config = DConfig::create("org.deepin.dde.application-manager", "org.deepin.dde.am", "", this); - if (!config->isValid()) { - qCWarning(logDdeIntegration) << "DConfig is invalid when getting launched times."; - } else { - static const QString AppsLaunchedTimes(u8"appsLaunchedTimes"); - const auto &value = config->value(AppsLaunchedTimes).toMap(); - updateAppsLaunchedTimes(value); - QObject::connect(config, &DConfig::valueChanged, this, [this, config](const QString &key) { - if (key != AppsLaunchedTimes) { - qCDebug(logDdeIntegration) << "Ignoring non-appsLaunchedTimes key:" << key; - return; - } - - qCInfo(logDdeIntegration) << "appsLaunchedTimes of DConfig changed, updating"; - const auto &value = config->value(AppsLaunchedTimes).toMap(); - updateAppsLaunchedTimes(value); - }); - } + m_pendingAppItems.clear(); + m_checkTimer->stop(); + m_checkCount = 0; } -void AppMgr::fetchAppItems() +bool AppMgr::isPendingAppItem(const QString &desktopId) const { - qCDebug(logDdeIntegration) << "Begin to fetch apps."; - const auto reply = m_objectManager->GetManagedObjects(); - QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); - connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *call){ - QDBusPendingReply reply = *call; - if (reply.isError()) { - qWarning() << "Failed to get apps from AM, " << reply.error(); - call->deleteLater(); - return; - } - qCDebug(logDdeIntegration) << "Fetched all AppItem, and start parsing data."; - QMap items; - const auto objects = reply.value(); - for (auto iter = objects.cbegin(); iter != objects.cend(); ++iter) { - const auto &objPath = iter.key(); - const ObjectInterfaceMap &objs = iter.value(); - auto appItem = parseDBus2AppItem(objs); - if (!appItem) { - continue; - } - - items[objPath.path()] = appItem; - watchingAppItemPropertyChanged(objPath.path(), appItem); - } - call->deleteLater(); - qCDebug(logDdeIntegration) << "Fetched all AppItem, and end up parsing data."; - - m_appItems = items; - Q_EMIT changed(); - }); - // TODO async to fetch apps. - watcher->waitForFinished(); + return m_pendingAppItems.contains(desktopId); } -void AppMgr::watchingAppItemAdded(const QString &key, AppItem *appItem) +void AppMgr::checkPendingAppItems() { - // Check if iconName is an absolute path and if the file exists - if (isAbsolutePathIcon(appItem->iconName)) { - QFileInfo fileInfo(appItem->iconName); - if (!fileInfo.exists()) { - // File doesn't exist, add to pending container - m_pendingAppItems[key] = appItem; - - // Start timer if not already running - if (!m_checkTimer->isActive()) { - m_checkTimer->start(); - } - return; + ++m_checkCount; + + QStringList readyItems; + for (auto it = m_pendingAppItems.begin(); it != m_pendingAppItems.end();) { + if (QFileInfo::exists(it.value())) { + readyItems.append(it.key()); + it = m_pendingAppItems.erase(it); + } else { + ++it; } } - - // Icon exists or is a system icon, proceed with normal logic - m_appItems[key] = appItem; - watchingAppItemPropertyChanged(key, appItem); - Q_EMIT changed(); -} -void AppMgr::watchingAppItemRemoved(const QString &key) -{ - // Check if the item is in pending container - if (m_pendingAppItems.contains(key)) { - auto pendingAppItem = m_pendingAppItems.value(key); - qCDebug(logDdeIntegration) << "Removing pending app item, desktopId" << pendingAppItem->id; - m_pendingAppItems.remove(key); - delete pendingAppItem; - return; - } - - auto appItem = m_appItems.value(key); - if (!appItem) { - qCWarning(logDdeIntegration) << "App item not found for key:" << key; - return; + if (m_checkCount >= 20) { + readyItems.append(m_pendingAppItems.keys()); + m_pendingAppItems.clear(); } - qCDebug(logDdeIntegration) << "App item removed, desktopId" << appItem->id; - if (auto handler = appItem->handler) { - qCDebug(logDdeIntegration) << "Deleting handler for removed app:" << appItem->id; - handler->disconnect(this); - handler->deleteLater(); - } - m_appItems.remove(key); - delete appItem; - Q_EMIT changed(); -} - -void AppMgr::checkPendingAppItems() -{ - m_checkCount++; if (m_pendingAppItems.isEmpty()) { m_checkTimer->stop(); - return; - } - - QList> itemsToProcess; - - // Check all pending items - for (auto it = m_pendingAppItems.begin(); it != m_pendingAppItems.end(); ) { - const QString &key = it.key(); - AppItem *appItem = it.value(); - - if (isAbsolutePathIcon(appItem->iconName)) { - QFileInfo fileInfo(appItem->iconName); - if (fileInfo.exists()) { - // File now exists, add to main container - itemsToProcess.append(qMakePair(key, appItem)); - it = m_pendingAppItems.erase(it); - continue; - } - } - ++it; - } - - // Process items whose icons now exist - for (const auto &itemPair : itemsToProcess) { - const QString &key = itemPair.first; - AppItem *appItem = itemPair.second; - - m_appItems[key] = appItem; - watchingAppItemPropertyChanged(key, appItem); - Q_EMIT changed(); - } - - // Check if timeout reached (60 seconds) - if (m_checkCount >= 20 && !m_pendingAppItems.isEmpty()) { - // Force process all remaining pending items - for (auto it = m_pendingAppItems.begin(); it != m_pendingAppItems.end(); ) { - const QString &key = it.key(); - AppItem *appItem = it.value(); - - m_appItems[key] = appItem; - watchingAppItemPropertyChanged(key, appItem); - Q_EMIT changed(); - - it = m_pendingAppItems.erase(it); - } - - m_checkTimer->stop(); - } else if (m_pendingAppItems.isEmpty()) { - m_checkTimer->stop(); + m_checkCount = 0; } -} - -bool AppMgr::isAbsolutePathIcon(const QString &iconName) const -{ - // Check if the icon name is an absolute path (starts with /) - return iconName.startsWith('/'); -} -AppMgr *AppMgr::instance() { - static AppMgr gInstance; - return &gInstance; + for (const QString &desktopId : std::as_const(readyItems)) + Q_EMIT pendingAppItemReady(desktopId); } diff --git a/src/ddeintegration/appmgr.h b/src/ddeintegration/appmgr.h index 6fed576e..8cd00373 100644 --- a/src/ddeintegration/appmgr.h +++ b/src/ddeintegration/appmgr.h @@ -1,80 +1,50 @@ -// 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 #pragma once -#include +#include #include -#include -#include -#include -#include -#include +#include +#include -DCORE_BEGIN_NAMESPACE -class DConfig; -DCORE_END_NAMESPACE -class __AppManager1Application; -class __AppManager1ApplicationObjectManager; -class AppMgr : public QObject +class QTimer; + +class AppMgr final : public QObject { Q_OBJECT -public: - explicit AppMgr(QObject *parent = nullptr); - ~AppMgr(); - struct AppItem - { - QPointer<__AppManager1Application> handler; - QString id; - QString name; - QString displayName; - QString iconName; - QStringList categories; - qint64 installedTime = 0; - qint64 lastLaunchedTime = 0; - qint64 launchedTimes = 0; - bool isAutoStart = false; - QString appId; - QString vendor; - QString genericName; - }; +public: static AppMgr *instance(); - static bool launchApp(const QString & desktopId, const QString & activationToken = {}); - static bool autoStart(const QString & desktopId); - static void setAutoStart(const QString & desktopId, bool autoStart); - static bool disableScale(const QString & desktopId); - static void setDisableScale(const QString & desktopId, bool disableScale); - static bool isOnDesktop(const QString & desktopId); - static bool sendToDesktop(const QString & desktopId); - static bool removeFromDesktop(const QString & desktopId); + static bool launchApp(const QString &desktopId, const QString &activationToken = {}); + static bool autoStart(const QString &desktopId); + static void setAutoStart(const QString &desktopId, bool enabled); + static bool disableScale(const QString &desktopId); + static void setDisableScale(const QString &desktopId, bool disabled); + static bool sendToDesktop(const QString &desktopId); + static bool removeFromDesktop(const QString &desktopId); - bool isValid() const; - QList allAppInfosShouldBeShown() const; - AppMgr::AppItem * appItem(const QString &id) const; + bool waitForIcon(const QString &desktopId, const QString &iconName); + void cancelPendingAppItem(const QString &desktopId); + void clearPendingAppItems(); + bool isPendingAppItem(const QString &desktopId) const; Q_SIGNALS: - void changed(); - void itemDataChanged(const QString &id); + void pendingAppItemReady(const QString &desktopId); -private slots: +private Q_SLOTS: void checkPendingAppItems(); private: - void fetchAppItems(); - void loadAppsLaunchedTimes(); - void watchingAppItemAdded(const QString &key, AppMgr::AppItem *appItem); - void watchingAppItemRemoved(const QString &key); - void watchingAppItemPropertyChanged(const QString &key, AppMgr::AppItem *appItem); - void updateAppsLaunchedTimes(const QVariantMap &appsLaunchedTimes); - bool isAbsolutePathIcon(const QString &iconName) const; + explicit AppMgr(QObject *parent = nullptr); -private: - __AppManager1ApplicationObjectManager *m_objectManager; - QMap m_appItems; - QMap m_pendingAppItems; + QVariant readProperty(const QString &desktopId, const QString &property) const; + bool setProperty(const QString &desktopId, const QString &property, const QVariant &value); + bool callBoolMethod(const QString &desktopId, const QString &method) const; + + QHash m_pendingAppItems; QTimer *m_checkTimer; - int m_checkCount; + int m_checkCount = 0; }; diff --git a/src/ddeintegration/types/amglobaltypes.h b/src/ddeintegration/types/amglobaltypes.h deleted file mode 100644 index 12de5648..00000000 --- a/src/ddeintegration/types/amglobaltypes.h +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: CC0-1.0 - -#pragma once - -#include -#include -#include - -using ObjectInterfaceMap = QMap; -using ObjectMap = QMap; -using QStringMap = QMap; -using PropMap = QMap; - -Q_DECLARE_METATYPE(ObjectInterfaceMap) -Q_DECLARE_METATYPE(ObjectMap) -Q_DECLARE_METATYPE(QStringMap) -Q_DECLARE_METATYPE(PropMap) - -inline void registerComplexDbusType() // FIXME: test shouldn't associate with DBus -{ - qRegisterMetaType(); - qDBusRegisterMetaType(); - qRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - qRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); -} - -inline const QDBusArgument &operator>>(const QDBusArgument &argument, QStringMap &map) -{ - argument.beginMap(); - while (!argument.atEnd()) { - QString key; - QString value; - argument.beginMapEntry(); - argument >> key >> value; - argument.endMapEntry(); - map.insert(key, value); - } - argument.endMap(); - return argument; -} diff --git a/src/ddeintegration/types/objectmap.h b/src/ddeintegration/types/objectmap.h deleted file mode 100644 index 82b7e85a..00000000 --- a/src/ddeintegration/types/objectmap.h +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: CC0-1.0 - -#pragma once - -inline void registerObjectMapMetaType() { } diff --git a/src/ddeintegration/types/propmap.h b/src/ddeintegration/types/propmap.h deleted file mode 100644 index 8438be63..00000000 --- a/src/ddeintegration/types/propmap.h +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: CC0-1.0 - -#pragma once - -inline void registerPropMapMetaType() {} diff --git a/src/ddeintegration/types/qstringmap.h b/src/ddeintegration/types/qstringmap.h deleted file mode 100644 index 4ee698f2..00000000 --- a/src/ddeintegration/types/qstringmap.h +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: CC0-1.0 - -#pragma once - -inline void registerQStringMapMetaType() {} diff --git a/src/ddeintegration/xml/org.desktopspec.ApplicationManager1.Application.xml b/src/ddeintegration/xml/org.desktopspec.ApplicationManager1.Application.xml deleted file mode 100644 index f15d40cc..00000000 --- a/src/ddeintegration/xml/org.desktopspec.ApplicationManager1.Application.xml +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/ddeintegration/xml/org.desktopspec.ObjectManager1.xml b/src/ddeintegration/xml/org.desktopspec.ObjectManager1.xml deleted file mode 100644 index 71dd35b2..00000000 --- a/src/ddeintegration/xml/org.desktopspec.ObjectManager1.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/models/CMakeLists.txt b/src/models/CMakeLists.txt index 7c95ebde..6038283a 100644 --- a/src/models/CMakeLists.txt +++ b/src/models/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: CC0-1.0 @@ -32,7 +32,6 @@ FILES target_sources(launcher-models PRIVATE appsmodel.cpp - appitem.cpp appitem.h sortproxymodel.cpp searchfilterproxymodel.cpp categorizedsortproxymodel.cpp @@ -54,7 +53,6 @@ target_link_libraries(launcher-models PRIVATE ${DTK_NS}::Core gio-utils - dde-integration-dbus launcher-utils ) diff --git a/src/models/appitem.cpp b/src/models/appitem.cpp deleted file mode 100644 index e4fb41bd..00000000 --- a/src/models/appitem.cpp +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "appitem.h" - -#include -#include -Q_LOGGING_CATEGORY(logModels, "org.deepin.dde.launchpad.models") - -AppItem::AppItem(const QString &freedesktopId) - : QStandardItem() -{ - qCDebug(logModels) << "Creating AppItem:" << freedesktopId; - setData(freedesktopId, AppItem::DesktopIdRole); -} - -AppItem::~AppItem() -{ - qCDebug(logModels) << "Destroying AppItem"; -} - -const QString AppItem::freedesktopId() const -{ - return data(AppItem::DesktopIdRole).toString(); -} - -const QString AppItem::name() const -{ - return data(AppItem::NameRole).toString(); -} - -void AppItem::setName(const QString &name) -{ - setData(name, AppItem::NameRole); -} - -const QString AppItem::displayName() const -{ - return text(); -} - -void AppItem::setDisplayName(const QString &name) -{ - setText(name); -} - -const QString AppItem::iconName() const -{ - return data(AppItem::IconNameRole).toString(); -} - -void AppItem::setIconName(const QString &iconName) -{ - QString name = iconName; - if (QFileInfo(iconName).isAbsolute()) { - name = QUrl::fromLocalFile(iconName).toString(); // path ==> file://path - } - - const auto& finalName = iconName.isEmpty() ? "application-x-desktop" : name; - qCDebug(logModels) << "Final icon name:" << finalName; - setData(finalName, AppItem::IconNameRole); -} - -const QStringList AppItem::categories() const -{ - return data(AppItem::Categories).toStringList(); -} - -void AppItem::setCategories(const QStringList &categories) -{ - qCDebug(logModels) << "Setting categories:" << categories; - setData(categories, AppItem::Categories); -} - -AppItem::DDECategories AppItem::ddeCategory() const -{ - return data(AppItem::DDECategoryRole).value(); -} - -void AppItem::setDDECategory(DDECategories category) -{ - qCDebug(logModels) << "Setting DDE category:" << static_cast(category); - setData(category, AppItem::DDECategoryRole); -} - -qint64 AppItem::installedTime() const -{ - return data(AppItem::InstalledTimeRole).toLongLong(); -} - -void AppItem::setInstalledTime(qint64 time) -{ - qCDebug(logModels) << "Setting installed time:" << time; - setData(time, AppItem::InstalledTimeRole); -} - -qint64 AppItem::lastLaunchedTime() const -{ - return data(AppItem::LastLaunchedTimeRole).toLongLong(); -} - -void AppItem::setLastLaunchedTime(qint64 time) -{ - qCDebug(logModels) << "Setting last launched time:" << time; - setData(time, AppItem::LastLaunchedTimeRole); -} - -qint64 AppItem::launchedTimes() const -{ - return data(AppItem::LaunchedTimesRole).toLongLong(); -} - -void AppItem::setLaunchedTimes(qint64 times) -{ - qCDebug(logModels) << "Setting launched times count:" << times; - setData(times, AppItem::LaunchedTimesRole); -} - -bool AppItem::isAutoStart() const -{ - return data(AppItem::IsAutoStartRole).toBool(); -} - -void AppItem::setIsAutoStart(bool autostart) -{ - qCDebug(logModels) << "Setting autostart status:" << autostart; - setData(autostart, AppItem::IsAutoStartRole); -} - -const QString AppItem::vendor() const -{ - return data(AppItem::VendorRole).toString(); -} - -void AppItem::setVendor(const QString &vendor) -{ - qCDebug(logModels) << "Setting vendor:" << vendor; - setData(vendor, AppItem::VendorRole); -} - -const QString AppItem::genericName() const -{ - return data(AppItem::GenericNameRole).toString(); -} - -void AppItem::setGenericName(const QString &genericName) -{ - qCDebug(logModels) << "Setting generic name:" << genericName; - setData(genericName, AppItem::GenericNameRole); -} - -// assign/update data from another AppItem object -// assume the desktopId is the same, will update other data. -// doesn't take the ownership of the passed appItem. -void AppItem::updateData(const AppItem *appItem) -{ - if (this == appItem) return; - - setData(appItem->data(Qt::DisplayRole), Qt::DisplayRole); - setData(appItem->data(AppItem::IconNameRole), AppItem::IconNameRole); - setData(appItem->data(AppItem::Categories), AppItem::Categories); - setData(appItem->data(AppItem::DDECategoryRole), AppItem::DDECategoryRole); - setData(appItem->data(AppItem::NameRole), AppItem::NameRole); - setData(appItem->data(AppItem::InstalledTimeRole), AppItem::InstalledTimeRole); - setData(appItem->data(AppItem::LastLaunchedTimeRole), AppItem::LastLaunchedTimeRole); - setData(appItem->data(AppItem::LaunchedTimesRole), AppItem::LaunchedTimesRole); - setData(appItem->data(AppItem::IsAutoStartRole), AppItem::IsAutoStartRole); - setData(appItem->data(AppItem::VendorRole), AppItem::VendorRole); - setData(appItem->data(AppItem::GenericNameRole), AppItem::GenericNameRole); -} - diff --git a/src/models/appitem.h b/src/models/appitem.h deleted file mode 100644 index 5b63a616..00000000 --- a/src/models/appitem.h +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -class AppItem : public QStandardItem -{ - Q_GADGET - QML_NAMED_ELEMENT(AppItem) - QML_UNCREATABLE("AppItem should only be created from C++ side") -public: - enum Roles { - DesktopIdRole = Qt::UserRole, - Categories, - DDECategoryRole, - IconNameRole, - NameRole, - InstalledTimeRole, - LastLaunchedTimeRole, - LaunchedTimesRole, - IsAutoStartRole, - VendorRole, - GenericNameRole, - ModelExtendedRole = 0x1000 - }; - Q_ENUM(Roles) - - // This is different from the menu-spec Main Categories list. - enum DDECategories { - Internet, // 网络模式 - Chat, // 社交模式 - Music, // 音乐模式 - Video, // 视频模式 - Graphics, // 图形图像 - Game, // - Office, // 办公模式 - Reading, // 阅读模式 - Development, // 编程开发模式 - System, // 系统管理模式 - Others, - }; - Q_ENUM(DDECategories) - -public: -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - // FIXME: why do we need this one? - explicit AppItem() { Q_ASSERT_X(false, "AppItem()", "qmlRegisterUncreatableType require this for unknown reason in Qt6, another ctor should be used"); } -#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - explicit AppItem(const QString &freedesktopId); - ~AppItem(); - - const QString freedesktopId() const; - const QString name() const; - void setName(const QString & name); - const QString displayName() const; - void setDisplayName(const QString & name); - const QString iconName() const; - void setIconName(const QString & iconName); - const QStringList categories() const; - void setCategories(const QStringList & categories); - DDECategories ddeCategory() const; - void setDDECategory(DDECategories category); - qint64 installedTime() const; - void setInstalledTime(qint64 time); - qint64 lastLaunchedTime() const; - void setLastLaunchedTime(qint64 time); - qint64 launchedTimes() const; - void setLaunchedTimes(qint64 times); - bool isAutoStart() const; - void setIsAutoStart(bool autostart); - const QString vendor() const; - void setVendor(const QString & vendor); - const QString genericName() const; - void setGenericName(const QString & genericName); - void updateData(const AppItem * appItem); -}; - -Q_DECLARE_METATYPE(AppItem::DDECategories); diff --git a/src/models/appsmodel.cpp b/src/models/appsmodel.cpp index db3883a4..cebe3b34 100644 --- a/src/models/appsmodel.cpp +++ b/src/models/appsmodel.cpp @@ -1,248 +1,428 @@ -// 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 "appsmodel.h" -#include "categoryutils.h" + +#include "../ddeintegration/appmgr.h" #include "iconutils.h" -#include -#include #include -#include #include -#include +#include #include -#include "appmgr.h" #include -Q_DECLARE_LOGGING_CATEGORY(logModels) +#include +#include +#include + +Q_LOGGING_CATEGORY(logModels, "org.deepin.dde.launchpad.models") DCORE_USE_NAMESPACE +namespace { +constexpr auto DesktopIdRoleName = "desktopId"; +constexpr auto NameRoleName = "name"; +constexpr auto IconNameRoleName = "iconName"; +constexpr auto NoDisplayRoleName = "noDisplay"; +constexpr auto DDECategoryRoleName = "ddeCategory"; +constexpr auto CategoriesRoleName = "categories"; +constexpr auto InstalledTimeRoleName = "installedTime"; +constexpr auto LastLaunchedTimeRoleName = "lastLaunchedTime"; +constexpr auto LaunchedTimesRoleName = "launchedTimes"; +constexpr auto AutoStartRoleName = "autoStart"; +constexpr auto VendorRoleName = "vendor"; +constexpr auto GenericNameRoleName = "genericName"; +} -static void updateAppItemFromAM(AppItem *appItem) +AppsModel::AppsModel(QObject *parent) + : QAbstractListModel(parent) + , m_iconCacheWatcher(new DFileWatcherManager(this)) + , m_iconUpdateTimer(new QTimer(this)) { - const QString id(appItem->freedesktopId()); - qCDebug(logModels) << "Updating AppItem from AppMgr:" << id; - auto item = AppMgr::instance()->appItem(id); - if (!item) { - qCWarning(logModels) << "Not existing item in AppMgr for the desktopId" << id; - return; - } + qCDebug(logModels) << "Initializing shared AppsModel adapter"; + const auto config = DConfig::create("org.deepin.dde.shell", "org.deepin.ds.launchpad", QString(), this); + Q_ASSERT_X(config->isValid(), "DConfig", "DConfig file is missing or invalid"); + m_excludedAppIdList = config->value("excludeAppIdList", QStringList {}).toStringList(); + m_iconUpdateTimer->setInterval(1000); + m_iconUpdateTimer->setSingleShot(true); - qCDebug(logModels) << "Update AppItem property for the desktopId" << id; - appItem->setName(item->name); - appItem->setDisplayName(item->displayName); - appItem->setIconName(item->iconName); - appItem->setCategories(item->categories); - appItem->setInstalledTime(item->installedTime); - appItem->setLastLaunchedTime(item->lastLaunchedTime); - appItem->setLaunchedTimes(item->launchedTimes); - appItem->setIsAutoStart(item->isAutoStart); -} + const QStringList paths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + const QString suffix = QStringLiteral("/icons/hicolor/icon-theme.cache"); + for (const QString &path : paths) { + if (QFileInfo::exists(path + suffix)) + m_iconCacheWatcher->add(path + suffix); + } -AppsModel::AppsModel(QObject *parent) - : QStandardItemModel(parent) - , m_dconfig(DConfig::create("org.deepin.dde.shell", "org.deepin.ds.launchpad")) - , m_tmUpdateCache(new QTimer(this)) -{ - qCDebug(logModels) << "Initializing AppsModel"; - Q_ASSERT_X(m_dconfig->isValid(), "DConfig", "DConfig file is missing or invalid"); - m_excludedAppIdList = m_dconfig->value("excludeAppIdList", QStringList{}).toStringList(); - - QHash defaultRoleNames = roleNames(); - defaultRoleNames.insert({ - {AppItem::DesktopIdRole, QByteArrayLiteral("desktopId")}, - {AppItem::DDECategoryRole, QByteArrayLiteral("category")}, - {AppItem::IconNameRole, QByteArrayLiteral("iconName")}, - {AppItem::InstalledTimeRole, QByteArrayLiteral("installedTime")}, - {AppItem::LastLaunchedTimeRole, QByteArrayLiteral("lastLaunchedTime")}, - {AppItem::LaunchedTimesRole, QByteArrayLiteral("launchedTimes")}, - {AppItem::IsAutoStartRole, QByteArrayLiteral("autoStart")}, - {AppItem::VendorRole, QByteArrayLiteral("vendor")}, - {AppItem::GenericNameRole, QByteArrayLiteral("genericName")}, - {AppsModel::TransliteratedRole, QByteArrayLiteral("transliterated")} - }); - setItemRoleNames(defaultRoleNames); + connect(m_iconCacheWatcher, &DFileWatcherManager::fileModified, m_iconUpdateTimer, qOverload<>(&QTimer::start)); + connect(m_iconCacheWatcher, &DFileWatcherManager::fileAttributeChanged, m_iconUpdateTimer, qOverload<>(&QTimer::start)); + connect(m_iconUpdateTimer, &QTimer::timeout, this, &AppsModel::updateIconData); + connect(AppMgr::instance(), &AppMgr::pendingAppItemReady, this, [this](const QString &desktopId) { + if (!m_sourceModel) + return; - QList items(allAppInfosShouldBeShown()); - QList duplicatedItems = addItems(items); - Q_ASSERT(duplicatedItems.isEmpty()); - qDebug() << rowCount(); + for (int sourceRow = 0; sourceRow < m_sourceModel->rowCount(); ++sourceRow) { + const QModelIndex sourceIndex = m_sourceModel->index(sourceRow, 0); + if (normalizedDesktopId(sourceData(sourceIndex, DesktopIdRoleName).toString()) != desktopId) + continue; + if (!acceptsSourceIndex(sourceIndex)) + return; - m_tmUpdateCache->setInterval(1000); - m_tmUpdateCache->setSingleShot(true); + const QPersistentModelIndex persistentSourceIndex(sourceIndex); + if (m_rows.contains(persistentSourceIndex)) + return; - connect(AppMgr::instance(), &AppMgr::changed, m_tmUpdateCache, qOverload<>(&QTimer::start)); - connect(AppMgr::instance(), &AppMgr::itemDataChanged, this, [this](const QString &id) { - const auto appItem = this->itemFromDesktopId(id); - if (!appItem) { - qWarning() << "Not existing item in AppsModel for the desktopId" << id; + const int adapterRow = adapterRowForSourceRow(sourceRow); + beginInsertRows({}, adapterRow, adapterRow); + m_rows.insert(adapterRow, persistentSourceIndex); + endInsertRows(); return; } - updateAppItemFromAM(appItem); }); - - m_fwIconCache = new DFileWatcherManager(this); - const QStringList paths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); - const QString suffix("/icons/hicolor/icon-theme.cache"); - for (const QString &path : paths) { - if (QFileInfo::exists(path + suffix)) - m_fwIconCache->add(path + suffix); - } - - connect(m_fwIconCache, &DFileWatcherManager::fileModified, m_tmUpdateCache, qOverload<>(&QTimer::start)); - connect(m_fwIconCache, &DFileWatcherManager::fileAttributeChanged, m_tmUpdateCache, qOverload<>(&QTimer::start)); - connect(m_tmUpdateCache, &QTimer::timeout, this, &AppsModel::updateModelData); } -void AppsModel::appendRows(const QList items) +void AppsModel::setSourceModel(QAbstractItemModel *model) { - // TODO: preformance improvement? - for (AppItem * item : items) { - appendRow(item); - } -} + if (m_sourceModel == model && (model || m_rows.isEmpty())) + return; -AppItem *AppsModel::itemFromDesktopId(const QString freedesktopId) const -{ - QModelIndexList indexes = match(index(0, 0, QModelIndex()), - AppItem::DesktopIdRole, freedesktopId, 1, Qt::MatchExactly); + for (const auto &connection : std::as_const(m_sourceConnections)) + QObject::disconnect(connection); + m_sourceConnections.clear(); + AppMgr::instance()->clearPendingAppItems(); - if (indexes.isEmpty()) return nullptr; + setReady(false); + beginResetModel(); + m_sourceModel = model; + m_rows.clear(); + endResetModel(); + + if (!model) + return; - return static_cast(itemFromIndex(indexes.at(0))); + m_sourceConnections << connect(model, &QAbstractItemModel::dataChanged, this, &AppsModel::updateSourceRows); + m_sourceConnections << connect(model, &QAbstractItemModel::rowsInserted, this, &AppsModel::insertSourceRows); + m_sourceConnections << connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &AppsModel::beginRemoveSourceRows); + m_sourceConnections << connect(model, &QAbstractItemModel::rowsRemoved, this, &AppsModel::endRemoveSourceRows); + m_sourceConnections << connect(model, &QAbstractItemModel::rowsMoved, this, &AppsModel::rebuildRows); + m_sourceConnections << connect(model, &QAbstractItemModel::modelReset, this, &AppsModel::rebuildRows); + m_sourceConnections << connect(model, &QAbstractItemModel::layoutChanged, this, &AppsModel::rebuildRows); + m_sourceConnections << connect(model, &QObject::destroyed, this, [this](QObject *) { + setSourceModel(nullptr); + }); + rebuildRows(); } -// the model takes the ownership for the items that actually added to the model. -// won't try to update item if there are existing ones. -// return the duplicated ones -const QList AppsModel::addItems(const QList &items) +QModelIndex AppsModel::indexFromDesktopId(const QString &desktopId) const { - QList append; - QList duplicated; + if (rowCount() == 0) + return {}; + const auto matches = match(index(0, 0), AppsModel::DesktopIdRole, + normalizedDesktopId(desktopId), 1, Qt::MatchExactly); + return matches.value(0); +} - for (AppItem * item : items) { - if (itemFromDesktopId(item->freedesktopId()) != nullptr) { - duplicated.append(item); - } else { - append.append(item); - } - } +bool AppsModel::ready() const +{ + return m_ready; +} - appendRows(append); - return duplicated; +QVariantMap AppsModel::ddeCategories() const +{ + return m_ddeCategories; } -// try to update items, if not exist, add them to the model. -// the reference item that were used to update the existing one won't replace the existing one, thus -// they will be in the returned item list. -// return the ones were not added to the model. -const QList AppsModel::updateItems(const QList &items) +int AppsModel::rowCount(const QModelIndex &parent) const { - QList append; - QList duplicated; + return parent.isValid() ? 0 : m_rows.size(); +} - for (AppItem * item : items) { - AppItem * existing = itemFromDesktopId(item->freedesktopId()); - if (existing != nullptr) { - existing->updateData(item); - if (existing != item) { - duplicated.append(item); - } - } else { - append.append(item); - } - } +void AppsModel::setReady(bool ready) +{ + if (m_ready == ready) + return; + m_ready = ready; + Q_EMIT readyChanged(ready); +} - appendRows(append); - return duplicated; +void AppsModel::setDdeCategories(const QVariantMap &categories) +{ + if (m_ddeCategories == categories) + return; + m_ddeCategories = categories; + Q_EMIT ddeCategoriesChanged(); } QVariant AppsModel::data(const QModelIndex &index, int role) const { + if (!index.isValid()) + return {}; + + if (index.row() < 0 || index.row() >= m_rows.size()) + return {}; + const QModelIndex sourceIndex = m_rows.at(index.row()); + if (!sourceIndex.isValid()) + return {}; + switch (role) { + case Qt::DisplayRole: + case AppsModel::NameRole: + return sourceData(sourceIndex, NameRoleName); + case AppsModel::DesktopIdRole: + return normalizedDesktopId(sourceData(sourceIndex, DesktopIdRoleName).toString()); + case AppsModel::CategoriesRole: + return sourceData(sourceIndex, CategoriesRoleName); + case AppsModel::DDECategoryRole: + return sourceData(sourceIndex, DDECategoryRoleName); + case AppsModel::IconNameRole: { + const QString iconName = sourceData(sourceIndex, IconNameRoleName).toString(); + if (iconName.isEmpty()) + return QStringLiteral("application-x-desktop"); + return QFileInfo(iconName).isAbsolute() ? QUrl::fromLocalFile(iconName).toString() : iconName; + } + case AppsModel::InstalledTimeRole: + return sourceData(sourceIndex, InstalledTimeRoleName); + case AppsModel::LastLaunchedTimeRole: + return sourceData(sourceIndex, LastLaunchedTimeRoleName); + case AppsModel::LaunchedTimesRole: + return sourceData(sourceIndex, LaunchedTimesRoleName); + case AppsModel::IsAutoStartRole: + return sourceData(sourceIndex, AutoStartRoleName); + case AppsModel::VendorRole: + return sourceData(sourceIndex, VendorRoleName); + case AppsModel::GenericNameRole: + return sourceData(sourceIndex, GenericNameRoleName); case AppsModel::TransliteratedRole: { - // TODO: 1. use icu::Transliterator for other locales - // 2. support polyphonic characters (e.g. Music: YinYue or YinLe) - const auto decodedDisplay = Dtk::Core::pinyin(index.data(Qt::DisplayRole).toString(), Dtk::Core::TS_NoneTone); - if (decodedDisplay.isEmpty()) return QString(); + const auto decodedDisplay = Dtk::Core::pinyin(data(index, Qt::DisplayRole).toString(), Dtk::Core::TS_NoneTone); + if (decodedDisplay.isEmpty()) + return QString(); const QString transliterated = decodedDisplay.constFirst(); - if (transliterated.isEmpty()) return transliterated; - const QChar & firstChar = transliterated.constData()[0]; - if (firstChar.isDigit()) return QString("#%1").arg(transliterated); - else if (!firstChar.isLetter()) return QString("&%1").arg(transliterated); - return transliterated; - } - case AppsModel::AllTransliteratedRole: { - // it's useful to search(e.g. Music: YinYue or YinLe -> YinYueYinLe) - const auto decodedDisplay = Dtk::Core::pinyin(index.data(Qt::DisplayRole).toString(), Dtk::Core::TS_NoneTone); - const QString &transliterated = decodedDisplay.join("."); + if (transliterated.isEmpty()) + return transliterated; + const QChar firstChar = transliterated.at(0); + if (firstChar.isDigit()) + return QStringLiteral("#") + transliterated; + if (!firstChar.isLetter()) + return QStringLiteral("&") + transliterated; return transliterated; } + case AppsModel::AllTransliteratedRole: + return Dtk::Core::pinyin(data(index, Qt::DisplayRole).toString(), Dtk::Core::TS_NoneTone).join('.'); default: - break; + return {}; } +} - return QStandardItemModel::data(index, role); +QHash AppsModel::roleNames() const +{ + return { + { Qt::DisplayRole, QByteArrayLiteral("display") }, + { AppsModel::DesktopIdRole, QByteArrayLiteral("desktopId") }, + { AppsModel::CategoriesRole, QByteArrayLiteral("categories") }, + { AppsModel::DDECategoryRole, QByteArrayLiteral("category") }, + { AppsModel::IconNameRole, QByteArrayLiteral("iconName") }, + { AppsModel::NameRole, QByteArrayLiteral("name") }, + { AppsModel::InstalledTimeRole, QByteArrayLiteral("installedTime") }, + { AppsModel::LastLaunchedTimeRole, QByteArrayLiteral("lastLaunchedTime") }, + { AppsModel::LaunchedTimesRole, QByteArrayLiteral("launchedTimes") }, + { AppsModel::IsAutoStartRole, QByteArrayLiteral("autoStart") }, + { AppsModel::VendorRole, QByteArrayLiteral("vendor") }, + { AppsModel::GenericNameRole, QByteArrayLiteral("genericName") }, + { AppsModel::TransliteratedRole, QByteArrayLiteral("transliterated") }, + { AppsModel::AllTransliteratedRole, QByteArrayLiteral("allTransliterated") }, + }; } -void AppsModel::updateModelData() +bool AppsModel::acceptsSourceIndex(const QModelIndex &sourceIndex) const { - qCDebug(logModels) << "Updating model data"; - IconUtils::tryUpdateIconCache(); + if (!sourceIndex.isValid()) + return false; - beginResetModel(); - qCInfo(logModels) << "Resetting model"; + const QString desktopId = normalizedDesktopId(sourceData(sourceIndex, DesktopIdRoleName).toString()); + if (desktopId.isEmpty() || m_excludedAppIdList.contains(desktopId)) + return false; - QList items(allAppInfosShouldBeShown()); - cleanUpInvalidApps(items); - QList duplicatedItems = updateItems(items); - for (AppItem * item : std::as_const(duplicatedItems)) { - delete item; + return !sourceData(sourceIndex, NoDisplayRoleName).toBool(); +} + +int AppsModel::sourceRole(const QByteArray &name) const +{ + return m_sourceModel ? m_sourceModel->roleNames().key(name, -1) : -1; +} + +QVariant AppsModel::sourceData(const QModelIndex &sourceIndex, const QByteArray &roleName) const +{ + const int role = sourceRole(roleName); + return role >= 0 && m_sourceModel ? m_sourceModel->data(sourceIndex, role) : QVariant(); +} + +QList AppsModel::mappedRoles(const QList &sourceRoles) const +{ + if (sourceRoles.isEmpty()) + return {}; + + QList result; + const auto appendIfChanged = [this, &sourceRoles, &result](int targetRole, const QByteArray &sourceName) { + if (sourceRoles.contains(sourceRole(sourceName))) + result.append(targetRole); + }; + + appendIfChanged(AppsModel::DesktopIdRole, DesktopIdRoleName); + appendIfChanged(AppsModel::CategoriesRole, CategoriesRoleName); + appendIfChanged(AppsModel::DDECategoryRole, DDECategoryRoleName); + appendIfChanged(AppsModel::IconNameRole, IconNameRoleName); + appendIfChanged(AppsModel::InstalledTimeRole, InstalledTimeRoleName); + appendIfChanged(AppsModel::LastLaunchedTimeRole, LastLaunchedTimeRoleName); + appendIfChanged(AppsModel::LaunchedTimesRole, LaunchedTimesRoleName); + appendIfChanged(AppsModel::IsAutoStartRole, AutoStartRoleName); + appendIfChanged(AppsModel::VendorRole, VendorRoleName); + appendIfChanged(AppsModel::GenericNameRole, GenericNameRoleName); + + if (sourceRoles.contains(sourceRole(NameRoleName))) { + result << Qt::DisplayRole << AppsModel::NameRole << AppsModel::TransliteratedRole << AppsModel::AllTransliteratedRole; } + QList uniqueRoles; + for (int role : std::as_const(result)) { + if (!uniqueRoles.contains(role)) + uniqueRoles.append(role); + } + return uniqueRoles; +} - endResetModel(); - qCInfo(logModels) << "Model data updated"; +QString AppsModel::normalizedDesktopId(const QString &sourceId) const +{ + if (sourceId.isEmpty() || sourceId.endsWith(QLatin1String(".desktop")) || sourceId.startsWith(QLatin1String("internal/"))) + return sourceId; + return sourceId + QStringLiteral(".desktop"); +} + +int AppsModel::adapterRowForSourceRow(int sourceRow) const +{ + int row = 0; + while (row < m_rows.size() && m_rows.at(row).row() < sourceRow) + ++row; + return row; +} + +void AppsModel::insertSourceRows(const QModelIndex &parent, int first, int last) +{ + if (parent.isValid() || !m_sourceModel) + return; + + QList insertedRows; + for (int sourceRow = first; sourceRow <= last; ++sourceRow) { + const QModelIndex sourceIndex = m_sourceModel->index(sourceRow, 0); + if (acceptsSourceIndex(sourceIndex) && !shouldDelaySourceIndex(sourceIndex)) + insertedRows.append(sourceIndex); + } + if (insertedRows.isEmpty()) + return; + + const int adapterRow = adapterRowForSourceRow(first); + beginInsertRows({}, adapterRow, adapterRow + insertedRows.size() - 1); + for (int offset = 0; offset < insertedRows.size(); ++offset) + m_rows.insert(adapterRow + offset, insertedRows.at(offset)); + endInsertRows(); +} + +void AppsModel::beginRemoveSourceRows(const QModelIndex &parent, int first, int last) +{ + if (parent.isValid()) + return; + + for (int sourceRow = first; sourceRow <= last; ++sourceRow) { + const QModelIndex sourceIndex = m_sourceModel->index(sourceRow, 0); + AppMgr::instance()->cancelPendingAppItem( + normalizedDesktopId(sourceData(sourceIndex, DesktopIdRoleName).toString())); + } + + const int adapterFirst = adapterRowForSourceRow(first); + int count = 0; + while (adapterFirst + count < m_rows.size() && m_rows.at(adapterFirst + count).row() <= last) + ++count; + if (count == 0) + return; + + beginRemoveRows({}, adapterFirst, adapterFirst + count - 1); + m_rows.remove(adapterFirst, count); + m_sourceRowsRemovalActive = true; } -// the caller manage the return values' ownership (i.e. might need to free them) -QList AppsModel::allAppInfosShouldBeShown() const +void AppsModel::endRemoveSourceRows(const QModelIndex &parent, int first, int last) { - QList items; - const auto list = AppMgr::instance()->allAppInfosShouldBeShown(); - for (auto appItem : list) { - if (m_excludedAppIdList.contains(appItem->id)) { + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) + if (!m_sourceRowsRemovalActive) + return; + + m_sourceRowsRemovalActive = false; + endRemoveRows(); +} + +void AppsModel::updateSourceRows(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles) +{ + if (topLeft.parent().isValid() || bottomRight.parent().isValid() || !m_sourceModel) + return; + + const int desktopIdRole = sourceRole(DesktopIdRoleName); + const int noDisplayRole = sourceRole(NoDisplayRoleName); + const bool mayChangeMembership = roles.isEmpty() || roles.contains(desktopIdRole) || roles.contains(noDisplayRole); + const QList targetRoles = mappedRoles(roles); + + for (int sourceRow = topLeft.row(); sourceRow <= bottomRight.row(); ++sourceRow) { + const QModelIndex sourceIndex = m_sourceModel->index(sourceRow, 0); + const QPersistentModelIndex persistentSourceIndex(sourceIndex); + const int currentRow = m_rows.indexOf(persistentSourceIndex); + const bool accepted = acceptsSourceIndex(sourceIndex); + + const QString desktopId = normalizedDesktopId(sourceData(sourceIndex, DesktopIdRoleName).toString()); + const bool pending = AppMgr::instance()->isPendingAppItem(desktopId); + if ((mayChangeMembership || pending) && accepted && currentRow < 0) { + if (shouldDelaySourceIndex(sourceIndex)) + continue; + const int adapterRow = adapterRowForSourceRow(sourceRow); + beginInsertRows({}, adapterRow, adapterRow); + m_rows.insert(adapterRow, persistentSourceIndex); + endInsertRows(); + continue; + } + if (mayChangeMembership && !accepted && currentRow >= 0) { + beginRemoveRows({}, currentRow, currentRow); + m_rows.removeAt(currentRow); + endRemoveRows(); continue; } - auto item = new AppItem(appItem->id); - item->setName(appItem->name); - item->setDisplayName(appItem->displayName); - item->setIconName(appItem->iconName); - item->setCategories(appItem->categories); - item->setDDECategory(AppItem::DDECategories(CategoryUtils::parseBestMatchedCategory(appItem->categories))); - item->setInstalledTime(appItem->installedTime); - item->setLastLaunchedTime(appItem->lastLaunchedTime); - item->setLaunchedTimes(appItem->launchedTimes); - item->setIsAutoStart(appItem->isAutoStart); - item->setVendor(appItem->vendor); - item->setGenericName(appItem->genericName); - items.append(item); + if (accepted && currentRow >= 0 && (roles.isEmpty() || !targetRoles.isEmpty())) + Q_EMIT dataChanged(index(currentRow, 0), index(currentRow, 0), targetRoles); } - return items; } -// remove apps that are not in the \l knownExistedApps list -void AppsModel::cleanUpInvalidApps(const QList knownExistedApps) +void AppsModel::rebuildRows() { - QSet existedApps; - for (const AppItem * app : knownExistedApps) { - existedApps.insert(app->freedesktopId()); - } - for (int i = rowCount() - 1; i >= 0; i--) { - const QString & appId(data(index(i, 0), AppItem::DesktopIdRole).toString()); - if (!existedApps.contains(appId)) { - removeRow(i); + beginResetModel(); + m_rows.clear(); + if (m_sourceModel) { + for (int row = 0; row < m_sourceModel->rowCount(); ++row) { + const QModelIndex sourceIndex = m_sourceModel->index(row, 0); + const QString desktopId = normalizedDesktopId(sourceData(sourceIndex, DesktopIdRoleName).toString()); + if (acceptsSourceIndex(sourceIndex) && !AppMgr::instance()->isPendingAppItem(desktopId)) + m_rows.append(sourceIndex); } } + endResetModel(); +} + +bool AppsModel::shouldDelaySourceIndex(const QModelIndex &sourceIndex) const +{ + const QString desktopId = normalizedDesktopId(sourceData(sourceIndex, DesktopIdRoleName).toString()); + const QString iconName = sourceData(sourceIndex, IconNameRoleName).toString(); + return AppMgr::instance()->waitForIcon(desktopId, iconName); +} + +void AppsModel::updateIconData() +{ + IconUtils::tryUpdateIconCache(); + if (rowCount() > 0) + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), { AppsModel::IconNameRole }); } diff --git a/src/models/appsmodel.h b/src/models/appsmodel.h index 2f8c40b2..5ca0a80d 100644 --- a/src/models/appsmodel.h +++ b/src/models/appsmodel.h @@ -1,32 +1,46 @@ -// 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 #pragma once -#include "appitem.h" - #include -#include -#include +#include +#include +#include +#include +#include namespace Dtk::Core { -class DConfig; class DFileWatcherManager; } -// List of applications and nothing else. -// Always in a single column so it's a one dimension model. -class AppsModel : public QStandardItemModel +class QTimer; + +// Adapts dde-shell's application model to the roles and desktop IDs expected by launchpad. +class AppsModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged FINAL) + Q_PROPERTY(QVariantMap ddeCategories READ ddeCategories NOTIFY ddeCategoriesChanged FINAL) QML_NAMED_ELEMENT(AppsModel) QML_SINGLETON + public: enum Roles { - TransliteratedRole = AppItem::ModelExtendedRole, + DesktopIdRole = Qt::UserRole, + CategoriesRole, + DDECategoryRole, + IconNameRole, + NameRole, + InstalledTimeRole, + LastLaunchedTimeRole, + LaunchedTimesRole, + IsAutoStartRole, + VendorRole, + GenericNameRole, + TransliteratedRole = 0x1000, AllTransliteratedRole, - NameRole = AppItem::NameRole, ProxyModelExtendedRole = 0x10000 }; Q_ENUM(Roles) @@ -44,25 +58,47 @@ class AppsModel : public QStandardItemModel return &instance(); } - void appendRows(const QList items); + void setSourceModel(QAbstractItemModel *sourceModel); + QModelIndex indexFromDesktopId(const QString &desktopId) const; + bool ready() const; + QVariantMap ddeCategories() const; - AppItem * itemFromDesktopId(const QString freedesktopId) const; - [[nodiscard("might need to free them")]] const QList addItems(const QList &items); - [[nodiscard("might need to free them")]] const QList updateItems(const QList &items); - - // QAbstractItemModel interface + int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; -private slots: - void updateModelData(); + QHash roleNames() const override; + +public Q_SLOTS: + void setReady(bool ready); + void setDdeCategories(const QVariantMap &categories); + +Q_SIGNALS: + void readyChanged(bool ready); + void ddeCategoriesChanged(); private: explicit AppsModel(QObject *parent = nullptr); - QList allAppInfosShouldBeShown() const; - void cleanUpInvalidApps(const QList knownExistedApps); + int sourceRole(const QByteArray &name) const; + QVariant sourceData(const QModelIndex &sourceIndex, const QByteArray &roleName) const; + QList mappedRoles(const QList &sourceRoles) const; + QString normalizedDesktopId(const QString &sourceId) const; + int adapterRowForSourceRow(int sourceRow) const; + void insertSourceRows(const QModelIndex &parent, int first, int last); + void beginRemoveSourceRows(const QModelIndex &parent, int first, int last); + void endRemoveSourceRows(const QModelIndex &parent, int first, int last); + void updateSourceRows(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles); + void rebuildRows(); + bool acceptsSourceIndex(const QModelIndex &sourceIndex) const; + bool shouldDelaySourceIndex(const QModelIndex &sourceIndex) const; + void updateIconData(); - Dtk::Core::DConfig * m_dconfig; QStringList m_excludedAppIdList; - Dtk::Core::DFileWatcherManager *m_fwIconCache = nullptr; - QTimer *m_tmUpdateCache = nullptr; + Dtk::Core::DFileWatcherManager *m_iconCacheWatcher = nullptr; + QTimer *m_iconUpdateTimer = nullptr; + QPointer m_sourceModel; + QList m_rows; + QList m_sourceConnections; + QVariantMap m_ddeCategories; + bool m_sourceRowsRemovalActive = false; + bool m_ready = false; }; diff --git a/src/models/categorizedsortproxymodel.cpp b/src/models/categorizedsortproxymodel.cpp index 20a4f528..e9dc7aa6 100644 --- a/src/models/categorizedsortproxymodel.cpp +++ b/src/models/categorizedsortproxymodel.cpp @@ -19,19 +19,14 @@ void CategorizedSortProxyModel::setCategoryType(CategoryType categoryType) { CategoryType oldCategoryType = this->categoryType(); - // Temporarily disable dynamic sort to prevent setSortRole from triggering - // a redundant sort. We trigger a single sort below via sort(0), - // which uses layoutAboutToBeChanged/layoutChanged instead of modelReset, - // preserving delegates. - const bool wasDynamic = dynamicSortFilter(); - setDynamicSortFilter(false); isFreeSort = (categoryType == FreeCategory); + int targetSortRole = sortRole(); switch (categoryType) { case Alphabetary: - setSortRole(AppsModel::TransliteratedRole); + targetSortRole = AppsModel::TransliteratedRole; break; case DDECategory: - setSortRole(AppItem::DDECategoryRole); + targetSortRole = AppsModel::DDECategoryRole; break; default: break; @@ -42,22 +37,23 @@ void CategorizedSortProxyModel::setCategoryType(CategoryType categoryType) config->setValue("categoryType", categoryType); } - // Use sort(0) instead of setDynamicSortFilter(true) because the latter - // calls d->sort() without setting proxy_sort_column, leaving it at -1 - // (the Qt 6 default). When source_sort_column is -1, - // QSortFilterProxyModelPrivate::sort_source_rows falls through to - // std::less{} (a no-op), so no sorting actually occurs. - // sort(0) properly sets proxy_sort_column = 0 and calls - // update_source_sort_column(), then d->sort() emits - // layoutAboutToBeChanged/layoutChanged so the view moves existing - // delegates instead of destroying and recreating them. - sort(0); - setDynamicSortFilter(wasDynamic); - - // Must update sectionRoleName after the sort so that the QML ListView - // evaluates section structure with the correct (already sorted) item order. - m_sectionRoleName = sortRoleName(); - emit sectionRoleNameChanged(); + // Update the section criteria before sorting. QQuickListView recalculates + // sections synchronously, while layout changes are applied during polish. + // Publishing the role first avoids recalculating sections against pending + // layout changes. + const QString targetSectionRoleName = QString(sourceModel()->roleNames().value(targetSortRole)); + if (m_sectionRoleName != targetSectionRoleName) { + m_sectionRoleName = targetSectionRoleName; + emit sectionRoleNameChanged(); + } + + if (sortRole() != targetSortRole) + setSortRole(targetSortRole); + + // The proxy starts with no sort column. Establish it once; subsequent + // setSortRole() calls perform the single required layout update. + if (sortColumn() < 0) + sort(0); qCInfo(logModels) << "Category type changed to:" << categoryType; emit categoryTypeChanged(); @@ -106,7 +102,7 @@ QList CategorizedSortProxyModel::DDECategorySections() const QSet ddeCategorySet; for (int i = 0; i < rowCount(); i++) { - auto value = data(index(i, 0), AppItem::DDECategoryRole); + auto value = data(index(i, 0), AppsModel::DDECategoryRole); if (value.isValid()) { ddeCategorySet.insert(value.toInt()); } diff --git a/src/models/favoritedproxymodel.cpp b/src/models/favoritedproxymodel.cpp index fda5bec4..ff0ac8a1 100644 --- a/src/models/favoritedproxymodel.cpp +++ b/src/models/favoritedproxymodel.cpp @@ -1,9 +1,8 @@ -// 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 "favoritedproxymodel.h" -#include "appitem.h" #include "appsmodel.h" #include @@ -68,13 +67,13 @@ bool FavoritedProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sou { QModelIndex modelIndex = this->sourceModel()->index(sourceRow, 0, sourceParent); - return m_favoritedAppIds.contains(modelIndex.data(AppItem::DesktopIdRole).toString()); + return m_favoritedAppIds.contains(modelIndex.data(AppsModel::DesktopIdRole).toString()); } bool FavoritedProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { - const int leftIndex = m_favoritedAppIds.indexOf(source_left.data(AppItem::DesktopIdRole).toString()); - const int rightIndex = m_favoritedAppIds.indexOf(source_right.data(AppItem::DesktopIdRole).toString()); + const int leftIndex = m_favoritedAppIds.indexOf(source_left.data(AppsModel::DesktopIdRole).toString()); + const int rightIndex = m_favoritedAppIds.indexOf(source_right.data(AppsModel::DesktopIdRole).toString()); return leftIndex < rightIndex; } diff --git a/src/models/itemarrangementproxymodel.cpp b/src/models/itemarrangementproxymodel.cpp index 56ab91c4..e90424bc 100644 --- a/src/models/itemarrangementproxymodel.cpp +++ b/src/models/itemarrangementproxymodel.cpp @@ -5,7 +5,6 @@ #include "itemarrangementproxymodel.h" #include "appsmodel.h" -#include "categoryutils.h" #include #include @@ -41,7 +40,7 @@ void ItemArrangementProxyModel::updateFolderName(int folderId, const QString &na ItemsPage * folder = folderById(folderId); folder->setName(name); - QModelIndexList matched = match(mapFromSource(m_folderModel.index(0, 0)), AppItem::DesktopIdRole, QString("internal/folders/%1").arg(folderId)); + QModelIndexList matched = match(mapFromSource(m_folderModel.index(0, 0)), AppsModel::DesktopIdRole, QString("internal/folders/%1").arg(folderId)); Q_ASSERT(!matched.isEmpty()); emit dataChanged(matched.constFirst(), matched.constFirst(), { Qt::DisplayRole }); @@ -161,10 +160,10 @@ void ItemArrangementProxyModel::commitDndOperation(const QString &dragId, const ItemsPage * dstFolder = createFolder(dstFolderId); qCDebug(logModels) << "Appending items to new folder page:" << dropId << dragId; dstFolder->appendPage({dropId, dragId}); - AppItem * dropItem = AppsModel::instance().itemFromDesktopId(dropId); - if (dropItem) { - AppItem::DDECategories dropCategories = AppItem::DDECategories(CategoryUtils::parseBestMatchedCategory(dropItem->categories())); - QString folderName = "internal/category/" + QString::number(dropCategories); + const QModelIndex dropItem = AppsModel::instance().indexFromDesktopId(dropId); + if (dropItem.isValid()) { + const int dropCategory = dropItem.data(AppsModel::DDECategoryRole).toInt(); + QString folderName = "internal/category/" + QString::number(dropCategory); qCDebug(logModels) << "New folder name:" << folderName; dstFolder->setName(folderName); } @@ -218,7 +217,7 @@ QVariant ItemArrangementProxyModel::data(const QModelIndex &index, int role) con if (idx < 0) { // regular applications, not a folder - QString id(data(index, AppItem::DesktopIdRole).toString()); + QString id(data(index, AppsModel::DesktopIdRole).toString()); if (id.isEmpty() || id.contains("internal")) { qCWarning(logModels) << "Invalid or internal ID:" << id << "index:" << index << "row:" << index.row() << "role:" << role; } @@ -240,7 +239,7 @@ QVariant ItemArrangementProxyModel::data(const QModelIndex &index, int role) con } else { // a folder QModelIndex srcIdx = mapToSource(index); - QString id = m_folderModel.itemFromIndex(srcIdx)->data(AppItem::DesktopIdRole).toString(); + QString id = m_folderModel.itemFromIndex(srcIdx)->data(AppsModel::DesktopIdRole).toString(); int folder, page, pos; if (role >= AppsModel::ProxyModelExtendedRole && role != IconsNameRole) { std::tie(folder, page, pos) = findItem(id, true); @@ -249,9 +248,9 @@ QVariant ItemArrangementProxyModel::data(const QModelIndex &index, int role) con switch (role) { case Qt::DisplayRole: return m_folders.value(id)->name(); - case AppItem::DesktopIdRole: + case AppsModel::DesktopIdRole: return id; - case AppItem::IsAutoStartRole: + case AppsModel::IsAutoStartRole: return false; case PageRole: return page; @@ -263,10 +262,9 @@ QVariant ItemArrangementProxyModel::data(const QModelIndex &index, int role) con const QStringList desktopIds = m_folders.value(id)->firstNItems(4); QStringList icons; for (const QString & id : desktopIds) { - AppItem * item = AppsModel::instance().itemFromDesktopId(id); - if (item) { - icons.append(item->iconName()); - } + const QModelIndex item = AppsModel::instance().indexFromDesktopId(id); + if (item.isValid()) + icons.append(item.data(AppsModel::IconNameRole).toString()); } return icons;//QStringList({"deepin-music"}); } @@ -299,6 +297,11 @@ ItemArrangementProxyModel::ItemArrangementProxyModel(QObject *parent) connect(&AppsModel::instance(), &AppsModel::rowsInserted, this, &ItemArrangementProxyModel::onSourceModelChanged); connect(&AppsModel::instance(), &AppsModel::rowsRemoved, this, &ItemArrangementProxyModel::onSourceModelChanged); + connect(&AppsModel::instance(), &AppsModel::modelReset, this, &ItemArrangementProxyModel::onSourceModelChanged); + connect(&AppsModel::instance(), &AppsModel::readyChanged, this, [this](bool ready) { + if (ready) + onSourceModelChanged(); + }); connect(&m_folderModel, &QStandardItemModel::rowsInserted, this, &ItemArrangementProxyModel::onFolderModelChanged); connect(&m_folderModel, &QStandardItemModel::rowsRemoved, this, &ItemArrangementProxyModel::onFolderModelChanged); @@ -351,7 +354,7 @@ void ItemArrangementProxyModel::saveItemArrangementToUserData() itemArrangementSettings.endGroup(); for (int i = 0; i < m_folderModel.rowCount(); i++) { - const QString & id = m_folderModel.index(i, 0).data(AppItem::DesktopIdRole).toString(); + const QString & id = m_folderModel.index(i, 0).data(AppsModel::DesktopIdRole).toString(); itemArrangementSettings.beginGroup("fullscreen/" + id.mid(17)); ItemsPage * page = m_folders.value(id); int pageCount = page->pageCount(); @@ -375,7 +378,7 @@ std::tuple ItemArrangementProxyModel::findItem(const QString &id, if (!searchTopLevelOnly) { for (int i = 0; i < m_folderModel.rowCount(); i++) { - const QString & folderId = m_folderModel.index(i, 0).data(AppItem::DesktopIdRole).toString(); + const QString & folderId = m_folderModel.index(i, 0).data(AppsModel::DesktopIdRole).toString(); std::tie(page, idx) = m_folders[folderId]->findItem(id); if (page != -1) { return std::make_tuple(QStringView{folderId}.mid(17).toInt(), page, idx); @@ -388,10 +391,13 @@ std::tuple ItemArrangementProxyModel::findItem(const QString &id, void ItemArrangementProxyModel::onSourceModelChanged() { + if (!AppsModel::instance().ready()) + return; + QSet appDesktopIdSet; int appsCount = AppsModel::instance().rowCount(); for (int i = 0; i < appsCount; i++) { - QString desktopId(AppsModel::instance().data(AppsModel::instance().index(i, 0), AppItem::DesktopIdRole).toString()); + QString desktopId(AppsModel::instance().data(AppsModel::instance().index(i, 0), AppsModel::DesktopIdRole).toString()); appDesktopIdSet.insert(desktopId); int folder; std::tie(folder, std::ignore, std::ignore) = findItem(desktopId); @@ -426,7 +432,7 @@ void ItemArrangementProxyModel::onSourceModelChanged() m_topLevel->removeItemsNotIn(appDesktopIdSet); for (int i = m_folderModel.rowCount() - 1; i >= 0 ; i--) { - const QString & folderId = m_folderModel.index(i, 0).data(AppItem::DesktopIdRole).toString(); + const QString & folderId = m_folderModel.index(i, 0).data(AppsModel::DesktopIdRole).toString(); m_folders.value(folderId)->removeItemsNotIn(appDesktopIdSet); if (m_folders.value(folderId)->pageCount() == 0) { removeFolder(QString(folderId).remove("internal/folders/")); @@ -475,7 +481,7 @@ ItemsPage *ItemArrangementProxyModel::createFolder(const QString &id) ItemsPage * page = new ItemsPage(4 * 3, this); m_folders.insert(fullId, page); QStandardItem * folder = new QStandardItem(fullId); - folder->setData(fullId, AppItem::DesktopIdRole); + folder->setData(fullId, AppsModel::DesktopIdRole); m_folderModel.appendRow(folder); connect(page, &ItemsPage::pageCountChanged, this, [this, fullId]() { diff --git a/src/models/itemarrangementproxymodel.h b/src/models/itemarrangementproxymodel.h index 5b70564e..1ed98b27 100644 --- a/src/models/itemarrangementproxymodel.h +++ b/src/models/itemarrangementproxymodel.h @@ -10,6 +10,7 @@ #include "appsmodel.h" #include +#include class ItemArrangementProxyModel : public QConcatenateTablesProxyModel { diff --git a/src/models/recentlyinstalledproxymodel.cpp b/src/models/recentlyinstalledproxymodel.cpp index e1d43f98..0768c226 100644 --- a/src/models/recentlyinstalledproxymodel.cpp +++ b/src/models/recentlyinstalledproxymodel.cpp @@ -1,10 +1,8 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #include "recentlyinstalledproxymodel.h" -#include "appitem.h" -#include "appsmodel.h" #include #include diff --git a/src/models/searchfilterproxymodel.cpp b/src/models/searchfilterproxymodel.cpp index 4cec35fd..548e17b6 100644 --- a/src/models/searchfilterproxymodel.cpp +++ b/src/models/searchfilterproxymodel.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 @@ -66,8 +66,8 @@ bool SearchFilterProxyModel::lessThan(const QModelIndex &source_left, const QMod } // 索引相同时,按启动次数排序:高频使用 > 低频使用 - int leftLaunchedTimes = source_left.data(AppItem::LaunchedTimesRole).toInt(); - int rightLaunchedTimes = source_right.data(AppItem::LaunchedTimesRole).toInt(); + int leftLaunchedTimes = source_left.data(AppsModel::LaunchedTimesRole).toInt(); + int rightLaunchedTimes = source_right.data(AppsModel::LaunchedTimesRole).toInt(); if (leftLaunchedTimes != rightLaunchedTimes) { bool result = leftLaunchedTimes < rightLaunchedTimes; @@ -87,13 +87,13 @@ int SearchFilterProxyModel::calculateWeight(const QModelIndex &modelIndex) const const QString & displayName = modelIndex.data(Qt::DisplayRole).toString(); const QString & name = modelIndex.data(AppsModel::NameRole).toString(); - const QString & vendor = modelIndex.data(AppItem::VendorRole).toString(); - const QString & genericName = modelIndex.data(AppItem::GenericNameRole).toString(); + const QString & vendor = modelIndex.data(AppsModel::VendorRole).toString(); + const QString & genericName = modelIndex.data(AppsModel::GenericNameRole).toString(); const QString & transliterated = modelIndex.data(AppsModel::AllTransliteratedRole).toString(); const QString & jianpin = Dtk::Core::firstLetters(displayName, TS_NoneTone).join(','); //包名搜索使用 - const QString & desktopId = modelIndex.data(AppItem::DesktopIdRole).toString(); + const QString & desktopId = modelIndex.data(AppsModel::DesktopIdRole).toString(); QString searchPatternDelBlank = searchPattern.pattern().toLower().remove(" "); diff --git a/tests/searchfilterproxymodeltest.cpp b/tests/searchfilterproxymodeltest.cpp index cc98f323..185c7aaa 100644 --- a/tests/searchfilterproxymodeltest.cpp +++ b/tests/searchfilterproxymodeltest.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,10 +12,25 @@ #include "../src/models/searchfilterproxymodel.h" #include "../src/models/appsmodel.h" -#include "../src/models/appitem.h" +#include "../src/models/categorizedsortproxymodel.h" namespace { Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +enum SourceRoles { + SourceDesktopIdRole = Qt::UserRole + 1, + SourceNameRole, + SourceIconNameRole, + SourceNoDisplayRole, + SourceDDECategoryRole, + SourceInstalledTimeRole, + SourceLastLaunchedTimeRole, + SourceLaunchedTimesRole, + SourceAutoStartRole, + SourceCategoriesRole, + SourceVendorRole, + SourceGenericNameRole, +}; } class TestSearchFilterProxyModel : public QObject @@ -32,15 +47,19 @@ private slots: void testJianpinSearch(); void testSorting(); void testSpecialCharacters(); + void testSharedModelAdapter(); + void testCategorySortSignalOrder(); private: void setupTestData(); - AppItem* createTestAppItem(const QString &desktopId, - const QString &name, - const QString &displayName, - const QString &genericName = QString(), - const QString &vendor = QString(), - int launchedTimes = 0); + QStandardItem *createTestAppItem(const QString &desktopId, + const QString &name, + const QString &displayName, + const QString &genericName = QString(), + const QString &vendor = QString(), + int launchedTimes = 0); + + QStandardItemModel m_sourceModel; }; void TestSearchFilterProxyModel::initTestCase() @@ -61,12 +80,24 @@ void TestSearchFilterProxyModel::cleanupTestCase() void TestSearchFilterProxyModel::setupTestData() { qCDebug(logTest) << "Setting up test data for SearchFilterProxyModel"; - // 清空当前模型数据 - AppsModel::instance().clear(); - qCDebug(logTest) << "Cleared AppsModel instance"; + m_sourceModel.clear(); + m_sourceModel.setItemRoleNames({ + { SourceDesktopIdRole, QByteArrayLiteral("desktopId") }, + { SourceNameRole, QByteArrayLiteral("name") }, + { SourceIconNameRole, QByteArrayLiteral("iconName") }, + { SourceNoDisplayRole, QByteArrayLiteral("noDisplay") }, + { SourceDDECategoryRole, QByteArrayLiteral("ddeCategory") }, + { SourceInstalledTimeRole, QByteArrayLiteral("installedTime") }, + { SourceLastLaunchedTimeRole, QByteArrayLiteral("lastLaunchedTime") }, + { SourceLaunchedTimesRole, QByteArrayLiteral("launchedTimes") }, + { SourceAutoStartRole, QByteArrayLiteral("autoStart") }, + { SourceCategoriesRole, QByteArrayLiteral("categories") }, + { SourceVendorRole, QByteArrayLiteral("vendor") }, + { SourceGenericNameRole, QByteArrayLiteral("genericName") }, + }); // 创建测试数据 - QList testItems; + QList testItems; // 英文应用 testItems.append(createTestAppItem("org.deepin.calculator", "Calculator", "Calculator", "Calculator", "deepin", 10)); @@ -99,31 +130,29 @@ void TestSearchFilterProxyModel::setupTestData() testItems.append(createTestAppItem("org.special.app14", "App*Name", "App*Name", "", "special", 14)); // 添加到模型 - AppsModel::instance().appendRows(testItems); + for (auto item : std::as_const(testItems)) + m_sourceModel.appendRow(item); + AppsModel::instance().setSourceModel(&m_sourceModel); + AppsModel::instance().setReady(true); qCInfo(logTest) << "Added" << testItems.size() << "test items to AppsModel"; } -AppItem* TestSearchFilterProxyModel::createTestAppItem(const QString &desktopId, - const QString &name, - const QString &displayName, - const QString &genericName, - const QString &vendor, - int launchedTimes) +QStandardItem *TestSearchFilterProxyModel::createTestAppItem(const QString &desktopId, + const QString &name, + const QString &displayName, + const QString &genericName, + const QString &vendor, + int launchedTimes) { qCDebug(logTest) << "Creating test app item:" << desktopId << "name:" << name << "launched times:" << launchedTimes; - AppItem *item = new AppItem(desktopId); - item->setName(name); - item->setDisplayName(displayName); - item->setIconName("application-default-icon"); - - if (!genericName.isEmpty()) - item->setGenericName(genericName); - - if (!vendor.isEmpty()) - item->setVendor(vendor); - - item->setLaunchedTimes(launchedTimes); - + auto item = new QStandardItem; + item->setData(desktopId, SourceDesktopIdRole); + item->setData(displayName.isEmpty() ? name : displayName, SourceNameRole); + item->setData(QStringLiteral("application-default-icon"), SourceIconNameRole); + item->setData(genericName, SourceGenericNameRole); + item->setData(vendor, SourceVendorRole); + item->setData(launchedTimes, SourceLaunchedTimesRole); + item->setData(launchedTimes % 11, SourceDDECategoryRole); return item; } @@ -144,21 +173,21 @@ void TestSearchFilterProxyModel::testBasicSearch() model.setFilterRegularExpression(QRegularExpression("Calculator")); qCDebug(logTest) << "Calculator search returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.calculator"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.calculator.desktop"); // 测试基本搜索 - 部分匹配 qCDebug(logTest) << "Testing partial match search for 'Text'"; model.setFilterRegularExpression(QRegularExpression("Text")); qCDebug(logTest) << "Text search returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.editor"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.editor.desktop"); // 测试基本搜索 - 不区分大小写 qCDebug(logTest) << "Testing case-insensitive search for 'calculator'"; model.setFilterRegularExpression(QRegularExpression("calculator")); qCDebug(logTest) << "Case-insensitive search returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.calculator"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.calculator.desktop"); // 测试基本搜索 - 多个结果 qCDebug(logTest) << "Testing multi-result search for 'e'"; @@ -178,14 +207,14 @@ void TestSearchFilterProxyModel::testChineseSearch() model.setFilterRegularExpression(QRegularExpression("音乐")); qCDebug(logTest) << "Chinese search for '音乐' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.music"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.music.desktop"); // 测试中文部分匹配 qCDebug(logTest) << "Testing Chinese partial match for '阅读'"; model.setFilterRegularExpression(QRegularExpression("阅读")); qCDebug(logTest) << "Chinese search for '阅读' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.reader"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.reader.desktop"); qCInfo(logTest) << "Chinese search tests completed successfully"; } @@ -199,26 +228,26 @@ void TestSearchFilterProxyModel::testPinyinSearch() model.setFilterRegularExpression(QRegularExpression("yinyue")); qCDebug(logTest) << "Pinyin search for 'yinyue' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.music"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.music.desktop"); // 测试拼音部分匹配 qCDebug(logTest) << "Testing Pinyin partial match for 'yuedu'"; model.setFilterRegularExpression(QRegularExpression("yuedu")); qCDebug(logTest) << "Pinyin search for 'yuedu' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.reader"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.reader.desktop"); // 测试拼音首字母匹配 qCDebug(logTest) << "Testing Pinyin initials match for 'rl'"; model.setFilterRegularExpression(QRegularExpression("rl")); qCDebug(logTest) << "Pinyin initials search for 'rl' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.calendar"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.calendar.desktop"); qCDebug(logTest) << "Testing Pinyin initials match for 'aqzx'"; model.setFilterRegularExpression(QRegularExpression("aqzx")); qCDebug(logTest) << "Pinyin initials search for 'aqzx' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.defender"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.defender.desktop"); qCInfo(logTest) << "Pinyin search tests completed successfully"; } @@ -232,14 +261,14 @@ void TestSearchFilterProxyModel::testJianpinSearch() model.setFilterRegularExpression(QRegularExpression("yy")); qCDebug(logTest) << "Jianpin search for 'yy' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.music"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.music.desktop"); // 测试简拼部分匹配 qCDebug(logTest) << "Testing Jianpin partial match for 'yd' (阅读)"; model.setFilterRegularExpression(QRegularExpression("yd")); qCDebug(logTest) << "Jianpin search for 'yd' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); - QCOMPARE(model.data(model.index(0, 0), AppItem::DesktopIdRole).toString(), "org.deepin.reader"); + QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.reader.desktop"); qCInfo(logTest) << "Jianpin search tests completed successfully"; } @@ -265,13 +294,13 @@ void TestSearchFilterProxyModel::testSorting() qCDebug(logTest) << "Checking first" << checkCount << "results for expected apps"; for (int i = 0; i < checkCount; ++i) { - QString desktopId = model.data(model.index(i, 0), AppItem::DesktopIdRole).toString(); + QString desktopId = model.data(model.index(i, 0), AppsModel::DesktopIdRole).toString(); qCDebug(logTest) << "Result" << i << ":" << desktopId; - if (desktopId == "org.deepin.browser") { + if (desktopId == "org.deepin.browser.desktop") { qCDebug(logTest) << "Found browser at position" << i; foundBrowser = true; - } else if (desktopId == "org.deepin.editor") { + } else if (desktopId == "org.deepin.editor.desktop") { qCDebug(logTest) << "Found editor at position" << i; foundEditor = true; } @@ -318,7 +347,7 @@ void TestSearchFilterProxyModel::testSpecialCharacters() QVERIFY(totalAppCount > 0); for (int i = 0; i < totalAppCount; ++i) { - QString desktopId = model.data(model.index(i, 0), AppItem::DesktopIdRole).toString(); + QString desktopId = model.data(model.index(i, 0), AppsModel::DesktopIdRole).toString(); qCDebug(logTest) << "Checking app at index" << i << ":" << desktopId; if (desktopId.startsWith("org.special.app")) { @@ -333,5 +362,75 @@ void TestSearchFilterProxyModel::testSpecialCharacters() qCInfo(logTest) << "Special characters search tests completed successfully"; } +void TestSearchFilterProxyModel::testSharedModelAdapter() +{ + auto &appsModel = AppsModel::instance(); + const int initialRowCount = appsModel.rowCount(); + QSignalSpy resetSpy(&appsModel, &QAbstractItemModel::modelReset); + QSignalSpy rowsInsertedSpy(&appsModel, &QAbstractItemModel::rowsInserted); + QSignalSpy rowsRemovedSpy(&appsModel, &QAbstractItemModel::rowsRemoved); + + auto hiddenItem = createTestAppItem("org.test.hidden", "Hidden", "Hidden"); + hiddenItem->setData(true, SourceNoDisplayRole); + m_sourceModel.appendRow(hiddenItem); + QCOMPARE(appsModel.rowCount(), initialRowCount); + QCOMPARE(rowsInsertedSpy.count(), 0); + + hiddenItem->setData(false, SourceNoDisplayRole); + QCOMPARE(appsModel.rowCount(), initialRowCount + 1); + QCOMPARE(rowsInsertedSpy.count(), 1); + QCOMPARE(appsModel.indexFromDesktopId("org.test.hidden.desktop").data(AppsModel::DesktopIdRole).toString(), + QStringLiteral("org.test.hidden.desktop")); + + auto suffixedItem = createTestAppItem("org.test.suffixed.desktop", "Suffixed", "Suffixed"); + m_sourceModel.appendRow(suffixedItem); + QCOMPARE(rowsInsertedSpy.count(), 2); + const QModelIndex suffixedIndex = appsModel.indexFromDesktopId("org.test.suffixed"); + QCOMPARE(suffixedIndex.data(AppsModel::DesktopIdRole).toString(), + QStringLiteral("org.test.suffixed.desktop")); + + QSignalSpy dataChangedSpy(&appsModel, &QAbstractItemModel::dataChanged); + suffixedItem->setData(QStringLiteral("Renamed"), SourceNameRole); + QCOMPARE(appsModel.indexFromDesktopId("org.test.suffixed.desktop").data(Qt::DisplayRole).toString(), + QStringLiteral("Renamed")); + QVERIFY(!dataChangedSpy.isEmpty()); + const auto changedRoles = dataChangedSpy.constLast().at(2).value>(); + QVERIFY(changedRoles.contains(Qt::DisplayRole)); + QVERIFY(changedRoles.contains(AppsModel::NameRole)); + QVERIFY(changedRoles.contains(AppsModel::TransliteratedRole)); + + m_sourceModel.removeRow(suffixedItem->row()); + QCOMPARE(rowsRemovedSpy.count(), 1); + m_sourceModel.removeRow(hiddenItem->row()); + QCOMPARE(rowsRemovedSpy.count(), 2); + QCOMPARE(appsModel.rowCount(), initialRowCount); + QCOMPARE(resetSpy.count(), 0); +} + +void TestSearchFilterProxyModel::testCategorySortSignalOrder() +{ + auto &model = CategorizedSortProxyModel::instance(); + model.setCategoryType(CategorizedSortProxyModel::DDECategory); + + QStringList signalOrder; + QObject connectionsGuard; + connect(&model, &CategorizedSortProxyModel::sectionRoleNameChanged, &connectionsGuard, + [&signalOrder] { signalOrder << QStringLiteral("section"); }); + connect(&model, &QAbstractItemModel::layoutAboutToBeChanged, &connectionsGuard, + [&signalOrder] { signalOrder << QStringLiteral("layoutAboutToChange"); }); + connect(&model, &QAbstractItemModel::layoutChanged, &connectionsGuard, + [&signalOrder] { signalOrder << QStringLiteral("layoutChanged"); }); + connect(&model, &CategorizedSortProxyModel::categoryTypeChanged, &connectionsGuard, + [&signalOrder] { signalOrder << QStringLiteral("category"); }); + + model.setCategoryType(CategorizedSortProxyModel::Alphabetary); + + QCOMPARE(model.sectionRoleName(), QStringLiteral("transliterated")); + QCOMPARE(signalOrder, QStringList({ QStringLiteral("section"), + QStringLiteral("layoutAboutToChange"), + QStringLiteral("layoutChanged"), + QStringLiteral("category") })); +} + QTEST_MAIN(TestSearchFilterProxyModel) -#include "searchfilterproxymodeltest.moc" \ No newline at end of file +#include "searchfilterproxymodeltest.moc"