Compare commits

..

6 commits

Author SHA1 Message Date
f09a1db84a Animate dynamic tiling window moves
Add a KWin JavaScript effect for compositor-side size and translation transitions, and enable it in convergence mode. Keep the tile preview effect as an outline driver so nested preview does not activate a SceneEffect surface.
2026-05-27 21:33:34 +02:00
495f76900f Restore dock hover tracking
Replace the fullscreen convergence pointer tracker with edge-local hover areas so dock buttons keep receiving hover, tooltip, and cursor events.
2026-05-27 21:32:18 +02:00
97abf33597 Add dynamic tiling layout menu
Expose layout requests and state through shell settings so Folio
can offer alternatives for the current tiled workspace. Keep KWin
as the owner of layout changes, snapshot request serials so requests
are not dropped, and hide the edge menu when fewer than two windows
are tiled.
2026-05-27 15:48:09 +02:00
1702027f7e 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.
2026-05-26 17:27:44 +02:00
d0262d0dd8 Stop double-reserving top chrome
Keep the convergence panel hit area non-reserving so the
explicit topbar-space surface remains the only top strut.
2026-05-26 17:27:44 +02:00
1a70b24c06 Convergence: stabilize workspace scroll gestures
Improve touchpad scroll handling for convergence workflows while avoiding gesture conflicts.

- make SwipeArea handle angleDelta-based touchpad wheel streams and phase-less sequences safely
- wire topbar ActionDrawerOpenSurface to virtual desktop info and horizontal workspace switching logic
- add dock pager wheel switching in FavouritesBar for the vertical wheel axis environments deliver
- prevent wallpaper/home surface touchpad scroll from opening app drawer in convergence
- extend convergence invariant checks for the new gesture paths
2026-05-25 21:59:19 +02:00
28 changed files with 1915 additions and 165 deletions

View file

