Add convergence launcher app lists

Record app launches from window creation so recent and most-used
lists survive after windows close.

Use those models in the convergence launcher and keep the hover
surface in the unified chrome.
This commit is contained in:
Marco Allegretti 2026-05-26 17:27:44 +02:00
parent d0262d0dd8
commit 1702027f7e
11 changed files with 773 additions and 19 deletions

View file

@ -26,6 +26,7 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio
qml/config.qml qml/config.qml
CPP_SOURCES CPP_SOURCES
applicationlistmodel.cpp applicationlistmodel.cpp
applicationusagemodel.cpp
delegatetoucharea.cpp delegatetoucharea.cpp
dragstate.cpp dragstate.cpp
favouritesmodel.cpp favouritesmodel.cpp

View file

@ -182,6 +182,17 @@ int ApplicationListModel::rowCount(const QModelIndex &parent) const
return m_delegates.count(); return m_delegates.count();
} }
std::shared_ptr<FolioDelegate> 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. // Sub-categories merged into their canonical parent, mirroring Kickoff's grouping.
static QString normalizeCategory(const QString &cat) static QString normalizeCategory(const QString &cat)
{ {

View file

@ -12,6 +12,8 @@
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QStringList> #include <QStringList>
#include <memory>
#include <KService> #include <KService>
#include "foliodelegate.h" #include "foliodelegate.h"
@ -45,6 +47,8 @@ public:
void load(); void load();
std::shared_ptr<FolioDelegate> delegateForStorageId(const QString &storageId) const;
Q_INVOKABLE QStringList allCategories() const; Q_INVOKABLE QStringList allCategories() const;
Q_SIGNALS: Q_SIGNALS:

View file

@ -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 <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
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<ApplicationUsageEntry> 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<ApplicationUsageEntry> 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<int, QByteArray> ApplicationUsageModel::roleNames() const
{
return {
{DelegateRole, QByteArrayLiteral("delegate")},
{LaunchCountRole, QByteArrayLiteral("launchCount")},
{LastUsedRole, QByteArrayLiteral("lastUsed")},
};
}
void ApplicationUsageModel::rebuild()
{
QList<ApplicationUsageEntry> entries = m_store ? m_store->entries() : QList<ApplicationUsageEntry>{};
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<ApplicationUsageEntry> nextEntries;
QList<std::shared_ptr<FolioDelegate>> 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();
}

View file

@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#pragma once
#include <QAbstractListModel>
#include <QDateTime>
#include <QHash>
#include <QList>
#include <QObject>
#include <QString>
#include <QtQmlIntegration/qqmlintegration.h>
#include <memory>
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<ApplicationUsageEntry> entries() const;
void recordUsage(const QString &storageId);
Q_SIGNALS:
void usageChanged();
private:
void load();
void save();
HomeScreen *m_homeScreen{nullptr};
QHash<QString, ApplicationUsageEntry> 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<int, QByteArray> roleNames() const override;
public Q_SLOTS:
void rebuild();
private:
HomeScreen *m_homeScreen{nullptr};
ApplicationUsageStore *m_store{nullptr};
Mode m_mode{RecentUsage};
QList<ApplicationUsageEntry> m_entries;
QList<std::shared_ptr<FolioDelegate>> m_delegates;
};

View file

@ -16,6 +16,7 @@ using namespace Qt::Literals::StringLiterals;
const QString CFG_GROUP_FOLIO = QStringLiteral("Folio"); const QString CFG_GROUP_FOLIO = QStringLiteral("Folio");
const QString CFG_KEY_FAVORITES = QStringLiteral("favorites"); 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_PAGES = QStringLiteral("pages");
const QString CFG_KEY_HOMESCREEN_ROWS = QStringLiteral("homeScreenRows"); const QString CFG_KEY_HOMESCREEN_ROWS = QStringLiteral("homeScreenRows");
@ -47,6 +48,17 @@ void FolioSettings::setFavorites(const QString &favoritesJson)
Q_EMIT m_homeScreen->configNeedsSaving(); 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 QString FolioSettings::pages() const
{ {
return generalConfigGroup().readEntry(CFG_KEY_PAGES, u"{}"_s); return generalConfigGroup().readEntry(CFG_KEY_PAGES, u"{}"_s);

View file

@ -53,6 +53,10 @@ public:
QString favorites() const; QString favorites() const;
void setFavorites(const QString &favoritesJson); void setFavorites(const QString &favoritesJson);
// JSON array
QString applicationUsage() const;
void setApplicationUsage(const QString &applicationUsageJson);
// JSON object // JSON object
QString pages() const; QString pages() const;
void setPages(const QString &pagesJson); void setPages(const QString &pagesJson);

View file

@ -4,6 +4,8 @@
#include "homescreen.h" #include "homescreen.h"
#include "windowlistener.h"
#include <virtualdesktopinfo.h> #include <virtualdesktopinfo.h>
#include <KWindowSystem> #include <KWindowSystem>
@ -58,6 +60,9 @@ HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVari
, m_widgetsManager{new WidgetsManager{this}} , m_widgetsManager{new WidgetsManager{this}}
, m_applicationListModel{new ApplicationListModel{this}} , m_applicationListModel{new ApplicationListModel{this}}
, m_applicationListSearchModel{new ApplicationListSearchModel{this, m_applicationListModel}} , 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_favouritesModel{new FavouritesModel{this}}
, m_pageListModel{new PageListModel{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::appletAdded, this, &HomeScreen::onAppletAdded);
connect(this, &Plasma::Containment::appletAboutToBeRemoved, this, &HomeScreen::onAppletAboutToBeRemoved); 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; HomeScreen::~HomeScreen() = default;
@ -129,6 +139,16 @@ ApplicationListSearchModel *HomeScreen::applicationListSearchModel()
return m_applicationListSearchModel; return m_applicationListSearchModel;
} }
ApplicationUsageModel *HomeScreen::recentApplicationsModel()
{
return m_recentApplicationsModel;
}
ApplicationUsageModel *HomeScreen::mostUsedApplicationsModel()
{
return m_mostUsedApplicationsModel;
}
FavouritesModel *HomeScreen::favouritesModel() FavouritesModel *HomeScreen::favouritesModel()
{ {
return m_favouritesModel; return m_favouritesModel;

View file

@ -9,6 +9,7 @@
#include <QVariant> #include <QVariant>
#include "applicationlistmodel.h" #include "applicationlistmodel.h"
#include "applicationusagemodel.h"
#include "delegatetoucharea.h" #include "delegatetoucharea.h"
#include "favouritesmodel.h" #include "favouritesmodel.h"
#include "folioapplication.h" #include "folioapplication.h"
@ -29,6 +30,8 @@ class HomeScreenState;
class FavouritesModel; class FavouritesModel;
class ApplicationListModel; class ApplicationListModel;
class ApplicationListSearchModel; class ApplicationListSearchModel;
class ApplicationUsageModel;
class ApplicationUsageStore;
class HomeScreen : public Plasma::Containment class HomeScreen : public Plasma::Containment
{ {
@ -41,6 +44,8 @@ class HomeScreen : public Plasma::Containment
Q_PROPERTY(WidgetsManager *WidgetsManager READ widgetsManager CONSTANT) Q_PROPERTY(WidgetsManager *WidgetsManager READ widgetsManager CONSTANT)
Q_PROPERTY(ApplicationListModel *ApplicationListModel READ applicationListModel CONSTANT) Q_PROPERTY(ApplicationListModel *ApplicationListModel READ applicationListModel CONSTANT)
Q_PROPERTY(ApplicationListSearchModel *ApplicationListSearchModel READ applicationListSearchModel 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(FavouritesModel *FavouritesModel READ favouritesModel CONSTANT)
Q_PROPERTY(PageListModel *PageListModel READ pageListModel CONSTANT) Q_PROPERTY(PageListModel *PageListModel READ pageListModel CONSTANT)
Q_PROPERTY(bool overviewActive READ overviewActive NOTIFY overviewActiveChanged) Q_PROPERTY(bool overviewActive READ overviewActive NOTIFY overviewActiveChanged)
@ -63,6 +68,8 @@ public:
WidgetsManager *widgetsManager(); WidgetsManager *widgetsManager();
ApplicationListModel *applicationListModel(); ApplicationListModel *applicationListModel();
ApplicationListSearchModel *applicationListSearchModel(); ApplicationListSearchModel *applicationListSearchModel();
ApplicationUsageModel *recentApplicationsModel();
ApplicationUsageModel *mostUsedApplicationsModel();
FavouritesModel *favouritesModel(); FavouritesModel *favouritesModel();
PageListModel *pageListModel(); PageListModel *pageListModel();
bool overviewActive() const; bool overviewActive() const;
@ -85,6 +92,9 @@ private:
WidgetsManager *m_widgetsManager{nullptr}; WidgetsManager *m_widgetsManager{nullptr};
ApplicationListModel *m_applicationListModel{nullptr}; ApplicationListModel *m_applicationListModel{nullptr};
ApplicationListSearchModel *m_applicationListSearchModel{nullptr}; ApplicationListSearchModel *m_applicationListSearchModel{nullptr};
ApplicationUsageStore *m_applicationUsageStore{nullptr};
ApplicationUsageModel *m_recentApplicationsModel{nullptr};
ApplicationUsageModel *m_mostUsedApplicationsModel{nullptr};
FavouritesModel *m_favouritesModel{nullptr}; FavouritesModel *m_favouritesModel{nullptr};
PageListModel *m_pageListModel{nullptr}; PageListModel *m_pageListModel{nullptr};
bool m_overviewActive{false}; bool m_overviewActive{false};

View file

@ -305,6 +305,27 @@ ContainmentItem {
readonly property real workAreaY: topBarHitHeight readonly property real workAreaY: topBarHitHeight
readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2) 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 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 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 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) 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. // True once the hover-reveal timer fires; cleared on hover-exit.
property bool hoverRevealing: false property bool hoverRevealing: false
property bool leftEdgeHovered: false
property bool leftLauncherHovered: false
property bool leftLauncherOpen: false
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing && windowMaximizedTracker.showingWindow && !hoverRevealing
@ -324,17 +348,77 @@ ContainmentItem {
function updateInputRegion() { function updateInputRegion() {
const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight) 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) { if (shouldHide && dockOffset >= dockHeight) {
MobileShell.ShellUtil.setInputRegions(convergenceChrome, [ regions.push(Qt.rect(0, height - revealStripHeight, width, revealStripHeight))
topBarRegion,
Qt.rect(0, height - revealStripHeight, width, revealStripHeight)
])
} else { } else {
MobileShell.ShellUtil.setInputRegions(convergenceChrome, [ regions.push(Qt.rect(0, height - dockHeight, width, dockHeight))
topBarRegion,
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: { onActiveChanged: {
@ -353,6 +437,7 @@ ContainmentItem {
} }
inputRegionTimer.restart() inputRegionTimer.restart()
} }
onLeftLauncherEnabledChanged: refreshLeftLauncherVisibility()
// Narrow the input region to a strip at the screen edge when hidden // Narrow the input region to a strip at the screen edge when hidden
// so that app controls near the bottom edge are not accidentally // so that app controls near the bottom edge are not accidentally
@ -385,6 +470,20 @@ ContainmentItem {
onTriggered: convergenceChrome.hoverRevealing = true 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 { Behavior on dockOffset {
MobileShell.MotionNumberAnimation { MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault type: MobileShell.Motion.SpatialDefault
@ -392,6 +491,13 @@ ContainmentItem {
} }
} }
Behavior on leftFrameBulgeDepth {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: root.shortAnimationDuration
}
}
Rectangle { Rectangle {
id: topBarSurface id: topBarSurface
anchors.top: parent.top anchors.top: parent.top
@ -411,6 +517,7 @@ ContainmentItem {
Shape { Shape {
id: workspaceFrame id: workspaceFrame
anchors.fill: parent anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
fillColor: convergenceChrome.chromeColor 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 } 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 } 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 } 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 } PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius } PathArc { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
} }
} }
Rectangle { Shape {
x: convergenceChrome.workAreaX id: workspaceFrameBorder
y: convergenceChrome.workAreaY anchors.fill: parent
width: convergenceChrome.workAreaWidth preferredRendererType: Shape.CurveRenderer
height: convergenceChrome.workAreaHeight
radius: convergenceChrome.frameRadius ShapePath {
color: "transparent" fillColor: "transparent"
border.width: 1 strokeColor: convergenceChrome.edgeColor
border.color: 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 { Rectangle {
@ -488,6 +662,236 @@ ContainmentItem {
Kirigami.Theme.colorSet: Kirigami.Theme.Window 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 // App-drawer overlay renders the popup drawer above application

View file

@ -105,8 +105,7 @@ require_line "$folio_main" "id: convergenceChrome"
require_line "$folio_main" "LayerShell.Window.scope: \"convergence-chrome\"" require_line "$folio_main" "LayerShell.Window.scope: \"convergence-chrome\""
require_line "$folio_main" "height: Screen.height" require_line "$folio_main" "height: Screen.height"
require_line "$folio_main" "MobileShell.StatusBar {" require_line "$folio_main" "MobileShell.StatusBar {"
require_line "$folio_main" "MobileShell.ShellUtil.setInputRegions(convergenceChrome, [" require_line "$folio_main" "MobileShell.ShellUtil.setInputRegions(convergenceChrome, regions)"
require_line "$folio_main" "readonly property real topBarHitHeight: topBarHeight + frameThickness"
require_line "$folio_main" "const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)" 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 dockHeight: MobileShell.Constants.convergenceDockHeight"
require_line "$folio_main" "readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight" 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 frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real frameRadius:" require_line "$folio_main" "readonly property real frameRadius:"
require_line "$folio_main" "readonly property real workAreaX: frameThickness" 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 workAreaY: topBarHitHeight"
require_line "$folio_main" "readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)" 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)" require_line "$folio_main" "readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2)"