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"