Compare commits

..

No commits in common. "a09f349a3448bac53c1fac4b0b0f706f0198a671" and "8f3a94b1048add03351f04d2bfc788f62b559ffe" have entirely different histories.

29 changed files with 50 additions and 765 deletions

2
CHANGELOG.md.license Normal file
View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A.
SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2026 Marco Allegretti
SPDX-License-Identifier: EUPL-1.2

View file

@ -6,7 +6,7 @@
![Desktop overview](screenshots/quick_DesktopDemo.webm) ![Desktop overview](screenshots/quick_DesktopDemo.webm)
![Docked mode](screenshots/quick_DesktopDemo_docked.webm) ![Docked mode](screenshots/quick_DesktopDemo_docked.webm)
![Tiling](screenshots/quick_DesktopDemo_tiling.webm) ![Tiling](screenshots/quick_DesktopDemo_tilling.webm)
Shift is a fork of [plasma-mobile](https://invent.kde.org/plasma/plasma-mobile). Shift is a fork of [plasma-mobile](https://invent.kde.org/plasma/plasma-mobile).
The upstream phone UI is untouched; convergence adds a layer on top. The upstream phone UI is untouched; convergence adds a layer on top.

View file

@ -91,18 +91,7 @@ QQuickItem *AppletHost::fullRepresentationFor(const QString &pluginId)
item->setPreloadFullRepresentation(true); item->setPreloadFullRepresentation(true);
auto *fullRepItem = item->fullRepresentationItem(); return item->fullRepresentationItem();
if (!fullRepItem) {
connect(
item,
&PlasmaQuick::AppletQuickItem::fullRepresentationItemChanged,
this,
[this, pluginId]() {
Q_EMIT appletReady(pluginId);
},
static_cast<Qt::ConnectionType>(Qt::AutoConnection | Qt::SingleShotConnection));
}
return fullRepItem;
} }
#include "applethost.moc" #include "applethost.moc"

View file

@ -32,9 +32,6 @@ public:
Q_INVOKABLE QQuickItem *fullRepresentationFor(const QString &pluginId); Q_INVOKABLE QQuickItem *fullRepresentationFor(const QString &pluginId);
Q_SIGNALS:
void appletReady(const QString &pluginId);
private: private:
void ensureCorona(); void ensureCorona();

View file

@ -186,7 +186,6 @@ Item {
// In convergence, cap the height so it doesn't stretch full-screen // In convergence, cap the height so it doesn't stretch full-screen
maximumHeight: isConvergence ? root.height * 0.6 : -1 maximumHeight: isConvergence ? root.height * 0.6 : -1
toolButtonsItem: toolButtons
} }
// Secondary swipe area for uses in portrait. // Secondary swipe area for uses in portrait.

View file

@ -25,9 +25,6 @@ Item {
property alias notificationWidget: notificationWidget property alias notificationWidget: notificationWidget
property real contentY: notificationWidget.listView.contentY property real contentY: notificationWidget.listView.contentY
// The sibling toolbar whose height must be subtracted from the available space.
property Item toolButtonsItem: null
property real topPadding: { property real topPadding: {
if (actionDrawer.mode == MobileShell.ActionDrawer.Portrait) if (actionDrawer.mode == MobileShell.ActionDrawer.Portrait)
return Kirigami.Units.largeSpacing; return Kirigami.Units.largeSpacing;
@ -45,8 +42,7 @@ Item {
property real maximumHeight: -1 property real maximumHeight: -1
height: { height: {
let toolH = toolButtonsItem ? toolButtonsItem.height : 0; let h = Math.min(actionDrawer.height - toolButtons.height, notificationWidget.listView.contentHeight + 10 + topMargin);
let h = Math.min(actionDrawer.height - toolH, notificationWidget.listView.contentHeight + Kirigami.Units.largeSpacing + topMargin);
return maximumHeight > 0 ? Math.min(h, maximumHeight) : h; return maximumHeight > 0 ? Math.min(h, maximumHeight) : h;
} }

View file

@ -241,22 +241,10 @@ Item {
asynchronous: true asynchronous: true
sourceComponent: PageIndicator { sourceComponent: PageIndicator {
id: pageIndicatorItem
count: swipeView.count count: swipeView.count
currentIndex: swipeView.currentIndex currentIndex: swipeView.currentIndex
interactive: true interactive: true
onCurrentIndexChanged: { onCurrentIndexChanged: swipeView.currentIndex = currentIndex
if (swipeView.currentIndex !== currentIndex)
swipeView.currentIndex = currentIndex;
}
Connections {
target: swipeView
function onCurrentIndexChanged() {
if (pageIndicatorItem.currentIndex !== swipeView.currentIndex)
pageIndicatorItem.currentIndex = swipeView.currentIndex;
}
}
delegate: Rectangle { delegate: Rectangle {
implicitWidth: 8 implicitWidth: 8

View file

@ -148,7 +148,7 @@ Item {
orientation: root.isVertical ? ListView.Vertical : ListView.Horizontal orientation: root.isVertical ? ListView.Vertical : ListView.Horizontal
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
clip: true clip: true
interactive: root.isVertical ? contentHeight > height : contentWidth > width interactive: contentWidth > width
model: root.taskModel model: root.taskModel
delegate: NavigationPanelButton { delegate: NavigationPanelButton {

View file

@ -92,7 +92,7 @@ Item {
// Hover highlight in convergence mode to indicate the bar is clickable // Hover highlight in convergence mode to indicate the bar is clickable
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Qt.rgba(1.0, 1.0, 1.0, 0.1) color: Qt.rgba(255, 255, 255, 0.1)
visible: ShellSettings.Settings.convergenceModeEnabled && statusBarHover.hovered visible: ShellSettings.Settings.convergenceModeEnabled && statusBarHover.hovered
} }

View file

@ -50,9 +50,6 @@ Item {
var operationName = mouse.button === Qt.RightButton ? "ContextMenu" : "Activate"; var operationName = mouse.button === Qt.RightButton ? "ContextMenu" : "Activate";
var operation = model.service.operationDescription(operationName); var operation = model.service.operationDescription(operationName);
if (!operation) {
return;
}
operation.x = taskIcon.mapToGlobal(0, 0).x; operation.x = taskIcon.mapToGlobal(0, 0).x;
operation.y = taskIcon.mapToGlobal(0, taskIcon.height).y; operation.y = taskIcon.mapToGlobal(0, taskIcon.height).y;
model.service.startOperationCall(operation); model.service.startOperationCall(operation);

View file

@ -1,10 +1,6 @@
# SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com> # SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
# SPDX-License-Identifier: BSD-2-Clause # SPDX-License-Identifier: BSD-2-Clause
if(NOT TARGET KF6::AuthCore)
find_package(KF6Auth NO_MODULE REQUIRED)
endif()
add_executable(waydroidhelper) add_executable(waydroidhelper)
target_sources(waydroidhelper PRIVATE waydroidhelper.cpp) target_sources(waydroidhelper PRIVATE waydroidhelper.cpp)
@ -16,6 +12,10 @@ target_link_libraries(waydroidhelper
install(TARGETS waydroidhelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) install(TARGETS waydroidhelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR})
if(NOT TARGET KF6::AuthCore)
find_package(KF6Auth NO_MODULE REQUIRED)
endif()
kauth_install_helper_files(waydroidhelper org.kde.plasma.mobileshell.waydroidhelper root) kauth_install_helper_files(waydroidhelper org.kde.plasma.mobileshell.waydroidhelper root)
kauth_install_actions(org.kde.plasma.mobileshell.waydroidhelper waydroidhelper.actions) kauth_install_actions(org.kde.plasma.mobileshell.waydroidhelper waydroidhelper.actions)

View file

@ -8,7 +8,6 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio
qml/AppDrawer.qml qml/AppDrawer.qml
qml/AppDrawerGrid.qml qml/AppDrawerGrid.qml
qml/AppDrawerHeader.qml qml/AppDrawerHeader.qml
qml/CategoryPanel.qml
qml/DelegateDragItem.qml qml/DelegateDragItem.qml
qml/DelegateDropArea.qml qml/DelegateDropArea.qml
qml/FavouritesBar.qml qml/FavouritesBar.qml

View file

@ -46,11 +46,7 @@ ApplicationListModel::~ApplicationListModel() = default;
QHash<int, QByteArray> ApplicationListModel::roleNames() const QHash<int, QByteArray> ApplicationListModel::roleNames() const
{ {
return { return {{DelegateRole, QByteArrayLiteral("delegate")}};
{DelegateRole, QByteArrayLiteral("delegate")},
{NameRole, QByteArrayLiteral("name")},
{CategoriesRole, QByteArrayLiteral("categories")},
};
} }
void ApplicationListModel::sycocaDbChanged() void ApplicationListModel::sycocaDbChanged()
@ -161,11 +157,6 @@ QVariant ApplicationListModel::data(const QModelIndex &index, int role) const
return QVariant(); return QVariant();
} }
return delegate->application()->name(); return delegate->application()->name();
case CategoriesRole:
if (!delegate->application()) {
return QVariant();
}
return QVariant::fromValue(delegate->application()->categories());
default: default:
return QVariant(); return QVariant();
} }
@ -180,54 +171,8 @@ int ApplicationListModel::rowCount(const QModelIndex &parent) const
return m_delegates.count(); return m_delegates.count();
} }
// Sub-categories merged into their canonical parent, mirroring Kickoff's grouping.
static QString normalizeCategory(const QString &cat)
{
if (cat == QLatin1String("Audio") || cat == QLatin1String("Video"))
return QStringLiteral("AudioVideo");
if (cat == QLatin1String("Settings"))
return QStringLiteral("System");
return cat;
}
static const QSet<QString> &mainCategories()
{
static const QSet<QString> s = {
QStringLiteral("AudioVideo"),
QStringLiteral("Development"),
QStringLiteral("Education"),
QStringLiteral("Game"),
QStringLiteral("Graphics"),
QStringLiteral("Network"),
QStringLiteral("Office"),
QStringLiteral("Science"),
QStringLiteral("System"),
QStringLiteral("Utility"),
};
return s;
}
QStringList ApplicationListModel::allCategories() const
{
QSet<QString> found;
for (const auto &del : m_delegates) {
if (!del->application())
continue;
for (const QString &raw : del->application()->categories()) {
const QString cat = normalizeCategory(raw);
if (mainCategories().contains(cat))
found.insert(cat);
}
}
QStringList result = found.values();
result.sort();
return result;
}
ApplicationListSearchModel::ApplicationListSearchModel(HomeScreen *parent, ApplicationListModel *model) ApplicationListSearchModel::ApplicationListSearchModel(HomeScreen *parent, ApplicationListModel *model)
: QSortFilterProxyModel(parent) : QSortFilterProxyModel(parent)
, m_homeScreen{parent}
{ {
setSourceModel(model); setSourceModel(model);
@ -240,52 +185,3 @@ ApplicationListSearchModel::ApplicationListSearchModel(HomeScreen *parent, Appli
sort(0, Qt::AscendingOrder); sort(0, Qt::AscendingOrder);
} }
QString ApplicationListSearchModel::categoryFilter() const
{
return m_categoryFilter;
}
void ApplicationListSearchModel::setCategoryFilter(const QString &filter)
{
if (m_categoryFilter == filter)
return;
beginFilterChange();
m_categoryFilter = filter;
Q_EMIT categoryFilterChanged();
endFilterChange(QSortFilterProxyModel::Direction::Rows);
}
bool ApplicationListSearchModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent))
return false;
if (m_categoryFilter.isEmpty())
return true;
auto *src = static_cast<ApplicationListModel *>(sourceModel());
if (!src)
return false;
const QModelIndex idx = src->index(sourceRow, 0, sourceParent);
auto *del = src->data(idx, ApplicationListModel::DelegateRole).value<FolioDelegate *>();
if (!del || !del->application())
return false;
if (m_categoryFilter == QLatin1String("__favorites__")) {
if (!m_homeScreen)
return false;
auto *favModel = m_homeScreen->favouritesModel();
if (!favModel)
return false;
return favModel->containsApplication(del->application()->storageId());
}
// Match both the canonical name and any raw aliases it absorbs.
const QStringList &cats = del->application()->categories();
for (const QString &raw : cats) {
if (normalizeCategory(raw) == m_categoryFilter)
return true;
}
return false;
}

View file

@ -9,8 +9,6 @@
#include <QObject> #include <QObject>
#include <QQuickItem> #include <QQuickItem>
#include <QSet> #include <QSet>
#include <QSortFilterProxyModel>
#include <QStringList>
#include <KService> #include <KService>
@ -33,7 +31,6 @@ public:
enum Roles { enum Roles {
DelegateRole = Qt::UserRole + 1, DelegateRole = Qt::UserRole + 1,
NameRole, NameRole,
CategoriesRole,
}; };
ApplicationListModel(HomeScreen *parent = nullptr); ApplicationListModel(HomeScreen *parent = nullptr);
@ -45,8 +42,6 @@ public:
void load(); void load();
Q_INVOKABLE QStringList allCategories() const;
Q_SIGNALS: Q_SIGNALS:
// Emitted when an application was detected to have been removed from the system // Emitted when an application was detected to have been removed from the system
void applicationRemoved(QString storageId); void applicationRemoved(QString storageId);
@ -69,21 +64,6 @@ class ApplicationListSearchModel : public QSortFilterProxyModel
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("") QML_UNCREATABLE("")
Q_PROPERTY(QString categoryFilter READ categoryFilter WRITE setCategoryFilter NOTIFY categoryFilterChanged)
public: public:
ApplicationListSearchModel(HomeScreen *parent = nullptr, ApplicationListModel *model = nullptr); ApplicationListSearchModel(HomeScreen *parent = nullptr, ApplicationListModel *model = nullptr);
QString categoryFilter() const;
void setCategoryFilter(const QString &filter);
Q_SIGNALS:
void categoryFilterChanged();
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
HomeScreen *m_homeScreen{nullptr};
QString m_categoryFilter;
}; };

View file

@ -15,7 +15,6 @@ FolioApplication::FolioApplication(KService::Ptr service, QObject *parent)
, m_name{service->name()} , m_name{service->name()}
, m_icon{service->icon()} , m_icon{service->icon()}
, m_storageId{service->storageId()} , m_storageId{service->storageId()}
, m_categories{service->categories()}
{ {
if (service->property<bool>(QStringLiteral("X-KDE-PlasmaMobile-UseGenericName"))) { if (service->property<bool>(QStringLiteral("X-KDE-PlasmaMobile-UseGenericName"))) {
m_name = service->genericName(); m_name = service->genericName();
@ -77,11 +76,6 @@ QString FolioApplication::storageId() const
return m_storageId; return m_storageId;
} }
QStringList FolioApplication::categories() const
{
return m_categories;
}
KWayland::Client::PlasmaWindow *FolioApplication::window() const KWayland::Client::PlasmaWindow *FolioApplication::window() const
{ {
return m_window; return m_window;

View file

@ -7,7 +7,6 @@
#include <QObject> #include <QObject>
#include <QQuickItem> #include <QQuickItem>
#include <QString> #include <QString>
#include <QStringList>
#include <KIO/ApplicationLauncherJob> #include <KIO/ApplicationLauncherJob>
#include <KService> #include <KService>
@ -34,7 +33,6 @@ class FolioApplication : public QObject, public std::enable_shared_from_this<Fol
Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged) Q_PROPERTY(QString icon READ icon NOTIFY iconChanged)
Q_PROPERTY(QString storageId READ storageId NOTIFY storageIdChanged) Q_PROPERTY(QString storageId READ storageId NOTIFY storageIdChanged)
Q_PROPERTY(QStringList categories READ categories CONSTANT)
public: public:
typedef std::shared_ptr<FolioApplication> Ptr; typedef std::shared_ptr<FolioApplication> Ptr;
@ -48,7 +46,6 @@ public:
QString name() const; QString name() const;
QString icon() const; QString icon() const;
QString storageId() const; QString storageId() const;
QStringList categories() const;
KWayland::Client::PlasmaWindow *window() const; KWayland::Client::PlasmaWindow *window() const;
void setName(QString &name); void setName(QString &name);
@ -70,6 +67,5 @@ private:
QString m_name; QString m_name;
QString m_icon; QString m_icon;
QString m_storageId; QString m_storageId;
QStringList m_categories;
KWayland::Client::PlasmaWindow *m_window{nullptr}; KWayland::Client::PlasmaWindow *m_window{nullptr};
}; };

View file

@ -8,6 +8,7 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import './delegate' import './delegate'
@ -89,6 +90,21 @@ Item {
} }
} }
// Close button for convergence mode
QQC2.ToolButton {
visible: ShellSettings.Settings.convergenceModeEnabled
icon.name: "window-close-symbolic"
icon.color: "white"
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
onClicked: folio.HomeScreenState.closeAppDrawer()
QQC2.ToolTip.text: i18n("Close")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
}
} }
} }

View file

@ -1,188 +0,0 @@
// SPDX-FileCopyrightText: Marco Allegretti
// SPDX-License-Identifier: EUPL 1.2
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
Rectangle {
id: root
required property Folio.HomeScreen folio
// Emitted when the user taps a tile.
signal categorySelected(string categoryId)
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
// Swallow clicks so the dismiss area underneath is not triggered.
MouseArea { anchors.fill: parent }
// ---------- helpers ----------
function catDisplayName(cat) {
switch (cat) {
case "AudioVideo": return i18n("Multimedia")
case "Development": return i18n("Development")
case "Education": return i18n("Education")
case "Game": return i18n("Games")
case "Graphics": return i18n("Graphics")
case "Network": return i18n("Internet")
case "Office": return i18n("Office")
case "Science": return i18n("Science")
case "System": return i18n("System")
case "Utility": return i18n("Utilities")
default: return cat
}
}
function catIconName(cat) {
switch (cat) {
case "AudioVideo": return "applications-multimedia"
case "Development": return "applications-development"
case "Education": return "applications-education"
case "Game": return "applications-games"
case "Graphics": return "applications-graphics"
case "Network": return "applications-internet"
case "Office": return "applications-office"
case "Science": return "applications-science"
case "System": return "applications-system"
case "Utility": return "applications-utilities"
default: return "applications-other"
}
}
// ---------- model ----------
ListModel { id: categoryModel }
function populate() {
categoryModel.clear()
categoryModel.append({ catId: "", catName: i18n("All Apps"), catIcon: "applications-all" })
const cats = folio.ApplicationListModel.allCategories()
for (let i = 0; i < cats.length; i++) {
categoryModel.append({
catId: cats[i],
catName: root.catDisplayName(cats[i]),
catIcon: root.catIconName(cats[i]),
})
}
}
Component.onCompleted: populate()
Connections {
target: folio.ApplicationListModel
function onRowsInserted() { root.populate() }
function onRowsRemoved() { root.populate() }
function onModelReset() { root.populate() }
}
// ---------- tile list ----------
QQC2.ScrollView {
id: scrollView
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
contentWidth: availableWidth
clip: true
QQC2.ScrollBar.vertical.policy: QQC2.ScrollBar.AsNeeded
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
Column {
width: scrollView.availableWidth
spacing: Kirigami.Units.smallSpacing
Repeater {
model: categoryModel
delegate: Rectangle {
id: tile
required property string catId
required property string catName
required property string catIcon
readonly property bool isActive:
folio.ApplicationListSearchModel.categoryFilter === catId
width: parent.width
height: Kirigami.Units.iconSizes.medium + 2 * Kirigami.Units.largeSpacing
radius: Kirigami.Units.cornerRadius
color: isActive
? Qt.rgba(Kirigami.Theme.highlightColor.r,
Kirigami.Theme.highlightColor.g,
Kirigami.Theme.highlightColor.b, 0.2)
: tileArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r,
Kirigami.Theme.textColor.g,
Kirigami.Theme.textColor.b, 0.2)
: tileArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r,
Kirigami.Theme.textColor.g,
Kirigami.Theme.textColor.b, 0.1)
: "transparent"
// Active accent bar on left edge
Rectangle {
visible: tile.isActive
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: Kirigami.Units.smallSpacing
anchors.bottomMargin: Kirigami.Units.smallSpacing
width: 3
radius: 2
color: Kirigami.Theme.highlightColor
}
Row {
anchors {
fill: parent
leftMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.smallSpacing
}
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
anchors.verticalCenter: parent.verticalCenter
width: Kirigami.Units.iconSizes.medium
height: width
source: tile.catIcon
active: tileArea.containsMouse || tile.isActive
}
PlasmaComponents.Label {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
- Kirigami.Units.iconSizes.medium
- Kirigami.Units.largeSpacing * 2
- Kirigami.Units.smallSpacing
text: tile.catName
elide: Text.ElideRight
font.weight: tile.isActive ? Font.Medium : Font.Normal
color: tile.isActive
? Kirigami.Theme.highlightColor
: Kirigami.Theme.textColor
}
}
MouseArea {
id: tileArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.categorySelected(tile.catId)
}
}
}
}
}
}

View file

@ -130,8 +130,8 @@ MouseArea {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
width: root.navButtonWidth width: root.navButtonWidth
color: homeMouseArea.containsPress color: homeMouseArea.containsPress
? Qt.rgba(1, 1, 1, 0.2) ? Qt.rgba(255, 255, 255, 0.2)
: (homeMouseArea.containsMouse ? Qt.rgba(1, 1, 1, 0.1) : "transparent") : (homeMouseArea.containsMouse ? Qt.rgba(255, 255, 255, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
Kirigami.Icon { Kirigami.Icon {
@ -160,8 +160,8 @@ MouseArea {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
width: root.navButtonWidth width: root.navButtonWidth
color: overviewMouseArea.containsPress color: overviewMouseArea.containsPress
? Qt.rgba(1, 1, 1, 0.2) ? Qt.rgba(255, 255, 255, 0.2)
: (overviewMouseArea.containsMouse ? Qt.rgba(1, 1, 1, 0.1) : "transparent") : (overviewMouseArea.containsMouse ? Qt.rgba(255, 255, 255, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
Kirigami.Icon { Kirigami.Icon {
@ -854,8 +854,8 @@ MouseArea {
anchors.fill: parent anchors.fill: parent
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
color: taskMouseArea.containsPress color: taskMouseArea.containsPress
? Qt.rgba(1.0, 1.0, 1.0, 0.2) ? Qt.rgba(255, 255, 255, 0.2)
: (taskMouseArea.containsMouse ? Qt.rgba(1.0, 1.0, 1.0, 0.1) : "transparent") : (taskMouseArea.containsMouse ? Qt.rgba(255, 255, 255, 0.1) : "transparent")
} }
// Task icon // Task icon

View file

@ -126,7 +126,7 @@ Folio.DelegateTouchArea {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
color: Qt.rgba(1.0, 1.0, 1.0, 0.1) color: Qt.rgba(255, 255, 255, 0.1)
visible: ShellSettings.Settings.convergenceModeEnabled && root.hovered visible: ShellSettings.Settings.convergenceModeEnabled && root.hovered
} }
} }

View file

@ -18,10 +18,6 @@ import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.layershell 1.0 as LayerShell import org.kde.layershell 1.0 as LayerShell
import org.kde.plasma.private.sessions 2.0
import org.kde.coreaddons as KCoreAddons
import org.kde.kcmutils as KCM
import org.kde.kirigamiaddons.components as KirigamiAddonsComponents
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
@ -198,19 +194,12 @@ ContainmentItem {
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// Auto-hide: slide dock content off-screen when a window is // Auto-hide: slide dock content off-screen when a window is
// maximized. The reveal strip at the screen edge brings it back. // maximized. A HoverHandler brings it back on mouse proximity.
property real dockOffset: 0 property real dockOffset: 0
readonly property real dockHeight: Kirigami.Units.gridUnit * 3 readonly property real dockHeight: Kirigami.Units.gridUnit * 3
readonly property bool dockHovered: dockHoverHandler.hovered
// Height of the input-receive strip kept at the screen edge when
// the dock is hidden. Matches the navigation panel convention.
readonly property real revealStripHeight: Kirigami.Units.gridUnit
// True once the hover-reveal timer fires; cleared on hover-exit.
property bool hoverRevealing: false
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing && windowMaximizedTracker.showingWindow && !dockHovered
onShouldHideChanged: { onShouldHideChanged: {
if (shouldHide) { if (shouldHide) {
@ -220,38 +209,8 @@ ContainmentItem {
} }
} }
// Narrow the input region to a strip at the screen edge when hidden
// so that app controls near the bottom edge are not accidentally
// intercepted. Mirrors the same pattern used by NavigationPanel.
onDockOffsetChanged: {
if (dockOffset >= dockHeight) {
MobileShell.ShellUtil.setInputRegion(dockOverlay,
Qt.rect(0, dockOverlay.height - revealStripHeight,
dockOverlay.width, revealStripHeight))
} else if (dockOffset === 0) {
MobileShell.ShellUtil.setInputRegion(dockOverlay, Qt.rect(0, 0, 0, 0))
}
}
// Delay reveal by 300 ms so a quick edge graze does not pop the
// dock up mid-interaction with the underlying application.
Timer {
id: hoverRevealTimer
interval: 300
repeat: false
onTriggered: dockOverlay.hoverRevealing = true
}
HoverHandler { HoverHandler {
id: dockHoverHandler id: dockHoverHandler
onHoveredChanged: {
if (hovered) {
hoverRevealTimer.start()
} else {
hoverRevealTimer.stop()
dockOverlay.hoverRevealing = false
}
}
} }
Behavior on dockOffset { Behavior on dockOffset {
@ -263,7 +222,9 @@ ContainmentItem {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5) Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Kirigami.Theme.backgroundColor
transform: Translate { y: dockOverlay.dockOffset } transform: Translate { y: dockOverlay.dockOffset }
} }
@ -347,297 +308,7 @@ ContainmentItem {
target: folio.HomeScreenState target: folio.HomeScreenState
function onAppDrawerOpened() { function onAppDrawerOpened() {
folio.ApplicationListSearchModel.categoryFilter = "" overlayDrawer.forceActiveFocus();
overlayDrawer.forceActiveFocus()
}
}
}
// Drop shadow rendered separately so categoryPanel itself needs no
// layer FBO (which would rasterize and blur the icons inside).
Rectangle {
id: categoryPanelShadow
width: categoryPanel.width
height: categoryPanel.height
x: categoryPanel.x
y: categoryPanel.y
radius: categoryPanel.radius
color: categoryPanel.color
opacity: categoryPanel.opacity
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 0
verticalOffset: 2
radius: 12
samples: 25
color: Qt.rgba(0, 0, 0, 0.4)
}
}
CategoryPanel {
id: categoryPanel
folio: root.folio
width: Kirigami.Units.gridUnit * 9
height: overlayDrawer.popupHeight
x: overlayDrawer.x + overlayDrawer.width + Kirigami.Units.smallSpacing
y: overlayDrawer.y
opacity: overlayDrawer.opacity
onCategorySelected: (catId) => {
folio.ApplicationListSearchModel.categoryFilter = catId
overlayDrawerHeader.clearSearchText()
}
}
// Drop shadow rendered separately so powerPanel itself needs no layer FBO,
// which would rasterize and blur the icons inside.
Rectangle {
id: powerPanelShadow
width: powerPanel.width
height: powerPanel.height
x: powerPanel.x
y: powerPanel.y
radius: powerPanel.radius
color: powerPanel.color
opacity: powerPanel.opacity
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 0
verticalOffset: 2
radius: 12
samples: 25
color: Qt.rgba(0, 0, 0, 0.4)
}
}
Rectangle {
id: powerPanel
// Width: just enough for one icon button + side margins
readonly property real tileSize: Kirigami.Units.iconSizes.medium + 2 * Kirigami.Units.largeSpacing
width: tileSize
height: overlayDrawer.popupHeight
x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
y: overlayDrawer.y
opacity: overlayDrawer.opacity
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
MouseArea {
anchors.fill: parent
}
KCoreAddons.KUser {
id: kuser
}
SessionManagement {
id: powerSession
}
// Close button anchored to top smaller than power icons
Rectangle {
id: closeButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
height: Kirigami.Units.iconSizes.smallMedium + 2 * Kirigami.Units.smallSpacing
radius: Kirigami.Units.cornerRadius
color: closeArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: closeArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.smallMedium
height: width
source: "window-close-symbolic"
active: closeArea.containsMouse
}
PlasmaComponents.ToolTip {
text: i18n("Close")
visible: closeArea.containsMouse
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: folio.HomeScreenState.closeAppDrawer()
}
}
// Separator below close button
Rectangle {
anchors.top: closeButton.bottom
anchors.topMargin: Kirigami.Units.smallSpacing
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
height: 1
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.15)
}
// Power buttons centred vertically in the panel
Column {
id: powerColumn
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Rectangle {
width: parent.width
height: width
radius: Kirigami.Units.cornerRadius
color: lockArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: lockArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-lock-screen"
active: lockArea.containsMouse
}
PlasmaComponents.ToolTip {
text: i18n("Lock Screen")
visible: lockArea.containsMouse
}
MouseArea {
id: lockArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerSession.lock()
folio.HomeScreenState.closeAppDrawer()
}
}
}
Rectangle {
width: parent.width
height: width
radius: Kirigami.Units.cornerRadius
color: rebootArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: rebootArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-reboot"
active: rebootArea.containsMouse
}
PlasmaComponents.ToolTip {
text: i18n("Restart")
visible: rebootArea.containsMouse
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
folio.HomeScreenState.closeAppDrawer()
powerSession.requestReboot()
}
}
}
Rectangle {
width: parent.width
height: width
radius: Kirigami.Units.cornerRadius
color: shutdownArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: shutdownArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-shutdown"
active: shutdownArea.containsMouse
}
PlasmaComponents.ToolTip {
text: i18n("Shut Down")
visible: shutdownArea.containsMouse
}
MouseArea {
id: shutdownArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
folio.HomeScreenState.closeAppDrawer()
powerSession.requestShutdown()
}
}
}
}
// Separator above user avatar
Rectangle {
anchors.bottom: userSection.top
anchors.bottomMargin: Kirigami.Units.smallSpacing
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
height: 1
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.15)
}
// User avatar anchored to bottom tooltip shows name, click opens user settings
Rectangle {
id: userSection
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
height: width
radius: Kirigami.Units.cornerRadius
color: userArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: userArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
KirigamiAddonsComponents.Avatar {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: kuser.faceIconUrl
name: kuser.fullName || kuser.loginName
}
PlasmaComponents.ToolTip {
text: kuser.fullName || kuser.loginName
visible: userArea.containsMouse
}
MouseArea {
id: userArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
KCM.KCMLauncher.openSystemSettings("kcm_users")
folio.HomeScreenState.closeAppDrawer()
}
} }
} }
} }

View file

@ -158,13 +158,13 @@ ContainmentItem {
color: "transparent" color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
// height is set by layer-shell anchoring; provide a fallback. // height is set by layer-shell anchoring; provide a fallback.
height: root.navigationPanelHeight height: Kirigami.Units.gridUnit * 3
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
LayerShell.Window.scope: "dock-space" LayerShell.Window.scope: "dock-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: root.navigationPanelHeight LayerShell.Window.exclusionZone: Kirigami.Units.gridUnit * 3
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
} }

View file

@ -11,43 +11,6 @@ Loader {
property var currentWindow property var currentWindow
// Window that needs geometry clamping after un-maximize in convergence
// mode. Set in onMaximizedChanged and consumed by the timer below.
property var pendingConstrainWindow: null
// After a window is un-maximized in convergence mode, the dockSpaceReserver
// LayerShell surface needs one Wayland roundtrip to (re)commit its exclusive
// zone so that KWin updates MaximizeArea. We wait 200 ms well within the
// dock slide-in animation then clamp the window bottom to MaximizeArea so
// it cannot overlap the dock.
Timer {
id: constrainAfterRestoreTimer
interval: 200
onTriggered: {
const window = root.pendingConstrainWindow
root.pendingConstrainWindow = null
if (!window || window.deleted || !window.normalWindow) return
if (!ShellSettings.Settings.convergenceModeEnabled) return
const output = window.output
const desktop = window.desktops[0]
if (!desktop) return
const maxRect = KWinComponents.Workspace.clientArea(
KWinComponents.Workspace.MaximizeArea, output, desktop)
const geo = window.frameGeometry
const maxBottom = maxRect.y + maxRect.height
if (geo.y + geo.height > maxBottom) {
// Clip the bottom edge to MaximizeArea; preserve top position
// and width. Ensure height is at least 100px to avoid
// pathological cases where the window starts above maxRect.
const newH = Math.max(100, maxBottom - geo.y)
window.frameGeometry = Qt.rect(geo.x, geo.y, geo.width, newH)
}
}
}
function run(window) { function run(window) {
// HACK: don't maximize xwaylandvideobridge // HACK: don't maximize xwaylandvideobridge
// see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324 // see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324
@ -104,14 +67,6 @@ Loader {
root.run(currentWindow); root.run(currentWindow);
}); });
root.run(currentWindow); root.run(currentWindow);
// Schedule a deferred geometry clamp so that the restored window
// doesn't overlap the dock after the dockSpaceReserver exclusive
// zone is re-committed over a Wayland roundtrip.
if (ShellSettings.Settings.convergenceModeEnabled
&& ShellSettings.Settings.autoHidePanelsEnabled) {
root.pendingConstrainWindow = currentWindow
constrainAfterRestoreTimer.restart()
}
} }
} }