@ -10,9 +10,12 @@
#include <QObject> #include <QObject>
#include <QTabletEvent> #include <QTabletEvent>
#include <QTouchEvent> #include <QTouchEvent>
#include <QWheelEvent>
// how many pixels to move before it starts being registered as a swipe // how many pixels to move before it starts being registered as a swipe
const int SWIPE_REGISTER_THRESHOLD = 10; const int SWIPE_REGISTER_THRESHOLD = 10;
const qreal WHEEL_STEP_PIXEL_DELTA = 48.0;
const int TOUCHPAD_SCROLL_END_TIMEOUT = 160;
SwipeArea::SwipeArea(QQuickItem *parent) SwipeArea::SwipeArea(QQuickItem *parent)
: QQuickItem{parent} : QQuickItem{parent}
@ -20,6 +23,10 @@ SwipeArea::SwipeArea(QQuickItem *parent)
setAcceptTouchEvents(true); setAcceptTouchEvents(true);
setAcceptedMouseButtons(Qt::LeftButton); setAcceptedMouseButtons(Qt::LeftButton);
setFiltersChildMouseEvents(true); setFiltersChildMouseEvents(true);
m_touchpadScrollEndTimer.setSingleShot(true);
m_touchpadScrollEndTimer.setInterval(TOUCHPAD_SCROLL_END_TIMEOUT);
connect(&m_touchpadScrollEndTimer, &QTimer::timeout, this, &SwipeArea::endTouchpadScroll);
} }
SwipeArea::Mode SwipeArea::mode() const SwipeArea::Mode SwipeArea::mode() const
@ -225,43 +232,71 @@ void SwipeArea::wheelEvent(QWheelEvent *event)
event->setAccepted(false); event->setAccepted(false);
const bool isTouchpad = event->deviceType() == QInputDevice::DeviceType::TouchPad;
QPointF scrollDelta = event->pixelDelta();
if (scrollDelta.isNull() && !event->angleDelta().isNull()) {
scrollDelta = QPointF(event->angleDelta()) / QWheelEvent::DefaultDeltasPerStep * WHEEL_STEP_PIXEL_DELTA;
}
switch (event->phase()) { switch (event->phase()) {
case Qt::ScrollBegin: case Qt::ScrollBegin:
if (!m_touchpadScrolling) { if (isTouchpad && !m_touchpadScrolling) {
event->accept(); event->accept();
startTouchpadScroll(event->points().first().position());
m_touchpadScrolling = true;
m_totalScrollDelta = QPointF{0, 0};
Q_EMIT touchpadScrollStarted(event->points().first().position());
} }
break; break;
case Qt::ScrollEnd: case Qt::ScrollEnd:
if (m_touchpadScrolling) { if (m_touchpadScrolling) {
m_touchpadScrolling = false; endTouchpadScroll();
m_totalScrollDelta = QPointF{0, 0}; event->accept();
Q_EMIT touchpadScrollEnded();
} }
break; return;
default: default:
break; break;
} }
// HACK: if it isn't the touchpad, we never get the isBeginEvent() and isEndEvent() events
if (!m_touchpadScrolling) { if (!m_touchpadScrolling) {
return; if (!isTouchpad || scrollDelta.isNull()) {
return;
}
startTouchpadScroll(event->points().first().position());
} }
for (auto &point : event->points()) { for (auto &point : event->points()) {
event->addPassiveGrabber(point, this); event->addPassiveGrabber(point, this);
} }
auto pixelDelta = event->pixelDelta(); m_totalScrollDelta = QPointF{m_totalScrollDelta + scrollDelta};
m_totalScrollDelta = QPointF{m_totalScrollDelta + pixelDelta}; Q_EMIT touchpadScrollMove(m_totalScrollDelta.x(), m_totalScrollDelta.y(), scrollDelta.x(), scrollDelta.y());
Q_EMIT touchpadScrollMove(m_totalScrollDelta.x(), m_totalScrollDelta.y(), pixelDelta.x(), pixelDelta.y());
m_touchpadScrollEndTimer.start();
event->accept(); event->accept();
} }
void SwipeArea::startTouchpadScroll(QPointF point)
{
if (m_touchpadScrolling) {
return;
}
m_touchpadScrolling = true;
m_totalScrollDelta = QPointF{0, 0};
Q_EMIT touchpadScrollStarted(point);
}
void SwipeArea::endTouchpadScroll()
{
if (!m_touchpadScrolling) {
return;
}
m_touchpadScrollEndTimer.stop();
m_touchpadScrolling = false;
m_totalScrollDelta = QPointF{0, 0};
Q_EMIT touchpadScrollEnded();
}
void SwipeArea::setMoving(bool moving) void SwipeArea::setMoving(bool moving)
{ {
m_moving = moving; m_moving = moving;

View file

@ -8,6 +8,7 @@
#include <QPointerEvent> #include <QPointerEvent>
#include <QQmlListProperty> #include <QQmlListProperty>
#include <QQuickItem> #include <QQuickItem>
#include <QTimer>
#include <QTouchEvent> #include <QTouchEvent>
/** /**
@ -84,6 +85,8 @@ private:
void handlePressEvent(QPointerEvent *event, QPointF point); void handlePressEvent(QPointerEvent *event, QPointF point);
void handleReleaseEvent(QPointerEvent *event, QPointF point); void handleReleaseEvent(QPointerEvent *event, QPointF point);
void handleMoveEvent(QPointerEvent *event, QPointF point); void handleMoveEvent(QPointerEvent *event, QPointF point);
void startTouchpadScroll(QPointF point);
void endTouchpadScroll();
Mode m_mode = Mode::BothAxis; Mode m_mode = Mode::BothAxis;
bool m_interactive = true; bool m_interactive = true;
@ -110,6 +113,8 @@ private:
// the total amount of distance scrolled // the total amount of distance scrolled
QPointF m_totalScrollDelta; QPointF m_totalScrollDelta;
QTimer m_touchpadScrollEndTimer;
}; };
QML_DECLARE_TYPE(SwipeArea) QML_DECLARE_TYPE(SwipeArea)

View file

@ -17,6 +17,15 @@ MobileShell.SwipeArea {
mode: MobileShell.SwipeArea.VerticalOnly mode: MobileShell.SwipeArea.VerticalOnly
required property ActionDrawer actionDrawer required property ActionDrawer actionDrawer
property var virtualDesktopInfo: null
readonly property real touchpadDirectionLockThreshold: 12
readonly property real touchpadWorkspaceSwitchThreshold: 80
readonly property real touchpadAxisDominance: 1.25
property point touchpadStartPoint: Qt.point(0, 0)
property string touchpadGestureMode: ""
property bool touchpadWorkspaceSwitched: false
function startSwipe() { function startSwipe() {
if (actionDrawer.intendedToBeVisible) { if (actionDrawer.intendedToBeVisible) {
@ -54,12 +63,96 @@ MobileShell.SwipeArea {
actionDrawer.offset += offsetY; actionDrawer.offset += offsetY;
} }
function workspaceScrollAvailable() {
return ShellSettings.Settings.convergenceModeEnabled
&& virtualDesktopInfo !== null
&& virtualDesktopInfo.numberOfDesktops > 1;
}
function desktopIndexForId(desktopId) {
if (virtualDesktopInfo === null || !virtualDesktopInfo.desktopIds) {
return -1;
}
for (let i = 0; i < virtualDesktopInfo.desktopIds.length; ++i) {
if (String(virtualDesktopInfo.desktopIds[i]) === String(desktopId)) {
return i;
}
}
return -1;
}
function activateAdjacentWorkspace(direction) {
if (!workspaceScrollAvailable()) {
return;
}
const currentIndex = desktopIndexForId(virtualDesktopInfo.currentDesktop);
if (currentIndex < 0) {
return;
}
const targetIndex = Math.max(0, Math.min(virtualDesktopInfo.desktopIds.length - 1, currentIndex + direction));
if (targetIndex !== currentIndex) {
virtualDesktopInfo.requestActivate(virtualDesktopInfo.desktopIds[targetIndex]);
}
}
function startTouchpadScroll(point) {
touchpadStartPoint = point;
touchpadGestureMode = "";
touchpadWorkspaceSwitched = false;
if (!ShellSettings.Settings.convergenceModeEnabled) {
touchpadGestureMode = "drawer";
startSwipeWithPoint(point);
}
}
function moveTouchpadScroll(totalDeltaX, totalDeltaY, deltaX, deltaY) {
if (touchpadGestureMode === "") {
const absX = Math.abs(totalDeltaX);
const absY = Math.abs(totalDeltaY);
if (absY >= touchpadDirectionLockThreshold && absY > absX * touchpadAxisDominance) {
touchpadGestureMode = "drawer";
startSwipeWithPoint(touchpadStartPoint);
updateOffset(totalDeltaY);
return;
} else if (workspaceScrollAvailable() && absX >= touchpadDirectionLockThreshold && absX > absY * touchpadAxisDominance) {
touchpadGestureMode = "workspace";
} else {
return;
}
}
if (touchpadGestureMode === "drawer") {
updateOffset(deltaY);
return;
}
if (!touchpadWorkspaceSwitched && Math.abs(totalDeltaX) >= touchpadWorkspaceSwitchThreshold) {
touchpadWorkspaceSwitched = true;
activateAdjacentWorkspace(totalDeltaX < 0 ? 1 : -1);
}
}
function endTouchpadScroll() {
if (touchpadGestureMode === "drawer") {
endSwipe();
}
touchpadGestureMode = "";
touchpadWorkspaceSwitched = false;
}
anchors.fill: parent anchors.fill: parent
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
hoverEnabled: true hoverEnabled: true
scrollGestureEnabled: false
cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
} }
@ -67,9 +160,9 @@ MobileShell.SwipeArea {
onSwipeEnded: endSwipe() onSwipeEnded: endSwipe()
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(deltaY); onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(deltaY);
onTouchpadScrollStarted: (point) => startSwipeWithPoint(point) onTouchpadScrollStarted: (point) => startTouchpadScroll(point)
onTouchpadScrollEnded: endSwipe() onTouchpadScrollEnded: endTouchpadScroll()
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(deltaY); onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveTouchpadScroll(totalDeltaX, totalDeltaY, deltaX, deltaY);
// In convergence mode, allow click to toggle the action drawer (mouse-friendly) // In convergence mode, allow click to toggle the action drawer (mouse-friendly)
onClicked: { onClicked: {

View file

@ -49,6 +49,8 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
Q_EMIT dynamicTilingEnabledChanged(); Q_EMIT dynamicTilingEnabledChanged();
Q_EMIT dynamicTilingWindowRequestChanged(); Q_EMIT dynamicTilingWindowRequestChanged();
Q_EMIT dynamicTilingWindowStateChanged(); Q_EMIT dynamicTilingWindowStateChanged();
Q_EMIT dynamicTilingLayoutRequestChanged();
Q_EMIT dynamicTilingLayoutStateChanged();
Q_EMIT snapLayoutsEnabledChanged(); Q_EMIT snapLayoutsEnabledChanged();
Q_EMIT allowLogoutChanged(); Q_EMIT allowLogoutChanged();
} }
@ -371,6 +373,69 @@ void MobileShellSettings::reportDynamicTilingWindowState(const QStringList &maxi
Q_EMIT dynamicTilingWindowStateChanged(); Q_EMIT dynamicTilingWindowStateChanged();
} }
QString MobileShellSettings::dynamicTilingLayoutRequestMode() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutRequestMode", QString{});
}
int MobileShellSettings::dynamicTilingLayoutRequestSerial() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutRequestSerial", 0);
}
void MobileShellSettings::requestDynamicTilingLayoutMode(const QString &mode)
{
if (mode.isEmpty()) {
return;
}
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
const int serial = group.readEntry("dynamicTilingLayoutRequestSerial", 0) + 1;
group.writeEntry("dynamicTilingLayoutRequestMode", mode, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutRequestSerial", serial, KConfigGroup::Notify);
m_config->sync();
Q_EMIT dynamicTilingLayoutRequestChanged();
}
QString MobileShellSettings::dynamicTilingLayoutMode() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutMode", QString{});
}
int MobileShellSettings::dynamicTilingLayoutWindowCount() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutWindowCount", 0);
}
int MobileShellSettings::dynamicTilingLayoutStateSerial() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutStateSerial", 0);
}
void MobileShellSettings::reportDynamicTilingLayoutState(const QString &mode, int windowCount)
{
const int normalizedWindowCount = windowCount < 0 ? 0 : windowCount;
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
if (group.readEntry("dynamicTilingLayoutMode", QString{}) == mode && group.readEntry("dynamicTilingLayoutWindowCount", 0) == normalizedWindowCount) {
return;
}
const int serial = group.readEntry("dynamicTilingLayoutStateSerial", 0) + 1;
group.writeEntry("dynamicTilingLayoutMode", mode, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutWindowCount", normalizedWindowCount, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutStateSerial", serial, KConfigGroup::Notify);
m_config->sync();
Q_EMIT dynamicTilingLayoutStateChanged();
}
bool MobileShellSettings::snapLayoutsEnabled() const bool MobileShellSettings::snapLayoutsEnabled() const
{ {
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP}; auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};

View file

@ -65,6 +65,11 @@ class MobileShellSettings : public QObject
Q_PROPERTY(int dynamicTilingWindowRequestSerial READ dynamicTilingWindowRequestSerial NOTIFY dynamicTilingWindowRequestChanged) Q_PROPERTY(int dynamicTilingWindowRequestSerial READ dynamicTilingWindowRequestSerial NOTIFY dynamicTilingWindowRequestChanged)
Q_PROPERTY(QStringList dynamicTilingMaximizedWindowIds READ dynamicTilingMaximizedWindowIds NOTIFY dynamicTilingWindowStateChanged) Q_PROPERTY(QStringList dynamicTilingMaximizedWindowIds READ dynamicTilingMaximizedWindowIds NOTIFY dynamicTilingWindowStateChanged)
Q_PROPERTY(int dynamicTilingWindowStateSerial READ dynamicTilingWindowStateSerial NOTIFY dynamicTilingWindowStateChanged) Q_PROPERTY(int dynamicTilingWindowStateSerial READ dynamicTilingWindowStateSerial NOTIFY dynamicTilingWindowStateChanged)
Q_PROPERTY(QString dynamicTilingLayoutRequestMode READ dynamicTilingLayoutRequestMode NOTIFY dynamicTilingLayoutRequestChanged)
Q_PROPERTY(int dynamicTilingLayoutRequestSerial READ dynamicTilingLayoutRequestSerial NOTIFY dynamicTilingLayoutRequestChanged)
Q_PROPERTY(QString dynamicTilingLayoutMode READ dynamicTilingLayoutMode NOTIFY dynamicTilingLayoutStateChanged)
Q_PROPERTY(int dynamicTilingLayoutWindowCount READ dynamicTilingLayoutWindowCount NOTIFY dynamicTilingLayoutStateChanged)
Q_PROPERTY(int dynamicTilingLayoutStateSerial READ dynamicTilingLayoutStateSerial NOTIFY dynamicTilingLayoutStateChanged)
// Snap layout picker — only meaningful in convergence mode when dynamic tiling is off. // Snap layout picker — only meaningful in convergence mode when dynamic tiling is off.
Q_PROPERTY(bool snapLayoutsEnabled READ snapLayoutsEnabled WRITE setSnapLayoutsEnabled NOTIFY snapLayoutsEnabledChanged) Q_PROPERTY(bool snapLayoutsEnabled READ snapLayoutsEnabled WRITE setSnapLayoutsEnabled NOTIFY snapLayoutsEnabledChanged)
@ -302,6 +307,13 @@ public:
int dynamicTilingWindowStateSerial() const; int dynamicTilingWindowStateSerial() const;
Q_INVOKABLE bool isDynamicTilingWindowMaximized(const QString &windowId) const; Q_INVOKABLE bool isDynamicTilingWindowMaximized(const QString &windowId) const;
Q_INVOKABLE void reportDynamicTilingWindowState(const QStringList &maximizedWindowIds); Q_INVOKABLE void reportDynamicTilingWindowState(const QStringList &maximizedWindowIds);
QString dynamicTilingLayoutRequestMode() const;
int dynamicTilingLayoutRequestSerial() const;
Q_INVOKABLE void requestDynamicTilingLayoutMode(const QString &mode);
QString dynamicTilingLayoutMode() const;
int dynamicTilingLayoutWindowCount() const;
int dynamicTilingLayoutStateSerial() const;
Q_INVOKABLE void reportDynamicTilingLayoutState(const QString &mode, int windowCount);
/** /**
* Whether the SHIFT snap layout picker is enabled. * Whether the SHIFT snap layout picker is enabled.
@ -362,6 +374,8 @@ Q_SIGNALS:
void dynamicTilingEnabledChanged(); void dynamicTilingEnabledChanged();
void dynamicTilingWindowRequestChanged(); void dynamicTilingWindowRequestChanged();
void dynamicTilingWindowStateChanged(); void dynamicTilingWindowStateChanged();
void dynamicTilingLayoutRequestChanged();
void dynamicTilingLayoutStateChanged();
void snapLayoutsEnabledChanged(); void snapLayoutsEnabledChanged();
void allowLogoutChanged(); void allowLogoutChanged();
void lockscreenLeftButtonActionChanged(); void lockscreenLeftButtonActionChanged();

View file

@ -11,6 +11,7 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio
qml/CategoryPanel.qml qml/CategoryPanel.qml
qml/DelegateDragItem.qml qml/DelegateDragItem.qml
qml/DelegateDropArea.qml qml/DelegateDropArea.qml
qml/DynamicTilingLayoutMenu.qml
qml/FavouritesBar.qml qml/FavouritesBar.qml
qml/FolderView.qml qml/FolderView.qml
qml/FolderViewTitle.qml qml/FolderViewTitle.qml
@ -26,6 +27,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

@ -0,0 +1,298 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes 1.8
import org.kde.kirigami as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
Item {
id: root
property int windowCount: 0
property string currentMode: ""
property color surfaceColor: Kirigami.Theme.backgroundColor
property int animationDuration: Kirigami.Units.shortDuration
property real maxHeight: 0
signal layoutModeRequested(string mode)
signal dismissRequested()
readonly property int clampedWindowCount: clampedLayoutWindowCount(windowCount)
readonly property var layoutOptions: layoutOptionsForWindowCount(clampedWindowCount)
readonly property int optionCount: layoutOptions.length
readonly property real rowHeight: Math.max(Kirigami.Units.gridUnit * 2.4,
Kirigami.Units.iconSizes.medium + Kirigami.Units.smallSpacing * 2)
readonly property real naturalHeight: Kirigami.Units.gridUnit * 2.2
+ Math.max(1, optionCount) * rowHeight
+ Kirigami.Units.smallSpacing * 3
readonly property real preferredHeight: maxHeight > 0 ? Math.min(naturalHeight, maxHeight) : naturalHeight
readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, height * 0.24)
clip: true
function clampedLayoutWindowCount(windowCount) {
const count = Math.round(Number(windowCount) || 0)
return Math.max(0, Math.min(4, count))
}
function linearLayoutZones(windowCount, orientation) {
const count = Math.max(1, clampedLayoutWindowCount(windowCount))
let zones = []
for (let i = 0; i < count; i++) {
if (orientation === "horizontal") {
zones.push({ x: 0, y: i / count, w: 1, h: 1 / count })
} else {
zones.push({ x: i / count, y: 0, w: 1 / count, h: 1 })
}
}
return zones
}
function masterLayoutZones(windowCount) {
const count = clampedLayoutWindowCount(windowCount)
if (count <= 2) {
return linearLayoutZones(Math.max(1, count), "vertical")
}
let zones = [{ x: 0, y: 0, w: 0.58, h: 1 }]
const stackCount = count - 1
for (let i = 0; i < stackCount; i++) {
zones.push({ x: 0.58, y: i / stackCount, w: 0.42, h: 1 / stackCount })
}
return zones
}
function layoutOptionsForWindowCount(windowCount) {
const count = clampedLayoutWindowCount(windowCount)
if (count < 2) {
return []
}
if (count === 2) {
return [
{
mode: "columns",
selectedModes: ["master", "columns"],
name: i18n("Side by Side"),
summary: i18n("2 columns"),
zones: linearLayoutZones(count, "vertical")
},
{
mode: "rows",
selectedModes: ["rows"],
name: i18n("Stacked"),
summary: i18n("2 rows"),
zones: linearLayoutZones(count, "horizontal")
}
]
}
return [
{
mode: "master",
selectedModes: ["master"],
name: i18n("Master Stack"),
summary: i18n("1 + %1 stack", count - 1),
zones: masterLayoutZones(count)
},
{
mode: "columns",
selectedModes: ["columns"],
name: i18n("Columns"),
summary: i18n("%1 columns", count),
zones: linearLayoutZones(count, "vertical")
},
{
mode: "rows",
selectedModes: ["rows"],
name: i18n("Rows"),
summary: i18n("%1 rows", count),
zones: linearLayoutZones(count, "horizontal")
}
]
}
function emptyLayoutSummary() {
return clampedWindowCount === 1 ? i18n("1 window") : i18n("0 windows")
}
Behavior on opacity {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.EffectsFast
duration: root.animationDuration
}
}
Shape {
anchors.fill: parent
ShapePath {
fillColor: root.surfaceColor
strokeWidth: 0
startX: root.width
startY: 0
PathLine { x: root.cornerRadius; y: 0 }
PathArc {
x: 0
y: root.cornerRadius
radiusX: root.cornerRadius
radiusY: root.cornerRadius
direction: PathArc.Counterclockwise
}
PathLine { x: 0; y: root.height - root.cornerRadius }
PathArc {
x: root.cornerRadius
y: root.height
radiusX: root.cornerRadius
radiusY: root.cornerRadius
direction: PathArc.Counterclockwise
}
PathLine { x: root.width; y: root.height }
PathLine { x: root.width; y: 0 }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.Label {
Layout.fillWidth: true
text: i18n("Tiling Layout")
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
Repeater {
model: root.layoutOptions
delegate: MouseArea {
id: optionButton
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: root.rowHeight
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
readonly property bool selected: modelData.selectedModes.indexOf(root.currentMode) >= 0
onClicked: {
if (!selected) {
root.layoutModeRequested(modelData.mode)
}
root.dismissRequested()
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: optionButton.selected
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.24)
: optionButton.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
: "transparent"
border.width: optionButton.selected ? 1 : 0
border.color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.5)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Rectangle {
id: layoutPreviewFrame
Layout.preferredWidth: Kirigami.Units.gridUnit * 2.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.65
radius: Kirigami.Units.cornerRadius
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.06)
border.width: 1
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18)
Repeater {
model: optionButton.modelData.zones
delegate: Rectangle {
required property var modelData
x: Math.round(layoutPreviewFrame.width * modelData.x) + Kirigami.Units.smallSpacing / 2
y: Math.round(layoutPreviewFrame.height * modelData.y) + Kirigami.Units.smallSpacing / 2
width: Math.max(1, Math.round(layoutPreviewFrame.width * modelData.w) - Kirigami.Units.smallSpacing)
height: Math.max(1, Math.round(layoutPreviewFrame.height * modelData.h) - Kirigami.Units.smallSpacing)
radius: Math.max(1, Kirigami.Units.cornerRadius - 1)
color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.58)
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
PlasmaComponents.Label {
Layout.fillWidth: true
text: optionButton.modelData.name
elide: Text.ElideRight
maximumLineCount: 1
}
PlasmaComponents.Label {
Layout.fillWidth: true
text: optionButton.modelData.summary
opacity: 0.62
font: Kirigami.Theme.smallFont
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: root.optionCount === 0
spacing: Kirigami.Units.smallSpacing
Item { Layout.fillHeight: true }
PlasmaComponents.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: i18n("No Alternatives")
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
PlasmaComponents.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: root.emptyLayoutSummary()
opacity: 0.62
font: Kirigami.Theme.smallFont
elide: Text.ElideRight
maximumLineCount: 1
}
Item { Layout.fillHeight: true }
}
}
}

View file

@ -94,6 +94,8 @@ MouseArea {
// Virtual desktop pager (convergence mode, 2+ desktops) // Virtual desktop pager (convergence mode, 2+ desktops)
readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1 readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1
property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0 property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0
property int pagerWheelDelta: 0
property bool pagerWheelLocked: false
readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0 readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0
readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0 readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0
property real desktopButtonWidth: convergenceMode ? root.height : 0 property real desktopButtonWidth: convergenceMode ? root.height : 0
@ -128,6 +130,15 @@ MouseArea {
onTriggered: root.hideDockToolTip(root.activeDockToolTipItem) onTriggered: root.hideDockToolTip(root.activeDockToolTipItem)
} }
Timer {
id: pagerWheelEndTimer
interval: 160
onTriggered: {
root.pagerWheelDelta = 0
root.pagerWheelLocked = false
}
}
function requestDockToolTip(item) { function requestDockToolTip(item) {
activeDockToolTipItem = null activeDockToolTipItem = null
pendingDockToolTipItem = item pendingDockToolTipItem = item
@ -192,6 +203,50 @@ MouseArea {
return -1 return -1
} }
function activateAdjacentDesktop(direction) {
let ids = virtualDesktopInfo.desktopIds
if (!ids || ids.length <= 1) {
return
}
let currentIndex = root.desktopIndexForId(virtualDesktopInfo.currentDesktop)
if (currentIndex < 0) {
return
}
let targetIndex = Math.max(0, Math.min(ids.length - 1, currentIndex + direction))
if (targetIndex !== currentIndex) {
root.folio.activateVirtualDesktop(String(ids[targetIndex]))
}
}
function handlePagerWheel(wheel) {
if (!root.showPager) {
return
}
const axisDelta = wheel.angleDelta.y || -wheel.angleDelta.x
if (axisDelta === 0) {
return
}
pagerWheelEndTimer.restart()
if (root.pagerWheelLocked) {
return
}
root.pagerWheelDelta += axisDelta * (wheel.inverted ? -1 : 1)
if (root.pagerWheelDelta >= 120) {
root.pagerWheelLocked = true
root.pagerWheelDelta = 0
root.activateAdjacentDesktop(-1)
} else if (root.pagerWheelDelta <= -120) {
root.pagerWheelLocked = true
root.pagerWheelDelta = 0
root.activateAdjacentDesktop(1)
}
}
function dynamicTilingMoveToDesktopAction(desktopId) { function dynamicTilingMoveToDesktopAction(desktopId) {
let index = root.desktopIndexForId(desktopId) let index = root.desktopIndexForId(desktopId)
if (index < 0) { if (index < 0) {
@ -685,6 +740,7 @@ MouseArea {
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onWheel: (wheel) => root.handlePagerWheel(wheel)
onClicked: (mouse) => { onClicked: (mouse) => {
root.hideDockToolTip(leftDesktopBtn) root.hideDockToolTip(leftDesktopBtn)
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
@ -794,6 +850,7 @@ MouseArea {
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onWheel: (wheel) => root.handlePagerWheel(wheel)
onClicked: (mouse) => { onClicked: (mouse) => {
root.hideDockToolTip(rightDesktopBtn) root.hideDockToolTip(rightDesktopBtn)
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {

View file

@ -188,9 +188,21 @@ Item {
homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY); homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY);
} }
onTouchpadScrollStarted: homeScreenState.swipeStarted(0, 0); onTouchpadScrollStarted: {
onTouchpadScrollEnded: homeScreenState.swipeEnded(); if (!ShellSettings.Settings.convergenceModeEnabled) {
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY); root.homeScreenState.swipeStarted(0, 0);
}
}
onTouchpadScrollEnded: {
if (!ShellSettings.Settings.convergenceModeEnabled) {
root.homeScreenState.swipeEnded();
}
}
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => {
if (!ShellSettings.Settings.convergenceModeEnabled) {
root.homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY);
}
}
onPressedChanged: { onPressedChanged: {
if (pressed) { if (pressed) {

View file

@ -305,6 +305,43 @@ 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 rightEdgeHotzoneWidth: leftEdgeHotzoneWidth
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 layoutMenuWidth: Math.min(Kirigami.Units.gridUnit * 16, width * 0.34)
readonly property int layoutMenuWindowCount: Math.max(0, ShellSettings.Settings.dynamicTilingLayoutWindowCount)
readonly property bool layoutMenuEnabled: ShellSettings.Settings.dynamicTilingEnabled
&& layoutMenuWindowCount >= 2
&& root.folio.HomeScreenState.appDrawerOpenProgress <= 0
readonly property real leftFrameBulgeIdleDepth: Math.max(frameThickness * 0.45, Kirigami.Units.gridUnit * 0.16)
readonly property real leftFrameBulgeHoverDepth: 0
property real leftFrameBulgeDepth: !leftLauncherEnabled || leftLauncherOpen || leftEdgeHovered
? leftFrameBulgeHoverDepth
: leftFrameBulgeIdleDepth
property real rightFrameBulgeDepth: !layoutMenuEnabled || layoutMenuOpen || rightEdgeHovered
? 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 real rightFrameBulgeEffectiveDepth: Math.max(rightFrameBulgeDepth, 0.01)
readonly property real rightFrameBulgeApexX: workAreaX + workAreaWidth - rightFrameBulgeEffectiveDepth
readonly property real rightFrameBulgeHalfLength: leftFrameBulgeHalfLength
readonly property real rightFrameBulgeApexY: leftFrameBulgeApexY
readonly property real rightFrameBulgeEdgeTopY: rightFrameBulgeApexY - rightFrameBulgeHalfLength
readonly property real rightFrameBulgeEdgeBottomY: rightFrameBulgeApexY + rightFrameBulgeHalfLength
readonly property real rightFrameBulgeTangent: rightFrameBulgeHalfLength * 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 +352,12 @@ 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
property bool rightEdgeHovered: false
property bool layoutMenuHovered: false
property bool layoutMenuOpen: false
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing && windowMaximizedTracker.showingWindow && !hoverRevealing
@ -324,17 +367,87 @@ ContainmentItem {
function updateInputRegion() { function updateInputRegion() {
const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight) const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)
if (shouldHide && dockOffset >= dockHeight) { const leftEdgeRegion = Qt.rect(0, topBarHitHeight, leftEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight))
MobileShell.ShellUtil.setInputRegions(convergenceChrome, [ const rightEdgeRegion = Qt.rect(width - rightEdgeHotzoneWidth, topBarHitHeight, rightEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight))
topBarRegion, const leftLauncherRegion = Qt.rect(0,
Qt.rect(0, height - revealStripHeight, width, revealStripHeight) Math.max(0, height - dockHeight - leftLauncherHeight),
]) leftLauncherWidth,
} else { leftLauncherHeight)
MobileShell.ShellUtil.setInputRegions(convergenceChrome, [ const layoutMenuRegion = Qt.rect(rightLayoutMenu.x,
topBarRegion, rightLayoutMenu.y,
Qt.rect(0, height - dockHeight, width, dockHeight) rightLayoutMenu.width,
]) rightLayoutMenu.height)
let regions = [topBarRegion, leftEdgeRegion]
if (layoutMenuEnabled) {
regions.push(rightEdgeRegion)
} }
if (shouldHide && dockOffset >= dockHeight) {
regions.push(Qt.rect(0, height - revealStripHeight, width, revealStripHeight))
} else {
regions.push(Qt.rect(0, height - dockHeight, width, dockHeight))
}
if (leftLauncherOpen) {
regions.push(leftLauncherRegion)
}
if (layoutMenuEnabled && layoutMenuOpen) {
regions.push(layoutMenuRegion)
}
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()
leftEdgeHovered = false
leftLauncherHovered = false
leftLauncherOpen = false
inputRegionTimer.restart()
return
}
if (leftEdgeHovered || leftLauncherHovered) {
leftLauncherCloseTimer.stop()
leftLauncherOpen = true
} else {
leftLauncherCloseTimer.restart()
}
inputRegionTimer.restart()
}
function refreshLayoutMenuVisibility() {
if (!layoutMenuEnabled) {
layoutMenuCloseTimer.stop()
rightEdgeHovered = false
layoutMenuHovered = false
layoutMenuOpen = false
inputRegionTimer.restart()
return
}
if (rightEdgeHovered || layoutMenuHovered) {
layoutMenuCloseTimer.stop()
layoutMenuOpen = true
} else {
layoutMenuCloseTimer.restart()
}
inputRegionTimer.restart()
} }
onActiveChanged: { onActiveChanged: {
@ -353,6 +466,8 @@ ContainmentItem {
} }
inputRegionTimer.restart() inputRegionTimer.restart()
} }
onLeftLauncherEnabledChanged: refreshLeftLauncherVisibility()
onLayoutMenuEnabledChanged: refreshLayoutMenuVisibility()
// 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 +500,34 @@ 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()
}
}
}
Timer {
id: layoutMenuCloseTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: {
if (!convergenceChrome.rightEdgeHovered
&& !convergenceChrome.layoutMenuHovered
&& convergenceChrome.layoutMenuOpen) {
convergenceChrome.layoutMenuOpen = false
inputRegionTimer.restart()
}
}
}
Behavior on dockOffset { Behavior on dockOffset {
MobileShell.MotionNumberAnimation { MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault type: MobileShell.Motion.SpatialDefault
@ -392,6 +535,20 @@ ContainmentItem {
} }
} }
Behavior on leftFrameBulgeDepth {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: root.shortAnimationDuration
}
}
Behavior on rightFrameBulgeDepth {
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 +568,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
@ -427,24 +585,125 @@ ContainmentItem {
PathMove { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY } PathMove { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY } PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius } PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.rightFrameBulgeEdgeTopY }
PathCubic {
x: convergenceChrome.rightFrameBulgeApexX
y: convergenceChrome.rightFrameBulgeApexY
control1X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control1Y: convergenceChrome.rightFrameBulgeEdgeTopY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.rightFrameBulgeApexX
control2Y: convergenceChrome.rightFrameBulgeApexY - convergenceChrome.rightFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.rightFrameBulgeEdgeBottomY
control1X: convergenceChrome.rightFrameBulgeApexX
control1Y: convergenceChrome.rightFrameBulgeApexY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control2Y: convergenceChrome.rightFrameBulgeEdgeBottomY - convergenceChrome.rightFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius } PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius }
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.rightFrameBulgeEdgeTopY }
PathCubic {
x: convergenceChrome.rightFrameBulgeApexX
y: convergenceChrome.rightFrameBulgeApexY
control1X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control1Y: convergenceChrome.rightFrameBulgeEdgeTopY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.rightFrameBulgeApexX
control2Y: convergenceChrome.rightFrameBulgeApexY - convergenceChrome.rightFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.rightFrameBulgeEdgeBottomY
control1X: convergenceChrome.rightFrameBulgeApexX
control1Y: convergenceChrome.rightFrameBulgeApexY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control2Y: convergenceChrome.rightFrameBulgeEdgeBottomY - convergenceChrome.rightFrameBulgeTangent
}
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 +747,307 @@ 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: leftEdgeHoverArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
enabled: convergenceChrome.leftLauncherEnabled
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: {
convergenceChrome.leftEdgeHovered = containsMouse
convergenceChrome.refreshLeftLauncherVisibility()
}
}
}
Item {
id: rightEdgeStrip
anchors.right: parent.right
anchors.top: topBarSurface.bottom
anchors.bottom: dockSurface.top
width: convergenceChrome.layoutMenuEnabled ? convergenceChrome.rightEdgeHotzoneWidth : 0
MouseArea {
id: rightEdgeHoverArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
enabled: convergenceChrome.layoutMenuEnabled
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: {
convergenceChrome.rightEdgeHovered = containsMouse
convergenceChrome.refreshLayoutMenuVisibility()
}
}
}
DynamicTilingLayoutMenu {
id: rightLayoutMenu
width: convergenceChrome.layoutMenuWidth
height: preferredHeight
x: convergenceChrome.width - width
y: convergenceChrome.height - convergenceChrome.dockHeight - height
visible: convergenceChrome.layoutMenuOpen
opacity: convergenceChrome.layoutMenuOpen ? 1 : 0
maxHeight: convergenceChrome.workAreaHeight * 0.5
windowCount: convergenceChrome.layoutMenuWindowCount
currentMode: ShellSettings.Settings.dynamicTilingLayoutMode
surfaceColor: convergenceChrome.chromeColor
animationDuration: root.shortAnimationDuration
HoverHandler {
enabled: convergenceChrome.layoutMenuOpen
onHoveredChanged: {
convergenceChrome.layoutMenuHovered = hovered
convergenceChrome.refreshLayoutMenuVisibility()
}
}
transform: Translate {
y: convergenceChrome.layoutMenuOpen ? 0 : Kirigami.Units.gridUnit
x: convergenceChrome.layoutMenuOpen ? 0 : rightLayoutMenu.width - convergenceChrome.rightEdgeHotzoneWidth
}
onLayoutModeRequested: (mode) => {
if (ShellSettings.Settings.requestDynamicTilingLayoutMode !== undefined) {
ShellSettings.Settings.requestDynamicTilingLayoutMode(mode)
}
}
onDismissRequested: {
convergenceChrome.layoutMenuOpen = false
inputRegionTimer.restart()
}
}
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)
HoverHandler {
enabled: convergenceChrome.leftLauncherOpen
onHoveredChanged: {
convergenceChrome.leftLauncherHovered = hovered
convergenceChrome.refreshLeftLauncherVisibility()
}
}
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

@ -51,6 +51,10 @@ Item {
value: drawer.visible value: drawer.visible
} }
TaskManager.VirtualDesktopInfo {
id: virtualDesktopInfo
}
//END API implementation //END API implementation
// Startup feedback fill animation // Startup feedback fill animation
@ -155,6 +159,7 @@ Item {
MobileShell.ActionDrawerOpenSurface { MobileShell.ActionDrawerOpenSurface {
id: swipeArea id: swipeArea
actionDrawer: drawer.actionDrawer actionDrawer: drawer.actionDrawer
virtualDesktopInfo: virtualDesktopInfo
anchors.fill: parent anchors.fill: parent
readonly property alias drawerVisible: drawer.visible readonly property alias drawerVisible: drawer.visible

View file

@ -96,7 +96,7 @@ ContainmentItem {
root.panel.thickness = root.panelHeight; root.panel.thickness = root.panelHeight;
root.panel.thickness = root.panelHeight; root.panel.thickness = root.panelHeight;
root.panel.visibilityMode = (!ShellSettings.Settings.convergenceModeEnabled && ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0; root.panel.visibilityMode = (ShellSettings.Settings.convergenceModeEnabled || ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0;
MobileShell.ShellUtil.setWindowLayer(root.panel, LayerShell.Window.LayerOverlay) MobileShell.ShellUtil.setWindowLayer(root.panel, LayerShell.Window.LayerOverlay)
root.updateTouchArea(); root.updateTouchArea();
} }
@ -152,9 +152,8 @@ ContainmentItem {
} }
// Invisible layer-shell surface that reserves screen space for the // Invisible layer-shell surface that reserves screen space for the
// status bar in convergence mode. The visible convergence top bar is // convergence status bar and one frame inset. The panel window itself
// rendered by Folio's unified chrome surface; this window only shrinks // stays non-reserving so this is the only top strut.
// KWin's MaximizeArea.
Window { Window {
id: topBarSpaceReserver id: topBarSpaceReserver
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled

View file

@ -54,15 +54,14 @@ QMap<QString, QMap<QString, QVariant>> getKwinrcSettings(KSharedConfig::Ptr m_mo
{"ElectricBorderTiling", convergenceModeEnabled} // drag to left/right edges to tile half-screen in convergence mode {"ElectricBorderTiling", convergenceModeEnabled} // drag to left/right edges to tile half-screen in convergence mode
}}, }},
{"Plugins", {"Plugins",
{ {{"blurEnabled", false}, // disable blur for performance reasons, we could reconsider in the future for more powerful devices
{"blurEnabled", false}, // disable blur for performance reasons, we could reconsider in the future for more powerful devices {"convergentwindowsEnabled", true}, // enable our convergent window plugin
{"convergentwindowsEnabled", true}, // enable our convergent window plugin {"mobiletaskswitcherEnabled", !convergenceModeEnabled}, // mobile task switcher on phone only; convergence uses standard Alt-Tab tabbox
{"mobiletaskswitcherEnabled", !convergenceModeEnabled}, // mobile task switcher on phone only; convergence uses standard Alt-Tab tabbox {"overviewEnabled", convergenceModeEnabled}, // enable KWin Overview effect in convergence mode for desktop-style task switching
{"overviewEnabled", convergenceModeEnabled}, // enable KWin Overview effect in convergence mode for desktop-style task switching {"screenedgeEnabled", convergenceModeEnabled}, // enable screen edge visual feedback in convergence mode (mouse hot corners)
{"screenedgeEnabled", convergenceModeEnabled}, // enable screen edge visual feedback in convergence mode (mouse hot corners) {"shift-tile-animationsEnabled", convergenceModeEnabled},
{"shift-tilingEnabled", convergenceModeEnabled}, {"shift-tilingEnabled", convergenceModeEnabled},
{"shift-tile-previewEnabled", convergenceModeEnabled} {"shift-tile-previewEnabled", convergenceModeEnabled}}},
}},
{"Wayland", {"Wayland",
{ {
{"VirtualKeyboardEnabled", true} // enable vkbd {"VirtualKeyboardEnabled", true} // enable vkbd
@ -80,7 +79,7 @@ QMap<QString, QMap<QString, QVariant>> getKwinrcSettings(KSharedConfig::Ptr m_mo
// Have a separate list here because we need to trigger DBus calls to load/unload each effect/script. // Have a separate list here because we need to trigger DBus calls to load/unload each effect/script.
// Make sure that the effect/script is added to the kwinrc "Plugins" section above! // Make sure that the effect/script is added to the kwinrc "Plugins" section above!
const QList<QString> KWIN_EFFECTS = {"blur", "mobiletaskswitcher", "overview", "screenedge", "shift-tile-preview"}; const QList<QString> KWIN_EFFECTS = {"blur", "mobiletaskswitcher", "overview", "screenedge", "shift-tile-animations", "shift-tile-preview"};
const QList<QString> KWIN_SCRIPTS = {"convergentwindows", "shift-tiling"}; const QList<QString> KWIN_SCRIPTS = {"convergentwindows", "shift-tiling"};
// .config/plasma-mobile/ksmserver - immutable settings: // .config/plasma-mobile/ksmserver - immutable settings:

View file

@ -8,4 +8,5 @@ function(add_kwin_effect name source)
endfunction() endfunction()
add_kwin_effect(shift-snap-assist shift-snap-assist) add_kwin_effect(shift-snap-assist shift-snap-assist)
add_kwin_effect(shift-tile-animations shift-tile-animations)
add_kwin_effect(shift-tile-preview shift-tile-preview) add_kwin_effect(shift-tile-preview shift-tile-preview)

View file

@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
/*global effect, effects, animate, animationTime, cancel, Effect, QEasingCurve */
"use strict";
class ShiftTileAnimationsEffect {
constructor() {
effect.configChanged.connect(this.loadConfig.bind(this));
effect.animationEnded.connect(this.cleanupWindow.bind(this));
effects.windowAdded.connect(this.manage.bind(this));
effects.windowClosed.connect(this.cleanupWindow.bind(this));
this.loadConfig();
for (const window of effects.stackingOrder) {
this.manage(window);
}
}
loadConfig() {
this.baseDuration = effect.readConfig("Duration", 185) || 185;
this.minimumDelta = effect.readConfig("MinimumDelta", 3) || 3;
}
manage(window) {
if (!window || window.shiftTileAnimationsManaged) {
return;
}
window.shiftTileAnimationsManaged = true;
window.shiftTileUserMoveResize = false;
window.windowFrameGeometryChanged.connect(this.onWindowFrameGeometryChanged.bind(this));
window.windowStartUserMovedResized.connect(this.onWindowStartUserMovedResized.bind(this));
window.windowFinishUserMovedResized.connect(this.onWindowFinishUserMovedResized.bind(this));
}
eligibleWindow(window) {
return window
&& !effects.hasActiveFullScreenEffect
&& window.visible
&& !window.deleted
&& window.managed
&& window.normalWindow
&& !window.fullScreen
&& !window.desktopWindow
&& !window.dock
&& !window.popup
&& !window.popupWindow
&& !window.outline;
}
validGeometry(geometry) {
return geometry && geometry.width > 0 && geometry.height > 0;
}
movedEnough(oldGeometry, newGeometry) {
return Math.abs(oldGeometry.x - newGeometry.x) >= this.minimumDelta
|| Math.abs(oldGeometry.y - newGeometry.y) >= this.minimumDelta
|| Math.abs(oldGeometry.width - newGeometry.width) >= this.minimumDelta
|| Math.abs(oldGeometry.height - newGeometry.height) >= this.minimumDelta;
}
durationFor(oldGeometry, newGeometry) {
const oldCenterX = oldGeometry.x + oldGeometry.width / 2;
const oldCenterY = oldGeometry.y + oldGeometry.height / 2;
const newCenterX = newGeometry.x + newGeometry.width / 2;
const newCenterY = newGeometry.y + newGeometry.height / 2;
const distanceX = newCenterX - oldCenterX;
const distanceY = newCenterY - oldCenterY;
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
const resize = Math.abs(newGeometry.width - oldGeometry.width) + Math.abs(newGeometry.height - oldGeometry.height);
const travelAllowance = Math.min(80, Math.round(Math.max(distance / 12, resize / 18)));
return animationTime(Math.max(135, Math.min(260, this.baseDuration + travelAllowance)));
}
cancelWindowAnimation(window) {
if (!window || window.shiftTileMoveAnimation === undefined) {
return;
}
cancel(window.shiftTileMoveAnimation);
window.shiftTileMoveAnimation = undefined;
window.setData(Effect.WindowForceBlurRole, null);
}
cleanupWindow(window) {
if (!window) {
return;
}
window.shiftTileMoveAnimation = undefined;
window.setData(Effect.WindowForceBlurRole, null);
}
onWindowStartUserMovedResized(window) {
if (!window) {
return;
}
window.shiftTileUserMoveResize = true;
this.cancelWindowAnimation(window);
}
onWindowFinishUserMovedResized(window) {
if (window) {
window.shiftTileUserMoveResize = false;
}
}
onWindowFrameGeometryChanged(window, oldGeometry) {
if (!this.eligibleWindow(window) || window.shiftTileUserMoveResize) {
return;
}
const newGeometry = window.geometry;
if (!this.validGeometry(oldGeometry) || !this.validGeometry(newGeometry) || !this.movedEnough(oldGeometry, newGeometry)) {
return;
}
this.cancelWindowAnimation(window);
window.setData(Effect.WindowForceBlurRole, true);
window.shiftTileMoveAnimation = animate({
window: window,
duration: this.durationFor(oldGeometry, newGeometry),
keepAlive: false,
animations: [{
type: Effect.Size,
from: {
value1: oldGeometry.width,
value2: oldGeometry.height
},
to: {
value1: newGeometry.width,
value2: newGeometry.height
},
curve: QEasingCurve.OutCubic
}, {
type: Effect.Translation,
from: {
value1: oldGeometry.x - newGeometry.x - (newGeometry.width / 2 - oldGeometry.width / 2),
value2: oldGeometry.y - newGeometry.y - (newGeometry.height / 2 - oldGeometry.height / 2)
},
to: {
value1: 0,
value2: 0
},
curve: QEasingCurve.OutCubic
}]
});
}
}
new ShiftTileAnimationsEffect();

View file

@ -0,0 +1,20 @@
{
"KPackageStructure": "KWin/Effect",
"KPlugin": {
"Authors": [
{
"Email": "marcoa@example.com",
"Name": "Marco Allegretti"
}
],
"Category": "Appearance",
"Description": "Animates SHIFT convergence window layout moves.",
"EnabledByDefault": false,
"Id": "shift-tile-animations",
"License": "EUPL-1.2",
"Name": "SHIFT Tile Animations",
"Version": "1.0"
},
"X-KDE-Ordering": 62,
"X-Plasma-API": "javascript"
}

View file

@ -3,7 +3,6 @@
import QtQuick import QtQuick
import org.kde.kwin as KWinComponents import org.kde.kwin as KWinComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
KWinComponents.SceneEffect { KWinComponents.SceneEffect {
@ -13,14 +12,11 @@ KWinComponents.SceneEffect {
readonly property int outerGap: 8 readonly property int outerGap: 8
readonly property int floatEscapeMargin: 32 readonly property int floatEscapeMargin: 32
readonly property int previewAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
readonly property int previewFadeDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
property var dragConnectedWindows: ({}) property var dragConnectedWindows: ({})
property var draggingWindow: null property var draggingWindow: null
property rect dragSourceGeometry: Qt.rect(0, 0, 0, 0) property rect dragSourceGeometry: Qt.rect(0, 0, 0, 0)
property bool animatePreview: false
property bool previewVisible: false property bool previewVisible: false
property string previewMode: "" property string previewMode: ""
property string previewScreenName: "" property string previewScreenName: ""
@ -126,14 +122,26 @@ KWinComponents.SceneEffect {
} }
function showPreview(mode, geometry, screenName) { function showPreview(mode, geometry, screenName) {
hidePreview(); if (!isActive() || !validRect(geometry)) {
hidePreview();
return;
}
previewMode = mode;
previewScreenName = screenName;
previewGeometry = insetPreviewGeometry(geometry);
previewVisible = true;
KWinComponents.Workspace.showOutline(previewGeometry);
} }
function hidePreview() { function hidePreview() {
previewVisible = false; previewVisible = false;
draggingWindow = null; draggingWindow = null;
dragSourceGeometry = Qt.rect(0, 0, 0, 0); dragSourceGeometry = Qt.rect(0, 0, 0, 0);
disableEffectTimer.restart(); previewMode = "";
previewScreenName = "";
previewGeometry = Qt.rect(0, 0, 0, 0);
KWinComponents.Workspace.hideOutline();
} }
function updatePreview(window, dragGeometry) { function updatePreview(window, dragGeometry) {
@ -189,44 +197,6 @@ KWinComponents.SceneEffect {
} }
} }
function previewFillColor(mode) {
if (mode === "float") {
return Qt.rgba(1.0, 0.62, 0.24, 0.18);
}
if (mode === "restore") {
return Qt.rgba(1.0, 1.0, 1.0, 0.10);
}
return Qt.rgba(0.18, 0.72, 0.66, 0.22);
}
function previewBorderColor(mode) {
if (mode === "float") {
return Qt.rgba(1.0, 0.72, 0.36, 0.72);
}
if (mode === "restore") {
return Qt.rgba(1.0, 1.0, 1.0, 0.36);
}
return Qt.rgba(0.64, 0.90, 0.86, 0.82);
}
Timer {
id: enableAnimationTimer
interval: 1
repeat: false
onTriggered: effect.animatePreview = true
}
Timer {
id: disableEffectTimer
interval: effect.previewFadeDuration
repeat: false
onTriggered: {
if (!effect.previewVisible) {
effect.visible = false;
}
}
}
Connections { Connections {
target: KWinComponents.Workspace target: KWinComponents.Workspace
@ -267,65 +237,5 @@ KWinComponents.SceneEffect {
} }
} }
delegate: Rectangle {
id: screenDelegate
readonly property var targetScreen: KWinComponents.SceneView.screen
readonly property bool previewOnScreen: effect.previewScreenName === targetScreen.name
color: "transparent"
Rectangle {
id: previewSurface
visible: opacity > 0
x: effect.previewGeometry.x - screenDelegate.targetScreen.geometry.x
y: effect.previewGeometry.y - screenDelegate.targetScreen.geometry.y
width: effect.previewGeometry.width
height: effect.previewGeometry.height
radius: 14
opacity: effect.previewVisible && screenDelegate.previewOnScreen ? 1 : 0
color: effect.previewFillColor(effect.previewMode)
border.width: 2
border.color: effect.previewBorderColor(effect.previewMode)
Behavior on x {
enabled: effect.animatePreview
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: effect.previewAnimationDuration }
}
Behavior on y {
enabled: effect.animatePreview
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: effect.previewAnimationDuration }
}
Behavior on width {
enabled: effect.animatePreview
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: effect.previewAnimationDuration }
}
Behavior on height {
enabled: effect.animatePreview
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: effect.previewAnimationDuration }
}
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: effect.previewFadeDuration }
}
Behavior on color {
MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: effect.previewFadeDuration }
}
Behavior on border.color {
MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: effect.previewFadeDuration }
}
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: Math.max(0, parent.radius - anchors.margins)
color: "transparent"
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.14)
opacity: effect.previewMode === "insert" ? 1 : 0.45
}
}
}
Component.onCompleted: connectExistingWindows() Component.onCompleted: connectExistingWindows()
} }

