mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 00:47:22 +00:00
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:
parent
d0262d0dd8
commit
1702027f7e
11 changed files with 773 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -182,6 +182,17 @@ int ApplicationListModel::rowCount(const QModelIndex &parent) const
|
|||
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.
|
||||
static QString normalizeCategory(const QString &cat)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
#include <QSortFilterProxyModel>
|
||||
#include <QStringList>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <KService>
|
||||
|
||||
#include "foliodelegate.h"
|
||||
|
|
@ -45,6 +47,8 @@ public:
|
|||
|
||||
void load();
|
||||
|
||||
std::shared_ptr<FolioDelegate> delegateForStorageId(const QString &storageId) const;
|
||||
|
||||
Q_INVOKABLE QStringList allCategories() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
|
|
|
|||
208
containments/homescreens/folio/applicationusagemodel.cpp
Normal file
208
containments/homescreens/folio/applicationusagemodel.cpp
Normal 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();
|
||||
}
|
||||
80
containments/homescreens/folio/applicationusagemodel.h
Normal file
80
containments/homescreens/folio/applicationusagemodel.h
Normal 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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
#include "homescreen.h"
|
||||
|
||||
#include "windowlistener.h"
|
||||
|
||||
#include <virtualdesktopinfo.h>
|
||||
|
||||
#include <KWindowSystem>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <QVariant>
|
||||
|
||||
#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};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue