panel: Overlay over lockscreen

Overlay the shell's status panel and quicksettings panel over the lockscreen, instead of rendering a second copy in the lockscreen theme. This will allow us to improve the lockscreen loading speed.

Key changes:
- Overlay quicksettings window and the status bar over the lockscreen when it is shown
- Refactor the top panel's showing logic to be cleaner (as it supports various overlay modes over fullscreen apps already)
- Implement lockscreen support to the status bar and quicksettings panel in the to panel
- Forward quicksettings panel requests for "unlock" over DBus to the lockscreen
- Add "raiselockscreen" QML plugin to easily request a window to be raised over the lockscreen

Notes:
- Now that we are sharing the quicksettings panel from the shell, notifications that are already there will be shown on the lockscreen (compared to right now, where only new notifications would be shown)

Depends on: 
- https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/2339
- https://invent.kde.org/plasma/kscreenlocker/-/merge_requests/283
- https://invent.kde.org/plasma/kwin/-/merge_requests/7839

Implements: https://invent.kde.org/plasma/plasma-mobile/-/issues/199

![Screencast_20250612_013325](/uploads/49e9981cb863056b4c0c46a144e5ee7d/Screencast_20250612_013325.webm)
This commit is contained in:
Devin Lin 2025-07-02 10:27:33 -04:00
parent aa368499f1
commit 3b6951bf1e
22 changed files with 823 additions and 531 deletions

View file

@ -34,6 +34,7 @@ include(ECMMarkNonGuiExecutable)
include(ECMGenerateHeaders)
include(ECMFindQmlModule)
include(ECMGenerateQmlTypes)
include(ECMConfiguredInstall)
include(ECMFindQmlModule)
include(GenerateExportHeader)
include(KDEGitCommitHooks)
@ -100,6 +101,14 @@ find_package(KWin ${PROJECT_DEP_VERSION} REQUIRED COMPONENTS
)
find_package(LayerShellQt REQUIRED)
find_package(Wayland REQUIRED)
find_package(PlasmaWaylandProtocols 1.8 CONFIG)
set_package_properties(PlasmaWaylandProtocols PROPERTIES
TYPE REQUIRED
PURPOSE "Collection of Plasma-specific Wayland protocols"
URL "https://invent.kde.org/libraries/plasma-wayland-protocols/"
)
find_package(LibKWorkspace CONFIG REQUIRED)
find_package(Libudev REQUIRED)

View file

@ -13,3 +13,4 @@ add_subdirectory(shellsettingsplugin)
add_subdirectory(wallpaperimageplugin)
add_subdirectory(dpmsplugin)
add_subdirectory(screenbrightnessplugin)
add_subdirectory(raiselockscreenplugin)

View file

