diff --git a/applets/clipboard/contents/ui/ClipboardPage.qml b/applets/clipboard/contents/ui/ClipboardPage.qml index 54061274c..c3138a015 100644 --- a/applets/clipboard/contents/ui/ClipboardPage.qml +++ b/applets/clipboard/contents/ui/ClipboardPage.qml @@ -1,126 +1,117 @@ /******************************************************************** This file is part of the KDE project. Copyright (C) 2014 Martin Gräßlin Copyright (C) 2014 Kai Uwe Broulik This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ import QtQuick 2.4 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 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras ColumnLayout { Keys.onPressed: { switch(event.key) { case Qt.Key_Up: { clipboardMenu.view.decrementCurrentIndex(); event.accepted = true; break; } case Qt.Key_Down: { clipboardMenu.view.incrementCurrentIndex(); event.accepted = true; break; } case Qt.Key_Enter: case Qt.Key_Return: { if (clipboardMenu.view.currentIndex >= 0) { var uuid = clipboardMenu.model.get(clipboardMenu.view.currentIndex).UuidRole if (uuid) { clipboardSource.service(uuid, "select") clipboardMenu.view.currentIndex = 0 } } break; } case Qt.Key_Escape: { if (filter.text != "") { filter.text = ""; event.accepted = true; } break; } default: { // forward key to filter // filter.text += event.text wil break if the key is backspace if (event.key === Qt.Key_Backspace && filter.text == "") { return; } if (event.text !== "" && !filter.activeFocus) { clipboardMenu.view.currentIndex = -1 if (event.matches(StandardKey.Paste)) { filter.paste(); } else { filter.text = ""; filter.text += event.text; } filter.forceActiveFocus(); event.accepted = true; } } } } - PlasmaExtras.Heading { - id: emptyHint - Layout.fillWidth: true - level: 3 - opacity: 0.6 - visible: clipboardMenu.model.count === 0 && filter.length === 0 - text: i18n("Clipboard is empty") - } - RowLayout { Layout.fillWidth: true - visible: !emptyHint.visible + visible: clipboardMenu.model.count > 0 PlasmaComponents.TextField { id: filter placeholderText: i18n("Search...") clearButtonShown: true Layout.fillWidth: true } PlasmaComponents.ToolButton { iconSource: "edit-clear-history" tooltip: i18n("Clear history") onClicked: clipboardSource.service("", "clearHistory") } } Menu { id: clipboardMenu model: PlasmaCore.SortFilterModel { sourceModel: clipboardSource.models.clipboard filterRole: "DisplayRole" filterRegExp: filter.text } supportsBarcodes: clipboardSource.data["clipboard"]["supportsBarcodes"] Layout.fillWidth: true Layout.fillHeight: true onItemSelected: clipboardSource.service(uuid, "select") onRemove: clipboardSource.service(uuid, "remove") onEdit: clipboardSource.edit(uuid) onBarcode: { var page = stack.push(barcodePage); page.show(uuid); } onAction: { clipboardSource.service(uuid, "action") clipboardMenu.view.currentIndex = 0 } } } diff --git a/applets/clipboard/contents/ui/Menu.qml b/applets/clipboard/contents/ui/Menu.qml index ecb8161d8..69b481f80 100644 --- a/applets/clipboard/contents/ui/Menu.qml +++ b/applets/clipboard/contents/ui/Menu.qml @@ -1,56 +1,70 @@ /******************************************************************** This file is part of the KDE project. Copyright (C) 2014 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ import QtQuick 2.0 import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.kirigami 2.12 as Kirigami + PlasmaExtras.ScrollArea { id: menu property alias view: menuListView property alias model: menuListView.model property bool supportsBarcodes signal itemSelected(string uuid) signal remove(string uuid) signal edit(string uuid) signal barcode(string uuid) signal action(string uuid) ListView { id: menuListView focus: true boundsBehavior: Flickable.StopAtBounds interactive: contentHeight > height highlight: PlasmaComponents.Highlight { } highlightMoveDuration: 0 highlightResizeDuration: 0 currentIndex: -1 delegate: ClipboardItemDelegate { width: menuListView.width supportsBarcodes: menu.supportsBarcodes onItemSelected: menu.itemSelected(uuid) onRemove: menu.remove(uuid) onEdit: menu.edit(uuid) onBarcode: menu.barcode(uuid) onAction: menu.action(uuid) } + + Kirigami.PlaceholderMessage { + id: emptyHint + + anchors.centerIn: parent + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: units.largeSpacing + + visible: menuListView.count === 0 + text: i18n("Clipboard is empty") + } } } diff --git a/applets/notifications/package/contents/ui/FullRepresentation.qml b/applets/notifications/package/contents/ui/FullRepresentation.qml index ad9eb4e25..874c97fc7 100644 --- a/applets/notifications/package/contents/ui/FullRepresentation.qml +++ b/applets/notifications/package/contents/ui/FullRepresentation.qml @@ -1,623 +1,622 @@ /* * Copyright 2018-2019 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ import QtQuick 2.10 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 2.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.extras 2.0 as PlasmaExtras -import org.kde.kirigami 2.11 as Kirigami +import org.kde.kirigami 2.12 as Kirigami import org.kde.kcoreaddons 1.0 as KCoreAddons import org.kde.notificationmanager 1.0 as NotificationManager import "global" PlasmaComponents3.Page { // TODO these should be configurable in the future readonly property int dndMorningHour: 6 readonly property int dndEveningHour: 20 Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical header: PlasmaExtras.PlasmoidHeading { ColumnLayout { anchors.fill: parent id: header visible: !Kirigami.Settings.isMobile Layout.fillWidth: true Layout.leftMargin: units.smallSpacing spacing: 0 RowLayout { Layout.fillWidth: true spacing: 0 RowLayout { id: dndRow spacing: units.smallSpacing enabled: NotificationManager.Server.valid PlasmaComponents3.CheckBox { id: dndCheck text: i18n("Do not disturb") spacing: units.smallSpacing checkable: true checked: Globals.inhibited // Let the menu open on press onPressed: { if (!Globals.inhibited) { dndMenu.date = new Date(); // shows ontop of CheckBox to hide the fact that it's unchecked // until you actually select something :) dndMenu.open(0, 0); } } // but disable only on click onClicked: { if (Globals.inhibited) { Globals.revokeInhibitions(); } } contentItem: RowLayout { spacing: dndCheck.spacing PlasmaCore.IconItem { Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + dndCheck.spacing Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + dndCheck.spacing : 0 source: "notifications-disabled" Layout.preferredWidth: units.iconSizes.smallMedium Layout.preferredHeight: units.iconSizes.smallMedium } PlasmaComponents.Label { text: i18n("Do not disturb") } } PlasmaComponents.ModelContextMenu { id: dndMenu property date date visualParent: dndCheck onClicked: { notificationSettings.notificationsInhibitedUntil = model.date; notificationSettings.save(); } model: { var model = []; // For 1 hour var d = dndMenu.date; d.setHours(d.getHours() + 1); d.setSeconds(0); model.push({date: d, text: i18n("For 1 hour")}); d = dndMenu.date; d.setHours(d.getHours() + 4); d.setSeconds(0); model.push({date: d, text: i18n("For 4 hours")}); // Until this evening if (dndMenu.date.getHours() < dndEveningHour) { d = dndMenu.date; // TODO make the user's preferred time schedule configurable d.setHours(dndEveningHour); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until this evening")}); } // Until next morning if (dndMenu.date.getHours() > dndMorningHour) { d = dndMenu.date; d.setDate(d.getDate() + 1); d.setHours(dndMorningHour); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until tomorrow morning")}); } // Until Monday // show Friday and Saturday, Sunday is "0" but for that you can use "until tomorrow morning" if (dndMenu.date.getDay() >= 5) { d = dndMenu.date; d.setHours(dndMorningHour); // wraps around if necessary d.setDate(d.getDate() + (7 - d.getDay() + 1)); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until Monday")}); } // Until "turned off" d = dndMenu.date; // Just set it to one year in the future so we don't need yet another "do not disturb enabled" property d.setFullYear(d.getFullYear() + 1); model.push({date: d, text: i18n("Until turned off")}); return model; } } } } Item { Layout.fillWidth: true } PlasmaComponents.ToolButton { iconName: "configure" // remove mnemonics tooltip: plasmoid.action("openKcm").text.replace(/([^&]*)&(.)([^&]*)/g, function (match, p1, p2, p3) { return p1.concat(p2, p3); }); visible: plasmoid.action("openKcm").enabled onClicked: plasmoid.action("openKcm").trigger() } PlasmaComponents.ToolButton { iconName: "edit-clear-history" tooltip: i18n("Clear History") enabled: plasmoid.action("clearHistory").visible onClicked: action_clearHistory() } } PlasmaExtras.DescriptiveLabel { Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium : 0 Layout.fillWidth: true wrapMode: Text.WordWrap textFormat: Text.PlainText text: { if (!Globals.inhibited) { return ""; } var inhibitedUntil = notificationSettings.notificationsInhibitedUntil; var inhibitedByApp = notificationSettings.notificationsInhibitedByApplication; var inhibitedByMirroredScreens = notificationSettings.inhibitNotificationsWhenScreensMirrored && notificationSettings.screensMirrored; var sections = []; // Show until time if valid but not if too far int he future if (!isNaN(inhibitedUntil.getTime()) && inhibitedUntil.getTime() - new Date().getTime() < 365 * 24 * 60 * 60 * 1000 /* 1 year*/) { sections.push(i18nc("Do not disturb until date", "Until %1", KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat))); } if (inhibitedByApp) { var inhibitionAppNames = notificationSettings.notificationInhibitionApplications; var inhibitionAppReasons = notificationSettings.notificationInhibitionReasons; for (var i = 0, length = inhibitionAppNames.length; i < length; ++i) { var name = inhibitionAppNames[i]; var reason = inhibitionAppReasons[i]; if (reason) { sections.push(i18nc("Do not disturb until app has finished (reason)", "While %1 is active (%2)", name, reason)); } else { sections.push(i18nc("Do not disturb until app has finished", "While %1 is active", name)); } } } if (inhibitedByMirroredScreens) { sections.push(i18nc("Do not disturb because external mirrored screens connected", "Screens are mirrored")) } return sections.join(" · "); } visible: text !== "" } } } ColumnLayout{ // FIXME fix popup size when resizing panel smaller (so it collapses) //Layout.preferredWidth: units.gridUnit * 18 //Layout.preferredHeight: units.gridUnit * 24 //Layout.minimumWidth: units.gridUnit * 10 //Layout.minimumHeight: units.gridUnit * 15 anchors.fill: parent spacing: units.smallSpacing // HACK forward focus to the list onActiveFocusChanged: { if (activeFocus) { list.forceActiveFocus(); } } Connections { target: plasmoid onExpandedChanged: { if (plasmoid.expanded) { list.positionViewAtBeginning(); list.currentIndex = -1; } } } // actual notifications PlasmaExtras.ScrollArea { Layout.fillWidth: true Layout.fillHeight: true Layout.preferredWidth: units.gridUnit * 18 Layout.preferredHeight: units.gridUnit * 24 Layout.leftMargin: units.smallSpacing ListView { id: list model: historyModel currentIndex: -1 Keys.onDeletePressed: { var idx = historyModel.index(currentIndex, 0); if (historyModel.data(idx, NotificationManager.Notifications.ClosableRole)) { historyModel.close(idx); // TODO would be nice to stay inside the current group when deleting an item } } Keys.onEnterPressed: Keys.onReturnPressed(event) Keys.onReturnPressed: { // Trigger default action, if any var idx = historyModel.index(currentIndex, 0); if (historyModel.data(idx, NotificationManager.Notifications.HasDefaultActionRole)) { historyModel.invokeDefaultAction(idx); return; } // Trigger thumbnail URL if there's one var urls = historyModel.data(idx, NotificationManager.Notifications.UrlsRole); if (urls && urls.length === 1) { Qt.openUrlExternally(urls[0]); historyModel.expire(idx); return; } // TODO for finished jobs trigger "Open" or "Open Containing Folder" action } Keys.onLeftPressed: setGroupExpanded(currentIndex, LayoutMirroring.enabled) Keys.onRightPressed: setGroupExpanded(currentIndex, !LayoutMirroring.enabled) Keys.onPressed: { switch (event.key) { case Qt.Key_Home: currentIndex = 0; break; case Qt.Key_End: currentIndex = count - 1; break; } } 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; } } } } highlightMoveDuration: 0 highlightResizeDuration: 0 // Not using PlasmaComponents.Highlight as this is only for indicating keyboard focus highlight: PlasmaCore.FrameSvgItem { imagePath: "widgets/listitem" prefix: "pressed" } add: Transition { SequentialAnimation { PropertyAction { property: "opacity"; value: 0 } PauseAnimation { duration: units.longDuration } ParallelAnimation { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: units.longDuration } NumberAnimation { property: "height"; from: 0; duration: units.longDuration } } } } addDisplaced: Transition { NumberAnimation { properties: "y"; duration: units.longDuration } } remove: Transition { id: removeTransition ParallelAnimation { NumberAnimation { property: "opacity"; to: 0; duration: units.longDuration } NumberAnimation { id: removeXAnimation property: "x" to: list.width duration: units.longDuration } } } removeDisplaced: Transition { SequentialAnimation { PauseAnimation { duration: units.longDuration } NumberAnimation { properties: "y"; duration: units.longDuration } } } // This is so the delegates can detect the change in "isInGroup" and show a separator section { property: "isInGroup" criteria: ViewSection.FullString } delegate: DraggableDelegate { id: delegate width: list.width contentItem: delegateLoader draggable: !model.isGroup && model.type != NotificationManager.Notifications.JobType onDismissRequested: { // Setting the animation target explicitly before removing the notification: // Using ViewTransition.item.x to get the x position in the animation // causes random crash in attached property access (cf. Bug 414066) if (x < 0) { removeXAnimation.to = -list.width; } historyModel.close(historyModel.index(index, 0)); } Loader { id: delegateLoader width: list.width sourceComponent: model.isGroup ? groupDelegate : notificationDelegate Component { id: groupDelegate NotificationHeader { applicationName: model.applicationName applicationIconSource: model.applicationIconName originName: model.originName || "" // don't show timestamp for group configurable: model.configurable closable: model.closable closeButtonTooltip: i18n("Close Group") onCloseClicked: { historyModel.close(historyModel.index(index, 0)) if (list.count === 0) { root.closePassivePlasmoid(); } } onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) } } Component { id: notificationDelegate ColumnLayout { spacing: units.smallSpacing RowLayout { Item { id: groupLineContainer Layout.fillHeight: true Layout.topMargin: units.smallSpacing width: units.iconSizes.small visible: model.isInGroup PlasmaCore.SvgItem { elementId: "vertical-line" svg: lineSvg anchors.horizontalCenter: parent.horizontalCenter width: units.iconSizes.small height: parent.height } } NotificationItem { Layout.fillWidth: true notificationType: model.type inGroup: model.isInGroup applicationName: model.applicationName applicationIconSource: model.applicationIconName originName: model.originName || "" time: model.updated || model.created // configure button on every single notifications is bit overwhelming configurable: !inGroup && model.configurable dismissable: model.type === NotificationManager.Notifications.JobType && model.jobState !== NotificationManager.Notifications.JobStateStopped && model.dismissed // TODO would be nice to be able to undismiss jobs even when they autohide && notificationSettings.permanentJobPopups dismissed: model.dismissed || false closable: model.closable summary: model.summary body: model.body || "" icon: model.image || model.iconName urls: model.urls || [] jobState: model.jobState || 0 percentage: model.percentage || 0 jobError: model.jobError || 0 suspendable: !!model.suspendable killable: !!model.killable jobDetails: model.jobDetails || null configureActionLabel: model.configureActionLabel || "" // In the popup the default action is triggered by clicking on the popup // however in the list this is undesirable, so instead show a clickable button // in case you have a non-expired notification in history (do not disturb mode) // unless it has the same label as an action readonly property bool addDefaultAction: (model.hasDefaultAction && model.defaultActionLabel && (model.actionLabels || []).indexOf(model.defaultActionLabel) === -1) ? true : false actionNames: { var actions = (model.actionNames || []); if (addDefaultAction) { actions.unshift("default"); // prepend } return actions; } actionLabels: { var labels = (model.actionLabels || []); if (addDefaultAction) { labels.unshift(model.defaultActionLabel); } return labels; } onCloseClicked: { historyModel.close(historyModel.index(index, 0)); if (list.count === 0) { root.closePassivePlasmoid(); } } onDismissClicked: { model.dismissed = false; root.closePassivePlasmoid(); } onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) onActionInvoked: { if (actionName === "default") { historyModel.invokeDefaultAction(historyModel.index(index, 0)); } else { historyModel.invokeAction(historyModel.index(index, 0), actionName); } // Keep it in the history historyModel.expire(historyModel.index(index, 0)); } onOpenUrl: { Qt.openUrlExternally(url); historyModel.expire(historyModel.index(index, 0)); } onFileActionInvoked: historyModel.expire(historyModel.index(index, 0)) onSuspendJobClicked: historyModel.suspendJob(historyModel.index(index, 0)) onResumeJobClicked: historyModel.resumeJob(historyModel.index(index, 0)) onKillJobClicked: historyModel.killJob(historyModel.index(index, 0)) } } PlasmaComponents.ToolButton { Layout.preferredWidth: minimumWidth iconName: 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) && delegate.ListView.nextSection !== delegate.ListView.section onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) } PlasmaCore.SvgItem { Layout.fillWidth: true Layout.bottomMargin: units.smallSpacing elementId: "horizontal-line" svg: lineSvg // property is only atached to the delegate itself (the Loader in our case) visible: (!model.isInGroup || delegate.ListView.nextSection !== delegate.ListView.section) && delegate.ListView.nextSection !== "" // don't show after last item } } } } } - PlasmaExtras.Heading { - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.WordWrap - level: 3 + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: units.largeSpacing + text: i18n("No unread notifications") visible: list.count === 0 && NotificationManager.Server.valid - enabled: false } ColumnLayout { id: serverUnavailableColumn width: list.width visible: list.count === 0 && !NotificationManager.Server.valid PlasmaExtras.Heading { Layout.fillWidth: true level: 3 opacity: 0.6 text: i18n("Notification service not available") wrapMode: Text.WordWrap } PlasmaComponents.Label { // Checking valid to avoid creating ServerInfo object if everything is alright readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner : null 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 } } } } } }