diff --git a/containments/homescreens/default/applicationlistmodel.cpp b/containments/homescreens/default/applicationlistmodel.cpp index a4519c24..ffcb0347 100644 --- a/containments/homescreens/default/applicationlistmodel.cpp +++ b/containments/homescreens/default/applicationlistmodel.cpp @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas - * SPDX-FileCopyrightText: 2022 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later -// Self #include "applicationlistmodel.h" -// Qt #include #include #include @@ -16,7 +11,6 @@ #include #include -// KDE #include #include #include @@ -25,11 +19,6 @@ #include #include -#include -#include -#include -#include - ApplicationListModel::ApplicationListModel(QObject *parent) : QAbstractListModel(parent) { @@ -72,12 +61,6 @@ QHash ApplicationListModel::roleNames() const {ApplicationLocationRole, QByteArrayLiteral("applicationLocation")}}; } -ApplicationListModel *ApplicationListModel::instance() -{ - static ApplicationListModel *model = new ApplicationListModel; - return model; -} - void ApplicationListModel::sycocaDbChanged() { m_applicationList.clear(); @@ -232,19 +215,16 @@ void ApplicationListModel::setMinimizedDelegate(int row, QQuickItem *delegate) } QWindow *delegateWindow = delegate->window(); - if (!delegateWindow) { return; } - using namespace KWayland::Client; KWayland::Client::PlasmaWindow *window = m_applicationList[row].window; if (!window) { return; } - Surface *surface = Surface::fromWindow(delegateWindow); - + KWayland::Client::Surface *surface = KWayland::Client::Surface::fromWindow(delegateWindow); if (!surface) { return; } @@ -261,19 +241,16 @@ void ApplicationListModel::unsetMinimizedDelegate(int row, QQuickItem *delegate) } QWindow *delegateWindow = delegate->window(); - if (!delegateWindow) { return; } - using namespace KWayland::Client; KWayland::Client::PlasmaWindow *window = m_applicationList[row].window; if (!window) { return; } - Surface *surface = Surface::fromWindow(delegateWindow); - + KWayland::Client::Surface *surface = KWayland::Client::Surface::fromWindow(delegateWindow); if (!surface) { return; } diff --git a/containments/homescreens/default/applicationlistmodel.h b/containments/homescreens/default/applicationlistmodel.h index d607ddd6..7e912a91 100644 --- a/containments/homescreens/default/applicationlistmodel.h +++ b/containments/homescreens/default/applicationlistmodel.h @@ -1,29 +1,19 @@ -/* - * SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas - * SPDX-FileCopyrightText: 2022 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once -// Qt #include #include #include #include #include -class QString; - -namespace KWayland -{ -namespace Client -{ -class PlasmaWindowManagement; -class PlasmaWindow; -} -} +#include +#include +#include +#include /** * @short The base application list, used directly by the app drawer. @@ -64,8 +54,6 @@ public: ApplicationListModel(QObject *parent = nullptr); ~ApplicationListModel() override; - static ApplicationListModel *instance(); - int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; QHash roleNames() const Q_DECL_OVERRIDE; diff --git a/containments/homescreens/halcyon/CMakeLists.txt b/containments/homescreens/halcyon/CMakeLists.txt index 798a492f..c90dbbc9 100644 --- a/containments/homescreens/halcyon/CMakeLists.txt +++ b/containments/homescreens/halcyon/CMakeLists.txt @@ -3,6 +3,11 @@ set(homescreen_SRCS homescreen.cpp + application.cpp + applicationfolder.cpp + applicationlistmodel.cpp + pinnedmodel.cpp + windowlistener.cpp ) add_library(plasma_containment_phone_homescreen_halcyon MODULE ${homescreen_SRCS}) diff --git a/containments/homescreens/halcyon/application.cpp b/containments/homescreens/halcyon/application.cpp new file mode 100644 index 00000000..57b08eb8 --- /dev/null +++ b/containments/homescreens/halcyon/application.cpp @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "application.h" +#include "windowlistener.h" + +#include + +#include +#include +#include + +Application::Application(QObject *parent, KService::Ptr service) + : QObject{parent} + , m_running{false} + , m_name{service->name()} + , m_icon{service->icon()} + , m_storageId{service->storageId()} +{ + auto windows = WindowListener::instance()->windowsFromStorageId(m_storageId); + if (windows.empty()) { + m_window = nullptr; + } else { + m_window = windows[0]; + } + + connect(WindowListener::instance(), &WindowListener::windowChanged, this, [this](QString storageId) { + if (storageId == m_storageId) { + auto windows = WindowListener::instance()->windowsFromStorageId(m_storageId); + if (windows.empty()) { + setWindow(nullptr); + } else { + setWindow(windows[0]); + } + } + }); +} + +Application *Application::fromJson(QJsonObject &obj, QObject *parent) +{ + QString storageId = obj[QStringLiteral("storageId")].toString(); + if (KService::Ptr service = KService::serviceByStorageId(storageId)) { + return new Application(parent, service); + } + return nullptr; +} + +QJsonObject Application::toJson() +{ + QJsonObject obj; + obj[QStringLiteral("type")] = "application"; + obj[QStringLiteral("storageId")] = m_storageId; + return obj; +} + +bool Application::running() const +{ + return m_window != nullptr; +} + +QString Application::name() const +{ + return m_name; +} + +QString Application::icon() const +{ + return m_icon; +} + +QString Application::storageId() const +{ + return m_storageId; +} + +KWayland::Client::PlasmaWindow *Application::window() const +{ + return m_window; +} + +void Application::setName(QString &name) +{ + m_name = name; + Q_EMIT nameChanged(); +} + +void Application::setIcon(QString &icon) +{ + m_icon = icon; + Q_EMIT iconChanged(); +} + +void Application::setStorageId(QString &storageId) +{ + m_storageId = storageId; + Q_EMIT storageIdChanged(); +} + +void Application::setWindow(KWayland::Client::PlasmaWindow *window) +{ + m_window = window; + Q_EMIT windowChanged(); +} + +void Application::runApplication() +{ + if (m_window) { + m_window->requestActivate(); + return; + } + + KService::Ptr service = KService::serviceByStorageId(m_storageId); + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + job->start(); +} + +void Application::setMinimizedDelegate(QQuickItem *delegate) +{ + QWindow *delegateWindow = delegate->window(); + if (!delegateWindow) { + return; + } + if (!m_window) { + return; + } + + KWayland::Client::Surface *surface = KWayland::Client::Surface::fromWindow(delegateWindow); + if (!surface) { + return; + } + + QRect rect = delegate->mapRectToScene(QRectF(0, 0, delegate->width(), delegate->height())).toRect(); + m_window->setMinimizedGeometry(surface, rect); +} + +void Application::unsetMinimizedDelegate(QQuickItem *delegate) +{ + QWindow *delegateWindow = delegate->window(); + if (!delegateWindow) { + return; + } + if (!m_window) { + return; + } + + KWayland::Client::Surface *surface = KWayland::Client::Surface::fromWindow(delegateWindow); + if (!surface) { + return; + } + + m_window->unsetMinimizedGeometry(surface); +} diff --git a/containments/homescreens/halcyon/application.h b/containments/homescreens/halcyon/application.h new file mode 100644 index 00000000..fb92bb4b --- /dev/null +++ b/containments/homescreens/halcyon/application.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +class Application : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool running READ running NOTIFY windowChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged) + Q_PROPERTY(QString storageId READ storageId NOTIFY storageIdChanged) + +public: + Application(QObject *parent = nullptr, KService::Ptr service = QExplicitlySharedDataPointer{nullptr}); + + static Application *fromJson(QJsonObject &obj, QObject *parent); // may return nullptr + QJsonObject toJson(); + + bool running() const; + QString name() const; + QString icon() const; + QString storageId() const; + KWayland::Client::PlasmaWindow *window() const; + + void setName(QString &name); + void setIcon(QString &icon); + void setStorageId(QString &storageId); + void setWindow(KWayland::Client::PlasmaWindow *window); + + Q_INVOKABLE void runApplication(); + Q_INVOKABLE void setMinimizedDelegate(QQuickItem *delegate); + Q_INVOKABLE void unsetMinimizedDelegate(QQuickItem *delegate); + +Q_SIGNALS: + void nameChanged(); + void iconChanged(); + void storageIdChanged(); + void windowChanged(); + +private: + bool m_running; + QString m_name; + QString m_icon; + QString m_storageId; + KWayland::Client::PlasmaWindow *m_window = nullptr; +}; diff --git a/containments/homescreens/halcyon/applicationfolder.cpp b/containments/homescreens/halcyon/applicationfolder.cpp new file mode 100644 index 00000000..1dddcca6 --- /dev/null +++ b/containments/homescreens/halcyon/applicationfolder.cpp @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "applicationfolder.h" + +#include + +ApplicationFolder::ApplicationFolder(QObject *parent) +{ +} + +ApplicationFolder *ApplicationFolder::fromJson(QJsonObject &obj, QObject *parent) +{ + QString name = obj[QStringLiteral("name")].toString(); + QList apps; + for (auto storageId : obj[QStringLiteral("apps")].toArray()) { + if (KService::Ptr service = KService::serviceByStorageId(storageId.toString())) { + apps.append(new Application(parent, service)); + } + } + + ApplicationFolder *folder = new ApplicationFolder(parent); + folder->setName(name); + folder->setApplications(apps); + return folder; +} + +QJsonObject ApplicationFolder::toJson() +{ + QJsonObject obj; + obj[QStringLiteral("type")] = "folder"; + obj[QStringLiteral("name")] = m_name; + + QJsonArray arr; + for (auto *application : m_applications) { + arr.append(QJsonValue::fromVariant(application->storageId())); + } + + obj[QStringLiteral("apps")] = arr; + + return obj; +} + +QString ApplicationFolder::name() const +{ + return m_name; +} + +void ApplicationFolder::setName(QString &name) +{ + m_name = name; + Q_EMIT nameChanged(); +} + +QList ApplicationFolder::applications() +{ + return m_applications; +} + +void ApplicationFolder::setApplications(QList applications) +{ + m_applications = applications; + Q_EMIT applicationsChanged(); +} diff --git a/containments/homescreens/halcyon/applicationfolder.h b/containments/homescreens/halcyon/applicationfolder.h new file mode 100644 index 00000000..732ed232 --- /dev/null +++ b/containments/homescreens/halcyon/applicationfolder.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "application.h" + +#include +#include + +#include + +#include +#include +#include +#include + +class ApplicationFolder : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QList applications READ applications NOTIFY applicationsChanged) + +public: + ApplicationFolder(QObject *parent = nullptr); + + static ApplicationFolder *fromJson(QJsonObject &obj, QObject *parent); + QJsonObject toJson(); + + QString name() const; + void setName(QString &name); + + QList applications(); + void setApplications(QList applications); + +Q_SIGNALS: + void nameChanged(); + void applicationsChanged(); + +private: + QString m_name; + QList m_applications; +}; diff --git a/containments/homescreens/halcyon/applicationlistmodel.cpp b/containments/homescreens/halcyon/applicationlistmodel.cpp new file mode 100644 index 00000000..ce8276ad --- /dev/null +++ b/containments/homescreens/halcyon/applicationlistmodel.cpp @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "applicationlistmodel.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +ApplicationListModel::ApplicationListModel(QObject *parent) + : QAbstractListModel(parent) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + connect(KSycoca::self(), qOverload(&KSycoca::databaseChanged), this, &ApplicationListModel::sycocaDbChanged); +#else + connect(KSycoca::self(), &KSycoca::databaseChanged, this, &ApplicationListModel::sycocaDbChanged); +#endif +} + +ApplicationListModel::~ApplicationListModel() = default; + +QHash ApplicationListModel::roleNames() const +{ + return {{ApplicationRole, QByteArrayLiteral("application")}}; +} + +void ApplicationListModel::sycocaDbChanged() +{ + m_applicationList.clear(); + loadApplications(); +} + +void ApplicationListModel::loadApplications() +{ + auto cfg = KSharedConfig::openConfig(QStringLiteral("applications-blacklistrc")); + auto blgroup = KConfigGroup(cfg, QStringLiteral("Applications")); + + const QStringList blacklist = blgroup.readEntry("blacklist", QStringList()); + + beginResetModel(); + + m_applicationList.clear(); + + QList unorderedList; + + auto filter = [blacklist](const KService::Ptr &service) -> bool { + if (service->noDisplay()) { + return false; + } + + if (!service->showOnCurrentPlatform()) { + return false; + } + + if (blacklist.contains(service->desktopEntryName())) { + return false; + } + + return true; + }; + + const KService::List apps = KApplicationTrader::query(filter); + + for (const KService::Ptr &service : apps) { + Application *application = new Application{this, service}; + unorderedList.append(application); + } + + std::sort(unorderedList.begin(), unorderedList.end(), [](const Application *a1, const Application *a2) { + return a1->name().compare(a2->name(), Qt::CaseInsensitive) < 0; + }); + + m_applicationList << unorderedList; + + endResetModel(); +} + +QVariant ApplicationListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + return QVariant::fromValue(m_applicationList.at(index.row())); +} + +int ApplicationListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_applicationList.count(); +} diff --git a/containments/homescreens/halcyon/applicationlistmodel.h b/containments/homescreens/halcyon/applicationlistmodel.h new file mode 100644 index 00000000..8d967384 --- /dev/null +++ b/containments/homescreens/halcyon/applicationlistmodel.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "application.h" + +#include +#include +#include +#include +#include + +/** + * @short The base application list, used directly by the app drawer. + */ +class ApplicationListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { ApplicationRole = Qt::UserRole + 1 }; + + ApplicationListModel(QObject *parent = nullptr); + ~ApplicationListModel() override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + Q_INVOKABLE void loadApplications(); + +public Q_SLOTS: + void sycocaDbChanged(); + +Q_SIGNALS: + void launchError(const QString &msg); + +protected: + QList m_applicationList; +}; diff --git a/containments/homescreens/halcyon/homescreen.cpp b/containments/homescreens/halcyon/homescreen.cpp index d33462c1..db8a7bf1 100644 --- a/containments/homescreens/halcyon/homescreen.cpp +++ b/containments/homescreens/halcyon/homescreen.cpp @@ -1,2 +1,45 @@ // SPDX-FileCopyrightText: 2022 Devin Lin -// SPDX-License-Identifier: LGPL-2.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "homescreen.h" +#include "application.h" +#include "applicationfolder.h" +#include "pinnedmodel.h" +#include "windowlistener.h" + +#include + +#include +#include +#include + +HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : Plasma::Containment{parent, data, args} +{ + setHasConfigurationInterface(true); + + WindowListener::instance(); // ensure it is created + + ApplicationListModel *applicationListModel = new ApplicationListModel{this}; + qmlRegisterSingletonType("org.kde.phone.homescreen.halcyon", + 1, + 0, + "ApplicationListModel", + [applicationListModel](QQmlEngine *, QJSEngine *) -> QObject * { + return applicationListModel; + }); + + PinnedModel *pinnedModel = new PinnedModel{this, this}; + qmlRegisterSingletonType("org.kde.phone.homescreen.halcyon", 1, 0, "PinnedModel", [pinnedModel](QQmlEngine *, QJSEngine *) -> QObject * { + return pinnedModel; + }); + + qmlRegisterType("org.kde.phone.homescreen.halcyon", 1, 0, "Application"); + qmlRegisterType("org.kde.phone.homescreen.halcyon", 1, 0, "ApplicationFolder"); +} + +HomeScreen::~HomeScreen() = default; + +K_PLUGIN_CLASS_WITH_JSON(HomeScreen, "metadata.json") + +#include "homescreen.moc" diff --git a/containments/homescreens/halcyon/homescreen.h b/containments/homescreens/halcyon/homescreen.h index d33462c1..1427f067 100644 --- a/containments/homescreens/halcyon/homescreen.h +++ b/containments/homescreens/halcyon/homescreen.h @@ -1,2 +1,17 @@ // SPDX-FileCopyrightText: 2022 Devin Lin -// SPDX-License-Identifier: LGPL-2.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +class HomeScreen : public Plasma::Containment +{ + Q_OBJECT + +public: + HomeScreen(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~HomeScreen() override; +}; diff --git a/containments/homescreens/halcyon/package/contents/ui/DrawerListDelegate.qml b/containments/homescreens/halcyon/package/contents/ui/DrawerListDelegate.qml index db94bc94..d01fe7ab 100644 --- a/containments/homescreens/halcyon/package/contents/ui/DrawerListDelegate.qml +++ b/containments/homescreens/halcyon/package/contents/ui/DrawerListDelegate.qml @@ -13,12 +13,19 @@ import org.kde.kquickcontrolsaddons 2.0 import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.phone.homescreen.halcyon 1.0 as Halcyon import org.kde.kirigami 2.19 as Kirigami MouseArea { id: delegate + property alias iconItem: icon + property Halcyon.Application application: model.application + + readonly property string applicationName: application ? application.name : "" + readonly property string applicationStorageId: application ? application.storageId : "" + readonly property string applicationIcon: application ? application.icon : "" signal launch(int x, int y, var source, string title, string storageId) signal dragStarted(string imageSource, int x, int y, string mimeData) @@ -33,8 +40,8 @@ MouseArea { Math.min(delegate.iconItem.width, delegate.iconItem.height)); } - MobileShell.ApplicationListModel.setMinimizedDelegate(index, delegate); - MobileShell.ApplicationListModel.runApplication(storageId); + application.setMinimizedDelegate(delegate); + application.runApplication(); } onPressAndHold: { @@ -44,10 +51,10 @@ MouseArea { onClicked: { // launch app - if (model.applicationRunning) { - delegate.launch(0, 0, "", model.applicationName, model.applicationStorageId); + if (application.running) { + delegate.launch(0, 0, "", applicationName, applicationStorageId); } else { - delegate.launch(delegate.x + (PlasmaCore.Units.smallSpacing * 2), delegate.y + (PlasmaCore.Units.smallSpacing * 2), icon.source, model.applicationName, model.applicationStorageId); + delegate.launch(delegate.x + (PlasmaCore.Units.smallSpacing * 2), delegate.y + (PlasmaCore.Units.smallSpacing * 2), icon.source, applicationName, applicationStorageId); } } hoverEnabled: true @@ -63,7 +70,7 @@ MouseArea { icon.name: "emblem-favorite" text: i18n("Remove from favourites") onClicked: { - MobileShell.FavoritesModel.removeFavorite(model.index); + Halcyon.PinnedModel.removeApp(model.index); } } onClosed: dialogLoader.active = false @@ -98,14 +105,14 @@ MouseArea { Layout.preferredHeight: Layout.minimumHeight usesPlasmaTheme: false - source: model.applicationIcon + source: applicationIcon Rectangle { anchors { horizontalCenter: parent.horizontalCenter bottom: parent.bottom } - visible: model.applicationRunning + visible: application.running radius: width width: PlasmaCore.Units.smallSpacing height: width @@ -132,7 +139,7 @@ MouseArea { maximumLineCount: 1 elide: Text.ElideRight - text: model.applicationName + text: applicationName font.pointSize: PlasmaCore.Theme.defaultFont.pointSize font.weight: Font.Bold diff --git a/containments/homescreens/halcyon/package/contents/ui/GridAppDelegate.qml b/containments/homescreens/halcyon/package/contents/ui/GridAppDelegate.qml index 236d1963..1833bf34 100644 --- a/containments/homescreens/halcyon/package/contents/ui/GridAppDelegate.qml +++ b/containments/homescreens/halcyon/package/contents/ui/GridAppDelegate.qml @@ -16,6 +16,7 @@ import org.kde.kquickcontrolsaddons 2.0 import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.phone.homescreen.halcyon 1.0 as Halcyon import org.kde.kirigami 2.19 as Kirigami @@ -24,6 +25,8 @@ MouseArea { width: GridView.view.cellWidth height: GridView.view.cellHeight + property Halcyon.Application application: model.application + property int reservedSpaceForLabel property alias iconItem: icon @@ -38,10 +41,10 @@ MouseArea { function launchApp() { // launch app - if (model.applicationRunning) { - delegate.launch(0, 0, "", model.applicationName, model.applicationStorageId); + if (application.running) { + delegate.launch(0, 0, "", application.name, application.storageId); } else { - delegate.launch(delegate.x + (PlasmaCore.Units.smallSpacing * 2), delegate.y + (PlasmaCore.Units.smallSpacing * 2), icon.source, model.applicationName, model.applicationStorageId); + delegate.launch(delegate.x + (PlasmaCore.Units.smallSpacing * 2), delegate.y + (PlasmaCore.Units.smallSpacing * 2), icon.source, application.name, application.storageId); } } @@ -56,7 +59,7 @@ MouseArea { icon.name: "emblem-favorite" text: i18n("Add to favourites") onClicked: { - MobileShell.FavoritesModel.addFavorite(model.applicationStorageId, 0, MobileShell.ApplicationListModel.Favorites); + Halcyon.PinnedModel.addApp(application.storageId, 0); } } onClosed: dialogLoader.active = false @@ -131,14 +134,14 @@ MouseArea { Layout.preferredHeight: Layout.minimumHeight usesPlasmaTheme: false - source: model.applicationIcon + source: application.icon Rectangle { anchors { horizontalCenter: parent.horizontalCenter bottom: parent.bottom } - visible: model.applicationRunning + visible: application.running radius: width width: PlasmaCore.Units.smallSpacing height: width @@ -161,7 +164,7 @@ MouseArea { verticalAlignment: Text.AlignTop elide: Text.ElideRight - text: model.applicationName + text: application.name font.pointSize: theme.defaultFont.pointSize * 0.85 font.weight: Font.Bold diff --git a/containments/homescreens/halcyon/package/contents/ui/GridAppList.qml b/containments/homescreens/halcyon/package/contents/ui/GridAppList.qml index 7c973413..aaa5c6ba 100644 --- a/containments/homescreens/halcyon/package/contents/ui/GridAppList.qml +++ b/containments/homescreens/halcyon/package/contents/ui/GridAppList.qml @@ -15,6 +15,7 @@ import org.kde.kirigami 2.10 as Kirigami import org.kde.plasma.private.nanoshell 2.0 as NanoShell import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.phone.homescreen.halcyon 1.0 as Halcyon GridView { id: gridView @@ -32,7 +33,7 @@ GridView { cacheBuffer: Math.max(0, rows * cellHeight) - model: MobileShell.ApplicationListModel + model: Halcyon.ApplicationListModel header: Controls.Control { implicitWidth: gridView.width @@ -59,6 +60,8 @@ GridView { delegate: GridAppDelegate { id: delegate + property Halcyon.Application application: model.application + width: gridView.cellWidth height: gridView.cellHeight reservedSpaceForLabel: gridView.reservedSpaceForLabel @@ -73,8 +76,8 @@ GridView { Math.min(delegate.iconItem.width, delegate.iconItem.height)); } - MobileShell.ApplicationListModel.setMinimizedDelegate(index, delegate); - MobileShell.ApplicationListModel.runApplication(storageId); + application.setMinimizedDelegate(delegate); + application.runApplication(); gridView.launched(); } } diff --git a/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml b/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml index 6515b615..75e6fcd9 100644 --- a/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml +++ b/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml @@ -13,6 +13,7 @@ import org.kde.draganddrop 2.0 as DragDrop import org.kde.kirigami 2.19 as Kirigami import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.phone.homescreen.halcyon 1.0 as Halcyon Item { id: root @@ -60,7 +61,7 @@ Item { } } - model: MobileShell.FavoritesModel + model: Halcyon.PinnedModel header: MobileShell.BaseItem { topPadding: Math.round(swipeView.height * 0.2) bottomPadding: PlasmaCore.Units.largeSpacing diff --git a/containments/homescreens/halcyon/package/contents/ui/main.qml b/containments/homescreens/halcyon/package/contents/ui/main.qml index 8739f91d..f4b83181 100644 --- a/containments/homescreens/halcyon/package/contents/ui/main.qml +++ b/containments/homescreens/halcyon/package/contents/ui/main.qml @@ -10,6 +10,7 @@ import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.phone.homescreen.halcyon 1.0 as Halcyon MobileShell.HomeScreen { id: root @@ -23,10 +24,7 @@ MobileShell.HomeScreen { } Component.onCompleted: { - MobileShell.ApplicationListModel.loadApplications(); - MobileShell.FavoritesModel.applet = plasmoid; - MobileShell.FavoritesModel.loadApplications(); - + Halcyon.ApplicationListModel.loadApplications(); forceActiveFocus(); } @@ -61,7 +59,7 @@ MobileShell.HomeScreen { Connections { target: MobileShell.HomeScreenControls.taskSwitcher function onVisibleChanged() { - searchWidget.close(); + search.close(); } } } diff --git a/containments/homescreens/halcyon/pinnedmodel.cpp b/containments/homescreens/halcyon/pinnedmodel.cpp new file mode 100644 index 00000000..785c4955 --- /dev/null +++ b/containments/homescreens/halcyon/pinnedmodel.cpp @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "pinnedmodel.h" + +#include +#include + +PinnedModel::PinnedModel(QObject *parent, Plasma::Applet *applet) + : QAbstractListModel{parent} + , m_applet{applet} +{ +} + +PinnedModel::~PinnedModel() = default; + +int PinnedModel::rowCount(const QModelIndex &parent) const +{ + return m_applications.count(); +} + +QVariant PinnedModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + switch (role) { + case IsFolderRole: + return m_folders.at(index.row()) != nullptr; + case ApplicationRole: + return QVariant::fromValue(m_applications.at(index.row())); + case FolderRole: + return QVariant::fromValue(m_folders.at(index.row())); + } + + return QVariant(); +} + +QHash PinnedModel::roleNames() const +{ + return {{IsFolderRole, "isFolder"}, {ApplicationRole, "application"}, {FolderRole, "folder"}}; +} + +void PinnedModel::addApp(const QString &storageId, int row) +{ + if (row < 0 && row > m_applications.size()) { + return; + } + + if (KService::Ptr service = KService::serviceByStorageId(storageId)) { + Application *app = new Application(this, service); + + beginInsertRows(QModelIndex(), row, row); + m_applications.insert(row, app); + m_folders.insert(0, nullptr); // maintain indicies + endInsertRows(); + + save(); + } +} + +void PinnedModel::removeApp(int row) +{ + if (row < 0 && row >= m_applications.size()) { + return; + } + + beginRemoveRows(QModelIndex(), row, row); + m_applications[row]->deleteLater(); + m_applications.removeAt(row); + m_folders.removeAt(row); // maintain indicies + endRemoveRows(); + + save(); +} + +void PinnedModel::load() +{ + if (!m_applet) { + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(m_applet->config().readEntry("Pinned", "{}").toUtf8()); + + beginResetModel(); + + for (QJsonValueRef r : doc.array()) { + QJsonObject obj = r.toObject(); + + if (obj[QStringLiteral("type")].toString() == "application") { + // read application + Application *app = Application::fromJson(obj, this); + if (app) { + m_applications.append(app); + m_folders.append(nullptr); + } + + } else if (obj[QStringLiteral("type")].toString() == "folder") { + // read folder + ApplicationFolder *folder = ApplicationFolder::fromJson(obj, this); + if (folder) { + m_applications.append(nullptr); + m_folders.append(folder); + } + } + } + + endResetModel(); +} + +void PinnedModel::save() +{ + if (!m_applet) { + return; + } + + QJsonArray arr; + for (int i = 0; i < m_applications.size() && i < m_folders.size(); i++) { + if (m_applications[i]) { + arr.push_back(m_applications[i]->toJson()); + } else if (m_folders[i]) { + arr.push_back(m_folders[i]->toJson()); + } + } + QByteArray data = QJsonDocument(arr).toJson(QJsonDocument::Compact); + + m_applet->config().writeEntry("Pinned", QString::fromStdString(data.toStdString())); + Q_EMIT m_applet->configNeedsSaving(); +} diff --git a/containments/homescreens/halcyon/pinnedmodel.h b/containments/homescreens/halcyon/pinnedmodel.h new file mode 100644 index 00000000..4dd6af83 --- /dev/null +++ b/containments/homescreens/halcyon/pinnedmodel.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "application.h" +#include "applicationfolder.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +class PinnedModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { IsFolderRole = Qt::UserRole + 1, ApplicationRole, FolderRole }; + + PinnedModel(QObject *parent = nullptr, Plasma::Applet *applet = nullptr); + ~PinnedModel() override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + Q_INVOKABLE void addApp(const QString &storageId, int row); + Q_INVOKABLE void removeApp(int row); + + Q_INVOKABLE void load(); + void save(); + +private: + QList m_applications; + QList m_folders; + + Plasma::Applet *m_applet; +}; diff --git a/containments/homescreens/halcyon/windowlistener.cpp b/containments/homescreens/halcyon/windowlistener.cpp new file mode 100644 index 00000000..318e6e64 --- /dev/null +++ b/containments/homescreens/halcyon/windowlistener.cpp @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "windowlistener.h" + +WindowListener::WindowListener(QObject *parent) + : QObject{parent} +{ + // initialize wayland window checking + KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(this); + if (!connection) { + return; + } + + auto *registry = new KWayland::Client::Registry(this); + registry->create(connection); + + connect(registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_windowManagement = registry->createPlasmaWindowManagement(name, version, this); + connect(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated, this, &WindowListener::windowCreated); + }); + + registry->setup(); + connection->roundtrip(); +} + +WindowListener *WindowListener::instance() +{ + static WindowListener *listener = new WindowListener(); + return listener; +} + +QList WindowListener::windowsFromStorageId(QString &storageId) const +{ + if (!m_windows.contains(storageId)) { + return {}; + } + return m_windows[storageId]; +} + +void WindowListener::windowCreated(KWayland::Client::PlasmaWindow *window) +{ + QString storageId = window->appId() + QStringLiteral(".desktop"); + + // ignore empty windows + if (storageId == ".desktop" || storageId == "org.kde.plasmashell.desktop") { + return; + } + + if (!m_windows.contains(storageId)) { + m_windows[storageId] = {}; + } + m_windows[storageId].push_back(window); + + // listen for window close + connect(window, &KWayland::Client::PlasmaWindow::unmapped, this, [this, storageId]() { + m_windows.remove(storageId); + Q_EMIT windowChanged(storageId); + }); + + Q_EMIT windowChanged(storageId); +} diff --git a/containments/homescreens/halcyon/windowlistener.h b/containments/homescreens/halcyon/windowlistener.h new file mode 100644 index 00000000..b6d3754e --- /dev/null +++ b/containments/homescreens/halcyon/windowlistener.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include +#include +#include +#include + +class WindowListener : public QObject +{ + Q_OBJECT + +public: + WindowListener(QObject *parent = nullptr); + + static WindowListener *instance(); + + QList windowsFromStorageId(QString &storageId) const; + +public Q_SLOTS: + void windowCreated(KWayland::Client::PlasmaWindow *window); + +Q_SIGNALS: + void windowChanged(QString storageId); + +private: + KWayland::Client::PlasmaWindowManagement *m_windowManagement = nullptr; + QHash> m_windows; // +};