@ -111,7 +111,10 @@ Item {
* Run pending action that was pending for authentication when unlockRequested() was emitted.
*/
function runPendingAction() {
list.pendingNotificationWithAction.runPendingAction();
if (list.pendingNotificationWithAction) {
list.pendingNotificationWithAction.runPendingAction();
list.pendingNotificationWithAction = null;
}
}
/**
@ -175,7 +178,7 @@ Item {
currentIndex: 0
property var pendingNotificationWithAction
property NotificationItem pendingNotificationWithAction: null
readonly property int animationDuration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.longDuration : 0

View file

@ -16,8 +16,6 @@ LockscreenDBusClient::LockscreenDBusClient(QObject *parent)
QStringLiteral("org.freedesktop.ScreenSaver"),
QStringLiteral("GetActive"));
const QDBusReply<bool> response = QDBusConnection::sessionBus().call(request);
QDBusConnection::sessionBus().callWithCallback(request, this, SLOT(slotLockscreenActiveChanged(bool)), SLOT(dbusError(QDBusError)));
QDBusConnection::sessionBus().connect(QStringLiteral("org.freedesktop.ScreenSaver"),

View file

@ -50,6 +50,7 @@ void ShellDBusClient::connectSignals()
connect(m_interface, &OrgKdePlasmashellInterface::openHomeScreenRequested, this, &ShellDBusClient::openHomeScreenRequested);
connect(m_interface, &OrgKdePlasmashellInterface::resetHomeScreenPositionRequested, this, &ShellDBusClient::resetHomeScreenPositionRequested);
connect(m_interface, &OrgKdePlasmashellInterface::showVolumeOSDRequested, this, &ShellDBusClient::showVolumeOSDRequested);
connect(m_interface, &OrgKdePlasmashellInterface::openLockScreenKeypadRequested, this, &ShellDBusClient::openLockScreenKeypadRequested);
updateDoNotDisturb();
updateIsTaskSwitcherVisible();
@ -151,6 +152,11 @@ void ShellDBusClient::showVolumeOSD()
m_interface->showVolumeOSD();
}
void ShellDBusClient::openLockScreenKeypad()
{
m_interface->openLockScreenKeypad();
}
void ShellDBusClient::updatePanelState()
{
auto reply = m_interface->panelState();

View file

@ -54,6 +54,7 @@ public:
Q_INVOKABLE void openHomeScreen();
Q_INVOKABLE void resetHomeScreenPosition();
Q_INVOKABLE void showVolumeOSD();
Q_INVOKABLE void openLockScreenKeypad();
Q_SIGNALS:
void panelStateChanged();
@ -68,6 +69,7 @@ Q_SIGNALS:
void openHomeScreenRequested();
void resetHomeScreenPositionRequested();
void showVolumeOSDRequested();
void openLockScreenKeypadRequested();
private Q_SLOTS:
void updateDoNotDisturb();

View file

@ -150,3 +150,8 @@ void ShellDBusObject::showVolumeOSD()
{
Q_EMIT showVolumeOSDRequested();
}
void ShellDBusObject::openLockScreenKeypad()
{
Q_EMIT openLockScreenKeypadRequested();
}

View file

@ -38,6 +38,7 @@ Q_SIGNALS:
Q_SCRIPTABLE void openHomeScreenRequested();
Q_SCRIPTABLE void resetHomeScreenPositionRequested();
Q_SCRIPTABLE void showVolumeOSDRequested();
Q_SCRIPTABLE void openLockScreenKeypadRequested();
public Q_SLOTS:
Q_SCRIPTABLE bool doNotDisturb();
@ -70,6 +71,7 @@ public Q_SLOTS:
Q_SCRIPTABLE void openHomeScreen();
Q_SCRIPTABLE void resetHomeScreenPosition();
Q_SCRIPTABLE void showVolumeOSD();
Q_SCRIPTABLE void openLockScreenKeypad();
private:
bool m_initialized{false};

View file

@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: LGPL-2.0-or-later
ecm_add_qml_module(raiselockscreenplugin URI org.kde.plasma.private.mobileshell.raiselockscreenplugin GENERATE_PLUGIN_SOURCE)
target_sources(raiselockscreenplugin PRIVATE
raiselockscreen.cpp
utils.h
)
target_link_libraries(raiselockscreenplugin PRIVATE
Qt::Qml
Qt::Quick
Qt::WaylandClient
Qt::WaylandClientPrivate
KF6::Service
KF6::Package
KF6::I18n
KF6::ConfigCore
KF6::Service
KF6::WindowSystem
Wayland::Client
)
qt6_generate_wayland_protocol_client_sources(raiselockscreenplugin FILES
${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-lockscreen-overlay-v1.xml)
ecm_finalize_qml_module(raiselockscreenplugin)

View file

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "raiselockscreen.h"
#include "utils.h"
#include <QQuickItem>
#include <QWaylandClientExtensionTemplate>
#include <qpa/qplatformwindow_p.h>
#include <KWaylandExtras>
#include <KWindowSystem>
#include "qwayland-kde-lockscreen-overlay-v1.h"
class WaylandAboveLockscreen : public QWaylandClientExtensionTemplate<WaylandAboveLockscreen>, public QtWayland::kde_lockscreen_overlay_v1
{
public:
WaylandAboveLockscreen()
: QWaylandClientExtensionTemplate<WaylandAboveLockscreen>(1)
{
initialize();
}
};
RaiseLockscreen::RaiseLockscreen(QObject *parent)
: QObject{parent}
, m_implementation(std::make_unique<WaylandAboveLockscreen>())
{
QObject::connect(KWaylandExtras::self(), &KWaylandExtras::xdgActivationTokenArrived, this, [this](int serial, const QString &token) {
if (!m_window || serial != m_serial) {
return;
}
qCDebug(LOGGING_CATEGORY) << "XDG activation token arrived, activating window:" << m_window;
// Activate window over lockscreen once we have activation token
KWindowSystem::setCurrentXdgActivationToken(token);
KWindowSystem::activateWindow(m_window);
});
}
RaiseLockscreen::~RaiseLockscreen()
{
}
QWindow *RaiseLockscreen::window() const
{
return m_window;
}
void RaiseLockscreen::setWindow(QWindow *window)
{
m_window = window;
Q_EMIT windowChanged();
}
bool RaiseLockscreen::initialized() const
{
return m_initialized;
}
void RaiseLockscreen::setInitialized(bool initialized)
{
m_initialized = initialized;
Q_EMIT initializedChanged();
}
void RaiseLockscreen::initializeOverlay(QQuickWindow *window)
{
if (!window || window == m_window) {
return;
}
setWindow(window);
setOverlay();
// also re-set the overlay when the compositor gets restarted
connect(m_implementation.get(), &WaylandAboveLockscreen::activeChanged, this, &RaiseLockscreen::setOverlay);
}
void RaiseLockscreen::setOverlay()
{
if (!m_implementation->isActive()) {
setInitialized(false);
qCWarning(LOGGING_CATEGORY) << "Unable to set overlay: wayland protocol is not active";
return;
}
auto waylandWindow = m_window->nativeInterface<QNativeInterface::Private::QWaylandWindow>();
if (!waylandWindow) {
// Add event filter to listen for when wayland window appears, and try again
m_window->installEventFilter(this);
setInitialized(false);
qCWarning(LOGGING_CATEGORY) << "Unable to set overlay: unable to get wayland window";
return;
}
// Listen to when new surface roles are created, and re-allow again.
// This can happen when a window is hidden, and then shown again (same surface, different surface role)
connect(waylandWindow, &QNativeInterface::Private::QWaylandWindow::surfaceRoleCreated, this, [this, waylandWindow]() {
m_implementation->allow(waylandWindow->surface());
setInitialized(true);
qCDebug(LOGGING_CATEGORY) << "Initialized overlay successfully";
});
if (waylandWindow->surface()) {
m_implementation->allow(waylandWindow->surface());
setInitialized(true);
qCDebug(LOGGING_CATEGORY) << "Initialized overlay successfully";
}
}
bool RaiseLockscreen::eventFilter(QObject *watched, QEvent *event)
{
auto window = qobject_cast<QQuickWindow *>(watched);
if (window && event->type() == QEvent::PlatformSurface) {
auto surfaceEvent = static_cast<QPlatformSurfaceEvent *>(event);
if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) {
m_window->removeEventFilter(this);
setOverlay();
}
}
return false;
}
void RaiseLockscreen::raiseOverlay()
{
if (!m_window) {
qCWarning(LOGGING_CATEGORY) << "Unable to raise overlay: no window set";
return;
}
if (!m_initialized) {
qCWarning(LOGGING_CATEGORY) << "Unable to raise overlay: window is not initialized for lockscreen overlaying, trying anyway...";
}
m_serial = KWaylandExtras::lastInputSerial(m_window);
qCDebug(LOGGING_CATEGORY) << "Attempting to raise overlay: " << m_window << m_initialized;
KWaylandExtras::requestXdgActivationToken(m_window, m_serial, QStringLiteral("org.kde.plasmashell.desktop"));
}

View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QObject>
#include <QQuickWindow>
#include <QWindow>
#include "qqml.h"
class WaylandAboveLockscreen;
/**
* A plugin to implement raising windows over the lockscreen.
*/
class RaiseLockscreen : public QObject
{
Q_OBJECT
Q_PROPERTY(QWindow *window READ window WRITE setWindow NOTIFY windowChanged)
Q_PROPERTY(bool initialized READ initialized NOTIFY initializedChanged)
QML_ELEMENT
public:
RaiseLockscreen(QObject *parent = nullptr);
~RaiseLockscreen() override;
QWindow *window() const;
void setWindow(QWindow *window);
bool initialized() const;
Q_INVOKABLE void initializeOverlay(QQuickWindow *window);
Q_INVOKABLE void raiseOverlay();
Q_SIGNALS:
void windowChanged();
void initializedChanged();
private:
void setInitialized(bool initialized);
void setOverlay();
bool eventFilter(QObject *watched, QEvent *event) override;
bool m_initialized = false;
QWindow *m_window = nullptr;
int m_serial = 0;
std::unique_ptr<WaylandAboveLockscreen> m_implementation;
};

View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QLoggingCategory>
static const QLoggingCategory &LOGGING_CATEGORY()
{
static const QLoggingCategory category("raiselockscreenplugin");
return category;
}

View file

@ -1,11 +1,15 @@
# SPDX-FileCopyrightText: 2017 Marco Martin <mart@kde.org>
# SPDX-FileCopyrightText: 2021 Aleix Pol <apol@kde.org>
# SPDX-FileCopyrightText: 2020-2021 Nicolas Fella <nicolas.fella@kde.org>
# SPDX-FileCopyrightText: 2022 Alexey Andreyev <aa13q@ya.ru>
# SPDX-License-Identifier: GPL-2.0-or-later
plasma_add_applet(org.kde.plasma.mobile.panel
QML_SOURCES
qml/main.qml
qml/LockscreenOverlay.qml
qml/StatusBarWrapper.qml
qml/StatusPanel.qml
CPP_SOURCES
phonepanel.cpp
)
@ -16,4 +20,5 @@ target_link_libraries(org.kde.plasma.mobile.panel PRIVATE
Plasma::Plasma
KF6::I18n
KF6::Service
KF6::WindowSystem
)

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Window
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.raiselockscreenplugin as RaiseLockscreenPlugin
import org.kde.layershell 1.0 as LayerShell
// Raise panel window over the lockscreen when it is shown
QtObject {
id: root
required property var window
onWindowChanged: {
// Window.window may start out null, we need to wait for it to exist
if (root.window && !raiseLockscreen.initialized) {
initializeLockscreenOverlay();
}
}
function raiseOverlay() {
if (MobileShellState.LockscreenDBusClient.lockscreenActive) {
console.log('Raising top panel over the lockscreen');
raiseLockscreen.raiseOverlay();
}
}
function initializeLockscreenOverlay() {
if (!root.window) {
return;
}
raiseLockscreen.initializeOverlay(root.window);
// Raise panel if lockscreen is already active
raiseOverlay();
}
// Raise panel over the lockscreen when it is enabled
readonly property var raiseLockscreen: RaiseLockscreenPlugin.RaiseLockscreen {
id: raiseLockscreen
Component.onCompleted: root.initializeLockscreenOverlay()
}
readonly property Connections lockscreenConnections: Connections {
target: MobileShellState.LockscreenDBusClient
function onLockscreenLocked() {
root.raiseOverlay();
}
}
}

View file

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2021-2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
Item {
id: root
// The full intended height of the status panel.
required property real statusPanelHeight
// Whether the background should be transparent, with content using a complementary theme on top.
required property bool transparentBackground
// Request the panel itself to reapply settings (ex. for updating touch area).
signal updatePanelPropertiesRequested()
Kirigami.Theme.colorSet: transparentBackground ? Kirigami.Theme.Complementary : Kirigami.Theme.Header
Kirigami.Theme.inherit: false
property real offset: 0
MobileShell.StatusBar {
id: topPanel
anchors.fill: parent
showSecondRow: false
showTime: !MobileShellState.LockscreenDBusClient.lockscreenActive // Don't show time on the lockscreen, since we already have a massive clock
showDropShadow: root.transparentBackground
backgroundColor: {
if (root.transparentBackground) {
return "transparent";
}
if (state == "default") {
return Kirigami.Theme.backgroundColor;
} else {
return Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95);
}
}
transform: [
Translate {
y: root.offset
}
]
}
states: [
State {
// Default panel state, which is shown in the UI.
name: "default"
PropertyChanges {
target: root; offset: 0
}
},
State {
// Panel is forced to be visible and overlaid over content (will be automatically hidden after a duration).
name: "visible"
PropertyChanges {
target: root; offset: 0
}
},
State {
// Panel is hidden and requires a gesture to be shown.
name: "hidden"
PropertyChanges {
target: root; offset: -root.statusPanelHeight
}
}
]
transitions: Transition {
SequentialAnimation {
ParallelAnimation {
PropertyAnimation {
properties: "offset"
easing.type: root.state === "hidden" ? Easing.InExpo : Easing.OutExpo
duration: Kirigami.Units.longDuration
}
}
ScriptAction {
script: {
root.updatePanelPropertiesRequested();
}
}
}
}
}

