Compare commits

...

15 commits

Author SHA1 Message Date
a09f349a34 Rename CHANGELOG to OVERVIEW, fix tiling typo
CHANGELOG.md held a project overview, not versioned entries.
Also fix a typo in the screenshot filename (tilling → tiling)
and the matching README reference.
2026-04-18 19:05:19 +02:00
a29019631e Fix KF6Auth find_package order in kauth helpers
find_package(KF6Auth) was placed after target_link_libraries, so
KF6::AuthCore could be undefined when the linker target was
created. Move it before target_link_libraries in both the
flashlight and waydroid helpers.
2026-04-18 19:05:11 +02:00
209570d3b2 Fix app list model filter ordering and reset handling
ApplicationListSearchModel: beginFilterChange() was called after
mutating m_categoryFilter, inverting the documented before/after
contract. Also add null checks for sourceModel() and
favouritesModel() in filterAcceptsRow.

CategoryPanel: add onModelReset to repopulate the category list
when the underlying model is fully reloaded, not just on individual
row insertions and removals.
2026-04-18 19:05:04 +02:00
667efec483 Fix panel interactive flag and reserved height
NavigationPanel: the taskStrip ListView checked contentWidth > width
to decide whether scrolling is enabled, which is wrong when the
panel is vertical. Check contentHeight > height in that case.

taskpanel: dockSpaceReserver used a hardcoded gridUnit * 3 for both
height and exclusionZone. Use navigationPanelHeight so the reserved
strip tracks the actual panel thickness.
2026-04-18 19:04:56 +02:00
9b94c200ee Fix action drawer toolButtons ref and page indicator sync
NotificationDrawer referenced toolButtons, a sibling defined in
ContentContainer, not a local property. Add toolButtonsItem: Item
and wire it at the call site. Replace the magic literal 10 with
Kirigami.Units.largeSpacing while here.

The PageIndicator had a one-way binding to swipeView.currentIndex
that would break after a user tap. Make it bidirectional with a
Connections block and a loop guard on both sides.
2026-04-18 19:04:49 +02:00
2059f14faf Add null guards in AppletHost and TaskWidget
AppletHost: setPreloadFullRepresentation(true) does not guarantee
fullRepresentationItem() is non-null on return — the applet loads
asynchronously. Wire a single-shot connection to
fullRepresentationItemChanged to emit appletReady() when the item
arrives.

TaskWidget: operationDescription() can return null for an unknown
operation name; bail out rather than dereferencing it.
2026-04-18 19:04:40 +02:00
76e027f630 Fix Qt.rgba color arguments
Qt.rgba expects 0.0–1.0 floats. Passing 255 for R/G/B saturates
every channel to 1.0, producing opaque white instead of the
intended semi-transparent tint. Fix all call sites across
FavouritesBar, StatusBar, and AbstractDelegate.
2026-04-18 19:04:22 +02:00
dd7f087fac Add category filter panel to the app drawer
A panel between the drawer and the power panel lists the XDG
categories installed on the device as icon-and-label tiles.
Tapping one filters the app list; text search still applies
on top. The filter resets each time the drawer opens. Audio,
Video and Settings fold into their parent groups to match
Kickoff.
2026-04-18 13:20:13 +02:00
2a0e4428c7 Move close button to power panel
The dismiss button in the drawer header was isolated from the
other controls. Moving it to the top of the power panel groups
everything in one place and lets the power icons sit centered
between it and the user avatar.
2026-04-18 13:20:13 +02:00
d0b4cbe2b0 Switch power panel buttons to icon tiles
Labels are gone; each action shows only an icon with a tooltip
on hover, keeping the panel narrow. A user avatar at the bottom
opens User Accounts on click. The shadow is on a backing
Rectangle rather than via layer.enabled, which would rasterize
the panel and blur icons at non-integer scale.
2026-04-18 13:20:13 +02:00
6e48055b40 Clamp restored window below the dock
KWin restores geometry before the compositor re-commits the
exclusive zone, so the window can land partly behind the dock.
A deferred check clips the bottom edge once the round-trip
completes.
2026-04-18 13:20:13 +02:00
0b9ae2eca7 Narrow dock hit area to edge strip when hidden
The hidden dock's window still covered full height, blocking
toolbar buttons in maximized apps. Reduce the input region to
a thin strip at the screen edge while off-screen and restore
it on reveal. A debounce timer prevents an accidental edge
graze from reopening the dock.
2026-04-18 13:20:13 +02:00
3466a8c5d5 Fix trailing period in webm license header 2026-04-18 08:23:29 +02:00
5c3c3b9720 Add translucent background to convergence dock
The dock's LayerShell window is transparent; icon labels are
hardcoded white (designed to float over wallpaper on mobile).
A dark semi-transparent fill keeps labels legible on any
wallpaper without coupling to the Plasma theme color.
2026-04-18 08:23:24 +02:00
e4f4232cf2 Add power panel alongside app drawer in convergence mode
Shows a narrow card to the right of the app drawer popup with
Lock Screen, Restart, and Shut Down buttons. Uses SessionManagement
from org.kde.plasma.private.sessions directly, the same QML type
Logout.qml already uses. No C++ changes needed. Shares the drawer
open/close animation and dismisses with it.
2026-04-17 13:58:22 +02:00
29 changed files with 765 additions and 50 deletions

