diff --git a/containments/homescreens/folio/CMakeLists.txt b/containments/homescreens/folio/CMakeLists.txt index a06c9db8..96405e36 100644 --- a/containments/homescreens/folio/CMakeLists.txt +++ b/containments/homescreens/folio/CMakeLists.txt @@ -26,6 +26,7 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio qml/config.qml CPP_SOURCES applicationlistmodel.cpp + applicationusagemodel.cpp delegatetoucharea.cpp dragstate.cpp favouritesmodel.cpp diff --git a/containments/homescreens/folio/applicationlistmodel.cpp b/containments/homescreens/folio/applicationlistmodel.cpp index d1018e20..dd828d84 100644 --- a/containments/homescreens/folio/applicationlistmodel.cpp +++ b/containments/homescreens/folio/applicationlistmodel.cpp @@ -182,6 +182,17 @@ int ApplicationListModel::rowCount(const QModelIndex &parent) const return m_delegates.count(); } +std::shared_ptr ApplicationListModel::delegateForStorageId(const QString &storageId) const +{ + for (const auto &delegate : m_delegates) { + if (delegate && delegate->application() && delegate->application()->storageId() == storageId) { + return delegate; + } + } + + return nullptr; +} + // Sub-categories merged into their canonical parent, mirroring Kickoff's grouping. static QString normalizeCategory(const QString &cat) { diff --git a/containments/homescreens/folio/applicationlistmodel.h b/containments/homescreens/folio/applicationlistmodel.h index 1a6a1109..bf6c76a5 100644 --- a/containments/homescreens/folio/applicationlistmodel.h +++ b/containments/homescreens/folio/applicationlistmodel.h @@ -12,6 +12,8 @@ #include #include +#include + #include #include "foliodelegate.h" @@ -45,6 +47,8 @@ public: void load(); + std::shared_ptr delegateForStorageId(const QString &storageId) const; + Q_INVOKABLE QStringList allCategories() const; Q_SIGNALS: diff --git a/containments/homescreens/folio/applicationusagemodel.cpp b/containments/homescreens/folio/applicationusagemodel.cpp new file mode 100644 index 00000000..c4b9d8e2 --- /dev/null +++ b/containments/homescreens/folio/applicationusagemodel.cpp @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#include "applicationusagemodel.h" + +#include "applicationlistmodel.h" +#include "foliodelegate.h" +#include "foliosettings.h" +#include "homescreen.h" + +#include +#include +#include + +namespace +{ +constexpr qsizetype s_maxStoredEntries = 24; + +QString normalizedStorageId(const QString &storageId) +{ + if (storageId.isEmpty()) { + return {}; + } + + if (storageId.endsWith(QLatin1String(".desktop"))) { + return storageId; + } + + return storageId + QStringLiteral(".desktop"); +} +} + +ApplicationUsageStore::ApplicationUsageStore(HomeScreen *parent) + : QObject{parent} + , m_homeScreen{parent} +{ + load(); +} + +QList ApplicationUsageStore::entries() const +{ + return m_entries.values(); +} + +void ApplicationUsageStore::recordUsage(const QString &storageId) +{ + const QString normalizedId = normalizedStorageId(storageId); + if (normalizedId.isEmpty()) { + return; + } + + auto &entry = m_entries[normalizedId]; + entry.storageId = normalizedId; + entry.launchCount = std::max(0, entry.launchCount) + 1; + entry.lastUsed = QDateTime::currentDateTimeUtc(); + + save(); + Q_EMIT usageChanged(); +} + +void ApplicationUsageStore::load() +{ + m_entries.clear(); + + if (!m_homeScreen) { + return; + } + + const QJsonDocument doc = QJsonDocument::fromJson(m_homeScreen->folioSettings()->applicationUsage().toUtf8()); + const QJsonArray usageArray = doc.array(); + + for (const QJsonValue &value : usageArray) { + const QJsonObject object = value.toObject(); + const QString storageId = normalizedStorageId(object.value(QStringLiteral("storageId")).toString()); + if (storageId.isEmpty()) { + continue; + } + + ApplicationUsageEntry entry; + entry.storageId = storageId; + entry.launchCount = object.value(QStringLiteral("launchCount")).toInt(); + entry.lastUsed = QDateTime::fromString(object.value(QStringLiteral("lastUsed")).toString(), Qt::ISODateWithMs); + + if (entry.launchCount <= 0 || !entry.lastUsed.isValid()) { + continue; + } + + m_entries.insert(storageId, entry); + } +} + +void ApplicationUsageStore::save() +{ + if (!m_homeScreen) { + return; + } + + QList entries = m_entries.values(); + std::sort(entries.begin(), entries.end(), [](const ApplicationUsageEntry &left, const ApplicationUsageEntry &right) { + return left.lastUsed > right.lastUsed; + }); + + if (entries.size() > s_maxStoredEntries) { + entries.resize(s_maxStoredEntries); + } + + QJsonArray usageArray; + for (const ApplicationUsageEntry &entry : std::as_const(entries)) { + QJsonObject object; + object.insert(QStringLiteral("storageId"), entry.storageId); + object.insert(QStringLiteral("launchCount"), entry.launchCount); + object.insert(QStringLiteral("lastUsed"), entry.lastUsed.toString(Qt::ISODateWithMs)); + usageArray.append(object); + } + + m_homeScreen->folioSettings()->setApplicationUsage(QString::fromUtf8(QJsonDocument(usageArray).toJson(QJsonDocument::Compact))); +} + +ApplicationUsageModel::ApplicationUsageModel(HomeScreen *homeScreen, ApplicationUsageStore *store, Mode mode) + : QAbstractListModel{homeScreen} + , m_homeScreen{homeScreen} + , m_store{store} + , m_mode{mode} +{ + if (m_store) { + connect(m_store, &ApplicationUsageStore::usageChanged, this, &ApplicationUsageModel::rebuild); + } + + if (m_homeScreen && m_homeScreen->applicationListModel()) { + auto *applicationListModel = m_homeScreen->applicationListModel(); + connect(applicationListModel, &QAbstractItemModel::rowsInserted, this, &ApplicationUsageModel::rebuild); + connect(applicationListModel, &QAbstractItemModel::rowsRemoved, this, &ApplicationUsageModel::rebuild); + connect(applicationListModel, &QAbstractItemModel::modelReset, this, &ApplicationUsageModel::rebuild); + } + + rebuild(); +} + +int ApplicationUsageModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_entries.size(); +} + +QVariant ApplicationUsageModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size()) { + return QVariant(); + } + + const ApplicationUsageEntry &entry = m_entries.at(index.row()); + switch (role) { + case Qt::DisplayRole: + case DelegateRole: + return QVariant::fromValue(m_delegates.at(index.row()).get()); + case LaunchCountRole: + return entry.launchCount; + case LastUsedRole: + return entry.lastUsed; + default: + return QVariant(); + } +} + +QHash ApplicationUsageModel::roleNames() const +{ + return { + {DelegateRole, QByteArrayLiteral("delegate")}, + {LaunchCountRole, QByteArrayLiteral("launchCount")}, + {LastUsedRole, QByteArrayLiteral("lastUsed")}, + }; +} + +void ApplicationUsageModel::rebuild() +{ + QList entries = m_store ? m_store->entries() : QList{}; + + std::sort(entries.begin(), entries.end(), [this](const ApplicationUsageEntry &left, const ApplicationUsageEntry &right) { + if (m_mode == MostUsed && left.launchCount != right.launchCount) { + return left.launchCount > right.launchCount; + } + + return left.lastUsed > right.lastUsed; + }); + + QList nextEntries; + QList> nextDelegates; + + if (m_homeScreen && m_homeScreen->applicationListModel()) { + for (const ApplicationUsageEntry &entry : std::as_const(entries)) { + auto delegate = m_homeScreen->applicationListModel()->delegateForStorageId(entry.storageId); + if (!delegate || !delegate->application()) { + continue; + } + + nextEntries.append(entry); + nextDelegates.append(delegate); + } + } + + beginResetModel(); + m_entries = nextEntries; + m_delegates = nextDelegates; + endResetModel(); +} \ No newline at end of file diff --git a/containments/homescreens/folio/applicationusagemodel.h b/containments/homescreens/folio/applicationusagemodel.h new file mode 100644 index 00000000..27a72b63 --- /dev/null +++ b/containments/homescreens/folio/applicationusagemodel.h @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include + +class FolioDelegate; +class HomeScreen; + +struct ApplicationUsageEntry { + QString storageId; + int launchCount = 0; + QDateTime lastUsed; +}; + +class ApplicationUsageStore : public QObject +{ + Q_OBJECT + +public: + explicit ApplicationUsageStore(HomeScreen *parent = nullptr); + + QList entries() const; + void recordUsage(const QString &storageId); + +Q_SIGNALS: + void usageChanged(); + +private: + void load(); + void save(); + + HomeScreen *m_homeScreen{nullptr}; + QHash m_entries; +}; + +class ApplicationUsageModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + enum Mode { + RecentUsage, + MostUsed, + }; + + enum Roles { + DelegateRole = Qt::UserRole + 1, + LaunchCountRole, + LastUsedRole, + }; + + ApplicationUsageModel(HomeScreen *homeScreen = nullptr, ApplicationUsageStore *store = nullptr, Mode mode = RecentUsage); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + +public Q_SLOTS: + void rebuild(); + +private: + HomeScreen *m_homeScreen{nullptr}; + ApplicationUsageStore *m_store{nullptr}; + Mode m_mode{RecentUsage}; + QList m_entries; + QList> m_delegates; +}; \ No newline at end of file diff --git a/containments/homescreens/folio/foliosettings.cpp b/containments/homescreens/folio/foliosettings.cpp index e4a54feb..a4126f37 100644 --- a/containments/homescreens/folio/foliosettings.cpp +++ b/containments/homescreens/folio/foliosettings.cpp @@ -16,6 +16,7 @@ using namespace Qt::Literals::StringLiterals; const QString CFG_GROUP_FOLIO = QStringLiteral("Folio"); const QString CFG_KEY_FAVORITES = QStringLiteral("favorites"); +const QString CFG_KEY_APPLICATION_USAGE = QStringLiteral("applicationUsage"); const QString CFG_KEY_PAGES = QStringLiteral("pages"); const QString CFG_KEY_HOMESCREEN_ROWS = QStringLiteral("homeScreenRows"); @@ -47,6 +48,17 @@ void FolioSettings::setFavorites(const QString &favoritesJson) Q_EMIT m_homeScreen->configNeedsSaving(); } +QString FolioSettings::applicationUsage() const +{ + return generalConfigGroup().readEntry(CFG_KEY_APPLICATION_USAGE, QStringLiteral("[]")); +} + +void FolioSettings::setApplicationUsage(const QString &applicationUsageJson) +{ + generalConfigGroup().writeEntry(CFG_KEY_APPLICATION_USAGE, applicationUsageJson); + Q_EMIT m_homeScreen->configNeedsSaving(); +} + QString FolioSettings::pages() const { return generalConfigGroup().readEntry(CFG_KEY_PAGES, u"{}"_s); diff --git a/containments/homescreens/folio/foliosettings.h b/containments/homescreens/folio/foliosettings.h index bea2f9ab..4c612689 100644 --- a/containments/homescreens/folio/foliosettings.h +++ b/containments/homescreens/folio/foliosettings.h @@ -53,6 +53,10 @@ public: QString favorites() const; void setFavorites(const QString &favoritesJson); + // JSON array + QString applicationUsage() const; + void setApplicationUsage(const QString &applicationUsageJson); + // JSON object QString pages() const; void setPages(const QString &pagesJson); diff --git a/containments/homescreens/folio/homescreen.cpp b/containments/homescreens/folio/homescreen.cpp index 3069c326..563d9076 100644 --- a/containments/homescreens/folio/homescreen.cpp +++ b/containments/homescreens/folio/homescreen.cpp @@ -4,6 +4,8 @@ #include "homescreen.h" +#include "windowlistener.h" + #include #include @@ -58,6 +60,9 @@ HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVari , m_widgetsManager{new WidgetsManager{this}} , m_applicationListModel{new ApplicationListModel{this}} , m_applicationListSearchModel{new ApplicationListSearchModel{this, m_applicationListModel}} + , m_applicationUsageStore{new ApplicationUsageStore{this}} + , m_recentApplicationsModel{new ApplicationUsageModel{this, m_applicationUsageStore, ApplicationUsageModel::RecentUsage}} + , m_mostUsedApplicationsModel{new ApplicationUsageModel{this, m_applicationUsageStore, ApplicationUsageModel::MostUsed}} , m_favouritesModel{new FavouritesModel{this}} , m_pageListModel{new PageListModel{this}} { @@ -84,6 +89,11 @@ HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVari connect(this, &Plasma::Containment::appletAdded, this, &HomeScreen::onAppletAdded); connect(this, &Plasma::Containment::appletAboutToBeRemoved, this, &HomeScreen::onAppletAboutToBeRemoved); + connect(WindowListener::instance(), &WindowListener::windowChanged, this, [this](QString storageId) { + if (!WindowListener::instance()->windowsFromStorageId(storageId).empty()) { + m_applicationUsageStore->recordUsage(storageId); + } + }); } HomeScreen::~HomeScreen() = default; @@ -129,6 +139,16 @@ ApplicationListSearchModel *HomeScreen::applicationListSearchModel() return m_applicationListSearchModel; } +ApplicationUsageModel *HomeScreen::recentApplicationsModel() +{ + return m_recentApplicationsModel; +} + +ApplicationUsageModel *HomeScreen::mostUsedApplicationsModel() +{ + return m_mostUsedApplicationsModel; +} + FavouritesModel *HomeScreen::favouritesModel() { return m_favouritesModel; diff --git a/containments/homescreens/folio/homescreen.h b/containments/homescreens/folio/homescreen.h index f8d1acd9..245b37c0 100644 --- a/containments/homescreens/folio/homescreen.h +++ b/containments/homescreens/folio/homescreen.h @@ -9,6 +9,7 @@ #include #include "applicationlistmodel.h" +#include "applicationusagemodel.h" #include "delegatetoucharea.h" #include "favouritesmodel.h" #include "folioapplication.h" @@ -29,6 +30,8 @@ class HomeScreenState; class FavouritesModel; class ApplicationListModel; class ApplicationListSearchModel; +class ApplicationUsageModel; +class ApplicationUsageStore; class HomeScreen : public Plasma::Containment { @@ -41,6 +44,8 @@ class HomeScreen : public Plasma::Containment Q_PROPERTY(WidgetsManager *WidgetsManager READ widgetsManager CONSTANT) Q_PROPERTY(ApplicationListModel *ApplicationListModel READ applicationListModel CONSTANT) Q_PROPERTY(ApplicationListSearchModel *ApplicationListSearchModel READ applicationListSearchModel CONSTANT) + Q_PROPERTY(ApplicationUsageModel *RecentApplicationsModel READ recentApplicationsModel CONSTANT) + Q_PROPERTY(ApplicationUsageModel *MostUsedApplicationsModel READ mostUsedApplicationsModel CONSTANT) Q_PROPERTY(FavouritesModel *FavouritesModel READ favouritesModel CONSTANT) Q_PROPERTY(PageListModel *PageListModel READ pageListModel CONSTANT) Q_PROPERTY(bool overviewActive READ overviewActive NOTIFY overviewActiveChanged) @@ -63,6 +68,8 @@ public: WidgetsManager *widgetsManager(); ApplicationListModel *applicationListModel(); ApplicationListSearchModel *applicationListSearchModel(); + ApplicationUsageModel *recentApplicationsModel(); + ApplicationUsageModel *mostUsedApplicationsModel(); FavouritesModel *favouritesModel(); PageListModel *pageListModel(); bool overviewActive() const; @@ -85,6 +92,9 @@ private: WidgetsManager *m_widgetsManager{nullptr}; ApplicationListModel *m_applicationListModel{nullptr}; ApplicationListSearchModel *m_applicationListSearchModel{nullptr}; + ApplicationUsageStore *m_applicationUsageStore{nullptr}; + ApplicationUsageModel *m_recentApplicationsModel{nullptr}; + ApplicationUsageModel *m_mostUsedApplicationsModel{nullptr}; FavouritesModel *m_favouritesModel{nullptr}; PageListModel *m_pageListModel{nullptr}; bool m_overviewActive{false}; diff --git a/containments/homescreens/folio/qml/main.qml b/containments/homescreens/folio/qml/main.qml index e1134aaa..e95a99ea 100644 --- a/containments/homescreens/folio/qml/main.qml +++ b/containments/homescreens/folio/qml/main.qml @@ -305,6 +305,27 @@ ContainmentItem { readonly property real workAreaY: topBarHitHeight readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2) readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2) + readonly property real leftEdgeHotzoneWidth: Math.max(frameThickness, Math.round(Kirigami.Units.gridUnit * 0.7)) + readonly property real leftLauncherWidth: Math.min(Kirigami.Units.gridUnit * 22, width * 0.42) + readonly property real leftLauncherHeight: Math.min(Kirigami.Units.gridUnit * 16, workAreaHeight * 0.66) + readonly property bool leftLauncherEnabled: root.folio.HomeScreenState.appDrawerOpenProgress <= 0 + readonly property real leftFrameBulgeIdleDepth: Math.max(frameThickness * 1.2, Kirigami.Units.gridUnit * 0.35) + readonly property real leftFrameBulgeHoverDepth: 0 + property real leftFrameBulgeDepth: !leftLauncherEnabled || leftLauncherOpen || leftEdgeHovered + ? leftFrameBulgeHoverDepth + : leftFrameBulgeIdleDepth + // Long, thin thickening of the lower-left workspace wall. Vertical + // tangents at all three anchors keep the curve smooth as it blends + // into the straight wall above and below. + readonly property real leftFrameBulgeEffectiveDepth: Math.max(leftFrameBulgeDepth, 0.01) + readonly property real leftFrameBulgeApexX: workAreaX + leftFrameBulgeEffectiveDepth + readonly property real leftFrameBulgeHalfLength: Kirigami.Units.gridUnit * 7.5 + readonly property real leftFrameBulgeApexY: workAreaY + workAreaHeight * 0.7 + readonly property real leftFrameBulgeEdgeTopY: leftFrameBulgeApexY - leftFrameBulgeHalfLength + readonly property real leftFrameBulgeEdgeBottomY: leftFrameBulgeApexY + leftFrameBulgeHalfLength + // Bezier control-handle length along the vertical tangent at each + // anchor. ~0.55 of the half-length gives a clean, taut oval profile. + readonly property real leftFrameBulgeTangent: leftFrameBulgeHalfLength * 0.55 readonly property color chromeColor: Kirigami.Theme.backgroundColor readonly property color edgeColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) readonly property int dockAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault) @@ -315,6 +336,9 @@ ContainmentItem { // True once the hover-reveal timer fires; cleared on hover-exit. property bool hoverRevealing: false + property bool leftEdgeHovered: false + property bool leftLauncherHovered: false + property bool leftLauncherOpen: false readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled && windowMaximizedTracker.showingWindow && !hoverRevealing @@ -324,17 +348,77 @@ ContainmentItem { function updateInputRegion() { const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight) + const leftEdgeRegion = Qt.rect(0, topBarHitHeight, leftEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight)) + const leftLauncherRegion = Qt.rect(0, + Math.max(0, height - dockHeight - leftLauncherHeight), + leftLauncherWidth, + leftLauncherHeight) + let regions = [topBarRegion, leftEdgeRegion] + if (shouldHide && dockOffset >= dockHeight) { - MobileShell.ShellUtil.setInputRegions(convergenceChrome, [ - topBarRegion, - Qt.rect(0, height - revealStripHeight, width, revealStripHeight) - ]) + regions.push(Qt.rect(0, height - revealStripHeight, width, revealStripHeight)) } else { - MobileShell.ShellUtil.setInputRegions(convergenceChrome, [ - topBarRegion, - Qt.rect(0, height - dockHeight, width, dockHeight) - ]) + regions.push(Qt.rect(0, height - dockHeight, width, dockHeight)) } + + if (leftLauncherOpen) { + regions.push(leftLauncherRegion) + } + + MobileShell.ShellUtil.setInputRegions(convergenceChrome, regions) + } + + function launchStorageId(storageId) { + if (!storageId || String(storageId).length === 0) { + return + } + + var normalizedId = String(storageId) + if (!normalizedId.endsWith(".desktop")) { + normalizedId += ".desktop" + } + MobileShell.AppLaunch.launchOrActivateApp(normalizedId) + } + + function refreshLeftLauncherVisibility() { + if (!leftLauncherEnabled) { + leftLauncherCloseTimer.stop() + leftLauncherOpen = false + inputRegionTimer.restart() + return + } + + if (leftEdgeHovered || leftLauncherHovered) { + leftLauncherCloseTimer.stop() + leftLauncherOpen = true + } else { + leftLauncherCloseTimer.restart() + } + inputRegionTimer.restart() + } + + function updateLeftLauncherHoverState(pointerX, pointerY, withinWindow) { + const insideEdge = withinWindow + && pointerX >= 0 + && pointerX <= leftEdgeHotzoneWidth + && pointerY >= topBarHitHeight + && pointerY <= (height - dockHeight) + + const launcherTop = height - dockHeight - leftLauncherHeight + const insideLauncher = withinWindow + && leftLauncherOpen + && pointerX >= 0 + && pointerX <= leftLauncherWidth + && pointerY >= launcherTop + && pointerY <= (launcherTop + leftLauncherHeight) + + if (leftEdgeHovered !== insideEdge) { + leftEdgeHovered = insideEdge + } + if (leftLauncherHovered !== insideLauncher) { + leftLauncherHovered = insideLauncher + } + refreshLeftLauncherVisibility() } onActiveChanged: { @@ -353,6 +437,7 @@ ContainmentItem { } inputRegionTimer.restart() } + onLeftLauncherEnabledChanged: refreshLeftLauncherVisibility() // Narrow the input region to a strip at the screen edge when hidden // so that app controls near the bottom edge are not accidentally @@ -385,6 +470,20 @@ ContainmentItem { onTriggered: convergenceChrome.hoverRevealing = true } + Timer { + id: leftLauncherCloseTimer + interval: Kirigami.Units.shortDuration + repeat: false + onTriggered: { + if (!convergenceChrome.leftEdgeHovered + && !convergenceChrome.leftLauncherHovered + && convergenceChrome.leftLauncherOpen) { + convergenceChrome.leftLauncherOpen = false + inputRegionTimer.restart() + } + } + } + Behavior on dockOffset { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault @@ -392,6 +491,13 @@ ContainmentItem { } } + Behavior on leftFrameBulgeDepth { + MobileShell.MotionNumberAnimation { + type: MobileShell.Motion.SpatialDefault + duration: root.shortAnimationDuration + } + } + Rectangle { id: topBarSurface anchors.top: parent.top @@ -411,6 +517,7 @@ ContainmentItem { Shape { id: workspaceFrame anchors.fill: parent + preferredRendererType: Shape.CurveRenderer ShapePath { fillColor: convergenceChrome.chromeColor @@ -431,20 +538,87 @@ ContainmentItem { PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius } PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight } PathArc { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius } + PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.leftFrameBulgeEdgeBottomY } + PathCubic { + x: convergenceChrome.leftFrameBulgeApexX + y: convergenceChrome.leftFrameBulgeApexY + control1X: convergenceChrome.workAreaX + control1Y: convergenceChrome.leftFrameBulgeEdgeBottomY - convergenceChrome.leftFrameBulgeTangent + control2X: convergenceChrome.leftFrameBulgeApexX + control2Y: convergenceChrome.leftFrameBulgeApexY + convergenceChrome.leftFrameBulgeTangent + } + PathCubic { + x: convergenceChrome.workAreaX + y: convergenceChrome.leftFrameBulgeEdgeTopY + control1X: convergenceChrome.leftFrameBulgeApexX + control1Y: convergenceChrome.leftFrameBulgeApexY - convergenceChrome.leftFrameBulgeTangent + control2X: convergenceChrome.workAreaX + control2Y: convergenceChrome.leftFrameBulgeEdgeTopY + convergenceChrome.leftFrameBulgeTangent + } PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius } PathArc { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius } } } - Rectangle { - x: convergenceChrome.workAreaX - y: convergenceChrome.workAreaY - width: convergenceChrome.workAreaWidth - height: convergenceChrome.workAreaHeight - radius: convergenceChrome.frameRadius - color: "transparent" - border.width: 1 - border.color: convergenceChrome.edgeColor + Shape { + id: workspaceFrameBorder + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: "transparent" + strokeColor: convergenceChrome.edgeColor + strokeWidth: 0.85 + joinStyle: ShapePath.RoundJoin + + startX: convergenceChrome.workAreaX + convergenceChrome.frameRadius + startY: convergenceChrome.workAreaY + PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY } + PathQuad { + x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth + y: convergenceChrome.workAreaY + convergenceChrome.frameRadius + controlX: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth + controlY: convergenceChrome.workAreaY + } + PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius } + PathQuad { + x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius + y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight + controlX: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth + controlY: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight + } + PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight } + PathQuad { + x: convergenceChrome.workAreaX + y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius + controlX: convergenceChrome.workAreaX + controlY: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight + } + PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.leftFrameBulgeEdgeBottomY } + PathCubic { + x: convergenceChrome.leftFrameBulgeApexX + y: convergenceChrome.leftFrameBulgeApexY + control1X: convergenceChrome.workAreaX + control1Y: convergenceChrome.leftFrameBulgeEdgeBottomY - convergenceChrome.leftFrameBulgeTangent + control2X: convergenceChrome.leftFrameBulgeApexX + control2Y: convergenceChrome.leftFrameBulgeApexY + convergenceChrome.leftFrameBulgeTangent + } + PathCubic { + x: convergenceChrome.workAreaX + y: convergenceChrome.leftFrameBulgeEdgeTopY + control1X: convergenceChrome.leftFrameBulgeApexX + control1Y: convergenceChrome.leftFrameBulgeApexY - convergenceChrome.leftFrameBulgeTangent + control2X: convergenceChrome.workAreaX + control2Y: convergenceChrome.leftFrameBulgeEdgeTopY + convergenceChrome.leftFrameBulgeTangent + } + PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius } + PathQuad { + x: convergenceChrome.workAreaX + convergenceChrome.frameRadius + y: convergenceChrome.workAreaY + controlX: convergenceChrome.workAreaX + controlY: convergenceChrome.workAreaY + } + } } Rectangle { @@ -488,6 +662,236 @@ ContainmentItem { Kirigami.Theme.colorSet: Kirigami.Theme.Window } } + + Item { + id: leftEdgeStrip + + anchors.left: parent.left + anchors.top: topBarSurface.bottom + anchors.bottom: dockSurface.top + width: convergenceChrome.leftEdgeHotzoneWidth + } + + MouseArea { + id: leftLauncherPointerTracker + + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + z: 10 + + onPositionChanged: (mouse) => { + convergenceChrome.updateLeftLauncherHoverState(mouse.x, mouse.y, true) + } + onExited: { + convergenceChrome.updateLeftLauncherHoverState(-1, -1, false) + } + } + + Item { + id: leftEdgeLauncher + + width: convergenceChrome.leftLauncherWidth + height: convergenceChrome.leftLauncherHeight + x: 0 + y: convergenceChrome.height - convergenceChrome.dockHeight - height + visible: convergenceChrome.leftLauncherOpen + opacity: convergenceChrome.leftLauncherOpen ? 1 : 0 + clip: true + + transform: Translate { + y: convergenceChrome.leftLauncherOpen ? 0 : Kirigami.Units.gridUnit + x: convergenceChrome.leftLauncherOpen ? 0 : -leftEdgeLauncher.width + convergenceChrome.leftEdgeHotzoneWidth + } + + Behavior on opacity { + MobileShell.MotionNumberAnimation { + type: MobileShell.Motion.EffectsFast + duration: root.shortAnimationDuration + } + } + + readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, height * 0.24) + + Shape { + id: leftLauncherSurface + anchors.fill: parent + + ShapePath { + fillColor: convergenceChrome.chromeColor + strokeWidth: 0 + + startX: 0 + startY: 0 + PathLine { x: leftEdgeLauncher.width - leftEdgeLauncher.cornerRadius; y: 0 } + PathArc { + x: leftEdgeLauncher.width + y: leftEdgeLauncher.cornerRadius + radiusX: leftEdgeLauncher.cornerRadius + radiusY: leftEdgeLauncher.cornerRadius + } + PathLine { x: leftEdgeLauncher.width; y: leftEdgeLauncher.height } + PathLine { x: 0; y: leftEdgeLauncher.height } + PathLine { x: 0; y: 0 } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.gridUnit * 0.65 + + ColumnLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + PlasmaComponents.Label { + Layout.fillWidth: true + text: i18n("Recently Used") + font.weight: Font.Medium + elide: Text.ElideRight + } + + ListView { + id: recentAppsList + + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 6.8 + clip: true + interactive: false + boundsBehavior: Flickable.StopAtBounds + model: folio.RecentApplicationsModel + + delegate: MouseArea { + required property int index + required property var model + + readonly property var delegateObject: model.delegate + readonly property var application: delegateObject ? delegateObject.application : null + readonly property bool validEntry: index < 5 && application !== null + + width: recentAppsList.width + height: validEntry ? Kirigami.Units.gridUnit * 1.35 : 0 + enabled: validEntry + hoverEnabled: validEntry + cursorShape: validEntry ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: { + if (application) { + convergenceChrome.launchStorageId(application.storageId) + } + } + + Rectangle { + anchors.fill: parent + radius: Kirigami.Units.cornerRadius + color: parent.containsMouse + ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08) + : "transparent" + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Kirigami.Units.smallSpacing + anchors.rightMargin: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.preferredHeight: Layout.preferredWidth + source: application ? application.icon : "" + } + + PlasmaComponents.Label { + Layout.fillWidth: true + text: application ? application.name : "" + elide: Text.ElideRight + maximumLineCount: 1 + } + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Kirigami.Units.smallSpacing + + PlasmaComponents.Label { + Layout.fillWidth: true + text: i18n("Most Used") + font.weight: Font.Medium + elide: Text.ElideRight + } + + ListView { + id: favouritesQuickList + + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + interactive: false + boundsBehavior: Flickable.StopAtBounds + model: folio.MostUsedApplicationsModel + + delegate: MouseArea { + required property int index + required property var model + + readonly property var delegateObject: model.delegate + readonly property var application: delegateObject ? delegateObject.application : null + readonly property bool validEntry: index < 6 && application !== null + + width: favouritesQuickList.width + height: validEntry ? Kirigami.Units.gridUnit * 1.35 : 0 + enabled: validEntry + hoverEnabled: validEntry + cursorShape: validEntry ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: { + if (application) { + convergenceChrome.launchStorageId(application.storageId) + } + } + + Rectangle { + anchors.fill: parent + radius: Kirigami.Units.cornerRadius + color: parent.containsMouse + ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08) + : "transparent" + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Kirigami.Units.smallSpacing + anchors.rightMargin: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.preferredHeight: Layout.preferredWidth + source: application ? application.icon : "" + } + + PlasmaComponents.Label { + Layout.fillWidth: true + text: application ? application.name : "" + elide: Text.ElideRight + maximumLineCount: 1 + } + } + } + } + } + } + } } // App-drawer overlay — renders the popup drawer above application diff --git a/tests/check-convergence-dock-invariant.sh b/tests/check-convergence-dock-invariant.sh index b2a670bc..5f2972b0 100644 --- a/tests/check-convergence-dock-invariant.sh +++ b/tests/check-convergence-dock-invariant.sh @@ -105,8 +105,7 @@ require_line "$folio_main" "id: convergenceChrome" require_line "$folio_main" "LayerShell.Window.scope: \"convergence-chrome\"" require_line "$folio_main" "height: Screen.height" require_line "$folio_main" "MobileShell.StatusBar {" -require_line "$folio_main" "MobileShell.ShellUtil.setInputRegions(convergenceChrome, [" -require_line "$folio_main" "readonly property real topBarHitHeight: topBarHeight + frameThickness" +require_line "$folio_main" "MobileShell.ShellUtil.setInputRegions(convergenceChrome, regions)" require_line "$folio_main" "const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)" require_line "$folio_main" "readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight" require_line "$folio_main" "readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight" @@ -115,6 +114,7 @@ require_line "$folio_main" "id: workspaceFrame" require_line "$folio_main" "readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness" require_line "$folio_main" "readonly property real frameRadius:" require_line "$folio_main" "readonly property real workAreaX: frameThickness" +require_line "$folio_main" "readonly property real topBarHitHeight: topBarHeight + frameThickness" require_line "$folio_main" "readonly property real workAreaY: topBarHitHeight" require_line "$folio_main" "readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)" require_line "$folio_main" "readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2)"