View file

@ -0,0 +1,185 @@
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Window
import QtQml.Models
import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.taskmanager as TaskManager
import org.kde.notificationmanager as NotificationManager
import org.kde.layershell 1.0 as LayerShell
Item {
id: root
// The base containment item
property ContainmentItem containmentItem
//BEGIN API implementation
Connections {
target: MobileShellState.ShellDBusClient
function onOpenActionDrawerRequested() {
drawer.actionDrawer.open();
}
function onCloseActionDrawerRequested() {
drawer.actionDrawer.close();
}
}
Binding {
target: MobileShellState.ShellDBusClient
property: "isActionDrawerOpen"
value: drawer.visible
}
//END API implementation
// Startup feedback fill animation
MobileShell.StartupFeedbackPanelFill {
id: startupFeedbackColorAnimation
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
fullHeight: containmentItem.height
screen: Plasmoid.screen
maximizedTracker: containmentItem.windowMaximizedTracker
visible: !containmentItem.fullscreen
}
// Status bar component
StatusBarWrapper {
id: statusBarWrapper
anchors.fill: parent
statusPanelHeight: MobileShell.Constants.topPanelHeight
transparentBackground: {
// If we are over the lockscreen, always have a transparent background.
if (MobileShellState.LockscreenDBusClient.lockscreenActive) {
return true;
}
return !containmentItem.showingApp && !containmentItem.fullscreen;
}
state: {
// If we are on the lockscreen, always show the status panel.
if (MobileShellState.LockscreenDBusClient.lockscreenActive) {
return "default";
}
return MobileShellState.ShellDBusClient.panelState;
}
onStateChanged: {
if (state != "hidden") {
containmentItem.setWindowProperties();
hiddenTimer.restart();
}
}
onUpdatePanelPropertiesRequested: containmentItem.setWindowProperties()
// Hide status bar panel if it is visible for 3 seconds (in forced "visible" mode).
Timer {
id: hiddenTimer
running: false
interval: 3000
onTriggered: {
if (statusBarWrapper.state == "visible") {
MobileShellState.ShellDBusClient.panelState = "hidden";
}
}
}
}
// Swiping area for swipe-down drawer
MobileShell.ActionDrawerOpenSurface {
id: swipeArea
actionDrawer: drawer.actionDrawer
anchors.fill: parent
readonly property alias drawerVisible: drawer.visible
readonly property alias offset: drawer.actionDrawer.offset
// if in a fullscreen app, the panels are visible, and the action drawer is opened
// set the panels to a hidden state
onDrawerVisibleChanged: {
if (statusBarWrapper.state == "visible") {
MobileShellState.ShellDBusClient.panelState = "hidden";
}
}
}
// Swipe-down drawer component
MobileShell.ActionDrawerWindow {
id: drawer
onVisibleChanged: {
if (visible && MobileShellState.LockscreenDBusClient.lockscreenActive) {
// This works as long the wayland surface is the same (no window.close(), just window.visible = false)
lockScreenOverlay.raiseOverlay();
}
}
LockscreenOverlay {
id: lockScreenOverlay
window: drawer
}
actionDrawer.restrictedPermissions: MobileShellState.LockscreenDBusClient.lockscreenActive
actionDrawer.notificationSettings: NotificationManager.Settings {}
actionDrawer.notificationModel: NotificationManager.Notifications {
showExpired: true
showDismissed: true
showJobs: drawer.actionDrawer.notificationSettings.jobsInNotifications
sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
groupMode: NotificationManager.Notifications.GroupApplicationsFlat
groupLimit: 2
expandUnread: true
blacklistedDesktopEntries: drawer.actionDrawer.notificationSettings.historyBlacklistedApplications
blacklistedNotifyRcNames: drawer.actionDrawer.notificationSettings.historyBlacklistedServices
urgencies: {
var urgencies = NotificationManager.Notifications.CriticalUrgency
| NotificationManager.Notifications.NormalUrgency;
if (drawer.actionDrawer.notificationSettings.lowPriorityHistory) {
urgencies |= NotificationManager.Notifications.LowUrgency;
}
return urgencies;
}
}
Connections {
target: drawer.actionDrawer
function onPermissionsRequested() {
MobileShellState.ShellDBusClient.openLockScreenKeypad();
}
}
Connections {
target: MobileShellState.LockscreenDBusClient
function onLockscreenUnlocked() {
// Run pending actions after the lockscreen gets unlocked
drawer.actionDrawer.runPendingNotificationAction();
}
}
}
}