View file

@ -4,10 +4,6 @@
add_executable(flashlighthelper) add_executable(flashlighthelper)
target_sources(flashlighthelper PRIVATE flashlighthelper.cpp) target_sources(flashlighthelper PRIVATE flashlighthelper.cpp)
if(NOT TARGET KF6::AuthCore)
find_package(KF6Auth NO_MODULE REQUIRED)
endif()
target_link_libraries(flashlighthelper target_link_libraries(flashlighthelper
Qt6::Core Qt6::Core
KF6::AuthCore KF6::AuthCore
@ -17,6 +13,10 @@ target_link_libraries(flashlighthelper
install(TARGETS flashlighthelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) install(TARGETS flashlighthelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR})
if(NOT TARGET KF6::AuthCore)
find_package(KF6Auth NO_MODULE REQUIRED)
endif()
kauth_install_helper_files(flashlighthelper org.kde.plasma.mobileshell.flashlighthelper root) kauth_install_helper_files(flashlighthelper org.kde.plasma.mobileshell.flashlighthelper root)
kauth_install_actions(org.kde.plasma.mobileshell.flashlighthelper flashlighthelper.actions) kauth_install_actions(org.kde.plasma.mobileshell.flashlighthelper flashlighthelper.actions)

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2026 Marco Allegretti SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2 SPDX-License-Identifier: EUPL-1.2