View file

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

2
OVERVIEW.md.license Normal file
View file

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

View file

@ -6,7 +6,7 @@
![Desktop overview](screenshots/quick_DesktopDemo.webm)
![Docked mode](screenshots/quick_DesktopDemo_docked.webm)
![Tiling](screenshots/quick_DesktopDemo_tilling.webm)
![Tiling](screenshots/quick_DesktopDemo_tiling.webm)
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.

View file

@ -91,7 +91,18 @@ QQuickItem *AppletHost::fullRepresentationFor(const QString &pluginId)
item->setPreloadFullRepresentation(true);
return item->fullRepresentationItem();
auto *fullRepItem = 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"

View file

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

View file

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

View file

@ -25,6 +25,9 @@ Item {
property alias notificationWidget: notificationWidget
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: {
if (actionDrawer.mode == MobileShell.ActionDrawer.Portrait)
return Kirigami.Units.largeSpacing;
@ -42,7 +45,8 @@ Item {
property real maximumHeight: -1
height: {
let h = Math.min(actionDrawer.height - toolButtons.height, notificationWidget.listView.contentHeight + 10 + topMargin);
let toolH = toolButtonsItem ? toolButtonsItem.height : 0;
let h = Math.min(actionDrawer.height - toolH, notificationWidget.listView.contentHeight + Kirigami.Units.largeSpacing + topMargin);
return maximumHeight > 0 ? Math.min(h, maximumHeight) : h;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,10 @@
# SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
# SPDX-License-Identifier: BSD-2-Clause
if(NOT TARGET KF6::AuthCore)
find_package(KF6Auth NO_MODULE REQUIRED)
endif()
add_executable(waydroidhelper)
target_sources(waydroidhelper PRIVATE waydroidhelper.cpp)
@ -12,10 +16,6 @@ target_link_libraries(waydroidhelper
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_actions(org.kde.plasma.mobileshell.waydroidhelper waydroidhelper.actions)

View file

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

View file

@ -46,7 +46,11 @@ ApplicationListModel::~ApplicationListModel() = default;
QHash<int, QByteArray> ApplicationListModel::roleNames() const
{
return {{DelegateRole, QByteArrayLiteral("delegate")}};
return {
{DelegateRole, QByteArrayLiteral("delegate")},
{NameRole, QByteArrayLiteral("name")},
{CategoriesRole, QByteArrayLiteral("categories")},
};
}
void ApplicationListModel::sycocaDbChanged()
@ -157,6 +161,11 @@ QVariant ApplicationListModel::data(const QModelIndex &index, int role) const
return QVariant();
}
return delegate->application()->name();
case CategoriesRole:
if (!delegate->application()) {
return QVariant();
}
return QVariant::fromValue(delegate->application()->categories());
default:
return QVariant();
}
@ -171,8 +180,54 @@ int ApplicationListModel::rowCount(const QModelIndex &parent) const
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)
: QSortFilterProxyModel(parent)
, m_homeScreen{parent}
{
setSourceModel(model);
@ -185,3 +240,52 @@ ApplicationListSearchModel::ApplicationListSearchModel(HomeScreen *parent, Appli
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,6 +9,8 @@
#include <QObject>
#include <QQuickItem>
#include <QSet>
#include <QSortFilterProxyModel>
#include <QStringList>
#include <KService>
@ -31,6 +33,7 @@ public:
enum Roles {
DelegateRole = Qt::UserRole + 1,
NameRole,
CategoriesRole,
};
ApplicationListModel(HomeScreen *parent = nullptr);
@ -42,6 +45,8 @@ public:
void load();
Q_INVOKABLE QStringList allCategories() const;
Q_SIGNALS:
// Emitted when an application was detected to have been removed from the system
void applicationRemoved(QString storageId);
@ -64,6 +69,21 @@ class ApplicationListSearchModel : public QSortFilterProxyModel
QML_ELEMENT
QML_UNCREATABLE("")
Q_PROPERTY(QString categoryFilter READ categoryFilter WRITE setCategoryFilter NOTIFY categoryFilterChanged)
public:
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,6 +15,7 @@ FolioApplication::FolioApplication(KService::Ptr service, QObject *parent)
, m_name{service->name()}
, m_icon{service->icon()}
, m_storageId{service->storageId()}
, m_categories{service->categories()}
{
if (service->property<bool>(QStringLiteral("X-KDE-PlasmaMobile-UseGenericName"))) {
m_name = service->genericName();
@ -76,6 +77,11 @@ QString FolioApplication::storageId() const
return m_storageId;
}
QStringList FolioApplication::categories() const
{
return m_categories;
}
KWayland::Client::PlasmaWindow *FolioApplication::window() const
{
return m_window;

View file

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

View file

@ -8,7 +8,6 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
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 './delegate'
@ -90,21 +89,6 @@ 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

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

View file

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

View file

@ -18,6 +18,10 @@ import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
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
@ -194,12 +198,19 @@ ContainmentItem {
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// Auto-hide: slide dock content off-screen when a window is
// maximized. A HoverHandler brings it back on mouse proximity.
// maximized. The reveal strip at the screen edge brings it back.
property real dockOffset: 0
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
&& windowMaximizedTracker.showingWindow && !dockHovered
&& windowMaximizedTracker.showingWindow && !hoverRevealing
onShouldHideChanged: {
if (shouldHide) {
@ -209,8 +220,38 @@ 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 {
id: dockHoverHandler
onHoveredChanged: {
if (hovered) {
hoverRevealTimer.start()
} else {
hoverRevealTimer.stop()
dockOverlay.hoverRevealing = false
}
}
}
Behavior on dockOffset {
@ -222,9 +263,7 @@ ContainmentItem {
Rectangle {
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Kirigami.Theme.backgroundColor
color: Qt.rgba(0, 0, 0, 0.5)
transform: Translate { y: dockOverlay.dockOffset }
}
@ -308,7 +347,297 @@ ContainmentItem {
target: folio.HomeScreenState
function onAppDrawerOpened() {
overlayDrawer.forceActiveFocus();
folio.ApplicationListSearchModel.categoryFilter = ""
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"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
// height is set by layer-shell anchoring; provide a fallback.
height: Kirigami.Units.gridUnit * 3
height: root.navigationPanelHeight
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
LayerShell.Window.scope: "dock-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: Kirigami.Units.gridUnit * 3
LayerShell.Window.exclusionZone: root.navigationPanelHeight
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
}

View file

@ -11,6 +11,43 @@ Loader {
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) {
// HACK: don't maximize xwaylandvideobridge
// see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324
@ -67,6 +104,14 @@ Loader {
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,6 +4,10 @@
add_executable(flashlighthelper)
target_sources(flashlighthelper PRIVATE flashlighthelper.cpp)
if(NOT TARGET KF6::AuthCore)
find_package(KF6Auth NO_MODULE REQUIRED)
endif()
target_link_libraries(flashlighthelper
Qt6::Core
KF6::AuthCore
@ -13,10 +17,6 @@ target_link_libraries(flashlighthelper
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_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