View file

@ -71,8 +71,10 @@ Item {
// Values keep callback references so KWin script reloads can disconnect them. // Values keep callback references so KWin script reloads can disconnect them.
property var dragConnectedWindows: ({}) property var dragConnectedWindows: ({})
property int lastWindowRequestSerial: ShellSettings.Settings.dynamicTilingWindowRequestSerial property int lastWindowRequestSerial: -1
property string lastPublishedMaximizedWindowIds: "__unpublished__" property string lastPublishedMaximizedWindowIds: "__unpublished__"
property int lastLayoutRequestSerial: -1
property string lastPublishedLayoutState: "__unpublished__"
// Drag state. // Drag state.
// //
@ -504,6 +506,7 @@ Item {
const focused = Object.assign({}, lastFocusedWindowKeys); const focused = Object.assign({}, lastFocusedWindowKeys);
delete focused[outputName]; delete focused[outputName];
lastFocusedWindowKeys = focused; lastFocusedWindowKeys = focused;
publishDynamicTilingLayoutState();
} }
function clearDisplacedWindowOwnersForLayout(outputName) { function clearDisplacedWindowOwnersForLayout(outputName) {
@ -654,6 +657,8 @@ Item {
for (let i = 0; i < cleanupLayouts.length; i++) { for (let i = 0; i < cleanupLayouts.length; i++) {
cleanupEmptyLayout(cleanupLayouts[i]); cleanupEmptyLayout(cleanupLayouts[i]);
} }
publishDynamicTilingLayoutState();
} }
function makeLeaf(win) { function makeLeaf(win) {
@ -842,6 +847,15 @@ Item {
const win = KWinComponents.Workspace.activeWindow; const win = KWinComponents.Workspace.activeWindow;
const activeName = layoutKeyForWindow(win); const activeName = layoutKeyForWindow(win);
if (activeName !== "") return activeName; if (activeName !== "") return activeName;
const desktop = KWinComponents.Workspace.currentDesktop;
const screens = KWinComponents.Workspace.screens;
if (desktop && screens && screens.length > 0) {
for (let i = 0; i < screens.length; i++) {
const currentDesktopName = layoutKeyFor(screens[i].name, desktop);
if (screenLayouts[currentDesktopName]) return currentDesktopName;
}
return layoutKeyFor(screens[0].name, desktop);
}
for (const name in screenLayouts) { for (const name in screenLayouts) {
return name; return name;
} }
@ -863,6 +877,21 @@ Item {
markLayoutChanged(transaction, outputName); markLayoutChanged(transaction, outputName);
applyLayoutTransaction(transaction); applyLayoutTransaction(transaction);
} }
publishDynamicTilingLayoutState();
}
function setLayoutMode(outputName, mode) {
if (!outputName || layoutModes.indexOf(mode) < 0) return;
setLayoutModeForScreen(outputName, mode);
const windows = orderedWindowsForScreen(outputName);
if (windows.length > 0) {
setStableLayout(outputName, windows);
const transaction = createLayoutTransaction();
markLayoutChanged(transaction, outputName);
applyLayoutTransaction(transaction);
}
publishDynamicTilingLayoutState();
} }
function containsLeaf(node, key) { function containsLeaf(node, key) {
@ -1281,6 +1310,37 @@ Item {
} }
} }
function publishDynamicTilingLayoutState() {
if (!isConvergence()) {
const disabledState = "|0";
if (disabledState === lastPublishedLayoutState) return;
lastPublishedLayoutState = disabledState;
if (ShellSettings.Settings.reportDynamicTilingLayoutState !== undefined) {
ShellSettings.Settings.reportDynamicTilingLayoutState("", 0);
}
return;
}
const outputName = outputNameForActiveWindow();
const mode = outputName !== "" ? layoutModeForScreen(outputName) : "";
const windowCount = outputName !== "" ? windowCountForLayout(outputName) : 0;
const serialized = mode + "|" + windowCount;
if (serialized === lastPublishedLayoutState) return;
lastPublishedLayoutState = serialized;
if (ShellSettings.Settings.reportDynamicTilingLayoutState !== undefined) {
ShellSettings.Settings.reportDynamicTilingLayoutState(mode, windowCount);
}
}
function clearDynamicTilingLayoutState() {
lastPublishedLayoutState = "__unpublished__";
if (ShellSettings.Settings.reportDynamicTilingLayoutState !== undefined) {
ShellSettings.Settings.reportDynamicTilingLayoutState("", 0);
}
}
function maximizedLayoutNameForWindow(win) { function maximizedLayoutNameForWindow(win) {
const key = windowKey(win); const key = windowKey(win);
if (!key) return ""; if (!key) return "";
@ -1682,6 +1742,19 @@ Item {
} }
} }
function handleLayoutModeRequest() {
const serial = ShellSettings.Settings.dynamicTilingLayoutRequestSerial;
if (serial === lastLayoutRequestSerial) return;
lastLayoutRequestSerial = serial;
if (!isConvergence()) return;
const mode = ShellSettings.Settings.dynamicTilingLayoutRequestMode;
if (layoutModes.indexOf(mode) < 0) return;
setLayoutMode(outputNameForActiveWindow(), mode);
}
function promoteWindow(win) { function promoteWindow(win) {
if (!isTileable(win)) return; if (!isTileable(win)) return;
@ -1771,10 +1844,12 @@ Item {
function onActiveWindowChanged() { function onActiveWindowChanged() {
root.rememberFocusedWindow(KWinComponents.Workspace.activeWindow); root.rememberFocusedWindow(KWinComponents.Workspace.activeWindow);
root.publishDynamicTilingLayoutState();
} }
function onCurrentDesktopChanged() { function onCurrentDesktopChanged() {
root.retileCurrentDesktopLayouts(); root.retileCurrentDesktopLayouts();
root.publishDynamicTilingLayoutState();
} }
function onScreensChanged() { function onScreensChanged() {
@ -1788,6 +1863,7 @@ Item {
} }
root.scheduleRetileAll(); root.scheduleRetileAll();
root.publishDynamicTilingLayoutState();
} }
} }
@ -1801,6 +1877,7 @@ Item {
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
adoptWindow(wins[i]); adoptWindow(wins[i]);
} }
publishDynamicTilingLayoutState();
} else { } else {
// Clear all tiles the convergentwindows script will re-maximize // Clear all tiles the convergentwindows script will re-maximize
restoreAllMaximizedLayouts(); restoreAllMaximizedLayouts();
@ -1809,6 +1886,7 @@ Item {
screenLayoutModes = {}; screenLayoutModes = {};
lastFocusedWindowKeys = {}; lastFocusedWindowKeys = {};
maximizedLayouts = {}; maximizedLayouts = {};
clearDynamicTilingLayoutState();
} }
} }
@ -1820,11 +1898,13 @@ Item {
screenLayoutModes = {}; screenLayoutModes = {};
lastFocusedWindowKeys = {}; lastFocusedWindowKeys = {};
maximizedLayouts = {}; maximizedLayouts = {};
clearDynamicTilingLayoutState();
} else if (isConvergence()) { } else if (isConvergence()) {
const wins = KWinComponents.Workspace.windows; const wins = KWinComponents.Workspace.windows;
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
adoptWindow(wins[i]); adoptWindow(wins[i]);
} }
publishDynamicTilingLayoutState();
} }
} }
@ -1834,6 +1914,7 @@ Item {
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
adoptWindow(wins[i]); adoptWindow(wins[i]);
} }
publishDynamicTilingLayoutState();
} else { } else {
// Tiling turned off leave windows where they are. // Tiling turned off leave windows where they are.
restoreAllMaximizedLayouts(); restoreAllMaximizedLayouts();
@ -1842,12 +1923,17 @@ Item {
screenLayoutModes = {}; screenLayoutModes = {};
lastFocusedWindowKeys = {}; lastFocusedWindowKeys = {};
maximizedLayouts = {}; maximizedLayouts = {};
clearDynamicTilingLayoutState();
} }
} }
function onDynamicTilingWindowRequestChanged() { function onDynamicTilingWindowRequestChanged() {
root.handleWindowTilingRequest(); root.handleWindowTilingRequest();
} }
function onDynamicTilingLayoutRequestChanged() {
root.handleLayoutModeRequest();
}
} }
// Drag handlers // Drag handlers
@ -2165,6 +2251,9 @@ Item {
// Component setup // Component setup
Component.onCompleted: { Component.onCompleted: {
lastWindowRequestSerial = ShellSettings.Settings.dynamicTilingWindowRequestSerial;
lastLayoutRequestSerial = ShellSettings.Settings.dynamicTilingLayoutRequestSerial;
// Connect to existing windows // Connect to existing windows
const wins = KWinComponents.Workspace.windows; const wins = KWinComponents.Workspace.windows;
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
@ -2172,10 +2261,12 @@ Item {
} }
rememberFocusedWindow(KWinComponents.Workspace.activeWindow); rememberFocusedWindow(KWinComponents.Workspace.activeWindow);
publishDynamicTilingWindowState(); publishDynamicTilingWindowState();
publishDynamicTilingLayoutState();
} }
Component.onDestruction: { Component.onDestruction: {
disconnectDragHandlers(); disconnectDragHandlers();
clearDynamicTilingWindowState(); clearDynamicTilingWindowState();
clearDynamicTilingLayoutState();
} }
} }