View file

@ -11,7 +11,6 @@ import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
@ -24,249 +23,97 @@ import org.kde.layershell 1.0 as LayerShell
ContainmentItem {
id: root
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
Plasmoid.status: PlasmaCore.Types.PassiveStatus // ensure that the panel never takes focus away from the running app
// filled in by the shell (Panel.qml) with the plasma-workspace PanelView
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
Plasmoid.status: PlasmaCore.Types.PassiveStatus // Ensure that the panel never takes focus away from the running app
// Filled in by the shell (Panel.qml) with the plasma-workspace PanelView
property var panel: null
onPanelChanged: setWindowProperties()
MobileShell.HapticsEffect {
id: haptics
// Whether the startup feedback is showing
readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback
// Whether an app is maximized and showing (does not include startup feedback)
readonly property bool showingApp: windowMaximizedTracker.showingWindow && !showingStartupFeedback
// Whether the currently showing app is in "fullscreen"
readonly property bool fullscreen: {
if (windowMaximizedTracker.isCurrentWindowFullscreen) {
return true;
}
// The "autoHidePanelsEnabled" settings option treats every app as a fullscreen window
return (ShellSettings.Settings.autoHidePanelsEnabled && showingApp);
}
onFullscreenChanged: {
MobileShellState.ShellDBusClient.panelState = fullscreen ? "hidden" : "default";
}
readonly property real statusPanelHeight: MobileShell.Constants.topPanelHeight
readonly property real intendedWindowThickness: statusPanelHeight
property WindowPlugin.WindowMaximizedTracker windowMaximizedTracker: WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
screenGeometry: Plasmoid.containment.screenGeometry
// use a timer so we don't have to maximize for every single pixel
// - improves performance if the shell is run in a window, and can be resized
Timer {
id: maximizeTimer
running: false
interval: 100
onTriggered: root.panel.maximize()
onShowingWindowChanged: {
// Hide panel when we open the task switcher and an app is "fullscreen"
if (windowMaximizedTracker.showingWindow
&& MobileShellState.ShellDBusClient.isTaskSwitcherVisible
&& (ShellSettings.Settings.autoHidePanelsEnabled || fullscreen)) {
MobileShellState.ShellDBusClient.panelState = "hidden";
}
}
}
function setWindowProperties() {
if (root.panel) {
root.panel.floating = false;
root.panel.maximize(); // maximize first, then we can apply offsets (otherwise they are overridden)
root.panel.thickness = statusPanelHeight;
root.panel.thickness = MobileShell.Constants.topPanelHeight;
root.panel.visibilityMode = ShellSettings.Settings.autoHidePanelsEnabled ? 3 : 0;
MobileShell.ShellUtil.setWindowLayer(root.panel, LayerShell.Window.LayerOverlay)
root.updateTouchArea();
}
}
// update the touch area when hidden to minimize the space the panel takes for touch input
// Update the touch area when hidden to minimize the space the panel takes for touch input
function updateTouchArea() {
const hiddenTouchAreaThickness = Kirigami.Units.gridUnit;
if (statusPanel.state == "hidden") {
if (MobileShellState.ShellDBusClient.panelState == "hidden") {
MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, root.panel.width, hiddenTouchAreaThickness));
} else {
MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, 0, 0));
}
}
Connections {
target: ShellSettings.Settings
function onAutoHidePanelsEnabled() {
root.setWindowProperties();
}
// Overlay the panel over the lockscreen when brought up
LockscreenOverlay {
window: root.Window.window
}
// only opaque if there are no maximized windows on this screen
readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback && startupFeedbackColorAnimation.visible && windowMaximizedTracker.windowCount === 1
readonly property bool showingApp: windowMaximizedTracker.showingWindow && !showingStartupFeedback
readonly property color backgroundColor: topPanel.colorScopeColor
readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen
readonly property bool fullscreen: isCurrentWindowFullscreen || (ShellSettings.Settings.autoHidePanelsEnabled && showingApp)
onFullscreenChanged: {
MobileShellState.ShellDBusClient.panelState = fullscreen ? "hidden" : "default";
}
WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
screenGeometry: Plasmoid.containment.screenGeometry
onShowingWindowChanged: {
if (windowMaximizedTracker.showingWindow && MobileShellState.ShellDBusClient.isTaskSwitcherVisible && (ShellSettings.Settings.autoHidePanelsEnabled || fullscreen)) {
MobileShellState.ShellDBusClient.panelState = "hidden";
statusPanel.offset = -root.statusPanelHeight;
}
}
}
// enforce thickness
// Enforce thickness of panel
Binding {
target: panel // assumed to be plasma-workspace "PanelView" component
property: "thickness"
value: MobileShell.Constants.topPanelHeight
}
//BEGIN API implementation
Connections {
target: MobileShellState.ShellDBusClient
target: ShellSettings.Settings
function onOpenActionDrawerRequested() {
drawer.actionDrawer.open();
}
function onCloseActionDrawerRequested() {
drawer.actionDrawer.close();
function onAutoHidePanelsEnabledChanged() {
root.setWindowProperties();
}
}
Binding {
target: MobileShellState.ShellDBusClient
property: "isActionDrawerOpen"
value: drawer.visible
}
//END API implementation
Component.onCompleted: {
root.setWindowProperties();
}
MobileShell.StartupFeedbackPanelFill {
id: startupFeedbackColorAnimation
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
fullHeight: root.height
screen: Plasmoid.screen
maximizedTracker: windowMaximizedTracker
visible: !root.fullscreen
}
Rectangle {
// Visual panel component
StatusPanel {
id: statusPanel
anchors.fill: parent
Kirigami.Theme.colorSet: root.showingApp ? Kirigami.Theme.Header : Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
color: statusPanel.state == "default" && (root.showingApp || root.fullscreen) ? Kirigami.Theme.backgroundColor : "transparent"
property real offset: 0
// top panel component
MobileShell.StatusBar {
id: topPanel
anchors.fill: parent
showDropShadow: !root.showingApp
backgroundColor: statusPanel.state != "default" && root.showingApp ? Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95) : "transparent"
transform: [
Translate {
y: statusPanel.offset
}
]
}
state: MobileShellState.ShellDBusClient.panelState
onStateChanged: {
if (statusPanel.state != "hidden") {
root.setWindowProperties();
hiddenTimer.restart();
}
}
Timer {
id: hiddenTimer
running: false
interval: 3000
onTriggered: {
if (statusPanel.state == "visible") {
MobileShellState.ShellDBusClient.panelState = "hidden";
}
}
}
states: [
State {
name: "default"
PropertyChanges {
target: statusPanel; offset: 0
}
},
State {
name: "visible"
PropertyChanges {
target: statusPanel; offset: 0
}
},
State {
name: "hidden"
PropertyChanges {
target: statusPanel; offset: -root.statusPanelHeight
}
}
]
transitions: Transition {
SequentialAnimation {
ParallelAnimation {
PropertyAnimation {
properties: "offset"; easing.type: statusPanel.state === "hidden" ? Easing.InExpo : Easing.OutExpo; duration: Kirigami.Units.longDuration
}
}
ScriptAction {
script: {
root.setWindowProperties();
}
}
}
}
}
// swiping area for swipe-down drawer
MobileShell.ActionDrawerOpenSurface {
id: swipeArea
actionDrawer: drawer.actionDrawer
anchors.fill: parent
readonly property alias drawerVisible: drawer.visible
readonly property alias offset: drawer.actionDrawer.offset
// if in a fullscreen app, the panels are visible, and the action drawer is opened
// set the panels to a hidden state
onDrawerVisibleChanged: {
if (statusPanel.state == "visible") {
MobileShellState.ShellDBusClient.panelState = "hidden";
}
}
}
// swipe-down drawer component
MobileShell.ActionDrawerWindow {
id: drawer
actionDrawer.notificationSettings: NotificationManager.Settings {}
actionDrawer.notificationModel: NotificationManager.Notifications {
showExpired: true
showDismissed: true
showJobs: drawer.actionDrawer.notificationSettings.jobsInNotifications
sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
groupMode: NotificationManager.Notifications.GroupApplicationsFlat
groupLimit: 2
expandUnread: true
blacklistedDesktopEntries: drawer.actionDrawer.notificationSettings.historyBlacklistedApplications
blacklistedNotifyRcNames: drawer.actionDrawer.notificationSettings.historyBlacklistedServices
urgencies: {
var urgencies = NotificationManager.Notifications.CriticalUrgency
| NotificationManager.Notifications.NormalUrgency;
if (drawer.actionDrawer.notificationSettings.lowPriorityHistory) {
urgencies |= NotificationManager.Notifications.LowUrgency;
}
return urgencies;
}
}
containmentItem: root
}
}

