diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 364e929f..03ca888e 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -1,3 +1,3 @@ -add_subdirectory(mobilehomescreencomponents) -install(DIRECTORY mobileshell/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/mobileshell) +add_subdirectory(mobilehomescreencomponents) +add_subdirectory(mobileshell) diff --git a/components/mobilehomescreencomponents/CMakeLists.txt b/components/mobilehomescreencomponents/CMakeLists.txt index f9114281..ffbb1b2b 100644 --- a/components/mobilehomescreencomponents/CMakeLists.txt +++ b/components/mobilehomescreencomponents/CMakeLists.txt @@ -5,7 +5,6 @@ set(mobilehomescreencomponentsplugin_SRCS applicationlistmodel.cpp favoritesmodel.cpp homescreenutils.cpp - quicksettingsmodel.cpp ) add_library(mobilehomescreencomponentsplugin ${mobilehomescreencomponentsplugin_SRCS}) diff --git a/components/mobilehomescreencomponents/mobilehomescreencomponentsplugin.cpp b/components/mobilehomescreencomponents/mobilehomescreencomponentsplugin.cpp index 793e2b7b..08741d72 100644 --- a/components/mobilehomescreencomponents/mobilehomescreencomponentsplugin.cpp +++ b/components/mobilehomescreencomponents/mobilehomescreencomponentsplugin.cpp @@ -25,15 +25,11 @@ #include "applicationlistmodel.h" #include "favoritesmodel.h" #include "homescreenutils.h" -#include "quicksettingsmodel.h" void MobileHomeScreenComponentsPlugin::registerTypes(const char *uri) { Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.private.mobilehomescreencomponents")); - qmlRegisterType(uri, 0, 1, "QuickSetting"); - qmlRegisterType(uri, 0, 1, "QuickSettingsModel"); - qmlRegisterSingletonType(uri, 0, 1, "HomeScreenUtils", [](QQmlEngine *, QJSEngine *) { return new HomeScreenUtils{}; diff --git a/components/mobileshell/CMakeLists.txt b/components/mobileshell/CMakeLists.txt new file mode 100644 index 00000000..e6bb9bd0 --- /dev/null +++ b/components/mobileshell/CMakeLists.txt @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2021 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +qt_add_dbus_interfaces(DBUS_SRCS dbus/org.kde.KWin.ScreenShot2.xml + dbus/org.kde.KScreen.xml) + +set(mobileshellplugin_SRCS + mobileshellplugin.cpp + shellutil.cpp + quicksettingsmodel.cpp + notifications/notificationthumbnailer.cpp + notifications/notificationfilemenu.cpp + ${DBUS_SRCS} +) + +add_library(mobileshellplugin ${mobileshellplugin_SRCS}) + +target_link_libraries(mobileshellplugin + PUBLIC + Qt::Core + PRIVATE + Qt::DBus + Qt::Qml + Qt::Gui + Qt::Quick + KF5::ConfigWidgets # for KStandardAction + KF5::KIOGui + KF5::Plasma + KF5::I18n + KF5::Notifications + KF5::PlasmaQuick + KF5::KIOGui + KF5::KIOWidgets # for PreviewJob + KF5::WaylandClient + KF5::Service + ) + +install(TARGETS mobileshellplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/mobileshell) + +install(DIRECTORY qml/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/mobileshell) diff --git a/containments/panel/dbus/org.kde.KScreen.xml b/components/mobileshell/dbus/org.kde.KScreen.xml similarity index 100% rename from containments/panel/dbus/org.kde.KScreen.xml rename to components/mobileshell/dbus/org.kde.KScreen.xml diff --git a/containments/panel/dbus/org.kde.KWin.ScreenShot2.xml b/components/mobileshell/dbus/org.kde.KWin.ScreenShot2.xml similarity index 100% rename from containments/panel/dbus/org.kde.KWin.ScreenShot2.xml rename to components/mobileshell/dbus/org.kde.KWin.ScreenShot2.xml diff --git a/containments/panel/dbus/org.kde.KWin.Screenshot.xml b/components/mobileshell/dbus/org.kde.KWin.Screenshot.xml similarity index 100% rename from containments/panel/dbus/org.kde.KWin.Screenshot.xml rename to components/mobileshell/dbus/org.kde.KWin.Screenshot.xml diff --git a/components/mobileshell/mobileshellplugin.cpp b/components/mobileshell/mobileshellplugin.cpp new file mode 100644 index 00000000..4a1d3100 --- /dev/null +++ b/components/mobileshell/mobileshellplugin.cpp @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include + +#include "mobileshellplugin.h" +#include "notifications/notificationfilemenu.h" +#include "notifications/notificationthumbnailer.h" +#include "quicksettingsmodel.h" +#include "shellutil.h" + +void MobileShellPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.private.mobileshell")); + + qmlRegisterSingletonType(uri, 1, 0, "ShellUtil", [](QQmlEngine *, QJSEngine *) -> QObject * { + return ShellUtil::instance(); + }); + + qmlRegisterType(uri, 1, 0, "QuickSetting"); + qmlRegisterType(uri, 1, 0, "QuickSettingsModel"); + + // notifications + qmlRegisterType(uri, 1, 0, "NotificationThumbnailer"); + qmlRegisterType(uri, 1, 0, "NotificationFileMenu"); +} diff --git a/components/mobileshell/mobileshellplugin.h b/components/mobileshell/mobileshellplugin.h new file mode 100644 index 00000000..23b850d9 --- /dev/null +++ b/components/mobileshell/mobileshellplugin.h @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +#include +#include + +class MobileShellPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/components/mobileshell/notifications/notificationfilemenu.cpp b/components/mobileshell/notifications/notificationfilemenu.cpp new file mode 100644 index 00000000..6e4b0ba1 --- /dev/null +++ b/components/mobileshell/notifications/notificationfilemenu.cpp @@ -0,0 +1,231 @@ +/* + SPDX-FileCopyrightText: 2016, 2019 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "notificationfilemenu.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // for KIO::trash +#include +#include +#include +#include + +NotificationFileMenu::NotificationFileMenu(QObject *parent) + : QObject(parent) +{ +} + +NotificationFileMenu::~NotificationFileMenu() = default; + +QUrl NotificationFileMenu::url() const +{ + return m_url; +} + +void NotificationFileMenu::setUrl(const QUrl &url) +{ + if (m_url != url) { + m_url = url; + Q_EMIT urlChanged(); + } +} + +QQuickItem *NotificationFileMenu::visualParent() const +{ + return m_visualParent.data(); +} + +void NotificationFileMenu::setVisualParent(QQuickItem *visualParent) +{ + if (m_visualParent.data() == visualParent) { + return; + } + + if (m_visualParent) { + disconnect(m_visualParent.data(), nullptr, this, nullptr); + } + m_visualParent = visualParent; + if (m_visualParent) { + connect(m_visualParent.data(), &QObject::destroyed, this, &NotificationFileMenu::visualParentChanged); + } + Q_EMIT visualParentChanged(); +} + +bool NotificationFileMenu::visible() const +{ + return m_visible; +} + +void NotificationFileMenu::setVisible(bool visible) +{ + if (m_visible == visible) { + return; + } + + if (visible) { + open(0, 0); + } +} + +void NotificationFileMenu::open(int x, int y) +{ + if (!m_visualParent || !m_visualParent->window()) { + return; + } + + if (!m_url.isValid()) { + return; + } + + KFileItem fileItem(m_url); + + auto menu = new QMenu(); + menu->setAttribute(Qt::WA_DeleteOnClose, true); + connect(menu, &QMenu::triggered, this, &NotificationFileMenu::actionTriggered); + + connect(menu, &QMenu::aboutToHide, this, [this] { + m_visible = false; + Q_EMIT visibleChanged(); + }); + + if (KProtocolManager::supportsListing(m_url)) { + QAction *openContainingFolderAction = menu->addAction(QIcon::fromTheme(QStringLiteral("folder-open")), i18n("Open Containing Folder")); + connect(openContainingFolderAction, &QAction::triggered, [this] { + KIO::highlightInFileManager({m_url}); + }); + } + + auto actions = new KFileItemActions(menu); + KFileItemListProperties itemProperties(KFileItemList({fileItem})); + actions->setItemListProperties(itemProperties); + actions->setParentWidget(menu); + + actions->insertOpenWithActionsTo(nullptr, menu, QStringList()); + + // KStandardAction? But then the Ctrl+C shortcut makes no sense in this context + QAction *copyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy")); + connect(copyAction, &QAction::triggered, this, [fileItem] { + // inspired by KDirModel::mimeData() + auto data = new QMimeData(); // who cleans it up? + KUrlMimeData::setUrls({fileItem.url()}, {fileItem.mostLocalUrl()}, data); + QApplication::clipboard()->setMimeData(data); + }); + + QAction *copyPathAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy-path")), i18nc("@action:incontextmenu", "Copy Location")); + connect(copyPathAction, &QAction::triggered, this, [fileItem] { + QString path = fileItem.localPath(); + if (path.isEmpty()) { + path = fileItem.url().toDisplayString(); + } + QApplication::clipboard()->setText(path); + }); + + menu->addSeparator(); + + const bool canTrash = itemProperties.isLocal() && itemProperties.supportsMoving(); + if (canTrash) { + auto moveToTrashLambda = [this] { + const QList urls{m_url}; + + KIO::JobUiDelegate uiDelegate; + if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) { + auto *job = KIO::trash(urls); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); + KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, urls, QUrl(QStringLiteral("trash:/")), job); + } + }; + auto moveToTrashAction = KStandardAction::moveToTrash(this, moveToTrashLambda, menu); + moveToTrashAction->setShortcut({}); // Can't focus notification to press Delete + menu->addAction(moveToTrashAction); + } + + KConfigGroup cg(KSharedConfig::openConfig(), "KDE"); + const bool showDeleteCommand = cg.readEntry("ShowDeleteCommand", false); + + if (itemProperties.supportsDeleting() && (!canTrash || showDeleteCommand)) { + auto deleteLambda = [this] { + const QList urls{m_url}; + + KIO::JobUiDelegate uiDelegate; + if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::DefaultConfirmation)) { + auto *job = KIO::del(urls); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); + } + }; + auto deleteAction = KStandardAction::deleteFile(this, deleteLambda, menu); + deleteAction->setShortcut({}); + menu->addAction(deleteAction); + } + + menu->addSeparator(); + + actions->addActionsTo(menu); + + menu->addSeparator(); + + QAction *propertiesAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Properties")); + connect(propertiesAction, &QAction::triggered, [fileItem] { + KPropertiesDialog *dialog = new KPropertiesDialog(fileItem.url()); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); + }); + + // this is a workaround where Qt will fail to realize a mouse has been released + // this happens if a window which does not accept focus spawns a new window that takes focus and X grab + // whilst the mouse is depressed + // https://bugreports.qt.io/browse/QTBUG-59044 + // this causes the next click to go missing + + // by releasing manually we avoid that situation + auto ungrabMouseHack = [this]() { + if (m_visualParent && m_visualParent->window() && m_visualParent->window()->mouseGrabberItem()) { + m_visualParent->window()->mouseGrabberItem()->ungrabMouse(); + } + }; + + QTimer::singleShot(0, m_visualParent, ungrabMouseHack); + // end workaround + + QPoint pos; + if (x == -1 && y == -1) { // align "bottom left of visualParent" + menu->adjustSize(); + + pos = m_visualParent->mapToGlobal(QPointF(0, m_visualParent->height())).toPoint(); + + if (!qApp->isRightToLeft()) { + pos.rx() += m_visualParent->width(); + pos.rx() -= menu->width(); + } + } else { + pos = m_visualParent->mapToGlobal(QPointF(x, y)).toPoint(); + } + + menu->setAttribute(Qt::WA_TranslucentBackground); + menu->winId(); + menu->windowHandle()->setTransientParent(m_visualParent->window()); + menu->popup(pos); + + m_visible = true; + Q_EMIT visibleChanged(); +} diff --git a/components/mobileshell/notifications/notificationfilemenu.h b/components/mobileshell/notifications/notificationfilemenu.h new file mode 100644 index 00000000..ca4fe442 --- /dev/null +++ b/components/mobileshell/notifications/notificationfilemenu.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2016, 2019 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class QAction; + +class NotificationFileMenu : public QObject +{ + Q_OBJECT + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + Q_PROPERTY(QQuickItem *visualParent READ visualParent WRITE setVisualParent NOTIFY visualParentChanged) + Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY visibleChanged) + +public: + explicit NotificationFileMenu(QObject *parent = nullptr); + ~NotificationFileMenu() override; + + QUrl url() const; + void setUrl(const QUrl &url); + + QQuickItem *visualParent() const; + void setVisualParent(QQuickItem *visualParent); + + bool visible() const; + void setVisible(bool visible); + + Q_INVOKABLE void open(int x, int y); + +Q_SIGNALS: + void actionTriggered(QAction *action); + + void urlChanged(); + void visualParentChanged(); + void visibleChanged(); + +private: + QUrl m_url; + QPointer m_visualParent; + bool m_visible = false; +}; diff --git a/components/mobileshell/notifications/notificationthumbnailer.cpp b/components/mobileshell/notifications/notificationthumbnailer.cpp new file mode 100644 index 00000000..8d157a92 --- /dev/null +++ b/components/mobileshell/notifications/notificationthumbnailer.cpp @@ -0,0 +1,157 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "notificationthumbnailer.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +NotificationThumbnailer::NotificationThumbnailer(QObject *parent) + : QObject(parent) +{ +} + +NotificationThumbnailer::~NotificationThumbnailer() = default; + +void NotificationThumbnailer::classBegin() +{ +} + +void NotificationThumbnailer::componentComplete() +{ + m_inited = true; + generatePreview(); +} + +QUrl NotificationThumbnailer::url() const +{ + return m_url; +} + +void NotificationThumbnailer::setUrl(const QUrl &url) +{ + if (m_url != url) { + m_url = url; + Q_EMIT urlChanged(); + + generatePreview(); + } +} + +QSize NotificationThumbnailer::size() const +{ + return m_size; +} + +void NotificationThumbnailer::setSize(const QSize &size) +{ + if (m_size != size) { + m_size = size; + Q_EMIT sizeChanged(); + + generatePreview(); + } +} + +bool NotificationThumbnailer::busy() const +{ + return m_busy; +} + +bool NotificationThumbnailer::hasPreview() const +{ + return !m_pixmap.isNull(); +} + +QPixmap NotificationThumbnailer::pixmap() const +{ + return m_pixmap; +} + +QSize NotificationThumbnailer::pixmapSize() const +{ + return m_pixmap.size(); +} + +QString NotificationThumbnailer::iconName() const +{ + return m_iconName; +} + +bool NotificationThumbnailer::menuVisible() const +{ + return m_menuVisible; +} + +void NotificationThumbnailer::generatePreview() +{ + if (!m_inited) { + return; + } + + if (!m_url.isValid() || !m_url.isLocalFile() || !m_size.isValid() || m_size.isEmpty()) { + return; + } + + auto maxSize = qMax(m_size.width(), m_size.height()); + + KConfigGroup previewSettings(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), "PreviewSettings"); + const QStringList enabledPlugins = previewSettings.readEntry("Plugins", KIO::PreviewJob::defaultPlugins()); + + KIO::PreviewJob *job = KIO::filePreview(KFileItemList({KFileItem(m_url)}), QSize(maxSize, maxSize), &enabledPlugins); + job->setScaleType(KIO::PreviewJob::Scaled); + job->setIgnoreMaximumSize(true); + + connect(job, &KIO::PreviewJob::gotPreview, this, [this](const KFileItem &item, const QPixmap &preview) { + Q_UNUSED(item); + m_pixmap = preview; + Q_EMIT pixmapChanged(); + + if (!m_iconName.isEmpty()) { + m_iconName.clear(); + Q_EMIT iconNameChanged(); + } + }); + + connect(job, &KIO::PreviewJob::failed, this, [this](const KFileItem &item) { + m_pixmap = QPixmap(); + Q_EMIT pixmapChanged(); + + const QString &iconName = item.determineMimeType().iconName(); + if (m_iconName != iconName) { + m_iconName = iconName; + Q_EMIT iconNameChanged(); + } + }); + + connect(job, &KJob::result, this, [this] { + m_busy = false; + Q_EMIT busyChanged(); + }); + + m_busy = true; + Q_EMIT busyChanged(); + + job->start(); +} diff --git a/components/mobileshell/notifications/notificationthumbnailer.h b/components/mobileshell/notifications/notificationthumbnailer.h new file mode 100644 index 00000000..b076a61b --- /dev/null +++ b/components/mobileshell/notifications/notificationthumbnailer.h @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +class NotificationThumbnailer : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + Q_PROPERTY(QSize size READ size WRITE setSize NOTIFY sizeChanged) + + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + Q_PROPERTY(bool hasPreview READ hasPreview NOTIFY pixmapChanged) + Q_PROPERTY(QPixmap pixmap READ pixmap NOTIFY pixmapChanged) + Q_PROPERTY(QSize pixmapSize READ pixmapSize NOTIFY pixmapChanged) + + Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged) + + Q_PROPERTY(bool menuVisible READ menuVisible NOTIFY menuVisibleChanged) + +public: + explicit NotificationThumbnailer(QObject *parent = nullptr); + ~NotificationThumbnailer() override; + + QUrl url() const; + void setUrl(const QUrl &url); + + QSize size() const; + void setSize(const QSize &size); + + bool busy() const; + bool hasPreview() const; + QPixmap pixmap() const; + QSize pixmapSize() const; + + QString iconName() const; + + bool menuVisible() const; + + void classBegin() override; + void componentComplete() override; + +Q_SIGNALS: + void menuVisibleChanged(); + + void urlChanged(); + void sizeChanged(); + void busyChanged(); + void pixmapChanged(); + void iconNameChanged(); + +private: + void generatePreview(); + + bool m_inited = false; + + bool m_menuVisible = false; + + QUrl m_url; + QSize m_size; + + bool m_busy = false; + + QPixmap m_pixmap; + + QString m_iconName; +}; diff --git a/components/mobileshell/HomeScreenControls.qml b/components/mobileshell/qml/HomeScreenControls.qml similarity index 100% rename from components/mobileshell/HomeScreenControls.qml rename to components/mobileshell/qml/HomeScreenControls.qml diff --git a/components/mobileshell/TopPanelControls.qml b/components/mobileshell/qml/TopPanelControls.qml similarity index 100% rename from components/mobileshell/TopPanelControls.qml rename to components/mobileshell/qml/TopPanelControls.qml diff --git a/components/mobileshell/qml/actiondrawer/ActionDrawer.qml b/components/mobileshell/qml/actiondrawer/ActionDrawer.qml new file mode 100644 index 00000000..a5b4e73f --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/ActionDrawer.qml @@ -0,0 +1,256 @@ +/* + * SPDX-FileCopyrightText: 2014 Marco Martin + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.nanoshell 2.0 as NanoShell + +import "../components" as Components + +/** + * Swipe top left - minimized quick settings, fully shown notifications list + * Swipe top right - full quick settings, minimized notifications list + * Swiping up and down on notifications list toggle minimized/maximized + * Swiping up and down on panel hides and shows the panel + */ + +NanoShell.FullScreenOverlay { + id: window + + /** + * The amount of pixels moved by touch/mouse in the process of opening/closing the panel. + */ + property real offset: 0 + + /** + * Whether the panel is being dragged. + */ + property bool dragging: false + + /** + * Whether the panel is open after touch/mouse release from the first opening swipe. + */ + property bool opened: false + + /** + * Direction the panel is currently moving in. + */ + property int direction: Components.Direction.None + + property int mode: (height > width && width <= largePortraitThreshold) ? ActionDrawer.Portrait : ActionDrawer.Landscape + + /** + * At some point, even if the screen is technically portrait, if we have a ton of width it'd be best to just show the landscape mode. + */ + readonly property real largePortraitThreshold: PlasmaCore.Units.gridUnit * 35 + + enum Mode { + Portrait = 0, + Landscape + } + + width: Screen.width + height: Screen.height + + color: "transparent" + + onOpenedChanged: { + if (opened) flickable.focus = true; + } + onActiveChanged: { + if (!active) { + close(); + } + } + + property real oldOffset + onOffsetChanged: { + if (offset < 0) { + offset = 0; + } + window.direction = (oldOffset === offset) + ? Components.Direction.None + : (offset > oldOffset ? Components.Direction.Down : Components.Direction.Up); + + oldOffset = offset; + + // close panel immediately after panel is not shown, and the flickable is not being dragged + if (opened && window.offset <= 0 && !flickable.dragging && !closeAnim.running && !openAnim.running) { + window.updateState(); + focus = false; + } + } + + function cancelAnimations() { + closeAnim.stop(); + openAnim.stop(); + } + function open() { + cancelAnimations(); + openAnim.restart(); + } + function closeImmediately() { + cancelAnimations(); + offset = 0; + closeAnim.finished(); + } + function close() { + cancelAnimations(); + closeAnim.restart(); + } + function expand() { + cancelAnimations(); + expandAnim.restart(); + } + function updateState() { + cancelAnimations(); + let openThreshold = PlasmaCore.Units.gridUnit; + + if (window.offset <= 0) { + // close immediately, so that we don't have to wait PlasmaCore.Units.longDuration + window.visible = false; + close(); + } else if (window.direction === Components.Direction.None || !window.opened) { + if (window.offset < openThreshold) { + close(); + } else { + open(); + } + } else if (window.offset > contentContainerLoader.maximizedQuickSettingsOffset) { + expand(); + } else if (window.offset > contentContainerLoader.minimizedQuickSettingsOffset) { + if (window.direction === Components.Direction.Down) { + expand(); + } else { + open(); + } + } else if (window.direction === Components.Direction.Down) { + open(); + } else { + close(); + } + } + Timer { + id: updateStateTimer + interval: 0 + onTriggered: updateState() + } + + PropertyAnimation on offset { + id: closeAnim + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + to: 0 + onFinished: { + window.visible = false; + window.opened = false; + } + } + PropertyAnimation on offset { + id: openAnim + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + to: contentContainerLoader.minimizedQuickSettingsOffset + onFinished: window.opened = true + } + PropertyAnimation on offset { + id: expandAnim + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + to: contentContainerLoader.maximizedQuickSettingsOffset + onFinished: window.opened = true; + } + + Flickable { + id: flickable + anchors.fill: parent + + contentWidth: window.width + contentHeight: window.height + 999999 + contentY: contentHeight / 2 + + // if the recent window.offset change was due to this flickable + property bool offsetChangedDueToContentY: false + Connections { + target: window + function onOffsetChanged() { + if (!flickable.offsetChangedDueToContentY) { + // ensure the flickable's contentY is not moving when other sources change window.offset + flickable.cancelFlick(); + } + flickable.offsetChangedDueToContentY = false; + } + } + + property real oldContentY + onContentYChanged: { + offsetChangedDueToContentY = true; + window.offset += oldContentY - contentY; + oldContentY = contentY; + } + + onMovementStarted: { + window.cancelAnimations(); + window.dragging = true; + } + onFlickStarted: window.dragging = true; + onMovementEnded: { + window.dragging = false; + window.updateState(); + } + onFlickEnded: { + window.dragging = true; + window.updateState(); + } + + onDraggingChanged: { + if (!dragging) { + window.dragging = false; + flickable.cancelFlick(); + window.updateState(); + } + } + + // the flickable is only used to measure drag changes, we implement our own UI component movements + // the window element is not affected by contentY changes (it's effectively anchored to the flickable) + Loader { + id: contentContainerLoader + + property real minimizedQuickSettingsOffset: item ? item.minimizedQuickSettingsOffset : 0 + property real maximizedQuickSettingsOffset: item ? item.maximizedQuickSettingsOffset : 0 + + y: flickable.contentY + width: window.width + height: window.height + + sourceComponent: window.mode == ActionDrawer.Portrait ? portraitContentContainer : landscapeContentContainer + } + + Component { + id: portraitContentContainer + PortraitContentContainer { + actionDrawer: window + width: window.width + height: window.height + } + } + + Component { + id: landscapeContentContainer + LandscapeContentContainer { + actionDrawer: window + width: window.width + height: window.height + } + } + } +} diff --git a/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml b/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml new file mode 100644 index 00000000..479c3d61 --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 + +/** + * Component that triggers the opening and closing of an ActionDrawer when dragged on with touch or mouse. + */ +MouseArea { + id: root + + required property ActionDrawer actionDrawer + + property int oldMouseY: 0 + + function startSwipe() { + actionDrawer.cancelAnimations(); + actionDrawer.dragging = true; + actionDrawer.opened = false; + + // must be after properties other are set, we cannot have actionDrawer.updateState() be called + actionDrawer.offset = 0; + actionDrawer.oldOffset = 0; + actionDrawer.visible = true; + } + + function endSwipe() { + actionDrawer.dragging = false; + actionDrawer.updateState(); + } + + function updateOffset(offsetY) { + actionDrawer.offset += offsetY; + } + + anchors.fill: parent + onPressed: { + oldMouseY = mouse.y; + startSwipe(); + } + onReleased: endSwipe() + onCanceled: endSwipe() + onPositionChanged: { + updateOffset(mouse.y - oldMouseY); + oldMouseY = mouse.y; + } +} diff --git a/components/mobileshell/qml/actiondrawer/LandscapeContentContainer.qml b/components/mobileshell/qml/actiondrawer/LandscapeContentContainer.qml new file mode 100644 index 00000000..cafbf5ef --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/LandscapeContentContainer.qml @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +import "../components" as Components +import "../widgets" as Widgets +import "quicksettings" + +/** + * Root element that contains all of the ActionDrawer's contents, and is anchored to the screen. + */ +PlasmaCore.ColorScope { + id: root + + required property var actionDrawer + + readonly property real minimizedQuickSettingsOffset: height + readonly property real maximizedQuickSettingsOffset: height + + colorGroup: PlasmaCore.Theme.ViewColorGroup + + function applyMinMax(val) { + return Math.max(0, Math.min(1, val)); + } + + // fullscreen background + Rectangle { + anchors.fill: parent + color: Qt.rgba(PlasmaCore.Theme.backgroundColor.r, PlasmaCore.Theme.backgroundColor.g, PlasmaCore.Theme.backgroundColor.b, 0.95) + opacity: Math.max(0, Math.min(1, actionDrawer.offset / root.minimizedQuickSettingsOffset)) + } + + PlasmaCore.DataSource { + id: timeSource + engine: "time" + connectedSources: ["Local"] + interval: 60 * 1000 + } + + // left side + ColumnLayout { + opacity: applyMinMax(root.actionDrawer.offset / root.maximizedQuickSettingsOffset) + spacing: 0 + anchors { + top: parent.top + topMargin: Math.min(root.width, root.height) * 0.06 + bottom: parent.bottom + bottomMargin: Math.min(root.width, root.height) * 0.06 + right: quickSettings.left + rightMargin: Math.min(root.width, root.height) * 0.06 + left: parent.left + leftMargin: Math.min(root.width, root.height) * 0.06 + } + + PlasmaComponents.Label { + id: clock + text: Qt.formatTime(timeSource.data.Local.DateTime, MobileShell.ShellUtil.isSystem24HourFormat ? "h:mm" : "h:mm ap") + verticalAlignment: Qt.AlignTop + Layout.fillWidth: true + + font.pixelSize: Math.min(40, Math.min(root.width, root.height) * 0.1) + font.weight: Font.ExtraLight + elide: Text.ElideRight + } + + PlasmaComponents.Label { + id: date + text: Qt.formatDate(timeSource.data.Local.DateTime, "ddd MMMM d") + verticalAlignment: Qt.AlignTop + color: PlasmaCore.ColorScope.disabledTextColor + Layout.fillWidth: true + Layout.topMargin: PlasmaCore.Units.smallSpacing + + font.pixelSize: Math.min(20, Math.min(root.width, root.height) * 0.05) + font.weight: Font.Light + } + + Widgets.NotificationsWidget { + // don't allow notifications widget to get too wide + Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25 + Layout.fillHeight: true + Layout.fillWidth: true + Layout.topMargin: Math.min(root.width, root.height) * 0.02 + } + } + + // right sidebar + QuickSettingsPanel { + id: quickSettings + height: Math.min(root.height, Math.max(quickSettings.minimizedHeight, actionDrawer.offset)) + width: intendedWidth + + readonly property real intendedWidth: 360 + + anchors.top: parent.top + anchors.right: parent.right + + actionDrawer: root.actionDrawer + fullHeight: root.height + + transform: Translate { + id: translate + y: Math.min(root.actionDrawer.offset - quickSettings.minimizedHeight, 0) + } + } +} diff --git a/components/mobileshell/qml/actiondrawer/PortraitContentContainer.qml b/components/mobileshell/qml/actiondrawer/PortraitContentContainer.qml new file mode 100644 index 00000000..280d5131 --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/PortraitContentContainer.qml @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents + +import "../components" as Components +import "../widgets" as Widgets +import "quicksettings" + +/** + * Root element that contains all of the ActionDrawer's contents, and is anchored to the screen. + */ +PlasmaCore.ColorScope { + id: root + + required property var actionDrawer + + readonly property real minimizedQuickSettingsOffset: quickSettings.minimizedHeight + readonly property real maximizedQuickSettingsOffset: minimizedQuickSettingsOffset + quickSettings.maxAddedHeight + + colorGroup: PlasmaCore.Theme.ViewColorGroup + + function applyMinMax(val) { + return Math.max(0, Math.min(1, val)); + } + + // fullscreen background + Rectangle { + anchors.fill: parent + color: Qt.rgba(PlasmaCore.Theme.backgroundColor.r, PlasmaCore.Theme.backgroundColor.g, PlasmaCore.Theme.backgroundColor.b, 0.95) + opacity: Math.max(0, Math.min(1, actionDrawer.offset / root.minimizedQuickSettingsOffset)) + } + + QuickSettingsDrawer { + id: quickSettings + z: 1 // ensure it's above notifications + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + actionDrawer: root.actionDrawer + + // opacity and move animation + property real offsetDist: actionDrawer.offset - minimizedQuickSettingsOffset + property real totalOffsetDist: maximizedQuickSettingsOffset - minimizedQuickSettingsOffset + minimizedToFullProgress: actionDrawer.opened ? applyMinMax(offsetDist / totalOffsetDist) : 0 + + addedHeight: { + if (!actionDrawer.opened) { + // over-scroll effect for initial opening + let progress = (root.actionDrawer.offset - minimizedQuickSettingsOffset) / quickSettings.maxAddedHeight; + let effectProgress = Math.atan(Math.max(0, progress)); + return quickSettings.maxAddedHeight * 0.25 * effectProgress; + } else { + return Math.max(0, Math.min(quickSettings.maxAddedHeight, root.actionDrawer.offset - minimizedQuickSettingsOffset)); + } + } + + transform: Translate { + id: translate + y: Math.min(root.actionDrawer.offset - minimizedQuickSettingsOffset, 0) + } + } + + Widgets.NotificationsWidget { + anchors { + top: quickSettings.top + topMargin: quickSettings.height + translate.y + bottom: parent.bottom + bottomMargin: PlasmaCore.Units.largeSpacing + left: parent.left + leftMargin: PlasmaCore.Units.largeSpacing + right: parent.right + rightMargin: PlasmaCore.Units.largeSpacing + } + opacity: applyMinMax(root.actionDrawer.offset / root.minimizedQuickSettingsOffset) + } +} diff --git a/containments/panel/package/contents/ui/quicksettings/BrightnessItem.qml b/components/mobileshell/qml/actiondrawer/quicksettings/BrightnessItem.qml similarity index 63% rename from containments/panel/package/contents/ui/quicksettings/BrightnessItem.qml rename to components/mobileshell/qml/actiondrawer/quicksettings/BrightnessItem.qml index 6ced2915..eba8452a 100644 --- a/containments/panel/package/contents/ui/quicksettings/BrightnessItem.qml +++ b/components/mobileshell/qml/actiondrawer/quicksettings/BrightnessItem.qml @@ -1,13 +1,12 @@ /* * SPDX-FileCopyrightText: 2012-2013 Daniel Nicoletti * SPDX-FileCopyrightText: 2013, 2015 Kai Uwe Broulik - * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2021 Devin Lin * * SPDX-License-Identifier: LGPL-2.0-or-later */ -import QtQuick 2.0 -import QtQuick.Controls 2.15 +import QtQuick 2.15 import QtQuick.Layouts 1.1 import org.kde.kirigami 2.12 as Kirigami @@ -15,7 +14,7 @@ import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PC3 Item { - id: brightnessRoot + id: root implicitHeight: brightnessRow.implicitHeight @@ -23,14 +22,31 @@ Item { property bool disableBrightnessUpdate: true readonly property int maximumScreenBrightness: pmSource.data["PowerDevil"] ? pmSource.data["PowerDevil"]["Maximum Screen Brightness"] || 0 : 0 + property QtObject updateScreenBrightnessJob + + function updateBrightnessUI() { + if (updateScreenBrightnessJob) + return; + + root.disableBrightnessUpdate = true; + console.log(pmSource.data["PowerDevil"]["Screen Brightness"]); + root.screenBrightness = pmSource.data["PowerDevil"]["Screen Brightness"]; + root.disableBrightnessUpdate = false; + } + onScreenBrightnessChanged: { - brightnessSlider.value = brightnessRoot.screenBrightness + brightnessSlider.value = root.screenBrightness + if (!disableBrightnessUpdate) { var service = pmSource.serviceForSource("PowerDevil"); var operation = service.operationDescription("setBrightness"); operation.brightness = screenBrightness; - operation.silent = true - service.startOperationCall(operation); + operation.silent = true; // don't show OSD + + updateScreenBrightnessJob = service.startOperationCall(operation); + updateScreenBrightnessJob.finished.connect(function (job) { + root.updateBrightnessUI(); + }); } } @@ -44,18 +60,7 @@ Item { connectSource(source); } } - onDataChanged: { - disableBrightnessUpdate = true; - brightnessRoot.screenBrightness = pmSource.data["PowerDevil"]["Screen Brightness"]; - disableBrightnessUpdate = false; - } - } - - Component.onCompleted: { - brightnessSlider.moved.connect(function() { - brightnessRoot.screenBrightness = brightnessSlider.value; - }); - disableBrightnessUpdate = false; + onDataChanged: root.updateBrightnessUI() } RowLayout { @@ -69,24 +74,26 @@ Item { PlasmaCore.IconItem { Layout.alignment: Qt.AlignVCenter Layout.leftMargin: PlasmaCore.Units.smallSpacing - Layout.preferredWidth: Math.round(PlasmaCore.Units.gridUnit * 1.75) + Layout.preferredWidth: PlasmaCore.Units.iconSizes.smallMedium Layout.preferredHeight: width source: "low-brightness" } - Slider { + PC3.Slider { id: brightnessSlider Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter - value: screenBrightness from: 1 - to: maximumScreenBrightness + to: root.maximumScreenBrightness + value: root.screenBrightness + + onMoved: root.screenBrightness = value; } PlasmaCore.IconItem { Layout.alignment: Qt.AlignVCenter Layout.rightMargin: PlasmaCore.Units.smallSpacing - Layout.preferredWidth: Math.round(PlasmaCore.Units.gridUnit * 1.75) + Layout.preferredWidth: PlasmaCore.Units.iconSizes.smallMedium Layout.preferredHeight: width source: "high-brightness" } diff --git a/components/mobileshell/qml/actiondrawer/quicksettings/Handle.qml b/components/mobileshell/qml/actiondrawer/quicksettings/Handle.qml new file mode 100644 index 00000000..5ac0afe7 --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/quicksettings/Handle.qml @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 + +import org.kde.plasma.core 2.0 as PlasmaCore + +Rectangle { + id: handle + + signal tapped() + + implicitWidth: PlasmaCore.Units.gridUnit * 3 + implicitHeight: 3 + radius: height + color: PlasmaCore.Theme.textColor + opacity: 0.5 + + TapHandler { + cursorShape: pressed ? Qt.ClosedHandCursor : Qt.PointingHandCursor + onTapped: handle.tapped() + } +} diff --git a/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettings.qml b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettings.qml new file mode 100644 index 00000000..5665f2fe --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettings.qml @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2014 Marco Martin + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore + +import "../../components" as Components +import "../../components/util.js" as Util + +/** + * Quick settings elements layout, change the height to clip. + */ +Item { + id: root + clip: true + + required property var actionDrawer + + readonly property real columns: Math.round(Util.applyMinMaxRange(3, 6, width / intendedColumnWidth)) + readonly property real columnWidth: Math.floor(width / columns) + readonly property real minimizedColumns: Math.round(Util.applyMinMaxRange(5, 8, width / intendedMinimizedColumnWidth)) + readonly property real minimizedColumnWidth: Math.floor(width / minimizedColumns) + + readonly property real rowHeight: columnWidth * 0.7 + readonly property real fullHeight: fullView.implicitHeight + + readonly property real intendedColumnWidth: 120 + readonly property real intendedMinimizedColumnWidth: PlasmaCore.Units.gridUnit * 3 + PlasmaCore.Units.largeSpacing + readonly property real minimizedRowHeight: PlasmaCore.Units.gridUnit * 3 + PlasmaCore.Units.largeSpacing + + property real minimizedViewProgress: 0 + property real fullViewProgress: 1 + + readonly property SettingsModel quickSettingsModel: SettingsModel { + actionDrawer: root.actionDrawer + } + + // view when fully open + ColumnLayout { + id: fullView + opacity: root.fullViewProgress + visible: opacity !== 0 + transform: Translate { y: (1 - fullView.opacity) * root.rowHeight } + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + // TODO add pages + Flow { + id: flow + spacing: 0 + Layout.fillWidth: true + + Repeater { + model: root.quickSettingsModel + delegate: Components.BaseItem { + required property var modelData + + height: root.rowHeight + width: root.columnWidth + padding: PlasmaCore.Units.smallSpacing + + contentItem: QuickSettingsFullDelegate { + text: modelData.text + status: modelData.status + icon: modelData.icon + enabled: modelData.enabled + settingsCommand: modelData.settingsCommand + toggleFunction: modelData.toggle + } + } + } + } + + BrightnessItem { + id: brightnessItem + Layout.topMargin: PlasmaCore.Units.smallSpacing * 2 + Layout.bottomMargin: PlasmaCore.Units.smallSpacing * 2 + Layout.leftMargin: PlasmaCore.Units.smallSpacing + Layout.rightMargin: PlasmaCore.Units.smallSpacing + Layout.fillWidth: true + } + } + + // view when in minimized mode + RowLayout { + id: minimizedView + spacing: 0 + opacity: root.minimizedViewProgress + visible: opacity !== 0 + transform: Translate { y: (1 - minimizedView.opacity) * -root.rowHeight } + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + Repeater { + model: root.quickSettingsModel + delegate: Components.BaseItem { + required property var modelData + required property var index + + implicitHeight: root.minimizedRowHeight + implicitWidth: root.minimizedColumnWidth + horizontalPadding: (width - PlasmaCore.Units.gridUnit * 3) / 2 + verticalPadding: (height - PlasmaCore.Units.gridUnit * 3) / 2 + visible: index <= root.minimizedColumns + + contentItem: QuickSettingsMinimizedDelegate { + text: modelData.text + status: modelData.status + icon: modelData.icon + enabled: modelData.enabled + settingsCommand: modelData.settingsCommand + toggleFunction: modelData.toggle + } + } + } + } +} diff --git a/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDelegate.qml b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDelegate.qml new file mode 100644 index 00000000..8f77a99d --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDelegate.qml @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.nanoshell 2.0 as NanoShell +import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.plasma.components 3.0 as PlasmaComponents + +import "../../components" as Components + +Components.BaseItem { + id: root + + // Model interface + required property string text + required property string status + required property string icon + required property bool enabled + required property string settingsCommand + required property var toggleFunction + + // set by children + property var iconItem + + readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.ColorUtils.adjustColor(PlasmaCore.ColorScope.highlightColor, {}), 1.25) + readonly property color disabledButtonBorderColor: Kirigami.ColorUtils.adjustColor(PlasmaCore.ColorScope.textColor, {"alpha": 0.2*255}) + readonly property color enabledButtonColor: Kirigami.ColorUtils.adjustColor(PlasmaCore.ColorScope.highlightColor, {alpha: 0.4*255}) + readonly property color enabledButtonPressedColor: Kirigami.ColorUtils.adjustColor(PlasmaCore.ColorScope.highlightColor, {alpha: 0.6*255}); + readonly property color disabledButtonColor: PlasmaCore.Theme.backgroundColor + readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1) + + function delegateClick() { + if (root.toggle) { + root.toggle(); + } else if (root.toggleFunction) { + root.toggleFunction(); + } else if (root.settingsCommand) { + NanoShell.StartupFeedback.open( + root.icon, + root.text, + iconItem.Kirigami.ScenePosition.x + iconItem.width/2, + iconItem.Kirigami.ScenePosition.y + iconItem.height/2, + Math.min(iconItem.width, iconItem.height)) + MobileShell.ShellUtil.executeCommand(root.settingsCommand); + root.closeRequested(); + } + } + + function delegatePressAndHold() { + if (root.settingsCommand) { + NanoShell.StartupFeedback.open( + root.icon, + root.text, + iconItem.Kirigami.ScenePosition.x + iconItem.width/2, + iconItem.Kirigami.ScenePosition.y + iconItem.height/2, + Math.min(iconItem.width, iconItem.height)) + closeRequested(); + MobileShell.ShellUtil.executeCommand(root.settingsCommand); + } else if (root.toggleFunction) { + root.toggleFunction(); + } + } +} diff --git a/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDrawer.qml b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDrawer.qml new file mode 100644 index 00000000..629462e5 --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsDrawer.qml @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.12 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +import "../../statusbar" as StatusBar +import "../../components" as Components +import "../../widgets" as Widgets +import "../" + +/** + * Quick settings drawer pulled down from the top (for portrait mode). + * For the landscape view quicksettings container, see QuickSettingsPanel. + */ +Components.BaseItem { + id: root + + required property var actionDrawer + + /** + * The amount of height to add to the panel (increasing the height of the quick settings area). + */ + property real addedHeight: 0 + + /** + * The maximum amount of added height to snap to the full height of the quick settings panel. + */ + readonly property real maxAddedHeight: quickSettings.fullHeight - minimizedQuickSettingsHeight // first row is part of minimized height + + /** + * Height of panel when in minimized mode. + */ + readonly property real minimizedHeight: bottomPadding + topPadding + statusBar.height + minimizedQuickSettingsHeight + mediaWidget.height + handle.fullHeight + + /** + * Height of just the QuickSettings component in minimized mode. + */ + readonly property real minimizedQuickSettingsHeight: quickSettings.minimizedRowHeight + PlasmaCore.Units.gridUnit + + /** + * Progress of showing the full quick settings view from pinned. + */ + property real minimizedToFullProgress: 1 + + // we need extra padding if the background side border is enabled + topPadding: PlasmaCore.Units.smallSpacing + leftPadding: PlasmaCore.Units.smallSpacing + rightPadding: PlasmaCore.Units.smallSpacing + bottomPadding: PlasmaCore.Units.smallSpacing * 4 + + background: PlasmaCore.FrameSvgItem { + enabledBorders: PlasmaCore.FrameSvg.BottomBorder + imagePath: "widgets/background" + } + + contentItem: Item { + id: containerItem + implicitHeight: column.implicitHeight + + // use container item so that our column doesn't get stretched if base item is anchored + ColumnLayout { + id: column + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: 0 + + StatusBar.StatusBar { + id: statusBar + Layout.fillWidth: true + Layout.preferredHeight: MobileShell.TopPanelControls.panelHeight + PlasmaCore.Units.gridUnit * 0.8 + + colorGroup: PlasmaCore.Theme.NormalColorGroup + backgroundColor: "transparent" + showSecondRow: true + showDropShadow: false + } + + QuickSettings { + id: quickSettings + Layout.preferredHeight: root.minimizedQuickSettingsHeight + root.addedHeight + Layout.topMargin: PlasmaCore.Units.smallSpacing + Layout.fillWidth: true + + actionDrawer: root.actionDrawer + minimizedViewProgress: 1 - root.minimizedToFullProgress + fullViewProgress: root.minimizedToFullProgress + height: root.minimizedQuickSettingsHeight + root.addedHeight + width: parent.width + } + + Widgets.MediaControlsWidget { + id: mediaWidget + property real fullHeight: height + Layout.topMargin + Layout.fillWidth: true + Layout.topMargin: PlasmaCore.Units.smallSpacing + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + } + + Handle { + id: handle + property real fullHeight: height + Layout.topMargin + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: PlasmaCore.Units.smallSpacing * 2 + + onTapped: { + if (root.minimizedToFullProgress < 0.5) { + root.actionDrawer.expand(); + } else { + root.actionDrawer.open(); + } + } + } + } + } +} diff --git a/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsFullDelegate.qml b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsFullDelegate.qml new file mode 100644 index 00000000..92426b84 --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsFullDelegate.qml @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.nanoshell 2.0 as NanoShell +import org.kde.plasma.components 3.0 as PlasmaComponents + +import "../../components" as Components + +QuickSettingsDelegate { + id: root + + padding: PlasmaCore.Units.smallSpacing * 2 + iconItem: icon + + background: Rectangle { + radius: PlasmaCore.Units.smallSpacing + border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor + color: { + if (root.enabled) { + return mouseArea.pressed ? root.enabledButtonPressedColor : root.enabledButtonColor + } else { + return mouseArea.pressed ? root.disabledButtonPressedColor : root.disabledButtonColor + } + } + } + + contentItem: MouseArea { + id: mouseArea + onClicked: root.delegateClick() + onPressAndHold: root.delegatePressAndHold() + + PlasmaCore.IconItem { + id: icon + anchors.top: parent.top + anchors.left: parent.left + implicitWidth: PlasmaCore.Units.iconSizes.small + implicitHeight: width + source: root.icon + } + + ColumnLayout { + id: column + spacing: PlasmaCore.Units.smallSpacing + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + + PlasmaComponents.Label { + Layout.fillWidth: true + elide: Text.ElideRight + text: root.text + font.pixelSize: PlasmaCore.Theme.defaultFont.pixelSize * 0.8 // TODO base height off of size of delegate + } + PlasmaComponents.Label { + Layout.fillWidth: true + elide: Text.ElideRight + // if no status is given, just use On/Off + text: root.status ? root.status : (root.enabled ? i18n("On") : i18n("Off")) + opacity: 0.6 + font.pixelSize: PlasmaCore.Theme.defaultFont.pixelSize * 0.8 + } + } + } +} + diff --git a/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsMinimizedDelegate.qml b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsMinimizedDelegate.qml new file mode 100644 index 00000000..726cc92a --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsMinimizedDelegate.qml @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.nanoshell 2.0 as NanoShell +import org.kde.plasma.components 3.0 as PlasmaComponents + +import "../../components" as Components + +QuickSettingsDelegate { + id: root + + iconItem: icon + + background: Rectangle { + radius: PlasmaCore.Units.smallSpacing + border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor + color: { + if (root.enabled) { + return mouseArea.pressed ? root.enabledButtonPressedColor : root.enabledButtonColor + } else { + return mouseArea.pressed ? root.disabledButtonPressedColor : root.disabledButtonColor + } + } + } + + contentItem: MouseArea { + id: mouseArea + onClicked: root.delegateClick() + onPressAndHold: root.delegatePressAndHold() + + PlasmaCore.IconItem { + id: icon + anchors.centerIn: parent + implicitWidth: PlasmaCore.Units.iconSizes.smallMedium + implicitHeight: width + source: root.icon + } + } +} + diff --git a/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsPanel.qml b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsPanel.qml new file mode 100644 index 00000000..b4ec8125 --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/quicksettings/QuickSettingsPanel.qml @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.12 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +import "../../statusbar" as StatusBar +import "../../components" as Components +import "../../widgets" as Widgets +import "../" + +/** + * Quick settings panel for landscape view (right sidebar). + * For the portrait view quicksettings container, see QuickSettingsDrawer. + */ +Components.BaseItem { + id: root + + required property var actionDrawer + + required property real fullHeight + + /** + * Height of panel when first pulled down. + */ + readonly property real minimizedHeight: bottomPadding + topPadding + statusBar.height + quickSettings.rowHeight + + /** + * Implicit height of the contents of the panel. + */ + readonly property real contentImplicitHeight: column.implicitHeight + + // we need extra padding since the background side border is enabled + topPadding: PlasmaCore.Units.smallSpacing * 4 + leftPadding: PlasmaCore.Units.smallSpacing * 4 + rightPadding: PlasmaCore.Units.smallSpacing * 4 + bottomPadding: PlasmaCore.Units.smallSpacing * 4 + + background: PlasmaCore.FrameSvgItem { + enabledBorders: PlasmaCore.FrameSvg.AllBorders + imagePath: "widgets/background" + } + + contentItem: Item { + id: containerItem + clip: true + + // use container item so that our column doesn't get stretched if base item is anchored + ColumnLayout { + id: column + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: root.fullHeight + spacing: 0 + + StatusBar.StatusBar { + id: statusBar + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5 + Layout.maximumHeight: Kirigami.Units.gridUnit * 1.5 + + colorGroup: PlasmaCore.Theme.NormalColorGroup + backgroundColor: "transparent" + showSecondRow: false + showDropShadow: false + showTime: false + } + + PlasmaComponents.ScrollView { + id: scrollView + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + Layout.maximumHeight: root.fullHeight - root.topPadding - root.bottomPadding - statusBar.height - mediaWidget.fullHeight - PlasmaCore.Units.smallSpacing + Layout.maximumWidth: column.width + + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + clip: true + + QuickSettings { + id: quickSettings + width: column.width + implicitHeight: quickSettings.fullHeight + + actionDrawer: root.actionDrawer + minimizedViewProgress: 0 + fullViewProgress: 1 + } + } + + Item { Layout.fillHeight: true } + } + + Widgets.MediaControlsWidget { + id: mediaWidget + property real fullHeight: visible ? height + PlasmaCore.Units.smallSpacing * 6 : 0 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: column.bottom + anchors.bottomMargin: root.bottomPadding * 2 + PlasmaCore.Units.smallSpacing * 2 // HACK: can't figure out a cleaner way to position + } + + Handle { + id: handle + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + } + } +} + diff --git a/containments/panel/package/contents/ui/quicksettings/SettingsModel.qml b/components/mobileshell/qml/actiondrawer/quicksettings/SettingsModel.qml similarity index 54% rename from containments/panel/package/contents/ui/quicksettings/SettingsModel.qml rename to components/mobileshell/qml/actiondrawer/quicksettings/SettingsModel.qml index b83d68a5..e639eaed 100644 --- a/containments/panel/package/contents/ui/quicksettings/SettingsModel.qml +++ b/components/mobileshell/qml/actiondrawer/quicksettings/SettingsModel.qml @@ -1,6 +1,6 @@ /* * SPDX-FileCopyrightText: 2015 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2021 Devin Lin * SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez * * SPDX-License-Identifier: LGPL-2.0-or-later @@ -9,35 +9,29 @@ import QtQuick 2.14 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.networkmanagement 0.2 as PlasmaNM + import org.kde.bluezqt 1.0 as BluezQt -import org.kde.plasma.private.nanoshell 2.0 as NanoShell -import org.kde.plasma.private.mobilehomescreencomponents 0.1 as HomeScreenComponents +import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PC3 +import org.kde.plasma.networkmanagement 0.2 as PlasmaNM +import org.kde.plasma.private.mobileshell 1.0 as MobileShell -HomeScreenComponents.QuickSettingsModel -{ - id: modelItem +MobileShell.QuickSettingsModel { + id: root + + required property var actionDrawer + property bool screenshotRequested: false - - signal panelClosed() - - onPanelClosed: { - if (screenshotRequested) { - plasmoid.nativeInterface.takeScreenshot(); - screenshotRequested = false; - } - } - HomeScreenComponents.QuickSetting { + MobileShell.QuickSetting { text: i18n("Settings") + status: i18n("Tap to open") icon: "configure" enabled: false settingsCommand: "plasma-open-settings" } - HomeScreenComponents.QuickSetting { + MobileShell.QuickSetting { PlasmaNM.Handler { id: nmHandler } @@ -54,7 +48,7 @@ HomeScreenComponents.QuickSettingsModel } enabled: enabledConnections.wirelessEnabled } - HomeScreenComponents.QuickSetting { + MobileShell.QuickSetting { text: i18n("Bluetooth") icon: "network-bluetooth" settingsCommand: "plasma-open-settings kcm_bluetooth" @@ -69,7 +63,7 @@ HomeScreenComponents.QuickSettingsModel } enabled: BluezQt.Manager.bluetoothOperational } - HomeScreenComponents.QuickSetting { + MobileShell.QuickSetting { text: i18n("Mobile Data") icon: "network-modem" settingsCommand: "plasma-open-settings kcm_mobile_broadband" @@ -78,35 +72,70 @@ HomeScreenComponents.QuickSettingsModel nmHandler.enableWwan(!enabledConnections.wwanEnabled) } } - HomeScreenComponents.QuickSetting { + MobileShell.QuickSetting { text: i18n("Flashlight") icon: "flashlight-on" - enabled: plasmoid.nativeInterface.torchEnabled + enabled: MobileShell.ShellUtil.torchEnabled function toggle() { - plasmoid.nativeInterface.toggleTorch() + MobileShell.ShellUtil.toggleTorch() } } - HomeScreenComponents.QuickSetting { + MobileShell.QuickSetting { text: i18n("Location") icon: "gps" enabled: false } - HomeScreenComponents.QuickSetting { + MobileShell.QuickSetting { text: i18n("Screenshot") + status: i18n("Tap to screenshot") icon: "spectacle" enabled: false function toggle() { - modelItem.screenshotRequested = true; - root.closeRequested(); + root.screenshotRequested = true; + root.actionDrawer.close(); + } + + Connections { + target: root.actionDrawer + function onVisibleChanged(visible) { + if (!visible && screenshotRequested) { + timer.restart(); + root.screenshotRequested = false + } + } + } + + // HACK: KWin's fade effect may have the window ending up being in the screenshot if taken too fast + Timer { + id: timer + interval: 500 + onTriggered: MobileShell.ShellUtil.takeScreenshot() } } - HomeScreenComponents.QuickSetting { + MobileShell.QuickSetting { text: i18n("Auto-rotate") icon: "rotation-allowed" settingsCommand: "plasma-open-settings kcm_kscreen" - enabled: plasmoid.nativeInterface.autoRotateEnabled + enabled: MobileShell.ShellUtil.autoRotateEnabled function toggle() { - plasmoid.nativeInterface.autoRotateEnabled = !enabled + MobileShell.ShellUtil.autoRotateEnabled = !enabled + } + } + MobileShell.QuickSetting { + text: i18n("Battery") + status: i18n("%1%", MobileShell.BatteryProvider.percent) + icon: "battery-full" + (MobileShell.BatteryProvider.pluggedIn ? "-charging" : "") + enabled: false + settingsCommand: "plasma-open-settings kcm_mobile_power" + } + MobileShell.QuickSetting { + text: i18n("Sound") + icon: "audio-speakers-symbolic" + status: i18n("%1%", MobileShell.VolumeProvider.volumeValue) + enabled: false + settingsCommand: "plasma-open-settings kcm_pulseaudio" + function toggle() { + volumeProvider.showVolumeOverlay() } } } diff --git a/components/mobileshell/qml/components/BaseItem.qml b/components/mobileshell/qml/components/BaseItem.qml new file mode 100644 index 00000000..43c4cf11 --- /dev/null +++ b/components/mobileshell/qml/components/BaseItem.qml @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.15 + +/** + * Serves a similar function as a QQC2.Control, but does not + * take input events, preventing conflicts with Flickable. + */ + +Item { + id: root + + property real topInset: 0 + property real bottomInset: 0 + property real leftInset: 0 + property real rightInset: 0 + + property real padding: 0 + property real verticalPadding: padding + property real horizontalPadding: padding + property real topPadding: verticalPadding + property real bottomPadding: verticalPadding + property real leftPadding: horizontalPadding + property real rightPadding: horizontalPadding + + property Item contentItem: Item {} + property Item background: Item {} + + implicitHeight: topPadding + bottomPadding + contentItem.implicitHeight + implicitWidth: leftPadding + rightPadding + contentItem.implicitWidth + + onContentItemChanged: { + contentItem.parent = contentItemLoader; + contentItem.anchors.fill = contentItemLoader; + contentItemLoader.children.push(contentItem); + } + + onBackgroundChanged: { + background.parent = backgroundLoader; + background.anchors.fill = backgroundLoader; + backgroundLoader.children.push(background); + } + + Item { + id: backgroundLoader + anchors.fill: parent + anchors.leftMargin: root.leftInset + anchors.rightMargin: root.rightInset + anchors.topMargin: root.topInset + anchors.bottomMargin: root.bottomInset + } + + Item { + id: contentItemLoader + anchors.fill: parent + anchors.leftMargin: root.leftPadding + anchors.rightMargin: root.rightPadding + anchors.topMargin: root.topPadding + anchors.bottomMargin: root.bottomPadding + } +} + diff --git a/components/mobileshell/qml/components/Direction.qml b/components/mobileshell/qml/components/Direction.qml new file mode 100644 index 00000000..64ae70bd --- /dev/null +++ b/components/mobileshell/qml/components/Direction.qml @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.15 + +QtObject { + enum Type { + None = 0, + Up, + Down, + Left, + Right + } +} + diff --git a/components/mobileshell/qml/components/util.js b/components/mobileshell/qml/components/util.js new file mode 100644 index 00000000..b6935f93 --- /dev/null +++ b/components/mobileshell/qml/components/util.js @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +function applyMinMaxRange(min, max, num) { + return Math.min(max, Math.max(min, num)); +} diff --git a/containments/panel/package/contents/ui/indicators/providers/BatteryProvider.qml b/components/mobileshell/qml/dataproviders/BatteryProvider.qml similarity index 90% rename from containments/panel/package/contents/ui/indicators/providers/BatteryProvider.qml rename to components/mobileshell/qml/dataproviders/BatteryProvider.qml index 92511efa..fb0da8e3 100644 --- a/containments/panel/package/contents/ui/indicators/providers/BatteryProvider.qml +++ b/components/mobileshell/qml/dataproviders/BatteryProvider.qml @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2021 Devin Lin * SPDX-FileCopyrightText: 2019 Marco Martin * * SPDX-License-Identifier: GPL-2.0-or-later @@ -12,6 +12,8 @@ import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.workspace.components 2.0 as PW +pragma Singleton + Item { property bool isVisible: pmSource.data["Battery"]["Has Cumulative"] property int percent: pmSource.data["Battery"]["Percent"] diff --git a/containments/panel/package/contents/ui/indicators/providers/BluetoothProvider.qml b/components/mobileshell/qml/dataproviders/BluetoothProvider.qml similarity index 94% rename from containments/panel/package/contents/ui/indicators/providers/BluetoothProvider.qml rename to components/mobileshell/qml/dataproviders/BluetoothProvider.qml index daf571a3..267b4437 100644 --- a/containments/panel/package/contents/ui/indicators/providers/BluetoothProvider.qml +++ b/components/mobileshell/qml/dataproviders/BluetoothProvider.qml @@ -1,5 +1,5 @@ /* - SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-FileCopyrightText: 2021 Devin Lin SPDX-FileCopyrightText: 2019 Marco Martin SPDX-FileCopyrightText: 2013-2017 Jan Grulich @@ -11,6 +11,8 @@ import QtQuick.Layouts 1.4 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.bluezqt 1.0 as BluezQt +pragma Singleton + QtObject { property bool isVisible: BluezQt.Manager.bluetoothOperational property string icon: deviceConnected ? "preferences-system-bluetooth-activated" : "preferences-system-bluetooth" diff --git a/components/mobileshell/qml/dataproviders/NotificationProvider.qml b/components/mobileshell/qml/dataproviders/NotificationProvider.qml new file mode 100644 index 00000000..e8e45d22 --- /dev/null +++ b/components/mobileshell/qml/dataproviders/NotificationProvider.qml @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +import QtQuick 2.2 + +import org.kde.notificationmanager 1.0 as NotificationManager + +pragma Singleton + +QtObject { + + property var notificationSettings: NotificationManager.Settings {} + + property var historyModel: NotificationManager.Notifications { + showExpired: true + showDismissed: true + showJobs: notificationSettings.jobsInNotifications + sortMode: NotificationManager.Notifications.SortByTypeAndUrgency + groupMode: NotificationManager.Notifications.GroupApplicationsFlat + groupLimit: 2 + expandUnread: true + blacklistedDesktopEntries: notificationSettings.historyBlacklistedApplications + blacklistedNotifyRcNames: notificationSettings.historyBlacklistedServices + urgencies: { + var urgencies = NotificationManager.Notifications.CriticalUrgency + | NotificationManager.Notifications.NormalUrgency; + if (notificationSettings.lowPriorityHistory) { + urgencies |= NotificationManager.Notifications.LowUrgency; + } + return urgencies; + } + } +} + + diff --git a/containments/panel/package/contents/ui/indicators/providers/SignalStrengthProvider.qml b/components/mobileshell/qml/dataproviders/SignalStrengthProvider.qml similarity index 86% rename from containments/panel/package/contents/ui/indicators/providers/SignalStrengthProvider.qml rename to components/mobileshell/qml/dataproviders/SignalStrengthProvider.qml index 44904210..2237cce2 100644 --- a/containments/panel/package/contents/ui/indicators/providers/SignalStrengthProvider.qml +++ b/components/mobileshell/qml/dataproviders/SignalStrengthProvider.qml @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2021 Devin Lin * SPDX-FileCopyrightText: 2015 Marco Martin * SPDX-FileCopyrightText: 2021 Tobias Fella * @@ -9,6 +9,8 @@ import QtQuick 2.1 import org.kde.plasma.mm 1.0 +pragma Singleton + QtObject { property string icon: "network-mobile-" + Math.floor(SignalIndicator.strength / 20) * 20 diff --git a/containments/panel/package/contents/ui/indicators/providers/VolumeProvider.qml b/components/mobileshell/qml/dataproviders/VolumeProvider.qml similarity index 64% rename from containments/panel/package/contents/ui/indicators/providers/VolumeProvider.qml rename to components/mobileshell/qml/dataproviders/VolumeProvider.qml index 9f67b235..3edf1775 100644 --- a/containments/panel/package/contents/ui/indicators/providers/VolumeProvider.qml +++ b/components/mobileshell/qml/dataproviders/VolumeProvider.qml @@ -1,5 +1,5 @@ /* - SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-FileCopyrightText: 2021 Devin Lin SPDX-FileCopyrightText: 2019 Aditya Mehra SPDX-FileCopyrightText: 2014-2015 Harald Sitter @@ -11,10 +11,21 @@ import QtQuick.Layouts 1.4 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.private.volume 0.1 -import "../../volumeosd" +import "../osd/volume" + +pragma Singleton QtObject { + /** + * Whether or not to bind the volume global key shortcuts. + * We should never bind the shortcut multiple times in the shell, or else they may not work at all. + * + * We only set this to true when loaded for the panel containment (and NOT in the lockscreen). + */ + property bool bindShortcuts: false + property bool isVisible: paSinkModel.preferredSink && paSinkModel.preferredSink.muted + property string icon: paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink) ? iconName(paSinkModel.preferredSink.volume, paSinkModel.preferredSink.muted) : iconName(0, true) @@ -22,6 +33,8 @@ QtObject { property bool volumeFeedback: true property int maxVolumeValue: Math.round(100 * PulseAudio.NormalVolume / 100.0) property int volumeStep: Math.round(5 * PulseAudio.NormalVolume / 100.0) + property int volumeValue + readonly property string dummyOutputName: "auto_null" function showVolumeOverlay() { @@ -80,7 +93,7 @@ QtObject { var percent = volumePercent(volume, maxVolumeValue); paSinkModel.preferredSink.muted = percent == 0; paSinkModel.preferredSink.volume = volume; - osd.volume = percent; + volumeValue = percent; osd.showOverlay(); playFeedback(); @@ -95,7 +108,7 @@ QtObject { var percent = volumePercent(volume, maxVolumeValue); paSinkModel.preferredSink.muted = percent == 0; paSinkModel.preferredSink.volume = volume; - osd.volume = percent; + volumeValue = percent; osd.showOverlay(); playFeedback(); } @@ -110,7 +123,7 @@ QtObject { var toMute = !paSinkModel.preferredSink.muted; paSinkModel.preferredSink.muted = toMute; - osd.volume = toMute ? 0 : volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue); + volumeValue = toMute ? 0 : volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue); osd.showOverlay(); if (!toMute) { @@ -123,7 +136,7 @@ QtObject { function onVolumeChanged() { var percent = volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue); - osd.volume = percent; + volumeValue = percent; } } property var updateVolumeOnSinkChange: Connections { @@ -132,49 +145,56 @@ QtObject { function onPreferredSinkChanged() { if (paSinkModel.preferredSink) { var percent = volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue); - osd.volume = percent; + volumeValue = percent; } } } property SinkModel paSinkModel: SinkModel {} - property VolumeOsd osd: VolumeOsd {} + property VolumeOSD osd: VolumeOSD { + volume: volumeValue + } property VolumeFeedback feedback: VolumeFeedback {} - property GlobalActionCollection actionCollection: GlobalActionCollection { - // KGlobalAccel cannot transition from kmix to something else, so if - // the user had a custom shortcut set for kmix those would get lost. - // To avoid this we hijack kmix name and actions. Entirely mental but - // best we can do to not cause annoyance for the user. - // The display name actually is updated to whatever registered last - // though, so as far as user visible strings go we should be fine. - // As of 2015-07-21: - // componentName: kmix - // actions: increase_volume, decrease_volume, mute - name: "kmix" - displayName: i18n("Audio") + // only bind the global shortcuts when told to + property var actionCollection: Loader { + active: bindShortcuts + + sourceComponent: GlobalActionCollection { + // KGlobalAccel cannot transition from kmix to something else, so if + // the user had a custom shortcut set for kmix those would get lost. + // To avoid this we hijack kmix name and actions. Entirely mental but + // best we can do to not cause annoyance for the user. + // The display name actually is updated to whatever registered last + // though, so as far as user visible strings go we should be fine. + // As of 2015-07-21: + // componentName: kmix + // actions: increase_volume, decrease_volume, mute + name: "kmix" + displayName: i18n("Audio") - GlobalAction { - objectName: "increase_volume" - text: i18n("Increase Volume") - shortcut: Qt.Key_VolumeUp - onTriggered: increaseVolume() - } + GlobalAction { + objectName: "increase_volume" + text: i18n("Increase Volume") + shortcut: Qt.Key_VolumeUp + onTriggered: increaseVolume() + } - GlobalAction { - objectName: "decrease_volume" - text: i18n("Decrease Volume") - shortcut: Qt.Key_VolumeDown - onTriggered: decreaseVolume() - } + GlobalAction { + objectName: "decrease_volume" + text: i18n("Decrease Volume") + shortcut: Qt.Key_VolumeDown + onTriggered: decreaseVolume() + } - GlobalAction { - objectName: "mute" - text: i18n("Mute") - shortcut: Qt.Key_VolumeMute - onTriggered: muteVolume() + GlobalAction { + objectName: "mute" + text: i18n("Mute") + shortcut: Qt.Key_VolumeMute + onTriggered: muteVolume() + } } } } diff --git a/containments/panel/package/contents/ui/indicators/providers/WifiProvider.qml b/components/mobileshell/qml/dataproviders/WifiProvider.qml similarity index 68% rename from containments/panel/package/contents/ui/indicators/providers/WifiProvider.qml rename to components/mobileshell/qml/dataproviders/WifiProvider.qml index 62a0ab03..837af03e 100644 --- a/containments/panel/package/contents/ui/indicators/providers/WifiProvider.qml +++ b/components/mobileshell/qml/dataproviders/WifiProvider.qml @@ -1,5 +1,5 @@ /* - SPDX-FileCopyrightText: 2021 Devin Lin + SPDX-FileCopyrightText: 2021 Devin Lin SPDX-FileCopyrightText: 2019 Marco Martin SPDX-FileCopyrightText: 2013-2017 Jan Grulich @@ -11,24 +11,25 @@ import QtQuick.Layouts 1.4 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.networkmanagement 0.2 as PlasmaNM -Item { - visible: false +pragma Singleton + +QtObject { property string icon: connectionIconProvider.connectionIcon property bool indicatorRunning: connectionIconProvider.connecting - PlasmaNM.NetworkStatus { + property var networkStatus: PlasmaNM.NetworkStatus { id: networkStatus } - PlasmaNM.NetworkModel { + property var networkModel: PlasmaNM.NetworkModel { id: connectionModel } - PlasmaNM.Handler { + property var handler: PlasmaNM.Handler { id: handler } - PlasmaNM.ConnectionIcon { + property var connectionIcon: PlasmaNM.ConnectionIcon { id: connectionIconProvider } } diff --git a/containments/panel/package/contents/ui/volumeosd/AudioApplet.qml b/components/mobileshell/qml/osd/volume/AudioApplet.qml similarity index 99% rename from containments/panel/package/contents/ui/volumeosd/AudioApplet.qml rename to components/mobileshell/qml/osd/volume/AudioApplet.qml index 21da0158..e2edfc04 100644 --- a/containments/panel/package/contents/ui/volumeosd/AudioApplet.qml +++ b/components/mobileshell/qml/osd/volume/AudioApplet.qml @@ -14,7 +14,6 @@ import QtGraphicalEffects 1.12 import org.kde.plasma.core 2.1 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtra -import org.kde.plasma.plasmoid 2.0 import org.kde.kquickcontrolsaddons 2.0 as KQCAddons import org.kde.plasma.private.volume 0.1 diff --git a/containments/panel/package/contents/ui/volumeosd/DeviceListItem.qml b/components/mobileshell/qml/osd/volume/DeviceListItem.qml similarity index 100% rename from containments/panel/package/contents/ui/volumeosd/DeviceListItem.qml rename to components/mobileshell/qml/osd/volume/DeviceListItem.qml diff --git a/containments/panel/package/contents/ui/volumeosd/ListItemBase.qml b/components/mobileshell/qml/osd/volume/ListItemBase.qml similarity index 100% rename from containments/panel/package/contents/ui/volumeosd/ListItemBase.qml rename to components/mobileshell/qml/osd/volume/ListItemBase.qml diff --git a/containments/panel/package/contents/ui/volumeosd/PopupCard.qml b/components/mobileshell/qml/osd/volume/PopupCard.qml similarity index 100% rename from containments/panel/package/contents/ui/volumeosd/PopupCard.qml rename to components/mobileshell/qml/osd/volume/PopupCard.qml diff --git a/containments/panel/package/contents/ui/volumeosd/StreamListItem.qml b/components/mobileshell/qml/osd/volume/StreamListItem.qml similarity index 100% rename from containments/panel/package/contents/ui/volumeosd/StreamListItem.qml rename to components/mobileshell/qml/osd/volume/StreamListItem.qml diff --git a/containments/panel/package/contents/ui/volumeosd/VolumeOsd.qml b/components/mobileshell/qml/osd/volume/VolumeOSD.qml similarity index 100% rename from containments/panel/package/contents/ui/volumeosd/VolumeOsd.qml rename to components/mobileshell/qml/osd/volume/VolumeOSD.qml diff --git a/containments/panel/package/contents/ui/volumeosd/icon.js b/components/mobileshell/qml/osd/volume/icon.js similarity index 100% rename from containments/panel/package/contents/ui/volumeosd/icon.js rename to components/mobileshell/qml/osd/volume/icon.js diff --git a/components/mobileshell/qml/qmldir b/components/mobileshell/qml/qmldir new file mode 100644 index 00000000..3abf8ea7 --- /dev/null +++ b/components/mobileshell/qml/qmldir @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2021 Devin Lin +# SPDX-License-Identifier: GPL-2.0-or-later + +module org.kde.plasma.private.mobileshell + +plugin mobileshellplugin + +# /actiondrawer +ActionDrawer 1.0 actiondrawer/ActionDrawer.qml +ActionDrawerOpenSurface 1.0 actiondrawer/ActionDrawerOpenSurface.qml + +# /components +BaseItem 1.0 components/BaseItem.qml +Direction 1.0 components/Direction.qml +PanelBackground 1.0 components/PanelBackground.qml + +# /dataproviders +singleton BatteryProvider 1.0 dataproviders/BatteryProvider.qml +singleton BluetoothProvider 1.0 dataproviders/BluetoothProvider.qml +singleton NotificationProvider 1.0 dataproviders/NotificationProvider.qml +singleton SignalStrengthProvider 1.0 dataproviders/SignalStrengthProvider.qml +singleton VolumeProvider 1.0 dataproviders/VolumeProvider.qml +singleton WifiProvider 1.0 dataproviders/WifiProvider.qml + +# /statusbar +StatusBar 1.0 statusbar/StatusBar.qml + +# /widgets +MediaControlsWidget 1.0 widgets/MediaControlsWidget.qml +NotificationsWidget 1.0 widgets/NotificationsWidget.qml + +# / +singleton HomeScreenControls 1.0 HomeScreenControls.qml +singleton TopPanelControls 1.0 TopPanelControls.qml diff --git a/components/mobileshell/qml/statusbar/ClockText.qml b/components/mobileshell/qml/statusbar/ClockText.qml new file mode 100644 index 00000000..ada27f11 --- /dev/null +++ b/components/mobileshell/qml/statusbar/ClockText.qml @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + + +import QtQuick 2.12 +import QtQuick.Layouts 1.15 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +import "indicators" as Indicators + +PlasmaComponents.Label { + id: clock + + required property PlasmaCore.DataSource source + + property bool is24HourTime: MobileShell.ShellUtil.isSystem24HourFormat + + text: Qt.formatTime(source.data.Local.DateTime, is24HourTime ? "h:mm" : "h:mm ap") + color: PlasmaCore.ColorScope.textColor + verticalAlignment: Qt.AlignVCenter + + TapHandler { + onTapped: { + MobileShell.ShellUtil.launchApp("org.kde.kclock"); + } + } +} diff --git a/containments/panel/package/contents/ui/IndicatorsRow.qml b/components/mobileshell/qml/statusbar/StatusBar.qml similarity index 61% rename from containments/panel/package/contents/ui/IndicatorsRow.qml rename to components/mobileshell/qml/statusbar/StatusBar.qml index df50937d..e96e86a9 100644 --- a/containments/panel/package/contents/ui/IndicatorsRow.qml +++ b/components/mobileshell/qml/statusbar/StatusBar.qml @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2015 Marco Martin * SPDX-FileCopyrightText: 2021 Devin Lin * - * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-License-Identifier: LGPL-2.0-or-later */ @@ -12,39 +12,75 @@ import QtQuick.Layouts 1.3 import QtQml.Models 2.12 import QtGraphicalEffects 1.12 -import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents - -import org.kde.plasma.workspace.components 2.0 as PlasmaWorkspace -import org.kde.taskmanager 0.1 as TaskManager - -import org.kde.plasma.private.nanoshell 2.0 as NanoShell import org.kde.plasma.private.mobileshell 1.0 as MobileShell -import "LayoutManager.js" as LayoutManager - import "indicators" as Indicators Item { - id: indicatorsRow - required property var colorGroup - required property bool showDropShadow - required property color backgroundColor + id: root + /** + * The color group used for status bar elements. + */ + required property var colorGroup + + /** + * Whether to show a drop shadow under the status bar. + */ + property bool showDropShadow: false + + /** + * The background color of the status bar. + */ + property color backgroundColor: "transparent" + + /** + * Whether to show a second row of the status bar, with more information. + */ property bool showSecondRow: false // show extra row with date and mobile provider + /** + * Whether to show time. If set to false, the signal strength indicator is moved in its place. + */ + property bool showTime: true + + /** + * Disables showing system tray indicators, preventing SIGABRT when used on the lockscreen. + */ + property bool disableSystemTray: false + property alias colorScopeColor: icons.backgroundColor property alias applets: appletIconsRow - property real textPixelSize: PlasmaCore.Units.gridUnit * 0.6 - property real elementSpacing: PlasmaCore.Units.smallSpacing * 1.5 + readonly property real textPixelSize: PlasmaCore.Units.gridUnit * 0.6 + readonly property real elementSpacing: PlasmaCore.Units.smallSpacing * 1.5 PlasmaCore.DataSource { id: timeSource engine: "time" connectedSources: ["Local"] interval: 60 * 1000 + intervalAlignment: PlasmaCore.Types.AlignToMinute + } + + property alias statusNotifierSource: statusNotifierSourceLoader.item + + Loader { + id: statusNotifierSourceLoader + active: !disableSystemTray + sourceComponent: PlasmaCore.DataSource { + id: statusNotifierSource + engine: "statusnotifieritem" + interval: 0 + onSourceAdded: { + connectSource(source) + } + Component.onCompleted: { + connectedSources = sources + } + } } DropShadow { @@ -63,7 +99,7 @@ Item { PlasmaCore.ColorScope { id: icons z: 1 - colorGroup: indicatorsRow.colorGroup + colorGroup: root.colorGroup anchors.fill: parent Controls.Control { @@ -88,21 +124,18 @@ Item { spacing: 0 // clock - PlasmaComponents.Label { - id: clock - property bool is24HourTime: plasmoid.nativeInterface.isSystem24HourFormat + ClockText { + visible: root.showTime Layout.fillHeight: true - - text: Qt.formatTime(timeSource.data.Local.DateTime, is24HourTime ? "h:mm" : "h:mm ap") - color: PlasmaCore.ColorScope.textColor - verticalAlignment: Qt.AlignVCenter font.pixelSize: textPixelSize - - TapHandler { - onTapped: { - plasmoid.nativeInterface.launchApp("org.kde.kclock"); - } - } + source: timeSource + } + + Indicators.SignalStrengthIndicator { + Layout.fillHeight: true + showLabel: true + visible: !root.showTime + textPixelSize: root.textPixelSize } // spacing in the middle @@ -117,21 +150,21 @@ Item { id: filteredStatusNotifiers filterRole: "Title" sourceModel: PlasmaCore.DataModel { - dataSource: statusNotifierSource + dataSource: statusNotifierSource ? statusNotifierSource : null } } delegate: TaskWidget { - Layout.leftMargin: indicatorsRow.elementSpacing + Layout.leftMargin: root.elementSpacing } } // applet indicators RowLayout { id: appletIconsRow - Layout.leftMargin: indicatorsRow.elementSpacing + Layout.leftMargin: root.elementSpacing Layout.fillHeight: true - spacing: indicatorsRow.elementSpacing + spacing: root.elementSpacing visible: children.length > 0 } @@ -140,28 +173,25 @@ Item { id: indicators Layout.leftMargin: PlasmaCore.Units.smallSpacing // applets have different spacing needs Layout.fillHeight: true - spacing: indicatorsRow.elementSpacing + spacing: root.elementSpacing - Indicators.SignalStrength { - provider: signalStrengthProvider + Indicators.SignalStrengthIndicator { + Layout.fillHeight: true + showLabel: false + visible: root.showTime + } + Indicators.BluetoothIndicator { Layout.fillHeight: true } - Indicators.Bluetooth { - provider: bluetoothProvider + Indicators.WifiIndicator { Layout.fillHeight: true } - Indicators.Wifi { - provider: wifiProvider + Indicators.VolumeIndicator { Layout.fillHeight: true } - Indicators.Volume { - provider: volumeProvider - Layout.fillHeight: true - } - Indicators.Battery { - provider: batteryProvider - spacing: indicatorsRow.elementSpacing - labelHeight: textPixelSize + Indicators.BatteryIndicator { + spacing: root.elementSpacing + textPixelSize: root.textPixelSize Layout.fillHeight: true } } @@ -170,19 +200,20 @@ Item { // extra row with date and mobile provider (for quicksettings panel) RowLayout { spacing: 0 - visible: indicatorsRow.showSecondRow + visible: root.showSecondRow Layout.fillWidth: true PlasmaComponents.Label { text: Qt.formatDate(timeSource.data.Local.DateTime, "ddd. MMMM d") color: PlasmaCore.ColorScope.disabledTextColor - font.pixelSize: indicatorsRow.textPixelSize * 0.8 + font.pixelSize: root.textPixelSize * 0.8 } Item { Layout.fillWidth: true } PlasmaComponents.Label { - text: signalStrengthProvider.label + visible: root.showTime + text: MobileShell.SignalStrengthProvider.label color: PlasmaCore.ColorScope.disabledTextColor - font.pixelSize: indicatorsRow.textPixelSize * 0.8 + font.pixelSize: root.textPixelSize * 0.8 horizontalAlignment: Qt.AlignRight } } diff --git a/containments/panel/package/contents/ui/TaskWidget.qml b/components/mobileshell/qml/statusbar/TaskWidget.qml similarity index 100% rename from containments/panel/package/contents/ui/TaskWidget.qml rename to components/mobileshell/qml/statusbar/TaskWidget.qml diff --git a/containments/panel/package/contents/ui/indicators/Battery.qml b/components/mobileshell/qml/statusbar/indicators/BatteryIndicator.qml similarity index 79% rename from containments/panel/package/contents/ui/indicators/Battery.qml rename to components/mobileshell/qml/statusbar/indicators/BatteryIndicator.qml index e36b5a12..e24f2bb7 100644 --- a/containments/panel/package/contents/ui/indicators/Battery.qml +++ b/components/mobileshell/qml/statusbar/indicators/BatteryIndicator.qml @@ -11,12 +11,11 @@ import QtQuick.Layouts 1.4 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.workspace.components 2.0 as PW - -import "providers" +import org.kde.plasma.private.mobileshell 1.0 as MobileShell RowLayout { - required property BatteryProvider provider - property real labelHeight + property MobileShell.BatteryProvider provider: MobileShell.BatteryProvider + property real textPixelSize: PlasmaCore.Units.gridUnit * 0.6 visible: provider.isVisible PW.BatteryIcon { @@ -37,6 +36,6 @@ RowLayout { Layout.alignment: Qt.AlignVCenter color: PlasmaCore.ColorScope.textColor - font.pixelSize: labelHeight + font.pixelSize: textPixelSize } } diff --git a/containments/panel/package/contents/ui/indicators/Bluetooth.qml b/components/mobileshell/qml/statusbar/indicators/BluetoothIndicator.qml similarity index 78% rename from containments/panel/package/contents/ui/indicators/Bluetooth.qml rename to components/mobileshell/qml/statusbar/indicators/BluetoothIndicator.qml index c802f968..92b85e42 100644 --- a/containments/panel/package/contents/ui/indicators/Bluetooth.qml +++ b/components/mobileshell/qml/statusbar/indicators/BluetoothIndicator.qml @@ -8,16 +8,17 @@ import QtQuick 2.2 import QtQuick.Layouts 1.4 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.bluezqt 1.0 as BluezQt -import "providers" +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +import org.kde.bluezqt 1.0 as BluezQt PlasmaCore.IconItem { id: connectionIcon - required property BluetoothProvider provider + property MobileShell.BluetoothProvider provider: MobileShell.BluetoothProvider - source: provider.icon; + source: provider.icon colorGroup: PlasmaCore.ColorScope.colorGroup visible: provider.isVisible diff --git a/look-and-feel/contents/lockscreen/indicators/SignalStrength.qml b/components/mobileshell/qml/statusbar/indicators/SignalStrengthIndicator.qml similarity index 65% rename from look-and-feel/contents/lockscreen/indicators/SignalStrength.qml rename to components/mobileshell/qml/statusbar/indicators/SignalStrengthIndicator.qml index 8250acd9..792eeaa1 100644 --- a/look-and-feel/contents/lockscreen/indicators/SignalStrength.qml +++ b/components/mobileshell/qml/statusbar/indicators/SignalStrengthIndicator.qml @@ -10,14 +10,16 @@ import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.mobileshell 1.0 as MobileShell Item { - required property QtObject provider - - property real labelPixelSize + property MobileShell.SignalStrengthProvider provider: MobileShell.SignalStrengthProvider - width: strengthIcon.height + label.width - Layout.minimumWidth: strengthIcon.height + label.width + property bool showLabel: true + property real textPixelSize: PlasmaCore.Units.gridUnit * 0.6 + + width: strengthIcon.width + label.width + Layout.minimumWidth: strengthIcon.width + label.width PlasmaCore.IconItem { id: strengthIcon @@ -29,15 +31,17 @@ Item { source: provider.icon } - + PlasmaComponents.Label { id: label + visible: showLabel + width: visible ? implicitWidth : 0 anchors.leftMargin: PlasmaCore.Units.smallSpacing anchors.left: strengthIcon.right anchors.verticalCenter: parent.verticalCenter text: provider.label color: PlasmaCore.ColorScope.textColor - font.pixelSize: labelPixelSize + font.pixelSize: textPixelSize } } diff --git a/containments/panel/package/contents/ui/indicators/Volume.qml b/components/mobileshell/qml/statusbar/indicators/VolumeIndicator.qml similarity index 82% rename from containments/panel/package/contents/ui/indicators/Volume.qml rename to components/mobileshell/qml/statusbar/indicators/VolumeIndicator.qml index ffdf04fa..ccec78b7 100644 --- a/containments/panel/package/contents/ui/indicators/Volume.qml +++ b/components/mobileshell/qml/statusbar/indicators/VolumeIndicator.qml @@ -10,12 +10,11 @@ import QtQuick 2.2 import QtQuick.Layouts 1.4 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.private.volume 0.1 - -import "providers" +import org.kde.plasma.private.mobileshell 1.0 as MobileShell PlasmaCore.IconItem { id: paIcon - required property VolumeProvider provider + property MobileShell.VolumeProvider provider: MobileShell.VolumeProvider Layout.fillHeight: true Layout.preferredWidth: height diff --git a/containments/panel/package/contents/ui/indicators/Wifi.qml b/components/mobileshell/qml/statusbar/indicators/WifiIndicator.qml similarity index 86% rename from containments/panel/package/contents/ui/indicators/Wifi.qml rename to components/mobileshell/qml/statusbar/indicators/WifiIndicator.qml index cae11d0c..5d696d1c 100644 --- a/containments/panel/package/contents/ui/indicators/Wifi.qml +++ b/components/mobileshell/qml/statusbar/indicators/WifiIndicator.qml @@ -11,12 +11,11 @@ import QtQuick.Layouts 1.4 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.networkmanagement 0.2 as PlasmaNM - -import "providers" +import org.kde.plasma.private.mobileshell 1.0 as MobileShell PlasmaCore.IconItem { id: connectionIcon - required property WifiProvider provider + property MobileShell.WifiProvider provider: MobileShell.WifiProvider source: provider.icon colorGroup: PlasmaCore.ColorScope.colorGroup diff --git a/components/mobileshell/qml/widgets/MediaControlsWidget.qml b/components/mobileshell/qml/widgets/MediaControlsWidget.qml new file mode 100644 index 00000000..73f4fd61 --- /dev/null +++ b/components/mobileshell/qml/widgets/MediaControlsWidget.qml @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import "../components" as Components +import "mediacontrols" + +Components.BaseItem { + id: root + + visible: mpris2Source.hasPlayer + padding: visible ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0 + implicitHeight: visible ? bottomPadding + topPadding + PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing : 0 + + background: BlurredBackground { + imageSource: mpris2Source.albumArt + } + + contentItem: PlasmaCore.ColorScope { + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + width: root.width - root.leftPadding - root.rightPadding + + MediaControlsSource { + id: mpris2Source + } + + RowLayout { + id: controlsRow + width: parent.width + height: parent.height + spacing: 0 + + enabled: mpris2Source.canControl + + Image { + id: albumArt + Layout.preferredWidth: height + Layout.fillHeight: true + asynchronous: true + fillMode: Image.PreserveAspectFit + source: mpris2Source.albumArt + sourceSize.height: height + visible: status === Image.Loading || status === Image.Ready + } + + ColumnLayout { + Layout.leftMargin: albumArt.visible ? Kirigami.Units.largeSpacing : 0 + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.NoWrap + elide: Text.ElideRight + text: mpris2Source.track || i18n("No media playing") + textFormat: Text.PlainText + font.pointSize: PlasmaCore.Theme.defaultFont.pointSize + maximumLineCount: 1 + color: "white" + } + + QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.NoWrap + elide: Text.ElideRight + // if no artist is given, show player name instead + text: mpris2Source.artist || mpris2Source.identity || "" + textFormat: Text.PlainText + font.pointSize: PlasmaCore.Theme.smallestFont.pointSize + maximumLineCount: 1 + opacity: 0.9 + color: "white" + } + } + + PlasmaComponents3.ToolButton { + Layout.fillHeight: true + Layout.preferredWidth: height + + enabled: mpris2Source.canGoBack + icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" + icon.width: PlasmaCore.Units.iconSizes.small + icon.height: PlasmaCore.Units.iconSizes.small + onClicked: mpris2Source.goPrevious() + visible: mpris2Source.canGoBack || mpris2Source.canGoNext + Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Previous track") + } + + PlasmaComponents3.ToolButton { + Layout.fillHeight: true + Layout.preferredWidth: height + + icon.name: mpris2Source.playing ? "media-playback-pause" : "media-playback-start" + icon.width: PlasmaCore.Units.iconSizes.small + icon.height: PlasmaCore.Units.iconSizes.small + onClicked: mpris2Source.playPause() + Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Play or Pause media") + } + + PlasmaComponents3.ToolButton { + Layout.fillHeight: true + Layout.preferredWidth: height + + enabled: mpris2Source.canGoNext + icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" + icon.width: PlasmaCore.Units.iconSizes.small + icon.height: PlasmaCore.Units.iconSizes.small + onClicked: mpris2Source.goNext() + visible: mpris2Source.canGoBack || mpris2Source.canGoNext + Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Next track") + } + } + } +} diff --git a/components/mobileshell/qml/widgets/NotificationsWidget.qml b/components/mobileshell/qml/widgets/NotificationsWidget.qml new file mode 100644 index 00000000..09c44817 --- /dev/null +++ b/components/mobileshell/qml/widgets/NotificationsWidget.qml @@ -0,0 +1,178 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.12 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +import org.kde.notificationmanager 1.0 as NotificationManager + +import "notifications" + +/** + * Embeddable notification list widget optimized for mobile and touch. + * Used on the lockscreen and action drawer. + */ +Item { + id: root + + property var historyModel: MobileShell.NotificationProvider.historyModel + property var notificationSettings: MobileShell.NotificationProvider.notificationSettings + + function clearHistory() { + historyModel.clear(NotificationManager.Notifications.ClearExpired); + } + + function openNotificationSettings() { + MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications"); + } + + PlasmaCore.DataSource { + id: timeDataSource + engine: "time" + connectedSources: ["Local"] + interval: 60000 // 1 min + intervalAlignment: PlasmaCore.Types.AlignToMinute + } + + ListView { + id: list + model: historyModel + currentIndex: -1 + + boundsBehavior: Flickable.StopAtBounds + spacing: Kirigami.Units.largeSpacing + + anchors.fill: parent + + // TODO keyboard focus + highlightMoveDuration: 0 + highlightResizeDuration: 0 + highlight: Item {} + + section { + property: "isInGroup" + criteria: ViewSection.FullString + } + + PlasmaExtras.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (PlasmaCore.Units.largeSpacing * 4) + + text: i18n("Notification service not available") + visible: list.count === 0 && !NotificationManager.Server.valid + + PlasmaComponents3.Label { + // Checking valid to avoid creating ServerInfo object if everything is alright + readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner : null + // PlasmaExtras.PlaceholderMessage is internally a ColumnLayout, so we can use Layout.whatever properties here + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: currentOwner ? i18nc("Vendor and product name", "Notifications are currently provided by '%1 %2'", currentOwner.vendor, currentOwner.name) : "" + visible: currentOwner && currentOwner.vendor && currentOwner.name + } + } + + add: Transition { + SequentialAnimation { + PropertyAction { property: "opacity"; value: 0 } + PauseAnimation { duration: PlasmaCore.Units.longDuration } + ParallelAnimation { + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: PlasmaCore.Units.longDuration } + NumberAnimation { property: "height"; from: 0; duration: PlasmaCore.Units.longDuration } + } + } + } + addDisplaced: Transition { + NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } + } + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { duration: PlasmaCore.Units.longDuration } + NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } + } + } + + function isRowExpanded(row) { + var idx = historyModel.index(row, 0); + return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole); + } + + function setGroupExpanded(row, expanded) { + var rowIdx = historyModel.index(row, 0); + var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx); + var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx)); + + historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole); + + // If the current item went away when the group collapsed, scroll to the group heading + if (!persistentRowIdx || !persistentRowIdx.valid) { + if (persistentGroupIdx && persistentGroupIdx.valid) { + list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain); + // When closed via keyboard, also set a sane current index + if (list.currentIndex > -1) { + list.currentIndex = persistentGroupIdx.row; + } + } + } + } + + delegate: Loader { + id: delegateLoader + width: list.width + sourceComponent: model.isGroup ? groupDelegate : notificationDelegate + + required property var model + required property int index + + Component { + id: groupDelegate + NotificationGroupHeader { + applicationName: model.applicationName + applicationIconSource: model.applicationIconName + originName: model.originName || "" + timeSource: timeDataSource + } + } + + Component { + id: notificationDelegate + ColumnLayout { + spacing: PlasmaCore.Units.smallSpacing + + NotificationItem { + Layout.fillWidth: true + + model: delegateLoader.model + modelIndex: delegateLoader.index + notificationsModel: historyModel + timeSource: timeDataSource + } + + PlasmaComponents3.ToolButton { + icon.name: model.isGroupExpanded ? "arrow-up" : "arrow-down" + text: model.isGroupExpanded ? i18n("Show Fewer") + : i18nc("Expand to show n more notifications", + "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) + visible: (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded) + && delegateLoader.ListView.nextSection !== delegateLoader.ListView.section + onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) + height: visible ? implicitHeight : 0 + } + } + } + } + } +} diff --git a/components/mobileshell/qml/widgets/mediacontrols/BlurredBackground.qml b/components/mobileshell/qml/widgets/mediacontrols/BlurredBackground.qml new file mode 100644 index 00000000..90b113d2 --- /dev/null +++ b/components/mobileshell/qml/widgets/mediacontrols/BlurredBackground.qml @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtGraphicalEffects 1.12 + +import org.kde.plasma.core 2.0 as PlasmaCore + +Item { + id: root + property string imageSource + + // clip corners so that the image has rounded corners + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: img.width + height: img.height + Rectangle { + anchors.centerIn: parent + width: img.width + height: img.height + radius: PlasmaCore.Units.smallSpacing + } + } + } + + Image { + id: img + source: root.imageSource + asynchronous: true + + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + + // ensure text is readable + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.6) + } + + // apply lighten, saturate and blur effect + layer.enabled: true + layer.effect: HueSaturation { + cached: true + + lightness: 0.2 + saturation: 1.5 + + layer.enabled: true + layer.effect: FastBlur { + cached: true + radius: 64 + transparentBorder: false + } + } + } +} diff --git a/components/mobileshell/qml/widgets/mediacontrols/MediaControlsSource.qml b/components/mobileshell/qml/widgets/mediacontrols/MediaControlsSource.qml new file mode 100644 index 00000000..097f19d8 --- /dev/null +++ b/components/mobileshell/qml/widgets/mediacontrols/MediaControlsSource.qml @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 + +import org.kde.plasma.core 2.0 as PlasmaCore + +PlasmaCore.DataSource { + id: mpris2Source + + readonly property string source: "@multiplex" + readonly property var playerData: data[source] + + readonly property bool hasPlayer: sources.length > 1 && !!playerData + readonly property string identity: hasPlayer ? playerData.Identity : "" + readonly property bool playing: hasPlayer && playerData.PlaybackStatus === "Playing" + readonly property bool canControl: hasPlayer && playerData.CanControl + readonly property bool canGoBack: hasPlayer && playerData.CanGoPrevious + readonly property bool canGoNext: hasPlayer && playerData.CanGoNext + + readonly property var currentMetadata: hasPlayer ? playerData.Metadata : ({}) + + readonly property string track: { + const xesamTitle = currentMetadata["xesam:title"] + if (xesamTitle) { + return xesamTitle + } + // if no track title is given, print out the file name + const xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : "" + if (!xesamUrl) { + return "" + } + const lastSlashPos = xesamUrl.lastIndexOf('/') + if (lastSlashPos < 0) { + return "" + } + const lastUrlPart = xesamUrl.substring(lastSlashPos + 1) + return decodeURIComponent(lastUrlPart) + } + readonly property string artist: currentMetadata["xesam:artist"] || "" + readonly property string albumArt: currentMetadata["mpris:artUrl"] || "" + + engine: "mpris2" + connectedSources: [source] + + function startOperation(op) { + var service = serviceForSource(source) + var operation = service.operationDescription(op) + return service.startOperationCall(operation) + } + + function goPrevious() { + startOperation("Previous"); + } + function goNext() { + startOperation("Next"); + } + function playPause(source) { + startOperation("PlayPause"); + } +} diff --git a/components/mobileshell/qml/widgets/notifications/BaseNotificationItem.qml b/components/mobileshell/qml/widgets/notifications/BaseNotificationItem.qml new file mode 100644 index 00000000..47060141 --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/BaseNotificationItem.qml @@ -0,0 +1,141 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.12 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.notificationmanager 1.0 as NotificationManager + +import org.kde.kcoreaddons 1.0 as KCoreAddons + +Item { + id: notificationItem + required property NotificationManager.Notifications notificationsModel + + property var model + property int modelIndex + + property PlasmaCore.DataSource timeSource + + readonly property int notificationType: model.type + + readonly property bool inGroup: model.isInGroup + readonly property bool inHistory: true + + readonly property string applicationIconSource: model.applicationIconName + readonly property string applicationName: model.applicationName + readonly property string originName: model.originName || "" + + readonly property string summary: model.summary + readonly property var time: model.updated || model.created + + readonly property bool hasReplyAction: model.hasReplyAction || false + readonly property string replyActionLabel: model.replyActionLabel || "" + readonly property string replyPlaceholderText: model.replyPlaceholderText || "" + readonly property string replySubmitButtonText: model.replySubmitButtonText || "" + readonly property string replySubmitButtonIconName: model.replySubmitButtonIconName || "" + + // configure button on every single notifications is bit overwhelming + readonly property bool configurable: !inGroup && model.configurable + + readonly property bool dismissable: model.type === NotificationManager.Notifications.JobType + && model.jobState !== NotificationManager.Notifications.JobStateStopped + && model.dismissed + && notificationSettings.permanentJobPopups + readonly property bool dismissed: model.dismissed || false + readonly property bool closable: model.closable + + readonly property string body: model.body || "" + readonly property var icon: model.image || model.iconName + + readonly property var urls: model.urls || [] + + readonly property int jobState: model.jobState || 0 + readonly property int percentage: model.percentage || 0 + readonly property int jobError: model.jobError || 0 + readonly property bool suspendable: !!model.suspendable + readonly property bool killable: !!model.killable + + readonly property QtObject jobDetails: model.jobDetails || null + + readonly property string configureActionLabel: model.configureActionLabel || "" + readonly property bool hasDefaultAction: model.hasDefaultAction + readonly property bool addDefaultAction: (model.hasDefaultAction + && model.defaultActionLabel + && (model.actionLabels || []).indexOf(model.defaultActionLabel) === -1) ? true : false + readonly property var actionNames: { + var actions = (model.actionNames || []); + if (addDefaultAction) { + actions.unshift("default"); // prepend + } + return actions; + } + readonly property var actionLabels: { + var labels = (model.actionLabels || []); + if (addDefaultAction) { + labels.unshift(model.defaultActionLabel); + } + return labels; + } + + signal actionInvoked(string actionName) + signal replied(string text) + signal openUrl(string url) + signal fileActionInvoked(QtObject action) + + signal suspendJobClicked + signal resumeJobClicked + signal killJobClicked + + onActionInvoked: { + if (actionName === "default") { + notificationsModel.invokeDefaultAction(notificationsModel.index(modelIndex, 0)); + } else { + notificationsModel.invokeAction(notificationsModel.index(modelIndex, 0), actionName); + } + + expire(); + } + onOpenUrl: { + Qt.openUrlExternally(url); + expire(); + } + onFileActionInvoked: { + if (action.objectName === "movetotrash" || action.objectName === "deletefile") { + close(); + } else { + expire(); + } + } + onSuspendJobClicked: notificationsModel.suspendJob(notificationsModel.index(modelIndex, 0)) + onResumeJobClicked: notificationsModel.resumeJob(notificationsModel.index(modelIndex, 0)) + onKillJobClicked: notificationsModel.killJob(notificationsModel.index(modelIndex, 0)) + + function expire() { + if (model.resident) { + model.expired = true; + } else { + notificationsModel.expire(notificationsModel.index(modelIndex, 0)); + } + } + + function close() { + notificationsModel.close(notificationsModel.index(modelIndex, 0)); + } + + // TODO call + function configure() { + notificationsModel.configure(notificationsModel.index(modelIndex, 0)) + } +} + diff --git a/components/mobileshell/qml/widgets/notifications/NotificationBodyLabel.qml b/components/mobileshell/qml/widgets/notifications/NotificationBodyLabel.qml new file mode 100644 index 00000000..20f141dc --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/NotificationBodyLabel.qml @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2011 Marco Martin + * SPDX-FileCopyrightText: 2014, 2019 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.15 +import QtQuick.Window 2.2 + +import org.kde.plasma.components 3.0 as PlasmaComponents + +PlasmaComponents.Label { + id: bodyText + + background: Item {} + // Work around Qt bug where NativeRendering breaks for non-integer scale factors + // https://bugreports.qt.io/browse/QTBUG-67007 + renderType: Screen.devicePixelRatio % 1 !== 0 ? Text.QtRendering : Text.NativeRendering + + opacity: 0.6 + maximumLineCount: 3 + elide: Text.ElideRight + wrapMode: Text.Wrap + textFormat: TextEdit.RichText +} + diff --git a/components/mobileshell/qml/widgets/notifications/NotificationCard.qml b/components/mobileshell/qml/widgets/notifications/NotificationCard.qml new file mode 100644 index 00000000..ba8c9e5e --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/NotificationCard.qml @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.15 +import QtGraphicalEffects 1.12 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents + +Item { + id: root + + default property Item contentItem + + property bool tapEnabled: false + + property bool swipeGestureEnabled: false + + property real dragOffset: 0 + + signal tapped() + signal dismissRequested() + signal configureClicked() // TODO implement settings button + + onContentItemChanged: { + contentItem.parent = contentParent; + contentItem.anchors.fill = contentParent; + contentItem.anchors.margins = Kirigami.Units.largeSpacing; + contentParent.children.push(contentItem); + } + + implicitHeight: contentParent.implicitHeight + + NumberAnimation on dragOffset { + id: dragAnim + duration: PlasmaCore.Units.longDuration + onFinished: { + if (to !== 0) { + root.dismissRequested(); + } + } + } + + // glow + RectangularGlow { + visible: Math.abs(dragOffset) !== root.width + anchors.topMargin: 1 + anchors.leftMargin: 1 + anchors.fill: mainCard + cornerRadius: mainCard.radius * 2 + glowRadius: 2 + spread: 0.2 + color: Qt.lighter(PlasmaCore.Theme.backgroundColor, 0.1) + } + + // shadow + Rectangle { + visible: Math.abs(dragOffset) !== root.width + anchors.fill: mainCard + anchors.leftMargin: -1 + anchors.rightMargin: -1 + anchors.bottomMargin: -1 + + color: Qt.darker(PlasmaCore.Theme.backgroundColor, 1.3) + radius: PlasmaCore.Units.smallSpacing + } + + // card + Rectangle { + id: mainCard + anchors.left: parent.left + anchors.leftMargin: root.dragOffset > 0 ? root.dragOffset : 0 + anchors.right: parent.right + anchors.rightMargin: root.dragOffset < 0 ? -root.dragOffset : 0 + anchors.top: parent.top + + color: (root.tapEnabled && mouseArea.pressed) ? Qt.darker(PlasmaCore.Theme.backgroundColor, 1.1) : PlasmaCore.Theme.backgroundColor + radius: PlasmaCore.Units.smallSpacing + implicitHeight: contentParent.implicitHeight + clip: true + + // ensure this is behind the content to not interfere + MouseArea { + id: mouseArea + anchors.fill: parent + onClicked: { + if (root.tapEnabled) { + root.tapped() + } + } + } + + // content parent + Item { + id: contentParent + anchors.top: parent.top + anchors.left: root.dragOffset > 0 ? parent.left : undefined + anchors.right: root.dragOffset < 0 ? parent.right : undefined + + width: root.width + implicitHeight: contentItem.implicitHeight + contentItem.anchors.topMargin + contentItem.anchors.bottomMargin + } + } + + DragHandler { + id: dragHandler + enabled: root.swipeGestureEnabled + yAxis.enabled: false + + property real startDragOffset: 0 + property real startPosition: 0 + property bool startActive: false + + onTranslationChanged: { + if (startActive) { + startDragOffset = root.dragOffset; + startPosition = translation.x; + startActive = false; + } + root.dragOffset = startDragOffset + (translation.x - startPosition); + } + + onActiveChanged: { + dragAnim.stop(); + startActive = active; + + if (!active) { // release event + let threshold = PlasmaCore.Units.gridUnit * 5; // drag threshold + if (root.dragOffset > threshold) { + dragAnim.to = root.width; + } else if (root.dragOffset < -threshold) { + dragAnim.to = -root.width; + } else { + dragAnim.to = 0; + } + dragAnim.restart(); + } + } + } +} diff --git a/components/mobileshell/qml/widgets/notifications/NotificationFooterActions.qml b/components/mobileshell/qml/widgets/notifications/NotificationFooterActions.qml new file mode 100644 index 00000000..3d346ffe --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/NotificationFooterActions.qml @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents + +Item { + id: actionContainer + + required property BaseNotificationItem notification + + implicitHeight: Math.max(actionFlow.implicitHeight, replyLoader.height) + visible: actionRepeater.count > 0 + + Flow { + id: actionFlow + width: parent.width + spacing: PlasmaCore.Units.smallSpacing + layoutDirection: Qt.RightToLeft + enabled: !replyLoader.active + opacity: replyLoader.active ? 0 : 1 + + Behavior on opacity { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + // action buttons + Repeater { + id: actionRepeater + + model: { + const buttons = []; + var actionNames = (notificationItem.actionNames || []); + var actionLabels = (notificationItem.actionLabels || []); + for (var i = actionNames.length - 1; i >= 0; --i) { + buttons.push({ + actionName: actionNames[i], + label: actionLabels[i] + }); + } + + if (notificationItem.hasReplyAction) { + buttons.unshift({ + actionName: "inline-reply", + label: notificationItem.replyActionLabel || i18nc("Reply to message", "Reply") + }); + } + + return buttons; + } + + PlasmaComponents.ToolButton { + flat: false + text: modelData.label || "" + + onClicked: { + if (modelData.actionName === "inline-reply") { + replyLoader.beginReply(); + return; + } + notificationItem.actionInvoked(modelData.actionName); + } + } + } + } + + // inline reply field + Loader { + id: replyLoader + width: parent.width + height: active ? item.implicitHeight : 0 + + // When there is only one action and it is a reply action, show text field right away + active: false + visible: active + opacity: active ? 1 : 0 + x: active ? 0 : parent.width + + Behavior on x { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + Behavior on opacity { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + function beginReply() { + active = true + replyLoader.item.activate(); + } + + sourceComponent: NotificationReplyField { + placeholderText: notificationItem.replyPlaceholderText + buttonIconName: notificationItem.replySubmitButtonIconName + buttonText: notificationItem.replySubmitButtonText + onReplied: notificationItem.replied(text) + + replying: replyLoader.active + onBeginReplyRequested: replyLoader.beginReply() + } + } +} diff --git a/components/mobileshell/qml/widgets/notifications/NotificationGroupHeader.qml b/components/mobileshell/qml/widgets/notifications/NotificationGroupHeader.qml new file mode 100644 index 00000000..d4b5184a --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/NotificationGroupHeader.qml @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.notificationmanager 1.0 as NotificationManager + +import org.kde.kcoreaddons 1.0 as KCoreAddons + +import "util.js" as Util + +RowLayout { + id: notificationHeading + property int notificationType + + property var applicationIconSource + property string applicationName + property string originName + + property var time + property PlasmaCore.DataSource timeSource + + property int jobState + property QtObject jobDetails + + property real timeout: 5000 + property real remainingTime: 0 + + spacing: PlasmaCore.Units.smallSpacing + Layout.preferredHeight: Math.max(applicationNameLabel.implicitHeight, PlasmaCore.Units.iconSizes.small) + + PlasmaCore.IconItem { + id: applicationIconItem + Layout.topMargin: PlasmaCore.Units.smallSpacing + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + Layout.preferredWidth: PlasmaCore.Units.iconSizes.small + Layout.preferredHeight: PlasmaCore.Units.iconSizes.small + source: notificationHeading.applicationIconSource + usesPlasmaTheme: false + visible: valid + } + + PlasmaComponents.Label { + id: applicationNameLabel + Layout.leftMargin: PlasmaCore.Units.smallSpacing + Layout.fillWidth: true + opacity: 0.8 + textFormat: Text.PlainText + elide: Text.ElideLeft + text: notificationHeading.applicationName + (notificationHeading.originName ? " · " + notificationHeading.originName : "") + } + + Item { + id: spacer + Layout.fillWidth: true + } +} diff --git a/components/mobileshell/qml/widgets/notifications/NotificationItem.qml b/components/mobileshell/qml/widgets/notifications/NotificationItem.qml new file mode 100644 index 00000000..0d7c2589 --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/NotificationItem.qml @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.notificationmanager 1.0 as NotificationManager + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.kcoreaddons 1.0 as KCoreAddons + +import "util.js" as Util + +// notification properties are in BaseNotificationItem +BaseNotificationItem { + id: notificationItem + implicitHeight: mainCard.implicitHeight + + // notification heading for groups with one element + NotificationGroupHeader { + id: notificationHeading + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + PlasmaCore.ColorScope.colorGroup: PlasmaCore.Theme.HeaderColorGroup + PlasmaCore.ColorScope.inherit: false + + visible: !notificationItem.inGroup + height: visible ? implicitHeight : 0 + + applicationName: notificationItem.applicationName + applicationIconSource: notificationItem.applicationIconSource + originName: notificationItem.originName + + notificationType: notificationItem.notificationType + jobState: notificationItem.jobState + jobDetails: notificationItem.jobDetails + + time: notificationItem.time + timeSource: notificationItem.timeSource + } + + // notification + NotificationCard { + id: mainCard + anchors.topMargin: notificationHeading.visible ? Kirigami.Units.largeSpacing : 0 + anchors.top: notificationHeading.bottom + anchors.left: parent.left + anchors.right: parent.right + + tapEnabled: notificationItem.hasDefaultAction + onTapped: notificationItem.actionInvoked("default"); + swipeGestureEnabled: notificationItem.notificationType != NotificationManager.Notifications.JobType + onDismissRequested: notificationItem.close() + + ColumnLayout { + id: column + spacing: 0 + + // notification summary row + RowLayout { + Layout.fillWidth: true + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + + // notification summary + PlasmaComponents.Label { + id: summaryLabel + Layout.fillWidth: true + textFormat: Text.PlainText + maximumLineCount: 3 + wrapMode: Text.WordWrap + elide: Text.ElideRight + text: Util.determineNotificationHeadingText(notificationItem) + visible: text !== "" + font.weight: Font.DemiBold + } + + // notification timestamp + NotificationTimeText { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + notificationType: notificationItem.notificationType + jobState: notificationItem.jobState + jobDetails: notificationItem.jobDetails + + time: notificationItem.time + timeSource: notificationItem.timeSource + } + } + + // notification contents + RowLayout { + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + + // notification text + NotificationBodyLabel { + id: bodyLabel + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.fillWidth: true + + // HACK RichText does not allow to specify link color and since LineEdit + // does not support StyledText, we have to inject some CSS to force the color, + // cf. QTBUG-81463 and to some extent QTBUG-80354 + text: "" + notificationItem.body + + // Cannot do text !== "" because RichText adds some HTML tags even when empty + visible: notificationItem.body !== "" + } + + // notification icon + Item { + id: iconContainer + Layout.preferredWidth: PlasmaCore.Units.iconSizes.large + Layout.preferredHeight: PlasmaCore.Units.iconSizes.large + Layout.topMargin: PlasmaCore.Units.smallSpacing + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + + visible: iconItem.active + + PlasmaCore.IconItem { + id: iconItem + // don't show two identical icons + readonly property bool active: valid && source != notificationItem.applicationIconSource + anchors.fill: parent + usesPlasmaTheme: false + smooth: true + // don't show a generic "info" icon since this is a notification already + source: notificationItem.icon !== "dialog-information" ? notificationItem.icon : "" + visible: active + } + } + } + + // notification actions + NotificationFooterActions { + Layout.fillWidth: true + notification: notificationItem + } + + // thumbnails + Loader { + id: thumbnailStripLoader + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + active: notificationItem.urls.length > 0 + visible: active + asynchronous: true + sourceComponent: ThumbnailStrip { + leftPadding: -thumbnailStripLoader.Layout.leftMargin + rightPadding: -thumbnailStripLoader.Layout.rightMargin + topPadding: -notificationItem.thumbnailTopPadding + bottomPadding: -thumbnailStripLoader.Layout.bottomMargin + urls: notificationItem.urls + onOpenUrl: notificationItem.openUrl(url) + onFileActionInvoked: notificationItem.fileActionInvoked(action) + } + } + } + } +} diff --git a/components/mobileshell/qml/widgets/notifications/NotificationReplyField.qml b/components/mobileshell/qml/widgets/notifications/NotificationReplyField.qml new file mode 100644 index 00000000..c3f3b907 --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/NotificationReplyField.qml @@ -0,0 +1,59 @@ + +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +RowLayout { + id: replyRow + + signal beginReplyRequested + signal replied(string text) + + property bool replying: false + + property alias text: replyTextField.text + property string placeholderText + property string buttonIconName + property string buttonText + + spacing: PlasmaCore.Units.smallSpacing + + function activate() { + replyTextField.forceActiveFocus(); + } + + PlasmaComponents3.TextField { + id: replyTextField + Layout.fillWidth: true + placeholderText: replyRow.placeholderText || i18nc("Text field placeholder", "Type a reply…") + onAccepted: { + if (replyButton.enabled) { + replyRow.replied(text); + } + } + + // Catches mouse click when reply field is already shown to start a reply + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + visible: !replyRow.replying + onPressed: replyRow.beginReplyRequested() + } + } + + PlasmaComponents3.Button { + id: replyButton + icon.name: replyRow.buttonIconName || "document-send" + text: replyRow.buttonText || i18nc("@action:button", "Send") + enabled: replyTextField.length > 0 + onClicked: replyRow.replied(replyTextField.text) + } +} diff --git a/components/mobileshell/qml/widgets/notifications/NotificationTimeText.qml b/components/mobileshell/qml/widgets/notifications/NotificationTimeText.qml new file mode 100644 index 00000000..708a7fff --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/NotificationTimeText.qml @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.notificationmanager 1.0 as NotificationManager + +import org.kde.kcoreaddons 1.0 as KCoreAddons + +import "util.js" as Util + +PlasmaComponents.Label { + id: ageLabel + + property int notificationType: model.type + property int jobState + property QtObject jobDetails + + property var time + property PlasmaCore.DataSource timeSource + + // notification created/updated time changed + onTimeChanged: updateAgoText() + + Connections { + target: timeSource + // clock time changed + function onDataChanged() { + ageLabel.updateAgoText() + } + } + + Component.onCompleted: updateAgoText() + + function updateAgoText() { + ageLabel.agoText = Util.generateNotificationHeaderAgoText(time, jobState); + } + + font.pixelSize: PlasmaCore.Theme.defaultFont.pixelSize * 0.8 + + // the "n minutes ago" text, for jobs we show remaining time instead + // updated periodically by a Timer hence this property with generate() function + property string agoText: "" + visible: text !== "" + opacity: 0.6 + text: Util.generateNotificationHeaderRemainingText(notificationType, jobState, jobDetails) || agoText +} diff --git a/components/mobileshell/qml/widgets/notifications/ThumbnailStrip.qml b/components/mobileshell/qml/widgets/notifications/ThumbnailStrip.qml new file mode 100644 index 00000000..3d391ecf --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/ThumbnailStrip.qml @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + * + * SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import QtGraphicalEffects 1.0 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons + +MouseArea { + id: thumbnailArea + + // The protocol supports multiple URLs but so far it's only used to show + // a single preview image, so this code is simplified a lot to accommodate + // this usecase and drops everything else (fallback to app icon or ListView + // for multiple files) + property var urls + + readonly property alias menuOpen: fileMenu.visible + + property int _pressX: -1 + property int _pressY: -1 + + property int leftPadding: 0 + property int rightPadding: 0 + property int topPadding: 0 + property int bottomPadding: 0 + + signal openUrl(string url) + signal fileActionInvoked(QtObject action) + + implicitHeight: Math.max(menuButton.height + 2 * menuButton.anchors.topMargin, + Math.round(Math.min(width / 3, width / thumbnailer.ratio))) + + topPadding + bottomPadding + + MobileShell.NotificationFileMenu { + id: fileMenu + url: thumbnailer.url + visualParent: menuButton + onActionTriggered: thumbnailArea.fileActionInvoked(action) + } + + MobileShell.NotificationThumbnailer { + id: thumbnailer + + readonly property real ratio: pixmapSize.height ? pixmapSize.width / pixmapSize.height : 1 + + url: urls[0] + // height is dynamic, so request a "square" size and then show it fitting to aspect ratio + size: Qt.size(thumbnailArea.width, thumbnailArea.width) + } + + KQCAddons.QPixmapItem { + id: previewBackground + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + layer.enabled: true + opacity: 0.25 + pixmap: thumbnailer.pixmap + layer.effect: FastBlur { + source: previewBackground + anchors.fill: parent + radius: 30 + } + } + + Item { + anchors { + fill: parent + leftMargin: thumbnailArea.leftPadding + rightMargin: thumbnailArea.rightPadding + topMargin: thumbnailArea.topPadding + bottomMargin: thumbnailArea.bottomPadding + } + + KQCAddons.QPixmapItem { + id: previewPixmap + anchors.fill: parent + pixmap: thumbnailer.pixmap + smooth: true + fillMode: Image.PreserveAspectFit + } + + PlasmaCore.IconItem { + anchors.centerIn: parent + width: height + height: PlasmaCore.Units.roundToIconSize(parent.height) + usesPlasmaTheme: false + source: !thumbnailer.busy && !thumbnailer.hasPreview ? thumbnailer.iconName : "" + } + + PlasmaComponents3.BusyIndicator { + anchors.centerIn: parent + running: thumbnailer.busy + visible: thumbnailer.busy + } + + PlasmaComponents3.Button { + id: menuButton + anchors { + top: parent.top + right: parent.right + margins: PlasmaCore.Units.smallSpacing + } + Accessible.name: tooltip.text + icon.name: "application-menu" + checkable: true + + onPressedChanged: { + if (pressed) { + // fake "pressed" while menu is open + checked = Qt.binding(function() { + return fileMenu.visible; + }); + + fileMenu.visualParent = this; + // -1 tells it to "align bottom left of visualParent (this)" + fileMenu.open(-1, -1); + } + } + + PlasmaComponents3.ToolTip { + id: tooltip + text: i18n("More Options…") + } + } + } +} diff --git a/components/mobileshell/qml/widgets/notifications/util.js b/components/mobileshell/qml/widgets/notifications/util.js new file mode 100644 index 00000000..17b26098 --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/util.js @@ -0,0 +1,85 @@ +.import org.kde.notificationmanager 1.0 as NotificationManager +.import QtQml 2.15 as QtQml + +function determineNotificationHeadingText(notificationItem) { + if (notificationItem.notificationType === NotificationManager.Notifications.JobType) { + if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) { + if (notificationItem.summary) { + return i18nc("Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary); + } + } else if (notificationItem.jobState === NotificationManager.Notifications.JobStateStopped) { + if (notificationItem.jobError) { + if (notificationItem.summary) { + return i18nc("Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary); + } else { + return i18n("Job Failed"); + } + } else if (notificationItem.summary) { + return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary); + } + return i18nd("plasma_applet_org.kde.plasma.notifications", "Job Finished"); + } + } + // some apps use their app name as summary, avoid showing the same text twice + // try very hard to match the two + if (notificationItem.summary && notificationItem.summary.toLocaleLowerCase().trim() !== notificationItem.applicationName.toLocaleLowerCase().trim()) { + return notificationItem.summary; + } + return ""; +} + +function generateNotificationHeaderAgoText(time, jobState) { + if (!time || isNaN(time.getTime()) || jobState === NotificationManager.Notifications.JobStateRunning) { + return ""; + } + + const deltaMinutes = Math.floor((Date.now() - time.getTime()) / 1000 / 60); + if (deltaMinutes < 1) { + return ""; + } + + // Received less than an hour ago, show relative minutes + if (deltaMinutes < 60) { + return i18nc("Notification was added minutes ago, keep short", "%1m ago", deltaMinutes); + } + // Received less than a day ago, show time, 22 hours so the time isn't as ambiguous between today and yesterday + if (deltaMinutes < 60 * 22) { + return Qt.formatTime(time, Qt.locale().timeFormat(QtQml.Locale.ShortFormat).replace(/.ss?/i, "")); + } + + // Otherwise show relative date (Yesterday, "Last Sunday", or just date if too far in the past) + return KCoreAddons.Format.formatRelativeDate(time, QtQml.Locale.ShortFormat); +} + +function generateNotificationHeaderRemainingText(notificationType, jobState, jobDetails) { + if (notificationType !== NotificationManager.Notifications.JobType || jobState !== NotificationManager.Notifications.JobStateRunning) { + return ""; + } + + const details = jobDetails; + if (!details || !details.speed) { + return ""; + } + + var remaining = details.totalBytes - details.processedBytes; + if (remaining <= 0) { + return ""; + } + + var eta = remaining / details.speed; + if (eta < 0.5) { // Avoid showing "0 seconds remaining" + return ""; + } + + if (eta < 60) { // 1 minute + return i18nc("seconds remaining, keep short", "%1 s remaining", Math.round(eta)); + } + if (eta < 60 * 60) {// 1 hour + return i18nc("minutes remaining, keep short", "%1m remaining", Math.round(eta / 60)); + } + if (eta < 60 * 60 * 5) { // 5 hours max, if it takes even longer there's no real point in showing that + return i18nc("hours remaining, keep short", "%1h remaining", Math.round(eta / 60 / 60)); + } + + return ""; +} diff --git a/components/mobileshell/qmldir b/components/mobileshell/qmldir deleted file mode 100644 index 37fd2cd2..00000000 --- a/components/mobileshell/qmldir +++ /dev/null @@ -1,4 +0,0 @@ -module org.kde.plasma.private.mobileshell - -singleton HomeScreenControls 1.0 HomeScreenControls.qml -singleton TopPanelControls 1.0 TopPanelControls.qml diff --git a/components/mobilehomescreencomponents/quicksettingsmodel.cpp b/components/mobileshell/quicksettingsmodel.cpp similarity index 95% rename from components/mobilehomescreencomponents/quicksettingsmodel.cpp rename to components/mobileshell/quicksettingsmodel.cpp index 72f5dc2e..d07bff48 100644 --- a/components/mobilehomescreencomponents/quicksettingsmodel.cpp +++ b/components/mobileshell/quicksettingsmodel.cpp @@ -123,6 +123,15 @@ void QuickSetting::setText(const QString &text) Q_EMIT textChanged(text); } +void QuickSetting::setStatus(const QString &status) +{ + if (m_status == status) + return; + + m_status = status; + Q_EMIT statusChanged(status); +} + QQmlListProperty QuickSetting::children() { return QQmlListProperty(this, &m_children); diff --git a/components/mobilehomescreencomponents/quicksettingsmodel.h b/components/mobileshell/quicksettingsmodel.h similarity index 88% rename from components/mobilehomescreencomponents/quicksettingsmodel.h rename to components/mobileshell/quicksettingsmodel.h index 080120b4..ae5a2052 100644 --- a/components/mobilehomescreencomponents/quicksettingsmodel.h +++ b/components/mobileshell/quicksettingsmodel.h @@ -15,6 +15,7 @@ class QuickSetting : public QObject { Q_OBJECT Q_PROPERTY(QString text READ text WRITE setText REQUIRED NOTIFY textChanged) + Q_PROPERTY(QString status READ status WRITE setStatus REQUIRED NOTIFY statusChanged) // if no status is explicitly set, On/Off is used by default Q_PROPERTY(QString icon READ iconName WRITE setIconName REQUIRED NOTIFY iconNameChanged) Q_PROPERTY(QString settingsCommand READ settingsCommand WRITE setSettingsCommand NOTIFY settingsCommandChanged) Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) @@ -28,6 +29,10 @@ public: { return m_text; } + QString status() const + { + return m_status; + } QString iconName() const { return m_iconName; @@ -42,6 +47,7 @@ public: } void setText(const QString &text); + void setStatus(const QString &status); void setIconName(const QString &iconName); void setSettingsCommand(const QString &settingsCommand); void setEnabled(bool enabled); @@ -50,12 +56,14 @@ public: Q_SIGNALS: void enabledChanged(bool enabled); void textChanged(const QString &text); + void statusChanged(const QString &text); void iconNameChanged(const QString &icon); void settingsCommandChanged(const QString &settingsCommand); private: bool m_enabled = true; QString m_text; + QString m_status; QString m_iconName; QString m_settingsCommand; QList m_children; diff --git a/components/mobileshell/shellutil.cpp b/components/mobileshell/shellutil.cpp new file mode 100644 index 00000000..47fe7d98 --- /dev/null +++ b/components/mobileshell/shellutil.cpp @@ -0,0 +1,245 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * SPDX-FileCopyrightText: 2018 Bhushan Shah + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "shellutil.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FORMAT24H "HH:mm:ss" + +constexpr int SCREENSHOT_DELAY = 200; + +/* -- Static Helpers --------------------------------------------------------------------------- */ + +static QImage allocateImage(const QVariantMap &metadata) +{ + bool ok; + + const uint width = metadata.value(QStringLiteral("width")).toUInt(&ok); + if (!ok) { + return QImage(); + } + + const uint height = metadata.value(QStringLiteral("height")).toUInt(&ok); + if (!ok) { + return QImage(); + } + + const uint format = metadata.value(QStringLiteral("format")).toUInt(&ok); + if (!ok) { + return QImage(); + } + + return QImage(width, height, QImage::Format(format)); +} + +static QImage readImage(int fileDescriptor, const QVariantMap &metadata) +{ + QFile file; + if (!file.open(fileDescriptor, QFileDevice::ReadOnly, QFileDevice::AutoCloseHandle)) { + close(fileDescriptor); + return QImage(); + } + + QImage result = allocateImage(metadata); + if (result.isNull()) { + return QImage(); + } + + QDataStream stream(&file); + stream.readRawData(reinterpret_cast(result.bits()), result.sizeInBytes()); + + return result; +} + +ShellUtil::ShellUtil(QObject *parent) + : QObject{parent} +{ + // setHasConfigurationInterface(true); + m_kscreenInterface = new org::kde::KScreen(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/kscreen"), QDBusConnection::sessionBus(), this); + m_screenshotInterface = new OrgKdeKWinScreenShot2Interface(QStringLiteral("org.kde.KWin.ScreenShot2"), + QStringLiteral("/org/kde/KWin/ScreenShot2"), + QDBusConnection::sessionBus(), + this); + + m_localeConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig); + m_localeConfigWatcher = KConfigWatcher::create(m_localeConfig); + + // watch for changes to locale config, to update 12/24 hour time + connect(m_localeConfigWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void { + if (group.name() == "Locale") { + // we have to reparse for new changes (from system settings) + m_localeConfig->reparseConfiguration(); + Q_EMIT isSystem24HourFormatChanged(); + } + }); +} + +ShellUtil::~ShellUtil() = default; + +ShellUtil *ShellUtil::instance() +{ + static ShellUtil *inst = new ShellUtil(nullptr); + return inst; +} + +void ShellUtil::executeCommand(const QString &command) +{ + qWarning() << "Executing" << command; + const QStringList commandAndArguments = QProcess::splitCommand(command); + QProcess::startDetached(commandAndArguments.front(), commandAndArguments.mid(1)); +} + +void ShellUtil::toggleTorch() +{ + // FIXME this is hardcoded to the PinePhone for now + static auto FLASH_SYSFS_PATH = "/sys/devices/platform/led-controller/leds/white:flash/brightness"; + int fd = open(FLASH_SYSFS_PATH, O_WRONLY); + + if (fd < 0) { + qWarning() << "Unable to open file %s" << FLASH_SYSFS_PATH; + return; + } + + write(fd, m_running ? "0" : "1", 1); + close(fd); + m_running = !m_running; + Q_EMIT torchChanged(m_running); +} +bool ShellUtil::torchEnabled() const +{ + return m_running; +} +bool ShellUtil::autoRotate() +{ + QDBusPendingReply reply = m_kscreenInterface->getAutoRotate(); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << "Getting auto rotate failed:" << reply.error().name() << reply.error().message(); + return false; + } else { + return reply.value(); + } +} + +void ShellUtil::setAutoRotate(bool value) +{ + QDBusPendingReply<> reply = m_kscreenInterface->setAutoRotate(value); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << "Setting auto rotate failed:" << reply.error().name() << reply.error().message(); + } else { + emit autoRotateChanged(value); + } +} + +void ShellUtil::handleMetaDataReceived(const QVariantMap &metadata, int fd) +{ + const QString type = metadata.value(QStringLiteral("type")).toString(); + if (type != QLatin1String("raw")) { + qWarning() << "Unsupported metadata type:" << type; + return; + } + + auto watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [watcher]() { + watcher->deleteLater(); + + QString filePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + if (filePath.isEmpty()) { + qWarning() << "Couldn't find a writable location for the screenshot!"; + return; + } + QDir picturesDir(filePath); + if (!picturesDir.mkpath(QStringLiteral("Screenshots"))) { + qWarning() << "Couldn't create folder at" << picturesDir.path() + QStringLiteral("/Screenshots") << "to take screenshot."; + return; + } + filePath += QStringLiteral("/Screenshots/Screenshot_%1.png").arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_hhmmss"))); + const auto m_result = watcher->result(); + if (m_result.isNull() || !m_result.save(filePath)) { + qWarning() << "Screenshot failed"; + } else { + KNotification *notif = new KNotification("captured"); + notif->setComponentName(QStringLiteral("plasma_phone_components")); + notif->setTitle(i18n("New Screenshot")); + notif->setUrls({QUrl::fromLocalFile(filePath)}); + notif->setText(i18n("New screenshot saved to %1", filePath)); + notif->sendEvent(); + } + }); + watcher->setFuture(QtConcurrent::run(readImage, fd, metadata)); +} + +void ShellUtil::takeScreenshot() +{ + // wait ~200 ms to wait for rest of animations + QTimer::singleShot(SCREENSHOT_DELAY, [=]() { + int lPipeFds[2]; + if (pipe2(lPipeFds, O_CLOEXEC) != 0) { + qWarning() << "Could not take screenshot"; + return; + } + + // We don't have access to the ScreenPool so we'll just take the first screen + QVariantMap options; + options.insert(QStringLiteral("native-resolution"), true); + + auto pendingCall = m_screenshotInterface->CaptureScreen(qGuiApp->screens().constFirst()->name(), options, QDBusUnixFileDescriptor(lPipeFds[1])); + close(lPipeFds[1]); + auto pipeFileDescriptor = lPipeFds[0]; + + auto watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, pipeFileDescriptor]() { + watcher->deleteLater(); + const QDBusPendingReply reply = *watcher; + + if (reply.isError()) { + qWarning() << "Screenshot request failed:" << reply.error().message(); + } else { + handleMetaDataReceived(reply, pipeFileDescriptor); + } + }); + }); +} + +bool ShellUtil::isSystem24HourFormat() +{ + KConfigGroup localeSettings = KConfigGroup(m_localeConfig, "Locale"); + + QString timeFormat = localeSettings.readEntry("TimeFormat", QStringLiteral(FORMAT24H)); + return timeFormat == QStringLiteral(FORMAT24H); +} + +void ShellUtil::launchApp(const QString &app) +{ + const KService::Ptr appService = KService::serviceByDesktopName(app); + if (!appService) { + qWarning() << "Could not find" << app; + return; + } + auto job = new KIO::ApplicationLauncherJob(appService, this); + job->start(); +} diff --git a/components/mobileshell/shellutil.h b/components/mobileshell/shellutil.h new file mode 100644 index 00000000..1fb9e463 --- /dev/null +++ b/components/mobileshell/shellutil.h @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2015 Marco Martin + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +#include +#include + +#include "kscreeninterface.h" +#include "screenshot2interface.h" + +class ShellUtil : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool autoRotateEnabled READ autoRotate WRITE setAutoRotate NOTIFY autoRotateChanged); + Q_PROPERTY(bool torchEnabled READ torchEnabled NOTIFY torchChanged); + Q_PROPERTY(bool isSystem24HourFormat READ isSystem24HourFormat NOTIFY isSystem24HourFormatChanged); + +public: + ShellUtil(QObject *parent = nullptr); + ~ShellUtil() override; + static ShellUtil *instance(); + +public Q_SLOTS: + void executeCommand(const QString &command); + void launchApp(const QString &app); + void toggleTorch(); + void takeScreenshot(); + + bool autoRotate(); + void setAutoRotate(bool value); + + bool torchEnabled() const; + + bool isSystem24HourFormat(); + +Q_SIGNALS: + void autoRotateChanged(bool value); + void torchChanged(bool value); + void isSystem24HourFormatChanged(); + +private: + void handleMetaDataReceived(const QVariantMap &metadata, int fd); + bool m_running = false; + + KConfigWatcher::Ptr m_localeConfigWatcher; + KSharedConfig::Ptr m_localeConfig; + + org::kde::KScreen *m_kscreenInterface; + OrgKdeKWinScreenShot2Interface *m_screenshotInterface; +}; diff --git a/containments/panel/CMakeLists.txt b/containments/panel/CMakeLists.txt index c85e59ac..78afa385 100644 --- a/containments/panel/CMakeLists.txt +++ b/containments/panel/CMakeLists.txt @@ -1,9 +1,5 @@ -qt_add_dbus_interfaces(DBUS_SRCS dbus/org.kde.KWin.ScreenShot2.xml - dbus/org.kde.KScreen.xml) - set(phonepanel_SRCS phonepanel.cpp - ${DBUS_SRCS} ) add_library(plasma_applet_phonepanel MODULE ${phonepanel_SRCS}) @@ -15,8 +11,6 @@ target_link_libraries(plasma_applet_phonepanel Qt::DBus KF5::Plasma KF5::I18n - KF5::Notifications - KF5::KIOGui KF5::Service ) diff --git a/containments/panel/package/contents/applet/CompactApplet.qml b/containments/panel/package/contents/applet/CompactApplet.qml deleted file mode 100644 index 7a62dece..00000000 --- a/containments/panel/package/contents/applet/CompactApplet.qml +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2011 Marco Martin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.1 -import QtQuick.Layouts 1.1 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.plasmoid 2.0 - - -PlasmaCore.ToolTipArea { - id: appletRoot - objectName: "org.kde.desktop-CompactApplet" - anchors.fill: parent - - icon: plasmoid.icon - mainText: plasmoid.toolTipMainText - subText: plasmoid.toolTipSubText - location: if (plasmoid.parent && plasmoid.parent.parent.objectName === "hiddenTasksColumn" && plasmoid.location !== PlasmaCore.Types.LeftEdge) { - return PlasmaCore.Types.RightEdge; - } else { - return plasmoid.location; - } - active: !plasmoid.expanded - textFormat: plasmoid.toolTipTextFormat - mainItem: plasmoid.toolTipItem ? plasmoid.toolTipItem : null - - property Item fullRepresentation - property Item compactRepresentation - - Connections { - target: plasmoid - onContextualActionsAboutToShow: appletRoot.hideToolTip() - } - - - - Layout.minimumWidth: { - switch (plasmoid.formFactor) { - case PlasmaCore.Types.Vertical: - return 0; - case PlasmaCore.Types.Horizontal: - return height; - default: - return PlasmaCore.Units.gridUnit * 3; - } - } - - Layout.minimumHeight: { - switch (plasmoid.formFactor) { - case PlasmaCore.Types.Vertical: - return width; - case PlasmaCore.Types.Horizontal: - return 0; - default: - return PlasmaCore.Units.gridUnit * 3; - } - } - - onCompactRepresentationChanged: { - if (compactRepresentation) { - compactRepresentation.parent = appletRoot; - compactRepresentation.anchors.fill = appletRoot; - compactRepresentation.visible = true; - } - appletRoot.visible = true; - } -} - diff --git a/containments/panel/package/contents/config/main.xml b/containments/panel/package/contents/config/main.xml deleted file mode 100644 index 61a525b9..00000000 --- a/containments/panel/package/contents/config/main.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/containments/panel/package/contents/ui/DrawerBackground.qml b/containments/panel/package/contents/ui/DrawerBackground.qml deleted file mode 100644 index e922de7e..00000000 --- a/containments/panel/package/contents/ui/DrawerBackground.qml +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.6 -import QtQuick.Layouts 1.4 -import QtQuick.Controls 2.4 as QQC2 -import QtGraphicalEffects 1.12 - -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.kirigami 2.12 as Kirigami - -QQC2.Control { - id: root - - leftPadding: frameSvg.margins.left - topPadding: frameSvg.margins.top - rightPadding: frameSvg.margins.right - bottomPadding: frameSvg.margins.bottom - - background: PlasmaCore.FrameSvgItem { - id: frameSvg - imagePath: "widgets/background" - MouseArea { - anchors.fill: parent - } - } -} diff --git a/containments/panel/package/contents/ui/FullContainer.qml b/containments/panel/package/contents/ui/FullContainer.qml deleted file mode 100644 index 688899bd..00000000 --- a/containments/panel/package/contents/ui/FullContainer.qml +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQml.Models 2.12 - -import org.kde.kirigami 2.12 as Kirigami -import org.kde.plasma.core 2.0 as PlasmaCore - -DrawerBackground { - id: fullContainer - property Item applet - property ObjectModel fullRepresentationModel - property ListView fullRepresentationView - - visible: shouldBeVisible - - property bool shouldBeVisible: applet && (applet.status != PlasmaCore.Types.HiddenStatus && applet.status != PlasmaCore.Types.PassiveStatus) - - height: parent.height - width: visible ? quickSettings.width : 0 - Layout.minimumHeight: applet && applet.switchHeight - onShouldBeVisibleChanged: fullContainer.visible = fullContainer.shouldBeVisible - - Component.onCompleted: visibleChanged(); - onVisibleChanged: { - if (visible) { - for (var i = 0; i < fullRepresentationModel.count; ++i) { - if (fullRepresentationModel.get(i) === this) { - return; - } - } - if (applet && applet.pluginName == "org.kde.plasma.notifications") { - fullRepresentationModel.insert(0, this); - } else { - fullRepresentationModel.append(this); - } - - fullRepresentationView.forceLayout(); - - fullRepresentationView.currentIndex = ObjectModel.index; - fullRepresentationView.positionViewAtIndex(ObjectModel.index, ListView.Contain) - } else if (ObjectModel.index >= 0) { - fullRepresentationModel.remove(ObjectModel.index); - fullRepresentationView.forceLayout(); - } - if (!shouldBeVisible) { - visible = false; - } - } - Connections { - target: fullContainer.applet - function onActivated() { - if (!visible) { - return; - } - fullRepresentationView.currentIndex = ObjectModel.index; - } - } - Connections { - target: fullContainer.applet.fullRepresentationItem - function onParentChanged() { - fullContainer.applet.fullRepresentationItem.parent = fullContainer; - } - } -} diff --git a/containments/panel/package/contents/ui/FullNotificationsContainer.qml b/containments/panel/package/contents/ui/FullNotificationsContainer.qml deleted file mode 100644 index 1498bffa..00000000 --- a/containments/panel/package/contents/ui/FullNotificationsContainer.qml +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Layouts 1.3 - -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.notificationmanager 1.0 as NotificationManager - -FullContainer { - id: fullContainer - - shouldBeVisible: applet && historyModel.count > 0 - visible: shouldBeVisible - - NotificationManager.Notifications { - id: historyModel - showExpired: true - showDismissed: true - expandUnread: true - } -} diff --git a/containments/panel/package/contents/ui/LayoutManager.js b/containments/panel/package/contents/ui/LayoutManager.js deleted file mode 100644 index cf053966..00000000 --- a/containments/panel/package/contents/ui/LayoutManager.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 Marco Martin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - - -var layout; -var root; -var plasmoid; -var lastSpacer; - - -function restore() { - var configString = String(plasmoid.configuration.AppletOrder) - - //array, a cell for encoded item order - var itemsArray = configString.split(";"); - - //map applet id->order in panel - var idsOrder = new Object(); - //map order in panel -> applet pointer - var appletsOrder = new Object(); - - for (var i = 0; i < itemsArray.length; i++) { - //property name: applet id - //property value: order - idsOrder[itemsArray[i]] = i; - } - - for (var i = 0; i < plasmoid.applets.length; ++i) { - if (idsOrder[plasmoid.applets[i].id] !== undefined) { - appletsOrder[idsOrder[plasmoid.applets[i].id]] = plasmoid.applets[i]; - //ones that weren't saved in AppletOrder go to the end - } else { - appletsOrder["unordered"+i] = plasmoid.applets[i]; - } - } - - //finally, restore the applets in the correct order - for (var i in appletsOrder) { - root.addApplet(appletsOrder[i], -1, -1) - } - //rewrite, so if in the orders there were now invalid ids or if some were missing creates a correct list instead - save(); -} - -function save() { - var ids = new Array(); - for (var i = 0; i < layout.children.length; ++i) { - var child = layout.children[i]; - - if (child.applet) { - ids.push(child.applet.id); - } - } - plasmoid.configuration.AppletOrder = ids.join(';'); -} - -function removeApplet (applet) { - for (var i = layout.children.length - 1; i >= 0; --i) { - var child = layout.children[i]; - if (child.applet === applet) { - child.destroy(); - } - } -} - -//insert item2 before item1 -function insertBefore(item1, item2) { - if (item1 === item2) { - return; - } - var removed = new Array(); - - var child; - - var i; - for (i = layout.children.length - 1; i >= 0; --i) { - child = layout.children[i]; - removed.push(child); - child.parent = root; - - if (child === item1) { - break; - } - } - - item2.parent = layout; - - for (var j = removed.length - 1; j >= 0; --j) { - removed[j].parent = layout; - } - return i; -} - -//insert item2 after item1 -function insertAfter(item1, item2) { - if (item1 === item2) { - return; - } - var removed = new Array(); - - var child; - - var i; - for (i = layout.children.length - 1; i >= 0; --i) { - child = layout.children[i]; - //never ever insert after lastSpacer - if (child === lastSpacer && item1 === lastSpacer) { - removed.push(child); - child.parent = root; - break; - } else if (child === item1) { - break; - } - - removed.push(child); - child.parent = root; - } - - item2.parent = layout; - - for (var j = removed.length - 1; j >= 0; --j) { - removed[j].parent = layout; - } - return i; -} - -function insertAtIndex(item, position) { - if (position < 0 || position >= layout.children.length) { - return; - } - - //never ever insert after lastSpacer - if (layout.children[position] === lastSpacer) { - --position; - } - - var removedItems = new Array(); - - for (var i = position; i < layout.children.length; ++i) { - var child = layout.children[position]; - child.parent = root; - removedItems.push(child); - } - - item.parent = layout; - for (var i in removedItems) { - removedItems[i].parent = layout; - } -} - -function insertAtCoordinates(item, x, y) { - if (root.isHorizontal) { - y = layout.height / 2; - } else { - x = layout.width / 2; - } - var child = layout.childAt(x, y); - - if (!child || child === item) { - child = layout.children[0]; - } - item.parent = root; - - //PlasmaCore.Types.Vertical = 3 - if ((plasmoid.formFactor === 3 && y < child.y + child.height/2) || - (plasmoid.formFactor !== 3 && x < child.x + child.width/2)) { - return insertBefore(child, item); - } else { - return insertAfter(child, item); - } -} diff --git a/containments/panel/package/contents/ui/SlidingContainer.qml b/containments/panel/package/contents/ui/SlidingContainer.qml deleted file mode 100644 index b043b521..00000000 --- a/containments/panel/package/contents/ui/SlidingContainer.qml +++ /dev/null @@ -1,232 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2014 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.1 -import QtQuick.Window 2.2 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.plasma.private.nanoshell 2.0 as NanoShell - -NanoShell.FullScreenOverlay { - id: window - - property int offset: 0 // slide progress - property int openThreshold: PlasmaCore.Units.gridUnit * 2 - property bool userInteracting: false - property bool initiallyOpened: false // whether the panel is already open after a touch release (then don't restrict to collapsed height) - - // height when quicksettings is fully open - required property int fullyOpenHeight - - // flickable contentY - readonly property int openedContentY: wideScreen || offset > (collapsedHeight + openThreshold) ? -topEmptyAreaHeight : offsetToContentY(collapsedHeight) - readonly property int closedContentY: mainFlickable.contentHeight - - readonly property bool wideScreen: width > height || width > PlasmaCore.Units.gridUnit * 45 - readonly property int drawerWidth: wideScreen ? contentItem.implicitWidth : width - - property int drawerX: 0 - property alias fixedArea: mainScope - property alias flickable: mainFlickable - - color: "transparent" - property alias contentItem: contentArea.contentItem - property int topPanelHeight - property int collapsedHeight - property real topEmptyAreaHeight - - property bool appletsShown: false // whether notifications or media player applets are shown - - signal closed - - width: Screen.width - height: Screen.height - - Component.onCompleted: plasmoid.nativeInterface.panel = window; - - onVisibleChanged: if (!visible) { - closed() - } - onInitiallyOpenedChanged: { - if (initiallyOpened) mainFlickable.focus = true; - } - - function offsetToContentY(num) { return -num + window.fullyOpenHeight; } - function contentYToOffset(num) { return offsetToContentY(num); } - - // avoids binding loops - function updateOffset(delta) { - // only go to collapsed height for mousearea when not widescreen - let maximum = window.wideScreen ? window.fullyOpenHeight : collapsedHeight + openThreshold / 2; - offset = Math.max(0, Math.min(maximum, offset + delta)); - if (!mainFlickable.moving && !mainFlickable.dragging && !mainFlickable.flicking) { - mainFlickable.contentY = offsetToContentY(window.offset); - } - } - - enum MovementDirection { - None = 0, - Up, - Down - } - property int direction: SlidingContainer.MovementDirection.None - - function cancelAnimations() { - closeAnim.stop(); - openAnim.stop(); - } - function open() { - cancelAnimations(); - openAnim.restart(); - initiallyOpened = true; - } - function close() { - cancelAnimations(); - closeAnim.restart(); - initiallyOpened = false; - } - function expand() { - cancelAnimations(); - expandAnim.restart(); - initiallyOpened = true; - } - function updateState() { - cancelAnimations(); - if (window.offset <= 0) { - // close immediately, so that we don't have to wait PlasmaCore.Units.longDuration - window.visible = false; - close(); - } else if (window.direction === SlidingContainer.MovementDirection.None) { - if (window.offset < openThreshold) { - close(); - } else { - open(); - } - } else if (offset > openThreshold && window.direction === SlidingContainer.MovementDirection.Down) { - open(); - } else if (mainFlickable.contentY > openThreshold) { - close(); - } else { - open(); - } - } - Timer { - id: updateStateTimer - interval: 0 - onTriggered: updateState() - } - - onActiveChanged: { - if (!active) { - close(); - } - } - - PropertyAnimation { - id: closeAnim - target: mainFlickable - properties: "contentY" - duration: PlasmaCore.Units.longDuration - easing.type: Easing.InOutQuad - to: window.closedContentY - onFinished: { - window.visible = false; - } - } - PropertyAnimation { - id: openAnim - target: mainFlickable - properties: "contentY" - duration: PlasmaCore.Units.longDuration - easing.type: Easing.InOutQuad - to: window.openedContentY - } - PropertyAnimation { - id: expandAnim - target: mainFlickable - properties: "contentY" - duration: PlasmaCore.Units.longDuration - easing.type: Easing.InOutQuad - to: 0 - } - - // fullscreen background - Rectangle { - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.75) - opacity: (appletsShown ? 0.85 : 0.6) * Math.max(0, Math.min(1, offset / window.collapsedHeight)) - Behavior on opacity { // smooth opacity changes - NumberAnimation { duration: 70 } - } - } - - PlasmaCore.ColorScope { - id: mainScope - colorGroup: PlasmaCore.Theme.ViewColorGroup - anchors.fill: parent - - Flickable { - id: mainFlickable - anchors.fill: parent - - property real oldContentY - contentY: contentHeight - - onContentYChanged: { - if (contentY === oldContentY) { - window.direction = SlidingContainer.MovementDirection.None; - } else { - window.direction = contentY > oldContentY ? SlidingContainer.MovementDirection.Up : SlidingContainer.MovementDirection.Down; - } - window.offset = contentYToOffset(contentY); - oldContentY = contentY; - - // close panel immediately after panel is not shown, and the flickable is not being dragged - if (initiallyOpened && window.offset <= 0 && !mainFlickable.dragging && !closeAnim.running && !openAnim.running) { - window.updateState(); - focus = false; - } - } - - boundsMovement: Flickable.StopAtBounds - contentWidth: window.width - contentHeight: window.height - bottomMargin: window.height - onMovementStarted: { - window.cancelAnimations(); - window.userInteracting = true; - } - onFlickStarted: window.userInteracting = true; - onMovementEnded: { - window.userInteracting = false; - window.updateState(); - } - onFlickEnded: { - window.userInteracting = true; - window.updateState(); - } - - MouseArea { - id: dismissArea - z: 2 - width: parent.width - height: mainFlickable.contentHeight - onClicked: window.close(); - - // actual sliding contents - PlasmaComponents.Control { - id: contentArea - z: 1 - x: Math.max(0, Math.min(window.drawerX, window.width - window.drawerWidth)) - width: Math.min(window.width, window.drawerWidth) - } - } - } - } -} diff --git a/containments/panel/package/contents/ui/indicators/Indicator.qml b/containments/panel/package/contents/ui/indicators/Indicator.qml deleted file mode 100644 index 0011edc9..00000000 --- a/containments/panel/package/contents/ui/indicators/Indicator.qml +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.6 -import QtQuick.Layouts 1.4 - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents - - -RowLayout { - - property alias icon: icon.source - property alias text: label.text - PlasmaCore.IconItem { - id: icon - colorGroup: PlasmaCore.ColorScope.colorGroup - - Layout.fillHeight: true - Layout.preferredWidth: height - } - PlasmaComponents.Label { - id: label - visible: text.length > 0 - color: PlasmaCore.ColorScope.textColor - font.pixelSize: parent.height / 2 - } -} diff --git a/containments/panel/package/contents/ui/indicators/SignalStrength.qml b/containments/panel/package/contents/ui/indicators/SignalStrength.qml deleted file mode 100644 index 2b090bba..00000000 --- a/containments/panel/package/contents/ui/indicators/SignalStrength.qml +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Devin Lin - * SPDX-FileCopyrightText: 2015 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.1 -import QtQuick.Layouts 1.1 - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents - -import "providers" - -Item { - required property QtObject provider - - width: strengthIcon.height - Layout.minimumWidth: strengthIcon.height - - PlasmaCore.IconItem { - id: strengthIcon - colorGroup: PlasmaCore.ColorScope.colorGroup - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: height - height: parent.height - - source: provider.icon - } -} diff --git a/containments/panel/package/contents/ui/main.qml b/containments/panel/package/contents/ui/main.qml index d1fa759d..2eb94a47 100644 --- a/containments/panel/package/contents/ui/main.qml +++ b/containments/panel/package/contents/ui/main.qml @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 Devin Lin + * SPDX-FileCopyrightText: 2021 Devin Lin * SPDX-FileCopyrightText: 2015 Marco Martin * * SPDX-License-Identifier: GPL-2.0-or-later @@ -8,7 +8,6 @@ import QtQuick 2.12 import QtQuick.Layouts 1.3 import QtQml.Models 2.12 -import QtGraphicalEffects 1.12 import org.kde.kirigami 2.12 as Kirigami @@ -16,21 +15,19 @@ import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.plasma.workspace.components 2.0 as PlasmaWorkspace -import org.kde.taskmanager 0.1 as TaskManager - import org.kde.plasma.private.nanoshell 2.0 as NanoShell import org.kde.plasma.private.mobileshell 1.0 as MobileShell -import org.kde.plasma.private.mobilehomescreencomponents 0.1 as HomeScreenComponents - -import "LayoutManager.js" as LayoutManager - -import "quicksettings" -import "indicators" as Indicators -import "indicators/providers" as IndicatorProviders Item { id: root + + readonly property bool showingApp: !MobileShell.HomeScreenControls.homeScreenVisible + readonly property color backgroundColor: NanoShell.StartupFeedback.visible + ? NanoShell.StartupFeedback.backgroundColor + : topPanel.colorScopeColor + + Plasmoid.backgroundHints: showingApp ? PlasmaCore.Types.StandardBackground : PlasmaCore.Types.NoBackground + width: 480 height: PlasmaCore.Units.gridUnit @@ -44,330 +41,46 @@ Item { Binding { target: MobileShell.TopPanelControls property: "inSwipe" - value: slidingPanel.userInteracting + value: drawer.dragging } Connections { target: MobileShell.TopPanelControls function onStartSwipe() { - swipeMouseArea.startSwipe(0); + swipeArea.startSwipe(); } function onEndSwipe() { - swipeMouseArea.endSwipe(); + swipeArea.endSwipe(); } function onRequestRelativeScroll(offsetY) { - swipeMouseArea.updateOffset(offsetY); + swipeArea.updateOffset(offsetY); } } //END API implementation - - Plasmoid.backgroundHints: showingApp ? PlasmaCore.Types.StandardBackground : PlasmaCore.Types.NoBackground - - property Item toolBox - property int buttonHeight: width/4 - property bool reorderingApps: false - property var layoutManager: LayoutManager - - readonly property color backgroundColor: NanoShell.StartupFeedback.visible ? NanoShell.StartupFeedback.backgroundColor : topPanel.colorScopeColor - readonly property bool showingApp: !MobileShell.HomeScreenControls.homeScreenVisible - - readonly property bool hasTasks: tasksModel.count > 0 - - Containment.onAppletAdded: { - addApplet(applet, x, y); - LayoutManager.save(); - } - - function addApplet(applet, x, y) { - var compactContainer = compactContainerComponent.createObject(topPanel.applets) - print("Applet added: " + applet + " " + applet.title) - - applet.parent = compactContainer; - compactContainer.applet = applet; - applet.anchors.fill = compactContainer; - applet.visible = true; - - //FIXME: make a way to instantiate fullRepresentationItem without the open/close dance - applet.expanded = true - applet.expanded = false - - var fullContainer = null; - if (applet.pluginName == "org.kde.plasma.notifications") { - fullContainer = fullNotificationsContainerComponent.createObject(fullRepresentationView.contentItem, {"fullRepresentationModel": fullRepresentationModel, "fullRepresentationView": fullRepresentationView}); - } else { - fullContainer = fullContainerComponent.createObject(fullRepresentationView.contentItem, {"fullRepresentationModel": fullRepresentationModel, "fullRepresentationView": fullRepresentationView}); - } - - // applet.fullRepresentationItem.parent = fullContainer; - fullContainer.applet = applet; - fullContainer.contentItem = applet.fullRepresentationItem; - //applet.fullRepresentationItem.anchors.fill = fullContainer; - - } - + Component.onCompleted: { - LayoutManager.plasmoid = plasmoid; - LayoutManager.root = root; - LayoutManager.layout = appletsLayout; - LayoutManager.restore(); - } - - TaskManager.TasksModel { - id: tasksModel - sortMode: TaskManager.TasksModel.SortVirtualDesktop - groupMode: TaskManager.TasksModel.GroupDisabled - - screenGeometry: plasmoid.screenGeometry - //FIXME: workaround - Component.onCompleted: tasksModel.countChanged(); - } - - PlasmaCore.DataSource { - id: statusNotifierSource - engine: "statusnotifieritem" - interval: 0 - onSourceAdded: { - connectSource(source) - } - Component.onCompleted: { - connectedSources = sources - } - } - - RowLayout { - id: appletsLayout - Layout.minimumHeight: Math.max(root.height, Math.round(Layout.preferredHeight / root.height) * root.height) - } - - //todo: REMOVE? - Component { - id: compactContainerComponent - Item { - property Item applet - visible: applet && (applet.status != PlasmaCore.Types.HiddenStatus && applet.status != PlasmaCore.Types.PassiveStatus) - Layout.fillHeight: true - Layout.minimumWidth: applet && applet.compactRepresentationItem ? Math.max(applet.compactRepresentationItem.Layout.minimumWidth, topPanel.applets.height) : topPanel.applets.height - Layout.maximumWidth: Layout.minimumWidth - } - } - - Component { - id: fullContainerComponent - FullContainer {} - } - - Component { - id: fullNotificationsContainerComponent - FullNotificationsContainer {} - } - - // indicator providers - IndicatorProviders.BatteryProvider { - id: batteryProvider - - readonly property var setting: HomeScreenComponents.QuickSetting { - text: i18n("Battery") - icon: "battery-full" + (batteryProvider.pluggedIn ? "-charging" : "") - enabled: false - settingsCommand: "plasma-open-settings kcm_mobile_power" - } - Component.onCompleted: quickSettings.quickSettingsModel.include(setting) - } - IndicatorProviders.BluetoothProvider { - id: bluetoothProvider - } - property alias signalStrengthProvider: signalStrengthProviderLoader.item - Loader { - id: signalStrengthProviderLoader - source: Qt.resolvedUrl("indicators/providers/SignalStrengthProvider.qml") - } - IndicatorProviders.VolumeProvider { - id: volumeProvider - - readonly property var setting: HomeScreenComponents.QuickSetting { - text: i18n("Sound") - icon: "audio-speakers-symbolic" - enabled: false - settingsCommand: "plasma-open-settings kcm_pulseaudio" - function toggle() { - volumeProvider.showVolumeOverlay() - } - } - Component.onCompleted: quickSettings.quickSettingsModel.include(setting) - } - IndicatorProviders.WifiProvider { - id: wifiProvider + // we want to bind global shortcuts here + MobileShell.VolumeProvider.bindShortcuts = true; } // top panel component - IndicatorsRow { + MobileShell.StatusBar { id: topPanel anchors.fill: parent - z: 1 - colorGroup: showingApp ? PlasmaCore.Theme.HeaderColorGroup : PlasmaCore.Theme.ComplementaryColorGroup - backgroundColor: !showingApp ? "transparent" : root.backgroundColor - showDropShadow: !showingApp + showDropShadow: !root.showingApp + colorGroup: root.showingApp ? PlasmaCore.Theme.HeaderColorGroup : PlasmaCore.Theme.ComplementaryColorGroup + backgroundColor: !root.showingApp ? "transparent" : root.backgroundColor } - // initial swipe down gesture - MouseArea { - id: swipeMouseArea - z: 99 - property int oldMouseY: 0 - - function startSwipe(mouseX) { - slidingPanel.cancelAnimations(); - slidingPanel.drawerX = Math.min(Math.max(0, mouseX - slidingPanel.drawerWidth/2), slidingPanel.width - slidingPanel.contentItem.width) - slidingPanel.userInteracting = true; - slidingPanel.flickable.contentY = slidingPanel.closedContentY; - slidingPanel.visible = true; - } - - function endSwipe() { - slidingPanel.userInteracting = false; - slidingPanel.updateState(); - } - - function updateOffset(offsetY) { - slidingPanel.updateOffset(offsetY); - } - + MobileShell.ActionDrawerOpenSurface { + id: swipeArea + actionDrawer: drawer anchors.fill: parent - onPressed: { - oldMouseY = mouse.y; - startSwipe(mouse.x); - } - onReleased: endSwipe() - onCanceled: endSwipe() - onPositionChanged: { - updateOffset(mouse.y - oldMouseY); - oldMouseY = mouse.y; - } } - - // sliding component - SlidingContainer { - id: slidingPanel - width: plasmoid.availableScreenRect.width - height: plasmoid.availableScreenRect.height - topPanelHeight: topPanel.height - topEmptyAreaHeight: quickSettings.topEmptyAreaHeight - collapsedHeight: quickSettings.collapsedHeight - fullyOpenHeight: quickSettings.expandedHeight - - appletsShown: fullRepresentationView.count > 0 - - offset: quickSettings.height - - onClosed: quickSettings.closed() - - contentItem: MouseArea { - // mousearea captures touch presses so that the flickable picks them up for swiping - implicitWidth: slidingPanel.wideScreen ? panelContents.implicitWidth : slidingPanel.width - implicitHeight: Math.min(slidingPanel.height, quickSettings.implicitHeight) - - GridLayout { - id: panelContents - width: slidingPanel.wideScreen ? Math.min(parent.width, implicitWidth) : parent.width - - columns: slidingPanel.wideScreen ? 2 : 1 - rows: slidingPanel.wideScreen ? 1 : 2 - - QuickSettingsPanel { - id: quickSettings - - property int trueHeight: height + Math.round(Kirigami.Units.gridUnit * 1.5) // add height of bottom bar - - z: 4 - Layout.alignment: Qt.AlignTop - Layout.preferredWidth: slidingPanel.wideScreen ? Math.min(slidingPanel.width/2, PlasmaCore.Units.gridUnit * 25) : panelContents.width - - parentSlidingPanel: slidingPanel - onExpandRequested: slidingPanel.expand() - onCloseRequested: slidingPanel.close() - } - - // notifications and media player - ListView { - id: fullRepresentationView - implicitHeight: PlasmaCore.Units.gridUnit * 20 - Layout.preferredWidth: slidingPanel.wideScreen ? Math.min(slidingPanel.width/2, quickSettings.width*fullRepresentationModel.count) : panelContents.width - Layout.preferredHeight: slidingPanel.wideScreen - ? Math.min(PlasmaCore.Units.gridUnit * 20, Math.max(PlasmaCore.Units.gridUnit * 15, quickSettings.implicitHeight)) - : Math.min(plasmoid.screenGeometry.height - quickSettings.implicitHeight - bottomBar.height + slidingPanel.topEmptyAreaHeight, implicitHeight) - - z: 1 - interactive: true//count > 0 && width < contentWidth - - clip: slidingPanel.wideScreen - y: slidingPanel.wideScreen ? 0 : quickSettings.trueHeight - opacity: { - if (slidingPanel.wideScreen) { - return 1; - } else { - return fullRepresentationModel.count > 0 && slidingPanel.offset / slidingPanel.collapsedHeight; - } - } - //preferredHighlightBegin: slidingPanel.drawerX - - cacheBuffer: width * 100 - highlightFollowsCurrentItem: true - highlightRangeMode: ListView.ApplyRange - highlightMoveDuration: PlasmaCore.Units.longDuration - snapMode: slidingPanel.wideScreen ? ListView.NoSnap : ListView.SnapOneItem - model: ObjectModel { - id: fullRepresentationModel - } - orientation: ListView.Horizontal - - MouseArea { - parent: fullRepresentationView.contentItem - anchors.fill: parent - z: -1 - onClicked: slidingPanel.close() - } - } - } - } - DrawerBackground { - id: bottomBar - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - parent: slidingPanel.fixedArea - opacity: fullRepresentationView.opacity - visible: !slidingPanel.wideScreen && fullRepresentationModel.count > 1 - z: 100 - contentItem: RowLayout { - PlasmaComponents.TabBar { - Layout.fillWidth: true - Layout.fillHeight: true - position: PlasmaComponents.TabBar.Footer - Text { - text:fullRepresentationModel.count - } - Repeater { - model: fullRepresentationView.count - delegate: PlasmaComponents.TabButton { - implicitHeight: parent.height - text: fullRepresentationModel.get(index).applet.title - checked: fullRepresentationView.currentIndex === index - - onClicked: fullRepresentationView.currentIndex = index - } - } - } - PlasmaComponents.ToolButton { - Layout.fillHeight: true - Layout.preferredWidth: height - icon.name: "paint-none" - onClicked: slidingPanel.close(); - } - } - } + + MobileShell.ActionDrawer { + id: drawer } } diff --git a/containments/panel/package/contents/ui/quicksettings/Delegate.qml b/containments/panel/package/contents/ui/quicksettings/Delegate.qml deleted file mode 100644 index debe6dee..00000000 --- a/containments/panel/package/contents/ui/quicksettings/Delegate.qml +++ /dev/null @@ -1,146 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2015 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.1 -import QtQuick.Layouts 1.1 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.kirigami 2.12 as Kirigami -import org.kde.plasma.private.nanoshell 2.0 as NanoShell - -ColumnLayout { - id: delegateRoot - spacing: PlasmaCore.Units.smallSpacing - - signal closeRequested - signal panelClosed - - // Model interface - required property string text - required property string icon - required property bool enabled - required property string settingsCommand - required property var toggleFunction - property alias labelOpacity: label.opacity - - required property real boundingWidth - - property color disabledButtonColor: PlasmaCore.Theme.backgroundColor - property color disabledPressedButtonColor: Qt.darker(disabledButtonColor, 1.1) - property color enabledButtonColor: Kirigami.ColorUtils.adjustColor(PlasmaCore.ColorScope.highlightColor, {"alpha": 0.4*255}) - property color enabledPressedButtonColor: Kirigami.ColorUtils.adjustColor(PlasmaCore.ColorScope.highlightColor, {"alpha": 0.6*255}); - - Rectangle { - id: iconButton - Layout.preferredWidth: PlasmaCore.Units.iconSizes.large + PlasmaCore.Units.smallSpacing - Layout.minimumHeight: width - Layout.alignment: Qt.AlignHCenter - radius: PlasmaCore.Units.smallSpacing - border.color: delegateRoot.enabled ? - Qt.darker(Kirigami.ColorUtils.adjustColor(PlasmaCore.ColorScope.highlightColor, {}), 1.25) : - Kirigami.ColorUtils.adjustColor(PlasmaCore.ColorScope.textColor, {"alpha": 0.2*255}) - color: { - if (delegateRoot.enabled) { - return iconMouseArea.pressed ? enabledPressedButtonColor : enabledButtonColor - } else { - return iconMouseArea.pressed ? disabledPressedButtonColor : disabledButtonColor - } - } - - PlasmaCore.IconItem { - id: icon - anchors.centerIn: parent - implicitWidth: Math.round(parent.width * 0.6) - implicitHeight: width - source: delegateRoot.icon - } - - MouseArea { - id: iconMouseArea - anchors.fill: parent - onClicked: { - if (delegateRoot.toggle) { - delegateRoot.toggle(); - } else if (delegateRoot.toggleFunction) { - delegateRoot.toggleFunction(); - } else if (delegateRoot.settingsCommand) { - NanoShell.StartupFeedback.open( - delegateRoot.icon, - delegateRoot.text, - icon.Kirigami.ScenePosition.x + icon.width/2, - icon.Kirigami.ScenePosition.y + icon.height/2, - Math.min(icon.width, icon.height)) - plasmoid.nativeInterface.executeCommand(delegateRoot.settingsCommand); - root.closeRequested(); - } - } - onPressAndHold: { - if (delegateRoot.settingsCommand) { - NanoShell.StartupFeedback.open( - delegateRoot.icon, - delegateRoot.text, - icon.Kirigami.ScenePosition.x + icon.width/2, - icon.Kirigami.ScenePosition.y + icon.height/2, - Math.min(icon.width, icon.height)) - closeRequested(); - plasmoid.nativeInterface.executeCommand(delegateRoot.settingsCommand); - } else if (delegateRoot.toggleFunction) { - delegateRoot.toggleFunction(); - } - } - } - } - - PlasmaComponents.Label { - id: label - - Layout.maximumWidth: delegateRoot.boundingWidth - arrow.width - PlasmaCore.Units.smallSpacing * 2 - Layout.alignment: Qt.AlignHCenter - - text: delegateRoot.text - bottomPadding: PlasmaCore.Units.smallSpacing * 2 - horizontalAlignment: Text.AlignHCenter - font.pixelSize: PlasmaCore.Theme.defaultFont.pixelSize * 0.8 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - - PlasmaCore.SvgItem { - id: arrow - anchors { - left: parent.right - verticalCenter: parent.verticalCenter - verticalCenterOffset: -PlasmaCore.Units.smallSpacing - } - visible: delegateRoot.settingsCommand - width: PlasmaCore.Units.iconSizes.small/2 - height: width - elementId: "down-arrow" - svg: PlasmaCore.Svg { - imagePath: "widgets/arrows" - } - } - MouseArea { - id: labelMouseArea - anchors.fill: parent - onClicked: { - if (delegateRoot.settingsCommand) { - NanoShell.StartupFeedback.open( - delegateRoot.icon, - delegateRoot.text, - icon.Kirigami.ScenePosition.x + icon.width/2, - icon.Kirigami.ScenePosition.y + icon.height/2, - Math.min(icon.width, icon.height)) - plasmoid.nativeInterface.executeCommand(delegateRoot.settingsCommand); - closeRequested(); - } else if (delegateRoot.toggleFunction) { - delegateRoot.toggleFunction(); - } - } - } - } -} - diff --git a/containments/panel/package/contents/ui/quicksettings/QuickSettingsPanel.qml b/containments/panel/package/contents/ui/quicksettings/QuickSettingsPanel.qml deleted file mode 100644 index 383c955a..00000000 --- a/containments/panel/package/contents/ui/quicksettings/QuickSettingsPanel.qml +++ /dev/null @@ -1,224 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2015 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.14 -import QtQuick.Layouts 1.1 -import QtQuick.Window 2.2 -import QtGraphicalEffects 1.12 -import org.kde.kirigami 2.12 as Kirigami -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.networkmanagement 0.2 as PlasmaNM -import org.kde.bluezqt 1.0 as BluezQt -import org.kde.colorcorrect 0.1 as CC -import org.kde.plasma.private.nanoshell 2.0 as NanoShell - -import org.kde.plasma.components 3.0 as PC3 - -import "../" - -Item { - id: root - implicitWidth: column.implicitWidth + PlasmaCore.Units.smallSpacing * 6 - implicitHeight: expandedHeight - - signal expandRequested - signal closeRequested - signal closed - - property bool expandedMode: parentSlidingPanel.wideScreen - - readonly property real expandedRatio: expandedMode - ? 1 - : Math.max(0, Math.min(1, (parentSlidingPanel.offset - collapsedHeight) /(expandedHeight-collapsedHeight))) - - readonly property real topEmptyAreaHeight: parentSlidingPanel.userInteracting - ? (root.height - collapsedHeight) * (1 - expandedRatio) - : (expandedMode ? 0 : root.height - collapsedHeight) - - - readonly property real collapsedHeight: column.Layout.minimumHeight + background.margins.top + background.fixedMargins.bottom - - readonly property real expandedHeight: column.Layout.maximumHeight + background.margins.top + background.fixedMargins.bottom - - Connections { - target: root.parentSlidingPanel - function onUserInteractingChanged() { - if (!parentSlidingPanel.userInteracting) { - if (root.expandedRatio > 0.7) { - root.expandedMode = true; - } - } - } - } - - property NanoShell.FullScreenOverlay parentSlidingPanel - - Connections { - target: root.Window.window - function onVisibilityChanged() { - root.expandedMode = parentSlidingPanel.wideScreen; - } - } - - signal plasmoidTriggered(var applet, var id) - Layout.minimumHeight: flow.implicitHeight + PlasmaCore.Units.largeSpacing*2 - - onClosed: quickSettingsModel.panelClosed() - - readonly property SettingsModel quickSettingsModel: SettingsModel {} - - PlasmaCore.FrameSvgItem { - id: background - implicitHeight: root.expandedHeight - enabledBorders: parentSlidingPanel.wideScreen ? PlasmaCore.FrameSvg.AllBorders : PlasmaCore.FrameSvg.BottomBorder - anchors.fill: parent - imagePath: "widgets/background" - - ColumnLayout { - id: column - - anchors { - leftMargin: parent.fixedMargins.left - rightMargin: parent.fixedMargins.right - bottomMargin: parent.fixedMargins.bottom * (parentSlidingPanel.wideScreen ? 1 : 0.5) // HACK: fix the bottom arrow not being centered, bottom margins aren't properly calculated it seems - left: parent.left - right: parent.right - bottom: parent.bottom - } - - spacing: 0 - height: Layout.minimumHeight * (1 - root.expandedRatio) + (Layout.maximumHeight * root.expandedRatio) - - readonly property real cellSizeHint: PlasmaCore.Units.iconSizes.large + PlasmaCore.Units.smallSpacing * 6 - readonly property real columnWidth: Math.floor(width / Math.floor(width / cellSizeHint)) - - // top indicators (clock, widgets, etc.) - IndicatorsRow { - id: indicatorsRow - z: 1 - - Layout.leftMargin: -Kirigami.Units.largeSpacing - Layout.rightMargin: -Kirigami.Units.largeSpacing - Layout.fillWidth: true - Layout.preferredHeight: parentSlidingPanel.topPanelHeight + PlasmaCore.Units.smallSpacing * 2 - - showSecondRow: true - colorGroup: PlasmaCore.Theme.NormalColorGroup - backgroundColor: "transparent" - showDropShadow: false - } - - // quicksettings list - ColumnLayout { - clip: expandedRatio > 0 && expandedRatio < 1 // only clip when necessary to improve performance - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumHeight: flow.Layout.minimumHeight - - spacing: 0 - Layout.topMargin: PlasmaCore.Units.largeSpacing - Flow { - id: flow - Layout.fillWidth: true - Layout.minimumHeight: cellSizeHint - Layout.preferredHeight: implicitHeight - Layout.maximumHeight: (flow.cellSizeHint * Math.ceil((flow.children.length - 1) / flow.columns)) - - readonly property real cellSizeHint: PlasmaCore.Units.iconSizes.large + PlasmaCore.Units.smallSpacing * 6 - readonly property real columns: Math.floor(width / cellSizeHint) - readonly property real columnWidth: Math.floor(width / columns) - - spacing: 0 - - Repeater { - model: quickSettingsModel - delegate: Delegate { - id: delegateItem - required property var modelData - width: Math.max(implicitWidth + PlasmaCore.Units.smallSpacing * 2, boundingWidth) - boundingWidth: root.expandedRatio < 0.4 - ? flow.width / (flow.columns + 1) - : (flow.width / (flow.columns + 1)) * (1 - root.expandedRatio) + (flow.width / flow.columns) * root.expandedRatio - - labelOpacity: y > 0 ? 1 : root.expandedRatio - opacity: y <= 0 ? 1 : root.expandedRatio - text: modelData.text - icon: modelData.icon - enabled: modelData.enabled - settingsCommand: modelData.settingsCommand - toggleFunction: modelData.toggle - - Connections { - target: delegateItem - onCloseRequested: root.closeRequested(); - } - Connections { - target: root - onClosed: delegateItem.panelClosed(); - } - } - } - - move: Transition { - NumberAnimation { - duration: PlasmaCore.Units.shortDuration - easing.type: Easing.Linear - properties: "x,y" - } - } - } - BrightnessItem { - id: brightnessSlider - Layout.topMargin: PlasmaCore.Units.largeSpacing - Layout.bottomMargin: PlasmaCore.Units.smallSpacing - Layout.leftMargin: PlasmaCore.Units.largeSpacing - Layout.rightMargin: PlasmaCore.Units.largeSpacing - Layout.fillWidth: true - - opacity: root.expandedRatio - } - } - - // bottom "handle bar" - ColumnLayout { - id: bottomBar - spacing: 0 - visible: !parentSlidingPanel.wideScreen - - Layout.fillWidth: true - implicitHeight: visible ? Math.round(PlasmaCore.Units.gridUnit * 1.3) : 0 - - Kirigami.Separator { - Layout.fillWidth: true - color: PlasmaCore.Theme.disabledTextColor - opacity: 0.3 - } - - Kirigami.Icon { - color: PlasmaCore.Theme.disabledTextColor - source: expandedRatio >= 0.5 ? "go-up-symbolic" : "go-down-symbolic" - implicitWidth: PlasmaCore.Units.gridUnit - implicitHeight: width - Layout.alignment: Qt.AlignCenter - Layout.topMargin: Kirigami.Units.smallSpacing - Layout.bottomMargin: Kirigami.Units.smallSpacing - } - - TapHandler { - onTapped: { - if (root.expandedMode) { - root.closeRequested(); - } else { - root.expandRequested(); - root.expandedMode = true; - } - } - } - } - } - } -} diff --git a/containments/panel/phonepanel.cpp b/containments/panel/phonepanel.cpp index 0ef24ddc..106e73ba 100644 --- a/containments/panel/phonepanel.cpp +++ b/containments/panel/phonepanel.cpp @@ -7,238 +7,9 @@ #include "phonepanel.h" -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define FORMAT24H "HH:mm:ss" - -constexpr int SCREENSHOT_DELAY = 200; - -/* -- Static Helpers --------------------------------------------------------------------------- */ - -static QImage allocateImage(const QVariantMap &metadata) -{ - bool ok; - - const uint width = metadata.value(QStringLiteral("width")).toUInt(&ok); - if (!ok) { - return QImage(); - } - - const uint height = metadata.value(QStringLiteral("height")).toUInt(&ok); - if (!ok) { - return QImage(); - } - - const uint format = metadata.value(QStringLiteral("format")).toUInt(&ok); - if (!ok) { - return QImage(); - } - - return QImage(width, height, QImage::Format(format)); -} - -static QImage readImage(int fileDescriptor, const QVariantMap &metadata) -{ - QFile file; - if (!file.open(fileDescriptor, QFileDevice::ReadOnly, QFileDevice::AutoCloseHandle)) { - close(fileDescriptor); - return QImage(); - } - - QImage result = allocateImage(metadata); - if (result.isNull()) { - return QImage(); - } - - QDataStream stream(&file); - stream.readRawData(reinterpret_cast(result.bits()), result.sizeInBytes()); - - return result; -} - PhonePanel::PhonePanel(QObject *parent, const QVariantList &args) : Plasma::Containment(parent, args) { - // setHasConfigurationInterface(true); - m_kscreenInterface = new org::kde::KScreen(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/kscreen"), QDBusConnection::sessionBus(), this); - m_screenshotInterface = new OrgKdeKWinScreenShot2Interface(QStringLiteral("org.kde.KWin.ScreenShot2"), - QStringLiteral("/org/kde/KWin/ScreenShot2"), - QDBusConnection::sessionBus(), - this); - - m_localeConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig); - m_localeConfigWatcher = KConfigWatcher::create(m_localeConfig); - - // watch for changes to locale config, to update 12/24 hour time - connect(m_localeConfigWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void { - if (group.name() == "Locale") { - // we have to reparse for new changes (from system settings) - m_localeConfig->reparseConfiguration(); - Q_EMIT isSystem24HourFormatChanged(); - } - }); } PhonePanel::~PhonePanel() = default; - -void PhonePanel::executeCommand(const QString &command) -{ - qWarning() << "Executing" << command; - const QStringList commandAndArguments = QProcess::splitCommand(command); - QProcess::startDetached(commandAndArguments.front(), commandAndArguments.mid(1)); -} - -void PhonePanel::toggleTorch() -{ - // FIXME this is hardcoded to the PinePhone for now - static auto FLASH_SYSFS_PATH = "/sys/devices/platform/led-controller/leds/white:flash/brightness"; - int fd = open(FLASH_SYSFS_PATH, O_WRONLY); - - if (fd < 0) { - qWarning() << "Unable to open file %s" << FLASH_SYSFS_PATH; - return; - } - - write(fd, m_running ? "0" : "1", 1); - close(fd); - m_running = !m_running; - Q_EMIT torchChanged(m_running); -} -bool PhonePanel::torchEnabled() const -{ - return m_running; -} -bool PhonePanel::autoRotate() -{ - QDBusPendingReply reply = m_kscreenInterface->getAutoRotate(); - reply.waitForFinished(); - if (reply.isError()) { - qWarning() << "Getting auto rotate failed:" << reply.error().name() << reply.error().message(); - return false; - } else { - return reply.value(); - } -} - -void PhonePanel::setAutoRotate(bool value) -{ - QDBusPendingReply<> reply = m_kscreenInterface->setAutoRotate(value); - reply.waitForFinished(); - if (reply.isError()) { - qWarning() << "Setting auto rotate failed:" << reply.error().name() << reply.error().message(); - } else { - emit autoRotateChanged(value); - } -} - -void PhonePanel::handleMetaDataReceived(const QVariantMap &metadata, int fd) -{ - const QString type = metadata.value(QStringLiteral("type")).toString(); - if (type != QLatin1String("raw")) { - qWarning() << "Unsupported metadata type:" << type; - return; - } - - auto watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, [watcher]() { - watcher->deleteLater(); - - QString filePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); - if (filePath.isEmpty()) { - qWarning() << "Couldn't find a writable location for the screenshot!"; - return; - } - QDir picturesDir(filePath); - if (!picturesDir.mkpath(QStringLiteral("Screenshots"))) { - qWarning() << "Couldn't create folder at" << picturesDir.path() + QStringLiteral("/Screenshots") << "to take screenshot."; - return; - } - filePath += QStringLiteral("/Screenshots/Screenshot_%1.png").arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_hhmmss"))); - const auto m_result = watcher->result(); - if (m_result.isNull() || !m_result.save(filePath)) { - qWarning() << "Screenshot failed"; - } else { - KNotification *notif = new KNotification("captured"); - notif->setComponentName(QStringLiteral("plasma_phone_components")); - notif->setTitle(i18n("New Screenshot")); - notif->setUrls({QUrl::fromLocalFile(filePath)}); - notif->setText(i18n("New screenshot saved to %1", filePath)); - notif->sendEvent(); - } - }); - watcher->setFuture(QtConcurrent::run(readImage, fd, metadata)); -} - -void PhonePanel::takeScreenshot() -{ - // wait ~200 ms to wait for rest of animations - QTimer::singleShot(SCREENSHOT_DELAY, [=]() { - int lPipeFds[2]; - if (pipe2(lPipeFds, O_CLOEXEC) != 0) { - qWarning() << "Could not take screenshot"; - return; - } - - // We don't have access to the ScreenPool so we'll just take the first screen - QVariantMap options; - options.insert(QStringLiteral("native-resolution"), true); - - auto pendingCall = m_screenshotInterface->CaptureScreen( - qGuiApp->screens().constFirst()->name(), options, - QDBusUnixFileDescriptor(lPipeFds[1])); - close(lPipeFds[1]); - auto pipeFileDescriptor = lPipeFds[0]; - - auto watcher = new QDBusPendingCallWatcher(pendingCall, this); - connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, pipeFileDescriptor]() { - watcher->deleteLater(); - const QDBusPendingReply reply = *watcher; - - if (reply.isError()) { - qWarning() << "Screenshot request failed:" << reply.error().message(); - } else { - handleMetaDataReceived(reply, pipeFileDescriptor); - } - }); - }); -} - -bool PhonePanel::isSystem24HourFormat() -{ - KConfigGroup localeSettings = KConfigGroup(m_localeConfig, "Locale"); - - QString timeFormat = localeSettings.readEntry("TimeFormat", QStringLiteral(FORMAT24H)); - return timeFormat == QStringLiteral(FORMAT24H); -} - -void PhonePanel::launchApp(const QString &app) -{ - const KService::Ptr appService = KService::serviceByDesktopName(app); - if (!appService) { - qWarning() << "Could not find" << app; - return; - } - auto job = new KIO::ApplicationLauncherJob(appService, this); - job->start(); -} - -K_PLUGIN_CLASS_WITH_JSON(PhonePanel, "metadata.json") - -#include "phonepanel.moc" diff --git a/containments/panel/phonepanel.h b/containments/panel/phonepanel.h index 41a2ad12..276ad7b0 100644 --- a/containments/panel/phonepanel.h +++ b/containments/panel/phonepanel.h @@ -9,50 +9,13 @@ #include -#include -#include - -#include "kscreeninterface.h" -#include "screenshot2interface.h" - class PhonePanel : public Plasma::Containment { Q_OBJECT - Q_PROPERTY(bool autoRotateEnabled READ autoRotate WRITE setAutoRotate NOTIFY autoRotateChanged); - Q_PROPERTY(bool torchEnabled READ torchEnabled NOTIFY torchChanged); - Q_PROPERTY(bool isSystem24HourFormat READ isSystem24HourFormat NOTIFY isSystem24HourFormatChanged); public: PhonePanel(QObject *parent, const QVariantList &args); ~PhonePanel() override; - -public Q_SLOTS: - void executeCommand(const QString &command); - void launchApp(const QString &app); - void toggleTorch(); - void takeScreenshot(); - - bool autoRotate(); - void setAutoRotate(bool value); - - bool torchEnabled() const; - - bool isSystem24HourFormat(); - -signals: - void autoRotateChanged(bool value); - void torchChanged(bool value); - void isSystem24HourFormatChanged(); - -private: - void handleMetaDataReceived(const QVariantMap &metadata, int fd); - bool m_running = false; - - KConfigWatcher::Ptr m_localeConfigWatcher; - KSharedConfig::Ptr m_localeConfig; - - org::kde::KScreen *m_kscreenInterface; - OrgKdeKWinScreenShot2Interface *m_screenshotInterface; }; #endif diff --git a/look-and-feel/contents/lockscreen/Clock.qml b/look-and-feel/contents/lockscreen/Clock.qml index d8021e7d..bed6adb0 100644 --- a/look-and-feel/contents/lockscreen/Clock.qml +++ b/look-and-feel/contents/lockscreen/Clock.qml @@ -54,6 +54,7 @@ ColumnLayout { id: timeSource engine: "time" connectedSources: ["Local"] - interval: 1000 + interval: 60000 + intervalAlignment: PlasmaCore.Types.AlignToMinute } } diff --git a/look-and-feel/contents/lockscreen/LockScreen.qml b/look-and-feel/contents/lockscreen/LockScreen.qml index 2b53f4b6..da27b73d 100644 --- a/look-and-feel/contents/lockscreen/LockScreen.qml +++ b/look-and-feel/contents/lockscreen/LockScreen.qml @@ -9,9 +9,12 @@ import QtQuick 2.12 import QtQuick.Controls 1.1 import QtQuick.Layouts 1.1 import QtGraphicalEffects 1.12 + import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.workspace.keyboardlayout 1.0 import org.kde.notificationmanager 1.1 as Notifications +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + import "../components" PlasmaCore.ColorScope { @@ -78,9 +81,18 @@ PlasmaCore.ColorScope { left: parent.left right: parent.right } - height: PlasmaCore.Units.gridUnit + height: PlasmaCore.Units.gridUnit * 1.25 opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight) - sourceComponent: SimpleHeaderBar {} + sourceComponent: MobileShell.StatusBar { + id: statusBar + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + backgroundColor: "transparent" + + showSecondRow: false + showDropShadow: true + showTime: false + disableSystemTray: true // HACK: prevent SIGABRT + } } // phone lockscreen component @@ -125,11 +137,10 @@ PlasmaCore.ColorScope { alignment: Qt.AlignHCenter Layout.bottomMargin: PlasmaCore.Units.gridUnit * 2 // keep spacing even if media controls are gone } - MediaControls { + MobileShell.MediaControlsWidget { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25 - Layout.minimumWidth: PlasmaCore.Units.gridUnit * 15 Layout.leftMargin: PlasmaCore.Units.gridUnit Layout.rightMargin: PlasmaCore.Units.gridUnit } @@ -184,11 +195,10 @@ PlasmaCore.ColorScope { Layout.fillWidth: true Layout.minimumWidth: PlasmaCore.Units.gridUnit * 20 } - MediaControls { + MobileShell.MediaControlsWidget { Layout.alignment: Qt.AlignLeft Layout.fillWidth: true Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25 - Layout.minimumWidth: PlasmaCore.Units.gridUnit * 20 } } } diff --git a/look-and-feel/contents/lockscreen/MediaControls.qml b/look-and-feel/contents/lockscreen/MediaControls.qml deleted file mode 100644 index 2c5621bc..00000000 --- a/look-and-feel/contents/lockscreen/MediaControls.qml +++ /dev/null @@ -1,160 +0,0 @@ -/* - This file is part of the KDE project. - -SPDX-FileCopyrightText: 2016 Kai Uwe Broulik - -SPDX-License-Identifier: GPL-2.0-or-later -*/ - -import QtQuick 2.5 -import QtQuick.Layouts 1.1 - -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents3 -import org.kde.plasma.extras 2.0 as PlasmaExtras - -Item { - visible: mpris2Source.hasPlayer - implicitHeight: controlsRow.height + controlsRow.y - - RowLayout { - id: controlsRow - anchors.bottom: parent.bottom - y: PlasmaCore.Units.smallSpacing // some distance to the password field - width: parent.width - height: PlasmaCore.Units.gridUnit * 3 - spacing: 0 - - enabled: mpris2Source.canControl - - PlasmaCore.DataSource { - id: mpris2Source - - readonly property string source: "@multiplex" - readonly property var playerData: data[source] - - readonly property bool hasPlayer: sources.length > 1 && !!playerData - readonly property string identity: hasPlayer ? playerData.Identity : "" - readonly property bool playing: hasPlayer && playerData.PlaybackStatus === "Playing" - readonly property bool canControl: hasPlayer && playerData.CanControl - readonly property bool canGoBack: hasPlayer && playerData.CanGoPrevious - readonly property bool canGoNext: hasPlayer && playerData.CanGoNext - - readonly property var currentMetadata: hasPlayer ? playerData.Metadata : ({}) - - readonly property string track: { - var xesamTitle = currentMetadata["xesam:title"] - if (xesamTitle) { - return xesamTitle - } - // if no track title is given, print out the file name - var xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : "" - if (!xesamUrl) { - return "" - } - var lastSlashPos = xesamUrl.lastIndexOf('/') - if (lastSlashPos < 0) { - return "" - } - var lastUrlPart = xesamUrl.substring(lastSlashPos + 1) - return decodeURIComponent(lastUrlPart) - } - readonly property string artist: currentMetadata["xesam:artist"] || "" - readonly property string albumArt: currentMetadata["mpris:artUrl"] || "" - - engine: "mpris2" - connectedSources: [source] - - function startOperation(op) { - var service = serviceForSource(source) - var operation = service.operationDescription(op) - return service.startOperationCall(operation) - } - - function goPrevious() { - startOperation("Previous"); - } - function goNext() { - startOperation("Next"); - } - function playPause(source) { - startOperation("PlayPause"); - } - } - - Image { - id: albumArt - Layout.preferredWidth: height - Layout.fillHeight: true - asynchronous: true - fillMode: Image.PreserveAspectFit - source: mpris2Source.albumArt - sourceSize.height: height - visible: status === Image.Loading || status === Image.Ready - } - - Item { // spacer - width: PlasmaCore.Units.smallSpacing - height: 1 - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - PlasmaComponents3.Label { - Layout.fillWidth: true - wrapMode: Text.NoWrap - elide: Text.ElideRight - text: mpris2Source.track || i18nd("plasma_lookandfeel_org.kde.lookandfeel", "No media playing") - textFormat: Text.PlainText - font.pointSize: PlasmaCore.Theme.defaultFont.pointSize + 1 - maximumLineCount: 1 - } - - PlasmaExtras.DescriptiveLabel { - Layout.fillWidth: true - wrapMode: Text.NoWrap - elide: Text.ElideRight - // if no artist is given, show player name instead - text: mpris2Source.artist || mpris2Source.identity || "" - textFormat: Text.PlainText - font.pointSize: PlasmaCore.Theme.smallestFont.pointSize + 1 - maximumLineCount: 1 - } - } - - PlasmaComponents3.ToolButton { - enabled: mpris2Source.canGoBack - icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" - onClicked: { -// fadeoutTimer.running = false - mpris2Source.goPrevious() - } - visible: mpris2Source.canGoBack || mpris2Source.canGoNext - Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Previous track") - } - - PlasmaComponents3.ToolButton { - Layout.fillHeight: true - Layout.preferredWidth: height // make this button bigger - icon.name: mpris2Source.playing ? "media-playback-pause" : "media-playback-start" - onClicked: { -// fadeoutTimer.running = false - mpris2Source.playPause() - } - Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Play or Pause media") - } - - PlasmaComponents3.ToolButton { - enabled: mpris2Source.canGoNext - icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" - onClicked: { -// fadeoutTimer.running = false - mpris2Source.goNext() - } - visible: mpris2Source.canGoBack || mpris2Source.canGoNext - Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Next track") - } - } -} diff --git a/look-and-feel/contents/lockscreen/SimpleHeaderBar.qml b/look-and-feel/contents/lockscreen/SimpleHeaderBar.qml deleted file mode 100644 index 5b3dafff..00000000 --- a/look-and-feel/contents/lockscreen/SimpleHeaderBar.qml +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * SPDX-FileCopyrightText: 2020 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Controls 2.15 as Controls -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 - -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.plasma.workspace.components 2.0 as PW - -import "indicators" as Indicators -import "indicators/providers" as Providers - -// a simple version of the task panel -// in the future, it should share components with the existing task panel -PlasmaCore.ColorScope { - colorGroup: PlasmaCore.Theme.ComplementaryColorGroup - - property real textPixelSize: PlasmaCore.Units.gridUnit * 0.6 - - layer.enabled: true - layer.effect: DropShadow { - visible: true - cached: true - horizontalOffset: 0 - verticalOffset: 1 - radius: 6.0 - samples: 17 - color: Qt.rgba(0,0,0,0.6) - } - - Providers.SignalStrengthProvider { - id: signalStrengthProviderLoader - } - - Controls.Control { - topPadding: PlasmaCore.Units.smallSpacing - bottomPadding: PlasmaCore.Units.smallSpacing - rightPadding: PlasmaCore.Units.smallSpacing * 3 - leftPadding: PlasmaCore.Units.smallSpacing * 3 - - anchors.fill: parent - - contentItem: RowLayout { - id: row - spacing: 0 - - Indicators.SignalStrength { - provider: signalStrengthProviderLoader - labelPixelSize: textPixelSize - Layout.fillHeight: true - } - - // spacing in the middle - Item { - Layout.fillWidth: true - } - - RowLayout { - id: indicators - spacing: PlasmaCore.Units.smallSpacing * 1.5 - Layout.fillHeight: true - - Indicators.Bluetooth { - Layout.fillHeight: true - } - Indicators.Wifi { - Layout.fillHeight: true - } - Indicators.Volume { - Layout.fillHeight: true - } - Indicators.Battery { - spacing: PlasmaCore.Units.smallSpacing * 1.5 - labelPixelSize: textPixelSize - Layout.fillHeight: true - } - } - } - } -} diff --git a/look-and-feel/contents/lockscreen/indicators/Battery.qml b/look-and-feel/contents/lockscreen/indicators/Battery.qml deleted file mode 100644 index cac12546..00000000 --- a/look-and-feel/contents/lockscreen/indicators/Battery.qml +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.6 -import QtQuick.Layouts 1.4 - -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.plasma.workspace.components 2.0 as PW - - -RowLayout { - visible: pmSource.data["Battery"]["Has Cumulative"] - - property real labelPixelSize - - PW.BatteryIcon { - id: battery - Layout.preferredWidth: height - Layout.fillHeight: true - hasBattery: true - percent: pmSource.data["Battery"]["Percent"] - pluggedIn: pmSource.data["AC Adapter"] ? pmSource.data["AC Adapter"]["Plugged in"] : false - - height: batteryLabel.height - width: batteryLabel.height - - PlasmaCore.DataSource { - id: pmSource - engine: "powermanagement" - connectedSources: ["Battery", "AC Adapter"] - } - } - - PlasmaComponents.Label { - id: batteryLabel - text: i18n("%1%", battery.percent) - Layout.alignment: Qt.AlignVCenter - - color: PlasmaCore.ColorScope.textColor - font.pixelSize: labelPixelSize - } -} diff --git a/look-and-feel/contents/lockscreen/indicators/Bluetooth.qml b/look-and-feel/contents/lockscreen/indicators/Bluetooth.qml deleted file mode 100644 index 3ea5ef3c..00000000 --- a/look-and-feel/contents/lockscreen/indicators/Bluetooth.qml +++ /dev/null @@ -1,47 +0,0 @@ -/* - SPDX-FileCopyrightText: 2019 Marco Martin - SPDX-FileCopyrightText: 2013-2017 Jan Grulich - - SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL -*/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.4 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.bluezqt 1.0 as BluezQt - -PlasmaCore.IconItem { - id: connectionIcon - - property bool deviceConnected : false - - source: deviceConnected ? "preferences-system-bluetooth-activated" : "preferences-system-bluetooth"; - colorGroup: PlasmaCore.ColorScope.colorGroup - - visible: BluezQt.Manager.bluetoothOperational - - Layout.fillHeight: true - Layout.preferredWidth: height - function updateStatus() - { - var connectedDevices = []; - - for (var i = 0; i < BluezQt.Manager.devices.length; ++i) { - var device = BluezQt.Manager.devices[i]; - if (device.connected) { - connectedDevices.push(device); - } - } - deviceConnected = connectedDevices.length > 0; - } - - Component.onCompleted: { - BluezQt.Manager.deviceAdded.connect(updateStatus); - BluezQt.Manager.deviceRemoved.connect(updateStatus); - BluezQt.Manager.deviceChanged.connect(updateStatus); - BluezQt.Manager.bluetoothBlockedChanged.connect(updateStatus); - BluezQt.Manager.bluetoothOperationalChanged.connect(updateStatus); - - updateStatus(); - } -} diff --git a/look-and-feel/contents/lockscreen/indicators/Indicator.qml b/look-and-feel/contents/lockscreen/indicators/Indicator.qml deleted file mode 100644 index 0011edc9..00000000 --- a/look-and-feel/contents/lockscreen/indicators/Indicator.qml +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.6 -import QtQuick.Layouts 1.4 - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents - - -RowLayout { - - property alias icon: icon.source - property alias text: label.text - PlasmaCore.IconItem { - id: icon - colorGroup: PlasmaCore.ColorScope.colorGroup - - Layout.fillHeight: true - Layout.preferredWidth: height - } - PlasmaComponents.Label { - id: label - visible: text.length > 0 - color: PlasmaCore.ColorScope.textColor - font.pixelSize: parent.height / 2 - } -} diff --git a/look-and-feel/contents/lockscreen/indicators/Volume.qml b/look-and-feel/contents/lockscreen/indicators/Volume.qml deleted file mode 100644 index 84e3eacd..00000000 --- a/look-and-feel/contents/lockscreen/indicators/Volume.qml +++ /dev/null @@ -1,63 +0,0 @@ -/* - SPDX-FileCopyrightText: 2019 Aditya Mehra - SPDX-FileCopyrightText: 2014-2015 Harald Sitter - - SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL -*/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.4 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.private.volume 0.1 - -PlasmaCore.IconItem { - - id: paIcon - Layout.fillHeight: true - Layout.preferredWidth: height - property bool volumeFeedback: true - property int maxVolumeValue: Math.round(100 * PulseAudio.NormalVolume / 100.0) - property int volumeStep: Math.round(5 * PulseAudio.NormalVolume / 100.0) - readonly property string dummyOutputName: "auto_null" - source: paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink) - ? iconName(paSinkModel.preferredSink.volume, paSinkModel.preferredSink.muted) - : iconName(0, true) - - colorGroup: PlasmaCore.ColorScope.colorGroup - - visible: paSinkModel.preferredSink && paSinkModel.preferredSink.muted - - function iconName(volume, muted, prefix) { - if (!prefix) { - prefix = "audio-volume"; - } - var icon = null; - var percent = volume / maxVolumeValue; - if (percent <= 0.0 || muted) { - icon = prefix + "-muted"; - } else if (percent <= 0.25) { - icon = prefix + "-low"; - } else if (percent <= 0.75) { - icon = prefix + "-medium"; - } else { - icon = prefix + "-high"; - } - return icon; - } - - function isDummyOutput(output) { - return output && output.name === dummyOutputName; - } - - SinkModel { - id: paSinkModel - } - - VolumeOSD { - id: osd - } - - VolumeFeedback { - id: feedback - } -} diff --git a/look-and-feel/contents/lockscreen/indicators/Wifi.qml b/look-and-feel/contents/lockscreen/indicators/Wifi.qml deleted file mode 100644 index 7ad10ce1..00000000 --- a/look-and-feel/contents/lockscreen/indicators/Wifi.qml +++ /dev/null @@ -1,46 +0,0 @@ -/* - SPDX-FileCopyrightText: 2019 Marco Martin - SPDX-FileCopyrightText: 2013-2017 Jan Grulich - - SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL -*/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.4 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.plasma.networkmanagement 0.2 as PlasmaNM - -PlasmaCore.IconItem { - id: connectionIcon - - source: connectionIconProvider.connectionIcon - colorGroup: PlasmaCore.ColorScope.colorGroup - - Layout.fillHeight: true - Layout.preferredWidth: height - - PlasmaComponents.BusyIndicator { - id: connectingIndicator - - anchors.fill: parent - running: connectionIconProvider.connecting - visible: running - } - - PlasmaNM.NetworkStatus { - id: networkStatus - } - - PlasmaNM.NetworkModel { - id: connectionModel - } - - PlasmaNM.Handler { - id: handler - } - - PlasmaNM.ConnectionIcon { - id: connectionIconProvider - } -} diff --git a/look-and-feel/contents/lockscreen/indicators/providers/SignalStrengthProvider.qml b/look-and-feel/contents/lockscreen/indicators/providers/SignalStrengthProvider.qml deleted file mode 100644 index 4a549b64..00000000 --- a/look-and-feel/contents/lockscreen/indicators/providers/SignalStrengthProvider.qml +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Devin Lin - * SPDX-FileCopyrightText: 2015 Marco Martin - * SPDX-FileCopyrightText: 2021 Tobias Fella - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.1 - -import org.kde.plasma.mm 1.0 - -QtObject { - - property string icon: "network-mobile-" + Math.floor(SignalIndicator.strength / 20) * 20 - - property string label: !SignalIndicator.available ? "" - : SignalIndicator.simLocked ? i18n("SIM Locked") : SignalIndicator.name -} - diff --git a/quicksettings/airplanemode/contents/ui/main.qml b/quicksettings/airplanemode/contents/ui/main.qml index 9d36bb41..eddf8f55 100644 --- a/quicksettings/airplanemode/contents/ui/main.qml +++ b/quicksettings/airplanemode/contents/ui/main.qml @@ -5,12 +5,13 @@ * SPDX-License-Identifier: LGPL-2.0-or-later */ -import org.kde.plasma.private.mobilehomescreencomponents 0.1 as HomeScreenComponents +import org.kde.plasma.private.mobileshell 1.0 as MobileShell import org.kde.plasma.networkmanagement 0.2 as PlasmaNM -HomeScreenComponents.QuickSetting { +MobileShell.QuickSetting { text: i18n("Airplane Mode") icon: "network-flightmode-on" + status: "" enabled: PlasmaNM.Configuration.airplaneModeEnabled PlasmaNM.Handler { diff --git a/quicksettings/nightcolor/contents/ui/main.qml b/quicksettings/nightcolor/contents/ui/main.qml index 845ee5b5..dc8f080e 100644 --- a/quicksettings/nightcolor/contents/ui/main.qml +++ b/quicksettings/nightcolor/contents/ui/main.qml @@ -7,12 +7,13 @@ */ import org.kde.colorcorrect 0.1 as CC -import org.kde.plasma.private.mobilehomescreencomponents 0.1 as HomeScreenComponents +import org.kde.plasma.private.mobileshell 1.0 as MobileShell -HomeScreenComponents.QuickSetting { +MobileShell.QuickSetting { text: i18n("Night Color") icon: "redshift-status-on" enabled: compositorAdaptor.active + status: "" settingsCommand: "plasma-open-settings kcm_nightcolor" CC.CompositorAdaptor { diff --git a/shell/contents/layout.js b/shell/contents/layout.js index 83fc982d..061109ba 100644 --- a/shell/contents/layout.js +++ b/shell/contents/layout.js @@ -7,7 +7,6 @@ for (var j = 0; j < desktopsArray.length; j++) { var panel = new Panel("org.kde.phone.panel"); panel.location = "top"; panel.addWidget("org.kde.plasma.notifications"); -panel.addWidget("org.kde.plasma.mediacontroller"); panel.height = 1.25 * gridUnit; // HACK: supposed to be gridUnit + smallSpacing, but it doesn't seem to give the correct number var bottomPanel = new Panel("org.kde.phone.taskpanel") diff --git a/shell/contents/updates/panelsfix.js b/shell/contents/updates/panelsfix.js index bbd2f104..1e6a2ca9 100644 --- a/shell/contents/updates/panelsfix.js +++ b/shell/contents/updates/panelsfix.js @@ -14,7 +14,6 @@ if (!topFound) { // keep widget list synced with the layout.js let topPanel = new Panel("org.kde.phone.panel") topPanel.addWidget("org.kde.plasma.notifications"); - topPanel.addWidget("org.kde.plasma.mediacontroller"); topPanel.location = "top"; topPanel.height = 1.25 * gridUnit; }