View file

@ -14,7 +14,10 @@ taskpanel="$repo_root/containments/taskpanel/qml/main.qml"
folio_main="$repo_root/containments/homescreens/folio/qml/main.qml" folio_main="$repo_root/containments/homescreens/folio/qml/main.qml"
shellutil_header="$repo_root/components/mobileshell/shellutil.h" shellutil_header="$repo_root/components/mobileshell/shellutil.h"
shellutil_cpp="$repo_root/components/mobileshell/shellutil.cpp" shellutil_cpp="$repo_root/components/mobileshell/shellutil.cpp"
swipearea_header="$repo_root/components/mobileshell/components/swipearea.h"
swipearea_cpp="$repo_root/components/mobileshell/components/swipearea.cpp"
folio_home="$repo_root/containments/homescreens/folio/qml/FolioHomeScreen.qml" folio_home="$repo_root/containments/homescreens/folio/qml/FolioHomeScreen.qml"
favourites_bar="$repo_root/containments/homescreens/folio/qml/FavouritesBar.qml"
folio_backend="$repo_root/containments/homescreens/folio/homescreen.h" folio_backend="$repo_root/containments/homescreens/folio/homescreen.h"
folio_backend_cpp="$repo_root/containments/homescreens/folio/homescreen.cpp" folio_backend_cpp="$repo_root/containments/homescreens/folio/homescreen.cpp"
action_content="$repo_root/components/mobileshell/qml/actiondrawer/private/ContentContainer.qml" action_content="$repo_root/components/mobileshell/qml/actiondrawer/private/ContentContainer.qml"
@ -48,7 +51,7 @@ require_line "$constants" "readonly property real convergenceWorkspaceFrameRadiu
require_line "$panel" "readonly property real convergenceWorkspaceFrameThickness:" require_line "$panel" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$panel" "root.panel.location = PlasmaCore.Types.TopEdge" require_line "$panel" "root.panel.location = PlasmaCore.Types.TopEdge"
require_line "$panel" "root.panel.offset = 0" require_line "$panel" "root.panel.offset = 0"
require_line "$panel" "root.panel.visibilityMode = (!ShellSettings.Settings.convergenceModeEnabled && ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0" require_line "$panel" "root.panel.visibilityMode = (ShellSettings.Settings.convergenceModeEnabled || ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0"
require_line "$panel" "readonly property real topBarHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight" require_line "$panel" "readonly property real topBarHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight"
require_line "$panel" "readonly property real topBarInputHeight: topBarHeight + convergenceWorkspaceFrameThickness" require_line "$panel" "readonly property real topBarInputHeight: topBarHeight + convergenceWorkspaceFrameThickness"
require_line "$panel" "? 0" require_line "$panel" "? 0"
@ -60,11 +63,34 @@ require_line "$panel" "visible: !ShellSettings.Settings.gamingModeEnabled"
require_line "$status_panel" "&& !ShellSettings.Settings.convergenceModeEnabled" require_line "$status_panel" "&& !ShellSettings.Settings.convergenceModeEnabled"
require_line "$status_panel" "visible: !ShellSettings.Settings.convergenceModeEnabled" require_line "$status_panel" "visible: !ShellSettings.Settings.convergenceModeEnabled"
require_line "$status_panel" "TaskManager.VirtualDesktopInfo {"
require_line "$status_panel" "MobileShell.ActionDrawerOpenSurface {" require_line "$status_panel" "MobileShell.ActionDrawerOpenSurface {"
require_line "$status_panel" "virtualDesktopInfo: virtualDesktopInfo"
require_line "$action_open_surface" "property var virtualDesktopInfo: null"
require_line "$action_open_surface" "touchpadWorkspaceSwitchThreshold"
require_line "$action_open_surface" "function activateAdjacentWorkspace(direction)"
require_line "$action_open_surface" "virtualDesktopInfo.requestActivate(virtualDesktopInfo.desktopIds[targetIndex]);"
require_line "$action_open_surface" "absY >= touchpadDirectionLockThreshold && absY > absX * touchpadAxisDominance"
require_line "$action_open_surface" "workspaceScrollAvailable() && absX >= touchpadDirectionLockThreshold && absX > absY * touchpadAxisDominance"
require_line "$action_open_surface" "MouseArea {" require_line "$action_open_surface" "MouseArea {"
require_line "$action_open_surface" "acceptedButtons: Qt.NoButton" require_line "$action_open_surface" "acceptedButtons: Qt.NoButton"
require_line "$action_open_surface" "hoverEnabled: true" require_line "$action_open_surface" "hoverEnabled: true"
require_line "$action_open_surface" "scrollGestureEnabled: false"
require_line "$action_open_surface" "cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor" require_line "$action_open_surface" "cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor"
require_line "$swipearea_cpp" "const qreal WHEEL_STEP_PIXEL_DELTA = 48.0;"
require_line "$swipearea_cpp" "const int TOUCHPAD_SCROLL_END_TIMEOUT = 160;"
require_line "$swipearea_cpp" "const bool isTouchpad = event->deviceType() == QInputDevice::DeviceType::TouchPad;"
require_line "$swipearea_cpp" "startTouchpadScroll(event->points().first().position());"
require_line "$swipearea_cpp" "m_touchpadScrollEndTimer.start();"
require_line "$swipearea_cpp" "if (scrollDelta.isNull() && !event->angleDelta().isNull())"
require_line "$swipearea_cpp" "QWheelEvent::DefaultDeltasPerStep * WHEEL_STEP_PIXEL_DELTA"
require_line "$swipearea_header" "QTimer m_touchpadScrollEndTimer;"
require_line "$favourites_bar" "function handlePagerWheel(wheel)"
require_line "$favourites_bar" "onWheel: (wheel) => root.handlePagerWheel(wheel)"
require_line "$favourites_bar" "root.activateAdjacentDesktop(1)"
require_line "$folio_home" "onTouchpadScrollStarted: {"
require_line "$folio_home" "onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => {"
require_line "$folio_home" "if (!ShellSettings.Settings.convergenceModeEnabled) {"
require_line "$status_bar_template" "panel.location = \"top\";" require_line "$status_bar_template" "panel.location = \"top\";"
@ -79,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"
@ -89,10 +114,29 @@ 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)"
require_line "$folio_main" "fillRule: ShapePath.OddEvenFill" require_line "$folio_main" "fillRule: ShapePath.OddEvenFill"
require_line "$folio_main" "id: leftEdgeHoverArea"
require_line "$folio_main" "id: rightEdgeHoverArea"
require_line "$folio_main" "cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor"
require_line "$folio_main" "convergenceChrome.leftEdgeHovered = containsMouse"
require_line "$folio_main" "convergenceChrome.rightEdgeHovered = containsMouse"
require_line "$folio_main" "convergenceChrome.leftLauncherHovered = hovered"
require_line "$folio_main" "convergenceChrome.layoutMenuHovered = hovered"
if grep -Fq "root.panel.visibilityMode = (!ShellSettings.Settings.convergenceModeEnabled && ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0" "$panel"; then
echo "Convergence panel hit area must not reserve extra top space" >&2
exit 1
fi
if grep -Fq "id: leftLauncherPointerTracker" "$folio_main"; then
echo "Folio convergence chrome must not cover the dock with a fullscreen hover tracker" >&2
exit 1
fi
require_line "$folio_main" "PathLine { x: convergenceChrome.width; y: convergenceChrome.height - convergenceChrome.dockHeight }" require_line "$folio_main" "PathLine { x: convergenceChrome.width; y: convergenceChrome.height - convergenceChrome.dockHeight }"
require_line "$folio_main" "PathLine { x: 0; y: convergenceChrome.height - convergenceChrome.dockHeight }" require_line "$folio_main" "PathLine { x: 0; y: convergenceChrome.height - convergenceChrome.dockHeight }"
require_line "$folio_main" "PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }" require_line "$folio_main" "PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }"