View file

@ -128,7 +128,7 @@ ContainmentItem {
Connections {
target: ShellSettings.Settings
function onAutoHidePanelsEnabled() {
function onAutoHidePanelsEnabledChanged() {
root.setWindowProperties();
}
}

View file

@ -1,145 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021-2024 Devin Lin <espidev@gmail.com>
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.12
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.components 3.0 as PC3
import org.kde.notificationmanager as NotificationManager
Item {
id: root
required property real openFactor
required property real statusBarHeight
property var notificationsModel: []
readonly property bool actionDrawerVisible: swipeArea.actionDrawer.intendedToBeVisible
signal passwordRequested()
// The status bar and quicksettings take a while to load, don't pause initial lockscreen loading for it
Timer {
id: loadTimer
running: true
repeat: false
onTriggered: {
statusBarLoader.active = true
actionDrawerLoader.active = true
}
}
// Add loading indicator when status bar has not loaded yet
PC3.BusyIndicator {
id: statusBarLoadingIndication
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
visible: statusBarLoader.status != Loader.Ready
implicitHeight: root.statusBarHeight
implicitWidth: root.statusBarHeight
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
}
// Status bar
Loader {
id: statusBarLoader
active: false
asynchronous: true
visible: status == Loader.Ready
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: root.statusBarHeight
sourceComponent: MobileShell.StatusBar {
id: statusBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: root.statusBarHeight
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
backgroundColor: "transparent"
showSecondRow: false
showDropShadow: true
showTime: false
disableSystemTray: true // prevent SIGABRT, since loading the system tray on the lockscreen leads to bad... things
}
}
// Drag down gesture to open action drawer
MobileShell.ActionDrawerOpenSurface {
id: swipeArea
actionDrawer: actionDrawerLoader.item ? actionDrawerLoader.item.actionDrawer : null
anchors.fill: statusBarLoader
}
// Dynamically load on swipe-down to avoid having to load at start
Loader {
id: actionDrawerLoader
active: false
asynchronous: true
visible: status == Loader.Ready
anchors.fill: parent
sourceComponent: Item {
property var actionDrawer: drawer
// Action drawer component
MobileShell.ActionDrawer {
id: drawer
anchors.fill: parent
visible: offset !== 0
restrictedPermissions: true
notificationSettings: NotificationManager.Settings {}
notificationModel: root.notificationsModel
notificationModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel
property bool requestNotificationAction: false
// notification button clicked, requesting auth
onPermissionsRequested: {
requestNotificationAction = true;
drawer.close();
root.passwordRequested();
}
}
// listen to authentication events
Connections {
target: authenticator
function onSucceeded() {
// run pending action if successfully unlocked
if (drawer.requestNotificationAction) {
drawer.runPendingAction();
drawer.requestNotificationAction = false;
}
}
function onFailed() {
drawer.requestNotificationAction = false;
}
}
}
}
}

View file

@ -9,6 +9,7 @@ import QtQuick.Layouts
import org.kde.plasma.core as PlasmaCore
import org.kde.notificationmanager as Notifications
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.dpmsplugin as DPMS
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
@ -30,7 +31,7 @@ Item {
readonly property bool isWidescreen: root.height < Kirigami.Units.gridUnit * 25 && (root.height < root.width * 0.75)
property bool notificationsShown: false
property var passwordBar: flickableLoader.item ? flickableLoader.item.flickable.passwordBar : null
property var passwordBar: flickable.passwordBar
Component.onCompleted: {
forceActiveFocus();
@ -38,12 +39,18 @@ Item {
// Listen for keyboard events, and focus on input area
Keys.onPressed: (event) => {
if (flickableLoader.item) {
root.lockScreenState.isKeyboardMode = true;
flickableLoader.item.flickable.goToOpenPosition();
passwordBar.textField.forceActiveFocus();
root.lockScreenState.isKeyboardMode = true;
flickable.goToOpenPosition();
passwordBar.textField.forceActiveFocus();
passwordBar.keyPress(event.text);
passwordBar.keyPress(event.text);
}
Connections {
target: MobileShellState.ShellDBusClient
function onOpenLockScreenKeypadRequested() {
flickable.goToOpenPosition();
}
}
@ -63,7 +70,7 @@ Item {
sourceComponent: WallpaperBlur {
source: wallpaper
opacity: flickableLoader.item ? flickableLoader.item.flickable.openFactor : 0
opacity: flickable.openFactor
}
}
@ -72,8 +79,8 @@ Item {
// Ensure keypad is opened when password is updated (ex. keyboard)
function onPasswordChanged() {
if (root.lockScreenState.password !== "" && flickableLoader.item) {
flickableLoader.item.flickable.goToOpenPosition();
if (root.lockScreenState.password !== "") {
flickable.goToOpenPosition();
}
}
}
@ -84,191 +91,130 @@ Item {
onDpmsTurnedOff: (screen) => {
if (screen.name === Screen.name) {
if (flickableLoader.item) {
flickableLoader.item.flickable.goToClosePosition();
}
flickable.goToClosePosition();
lockScreenState.resetPassword();
}
}
}
// Container for lockscreen contents
Item {
id: lockscreenContainer
anchors.fill: parent
// Header bar and action drawer
HeaderComponent {
id: headerBar
z: 1
FlickContainer {
id: flickable
anchors.fill: parent
statusBarHeight: MobileShell.Constants.topPanelHeight
openFactor: flickableLoader.item ? flickableLoader.item.flickable.openFactor : 0
property alias passwordBar: keypad.passwordBar
// Speed up animation when passwordless
animationDuration: root.lockScreenState.canBeUnlocked ? 400 : 800
// Distance to swipe to fully open keypad
keypadHeight: Kirigami.Units.gridUnit * 20
Component.onCompleted: {
// Go to closed position when loaded
flickable.position = 0;
flickable.goToClosePosition();
}
// Unlock lockscreen if it's already unlocked and keypad is opened
onOpened: {
if (root.lockScreenState.canBeUnlocked) {
Qt.quit();
}
}
// Unlock lockscreen if it's already unlocked and keypad is open
Connections {
target: root.lockScreenState
function onCanBeUnlockedChanged() {
if (root.lockScreenState.canBeUnlocked && flickable.openFactor > 0.8) {
Qt.quit();
}
}
}
// Clear entered password after closing keypad
onOpenFactorChanged: {
if (flickable.openFactor < 0.1 && !flickable.movingUp) {
root.passwordBar.clear();
}
}
// scroll up icon
BottomIconIndicator {
id: scrollUpIconLoader
lockScreenState: root.lockScreenState
opacity: Math.max(0, 1 - flickable.openFactor * 2)
anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.gridUnit + flickable.position * 0.1
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
id: keypadScrim
anchors.fill: parent
visible: opacity > 0
opacity: flickable.openFactor
color: Qt.rgba(0, 0, 0, 0.5)
}
MouseArea {
// Disable "double tap to lock" to avoid accidental locking
// when the keypad is open, and the user is typing their password.
enabled: flickable.openFactor < 0.1
anchors.fill: parent
onDoubleClicked: (mouse) => {
if (ShellSettings.KWinSettings.doubleTapWakeup) {
deviceLock.triggerLock();
}
}
MobileShell.DeviceLock {
id: deviceLock
}
}
Keypad {
id: keypad
visible: !root.lockScreenState.canBeUnlocked // don't show for passwordless login
anchors.fill: parent
openProgress: flickable.openFactor
lockScreenState: root.lockScreenState
// only show in last 50% of anim
opacity: (flickable.openFactor - 0.5) * 2
transform: Translate { y: (flickable.keypadHeight - flickable.position) * 0.1 }
}
}
LockScreenContent {
id: lockScreenContent
isVertical: !root.isWidescreen
opacity: Math.max(0, 1 - flickable.openFactor * 2)
transform: [
Scale {
origin.x: lockScreenContent.width / 2
origin.y: lockScreenContent.height / 2
yScale: 1 - (flickable.openFactor * 2) * 0.1
xScale: 1 - (flickable.openFactor * 2) * 0.1
}
]
lockScreenState: root.lockScreenState
notificationsModel: root.notifModel
onPasswordRequested: root.askPassword()
}
onNotificationsShownChanged: root.notificationsShown = notificationsShown
onPasswordRequested: flickable.goToOpenPosition()
// Add loading indicator when status bar has not loaded yet
PC3.BusyIndicator {
id: flickableLoadingBusyIndicator
anchors.centerIn: parent
visible: flickableLoader.status != Loader.Ready
scrollLock: flickable.openFactor > 0.2
z: scrollLock ? -1 : 0
implicitHeight: Kirigami.Units.iconSizes.huge
implicitWidth: Kirigami.Units.iconSizes.huge
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
}
// Load flickable async
Loader {
id: flickableLoader
active: false
asynchronous: true
opacity: status == Loader.Ready ? 1 : 0
visible: opacity > 0
anchors.fill: parent
Behavior on opacity {
NumberAnimation {}
}
// This take a while to load, don't pause initial lockscreen and wallpaper loading for it
Timer {
id: loadTimer
running: true
repeat: false
onTriggered: {
flickableLoader.active = true
}
}
// Container for lockscreen contents
sourceComponent: Item {
id: item
property alias flickable: flickable
FlickContainer {
id: flickable
anchors.fill: parent
property alias passwordBar: keypad.passwordBar
// Speed up animation when passwordless
animationDuration: root.lockScreenState.canBeUnlocked ? 400 : 800
// Distance to swipe to fully open keypad
keypadHeight: Kirigami.Units.gridUnit * 20
Component.onCompleted: {
// Go to closed position when loaded
flickable.position = 0;
flickable.goToClosePosition();
}
// Unlock lockscreen if it's already unlocked and keypad is opened
onOpened: {
if (root.lockScreenState.canBeUnlocked) {
Qt.quit();
}
}
// Unlock lockscreen if it's already unlocked and keypad is open
Connections {
target: root.lockScreenState
function onCanBeUnlockedChanged() {
if (root.lockScreenState.canBeUnlocked && flickable.openFactor > 0.8) {
Qt.quit();
}
}
}
// Clear entered password after closing keypad
onOpenFactorChanged: {
if (flickable.openFactor < 0.1 && !flickable.movingUp) {
root.passwordBar.clear();
}
}
// scroll up icon
BottomIconIndicator {
id: scrollUpIconLoader
lockScreenState: root.lockScreenState
opacity: Math.max(0, 1 - flickable.openFactor * 2)
anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.gridUnit + flickable.position * 0.1
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
id: keypadScrim
anchors.fill: parent
visible: opacity > 0
opacity: flickable.openFactor
color: Qt.rgba(0, 0, 0, 0.5)
}
MouseArea {
// Disable "double tap to lock" to avoid accidental locking
// when the keypad is open, and the user is typing their password.
enabled: flickable.openFactor < 0.1
anchors.fill: parent
onDoubleClicked: (mouse) => {
if (ShellSettings.KWinSettings.doubleTapWakeup) {
deviceLock.triggerLock();
}
}
MobileShell.DeviceLock {
id: deviceLock
}
}
Keypad {
id: keypad
visible: !root.lockScreenState.canBeUnlocked // don't show for passwordless login
anchors.fill: parent
openProgress: flickable.openFactor
lockScreenState: root.lockScreenState
// only show in last 50% of anim
opacity: (flickable.openFactor - 0.5) * 2
transform: Translate { y: (flickable.keypadHeight - flickable.position) * 0.1 }
}
}
LockScreenContent {
id: lockScreenContent
isVertical: !root.isWidescreen
opacity: Math.max(0, 1 - flickable.openFactor * 2)
transform: [
Scale {
origin.x: lockScreenContent.width / 2
origin.y: lockScreenContent.height / 2
yScale: 1 - (flickable.openFactor * 2) * 0.1
xScale: 1 - (flickable.openFactor * 2) * 0.1
}
]
lockScreenState: root.lockScreenState
notificationsModel: root.notifModel
onNotificationsShownChanged: root.notificationsShown = notificationsShown
onPasswordRequested: flickable.goToOpenPosition()
scrollLock: headerBar.actionDrawerVisible || (flickableLoader.item ? flickableLoader.item.flickable.openFactor > 0.2 : false)
z: scrollLock ? -1 : 0
anchors {
//topMargin: headerBar.statusBarHeight
top: item.top
bottom: item.bottom
left: item.left
right: item.right
}
}
}
}
}
}

View file

@ -40,7 +40,7 @@ ApplicationWindow {
anchors.left: parent.left
anchors.right: parent.right
height: Kirigami.Units.gridUnit * 1.25
height: MobileShell.Constants.topPanelHeight
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary

View file

@ -1,11 +1,13 @@
// SPDX-FileCopyrightText: 2022 Devin LIn <devin@kde.org>
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import org.kde.kirigami as Kirigami
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.notificationmanager as NotificationManager
import "../shell/contents/lockscreen" as LockScreen
@ -61,9 +63,48 @@ ApplicationWindow {
}
}
// component to test
// Component to test
LockScreen.LockScreen {
anchors.fill: parent
}
// Simulate "overlaid" status bar and quick settings panel
MobileShell.StatusBar {
id: statusBar
z: 1
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: Kirigami.Units.gridUnit * 1.25
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
backgroundColor: "transparent"
showSecondRow: false
showDropShadow: true
showTime: true
disableSystemTray: true // prevent SIGABRT, since loading the system tray leads to bad... things
}
MobileShell.ActionDrawerOpenSurface {
anchors.fill: statusBar
actionDrawer: drawer
z: 1
}
MobileShell.ActionDrawer {
id: drawer
z: 1
anchors.fill: parent
visible: offset !== 0
notificationSettings: NotificationManager.Settings {}
notificationModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel
notificationModel: NotificationManager.WatchedNotificationsModel {}
}
}