View file

@ -8,10 +8,13 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
effect_qml="$repo_root/kwin/effects/shift-tile-preview/contents/ui/main.qml" effect_qml="$repo_root/kwin/effects/shift-tile-preview/contents/ui/main.qml"
effect_metadata="$repo_root/kwin/effects/shift-tile-preview/metadata.json" effect_metadata="$repo_root/kwin/effects/shift-tile-preview/metadata.json"
animation_effect_js="$repo_root/kwin/effects/shift-tile-animations/contents/code/main.js"
animation_effect_metadata="$repo_root/kwin/effects/shift-tile-animations/metadata.json"
effects_cmake="$repo_root/kwin/effects/CMakeLists.txt" effects_cmake="$repo_root/kwin/effects/CMakeLists.txt"
tiling_script="$repo_root/kwin/scripts/shift-tiling/contents/ui/main.qml" tiling_script="$repo_root/kwin/scripts/shift-tiling/contents/ui/main.qml"
decoration_qml="$repo_root/kwin/decorations/org.shift.decoration/contents/ui/main.qml" decoration_qml="$repo_root/kwin/decorations/org.shift.decoration/contents/ui/main.qml"
running_apps_panel="$repo_root/containments/homescreens/folio/qml/RunningAppsPanel.qml" running_apps_panel="$repo_root/containments/homescreens/folio/qml/RunningAppsPanel.qml"
folio_main="$repo_root/containments/homescreens/folio/qml/main.qml"
env_config="$repo_root/envmanager/config.h" env_config="$repo_root/envmanager/config.h"
require_line() { require_line() {
@ -35,12 +38,17 @@ reject_line() {
} }
require_line "$effects_cmake" "add_kwin_effect(shift-tile-preview shift-tile-preview)" require_line "$effects_cmake" "add_kwin_effect(shift-tile-preview shift-tile-preview)"
require_line "$effects_cmake" "add_kwin_effect(shift-tile-animations shift-tile-animations)"
require_line "$effect_metadata" '"Id": "shift-tile-preview"' require_line "$effect_metadata" '"Id": "shift-tile-preview"'
require_line "$effect_metadata" '"X-Plasma-API": "declarativescript"' require_line "$effect_metadata" '"X-Plasma-API": "declarativescript"'
require_line "$animation_effect_metadata" '"Id": "shift-tile-animations"'
require_line "$animation_effect_metadata" '"X-Plasma-API": "javascript"'
require_line "$env_config" '{"shift-tilingEnabled", convergenceModeEnabled}' require_line "$env_config" '{"shift-tilingEnabled", convergenceModeEnabled}'
require_line "$env_config" '{"shift-tile-previewEnabled", convergenceModeEnabled}' require_line "$env_config" '{"shift-tile-previewEnabled", convergenceModeEnabled}'
require_line "$env_config" '{"shift-tile-animationsEnabled", convergenceModeEnabled}'
require_line "$env_config" '"shift-tile-preview"' require_line "$env_config" '"shift-tile-preview"'
require_line "$env_config" '"shift-tile-animations"'
require_line "$env_config" '"shift-tiling"' require_line "$env_config" '"shift-tiling"'
require_line "$effect_qml" "KWinComponents.SceneEffect" require_line "$effect_qml" "KWinComponents.SceneEffect"
@ -49,17 +57,32 @@ require_line "$effect_qml" "ShellSettings.Settings.dynamicTilingEnabled"
require_line "$effect_qml" "interactiveMoveResizeStarted.connect" require_line "$effect_qml" "interactiveMoveResizeStarted.connect"
require_line "$effect_qml" "interactiveMoveResizeStepped.connect" require_line "$effect_qml" "interactiveMoveResizeStepped.connect"
require_line "$effect_qml" "interactiveMoveResizeFinished.connect" require_line "$effect_qml" "interactiveMoveResizeFinished.connect"
require_line "$effect_qml" "Behavior on x"
require_line "$effect_qml" "Behavior on y"
require_line "$effect_qml" "Behavior on width"
require_line "$effect_qml" "Behavior on height"
require_line "$effect_qml" "showPreview(\"swap\"" require_line "$effect_qml" "showPreview(\"swap\""
require_line "$effect_qml" "showPreview(\"float\"" require_line "$effect_qml" "showPreview(\"float\""
require_line "$effect_qml" "showPreview(\"restore\"" require_line "$effect_qml" "showPreview(\"restore\""
require_line "$effect_qml" "KWinComponents.Workspace.showOutline(previewGeometry)"
require_line "$effect_qml" "KWinComponents.Workspace.hideOutline()"
reject_line "$effect_qml" "MobileShell.MotionNumberAnimation"
reject_line "$effect_qml" "MobileShell.MotionColorAnimation"
reject_line "$effect_qml" "disableEffectTimer"
require_line "$animation_effect_js" "class ShiftTileAnimationsEffect"
require_line "$animation_effect_js" "window.windowFrameGeometryChanged.connect"
require_line "$animation_effect_js" "window.windowStartUserMovedResized.connect"
require_line "$animation_effect_js" "window.windowFinishUserMovedResized.connect"
require_line "$animation_effect_js" "animationTime("
require_line "$animation_effect_js" "type: Effect.Size"
require_line "$animation_effect_js" "type: Effect.Translation"
require_line "$animation_effect_js" "curve: QEasingCurve.OutCubic"
require_line "$animation_effect_js" "window.shiftTileUserMoveResize"
require_line "$tiling_script" "readonly property int maxWindowsPerPage: 4" require_line "$tiling_script" "readonly property int maxWindowsPerPage: 4"
require_line "$tiling_script" "readonly property real stablePrimaryRatio: 0.58" require_line "$tiling_script" "readonly property real stablePrimaryRatio: 0.58"
require_line "$tiling_script" "readonly property var layoutModes: [\"master\", \"columns\", \"rows\"]" require_line "$tiling_script" "readonly property var layoutModes: [\"master\", \"columns\", \"rows\"]"
require_line "$tiling_script" "property int lastWindowRequestSerial: -1"
require_line "$tiling_script" "property int lastLayoutRequestSerial: -1"
require_line "$tiling_script" "lastWindowRequestSerial = ShellSettings.Settings.dynamicTilingWindowRequestSerial"
require_line "$tiling_script" "lastLayoutRequestSerial = ShellSettings.Settings.dynamicTilingLayoutRequestSerial"
require_line "$tiling_script" "function desktopKey(desktop)" require_line "$tiling_script" "function desktopKey(desktop)"
require_line "$tiling_script" "function desktopForWindow(win)" require_line "$tiling_script" "function desktopForWindow(win)"
require_line "$tiling_script" "function normalizeWindowDesktopScope(win)" require_line "$tiling_script" "function normalizeWindowDesktopScope(win)"
@ -126,6 +149,17 @@ require_line "$decoration_qml" "borders.bottom = normalCornerRadius;"
require_line "$decoration_qml" "PathArc { x: root.width - root.cornerRadius; y: root.height; radiusX: root.cornerRadius; radiusY: root.cornerRadius }" require_line "$decoration_qml" "PathArc { x: root.width - root.cornerRadius; y: root.height; radiusX: root.cornerRadius; radiusY: root.cornerRadius }"
require_line "$decoration_qml" "PathArc { x: 0; y: root.height - root.cornerRadius; radiusX: root.cornerRadius; radiusY: root.cornerRadius }" require_line "$decoration_qml" "PathArc { x: 0; y: root.height - root.cornerRadius; radiusX: root.cornerRadius; radiusY: root.cornerRadius }"
require_line "$folio_main" "readonly property int layoutMenuWindowCount: Math.max(0, ShellSettings.Settings.dynamicTilingLayoutWindowCount)"
require_line "$folio_main" "&& layoutMenuWindowCount >= 2"
require_line "$folio_main" "if (layoutMenuEnabled) {"
require_line "$folio_main" "regions.push(rightEdgeRegion)"
require_line "$folio_main" "if (layoutMenuEnabled && layoutMenuOpen)"
require_line "$folio_main" "rightEdgeHovered = false"
require_line "$folio_main" "layoutMenuHovered = false"
require_line "$folio_main" "id: rightEdgeHoverArea"
require_line "$folio_main" "convergenceChrome.rightEdgeHovered = containsMouse"
require_line "$folio_main" "width: convergenceChrome.layoutMenuEnabled ? convergenceChrome.rightEdgeHotzoneWidth : 0"
running_panel_group_disabled_count="$(grep -F "groupMode: TaskManager.TasksModel.GroupDisabled" "$running_apps_panel" | wc -l)" running_panel_group_disabled_count="$(grep -F "groupMode: TaskManager.TasksModel.GroupDisabled" "$running_apps_panel" | wc -l)"
if [[ "$running_panel_group_disabled_count" -ne 2 ]]; then if [[ "$running_panel_group_disabled_count" -ne 2 ]]; then
echo "Expected the Folio Running panel to disable grouping for both task models; found $running_panel_group_disabled_count" >&2 echo "Expected the Folio Running panel to disable grouping for both task models; found $running_panel_group_disabled_count" >&2
@ -155,6 +189,8 @@ reject_line "$tiling_script" "function tileInsertDirection(cursor, rect)"
reject_line "$tiling_script" "showDragOutline(\"insert\"" reject_line "$tiling_script" "showDragOutline(\"insert\""
reject_line "$tiling_script" "KWinComponents.Workspace.showOutline(dragOutlineRect)" reject_line "$tiling_script" "KWinComponents.Workspace.showOutline(dragOutlineRect)"
reject_line "$tiling_script" "KWinComponents.Workspace.hideOutline()" reject_line "$tiling_script" "KWinComponents.Workspace.hideOutline()"
reject_line "$tiling_script" "property int lastWindowRequestSerial: ShellSettings.Settings.dynamicTilingWindowRequestSerial"
reject_line "$tiling_script" "property int lastLayoutRequestSerial: ShellSettings.Settings.dynamicTilingLayoutRequestSerial"
reject_line "$effect_qml" "effect.visible = true" reject_line "$effect_qml" "effect.visible = true"
printf '%s\n' 'dynamic-tiles-motion-ok' printf '%s\n' 'dynamic-tiles-motion-ok'