diff --git a/applets/devicenotifier/package/contents/ui/FullRepresentation.qml b/applets/devicenotifier/package/contents/ui/FullRepresentation.qml index dcf62e3e5..cdf6d461b 100644 --- a/applets/devicenotifier/package/contents/ui/FullRepresentation.qml +++ b/applets/devicenotifier/package/contents/ui/FullRepresentation.qml @@ -1,228 +1,231 @@ /* * Copyright 2011 Viranch Mehta * Copyright 2012 Jacopo De Simoi * Copyright 2014 David Edmundson * Copyright 2014 Marco Martin * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 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 Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.2 import QtQuick.Window 2.2 import QtQuick.Layouts 1.1 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 MouseArea { id: fullRep property bool spontaneousOpen: false hoverEnabled: true Layout.minimumWidth: units.gridUnit * 12 Layout.minimumHeight: units.gridUnit * 12 PlasmaExtras.Heading { - width: parent.width + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap level: 3 - opacity: 0.6 text: i18n("No Devices Available") visible: notifierDialog.count === 0 && !devicenotifier.pendingDelegateRemoval + enabled: false } PlasmaCore.DataSource { id: userActivitySource engine: "powermanagement" connectedSources: "UserActivity" property int polls: 0 //poll only on plasmoid expanded interval: !fullRep.containsMouse && !fullRep.Window.active && spontaneousOpen && plasmoid.expanded ? 3000 : 0 onIntervalChanged: polls = 0; onDataChanged: { //only do when polling if (interval == 0 || polls++ < 1) { return; } if (userActivitySource.data["UserActivity"]["IdleTime"] < interval) { plasmoid.expanded = false; spontaneousOpen = false; } } } // this item is reparented to a delegate that is showing a message to draw focus to it PlasmaComponents.Highlight { id: messageHighlight visible: false OpacityAnimator { id: messageHighlightAnimator target: messageHighlight from: 1 to: 0 duration: 3000 easing.type: Easing.InOutQuad } Connections { target: statusSource onLastChanged: { if (!statusSource.last) { messageHighlightAnimator.stop() messageHighlight.visible = false } } } function highlight(item) { parent = item width = Qt.binding(function() { return item.width }) height = Qt.binding(function() { return item.height }) opacity = 1 // Animator is threaded so the old opacity might be visible for a frame or two visible = true messageHighlightAnimator.start() } } Connections { target: plasmoid onExpandedChanged: { if (!plasmoid.expanded) { statusSource.clearMessage() } } } Item { anchors.fill: parent PlasmaComponents.ToolButton { id: unmountAll visible: devicenotifier.mountedRemovables > 1; anchors.right: parent.right iconSource: "media-eject" tooltip: i18n("Click to safely remove all devices") text: i18n("Remove all") implicitWidth: minimumWidth } PlasmaExtras.ScrollArea { anchors.fill: parent anchors.top: unmountAll.top ListView { id: notifierDialog focus: true boundsBehavior: Flickable.StopAtBounds model: filterModel delegate: deviceItem highlight: PlasmaComponents.Highlight { } highlightMoveDuration: 0 highlightResizeDuration: 0 spacing: units.smallSpacing currentIndex: devicenotifier.currentIndex //this is needed to make SectionScroller actually work //acceptable since one doesn't have a billion of devices cacheBuffer: 1000 section { property: "Type Description" delegate: Item { height: childrenRect.height width: notifierDialog.width PlasmaExtras.Heading { level: 3 opacity: 0.6 text: section } } } } } } Component { id: deviceItem DeviceItem { width: notifierDialog.width udi: DataEngineSource Binding on icon { when: sdSource.data[udi] !== undefined value: sdSource.data[udi].Icon } Binding on deviceName { when: sdSource.data[udi] !== undefined value: sdSource.data[udi].Description } emblemIcon: Emblems && Emblems[0] ? Emblems[0] : "" state: sdSource.data[udi] ? sdSource.data[udi].State : 0 isRoot: sdSource.data[udi]["File Path"] === "/" percentUsage: { if (!sdSource.data[udi]) { return 0 } var freeSpace = new Number(sdSource.data[udi]["Free Space"]); var size = new Number(sdSource.data[udi]["Size"]); var used = size-freeSpace; return used*100/size; } freeSpaceText: sdSource.data[udi] && sdSource.data[udi]["Free Space Text"] ? sdSource.data[udi]["Free Space Text"] : "" actionIcon: mounted ? "media-eject" : "media-mount" actionVisible: model["Device Types"].indexOf("Portable Media Player") === -1 actionToolTip: { var types = model["Device Types"]; if (!mounted) { return i18n("Click to access this device from other applications.") } else if (types && types.indexOf("OpticalDisc") !== -1) { return i18n("Click to eject this disc.") } else { return i18n("Click to safely remove this device.") } } mounted: devicenotifier.isMounted(udi) onActionTriggered: { var operationName = mounted ? "unmount" : "mount"; var service = sdSource.serviceForSource(udi); var operation = service.operationDescription(operationName); service.startOperationCall(operation); } property int operationResult: (model["Operation result"]) onOperationResultChanged: { if (operationResult == 1) { devicenotifier.popupIcon = "dialog-ok" popupIconTimer.restart() } else if (operationResult == 2) { devicenotifier.popupIcon = "dialog-error" popupIconTimer.restart() } } Behavior on height { NumberAnimation { duration: units.shortDuration * 3 } } } } } diff --git a/applets/notifications/package/contents/ui/FullRepresentation.qml b/applets/notifications/package/contents/ui/FullRepresentation.qml index b3c297126..3c7d5bac1 100644 --- a/applets/notifications/package/contents/ui/FullRepresentation.qml +++ b/applets/notifications/package/contents/ui/FullRepresentation.qml @@ -1,629 +1,628 @@ /* * 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.kcoreaddons 1.0 as KCoreAddons import org.kde.notificationmanager 1.0 as NotificationManager import "global" 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 Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical spacing: units.smallSpacing // TODO these should be configurable in the future readonly property int dndMorningHour: 6 readonly property int dndEveningHour: 20 // HACK forward focus to the list onActiveFocusChanged: { if (activeFocus) { list.forceActiveFocus(); } } Connections { target: plasmoid onExpandedChanged: { if (plasmoid.expanded) { list.positionViewAtBeginning(); list.currentIndex = -1; } } } // header ColumnLayout { id: header visible: !Kirigami.Settings.isMobile Layout.fillWidth: true 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 !== "" } } PlasmaCore.SvgItem { visible: header.visible elementId: "horizontal-line" Layout.fillWidth: true // why is this needed here but not in the delegate? Layout.preferredHeight: naturalSize.height svg: PlasmaCore.Svg { id: lineSvg imagePath: "widgets/line" } } // actual notifications PlasmaExtras.ScrollArea { Layout.fillWidth: true Layout.fillHeight: true Layout.preferredWidth: units.gridUnit * 18 Layout.preferredHeight: units.gridUnit * 24 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 { - width: list.width - height: list.height - horizontalAlignment: Kirigami.Settings.isMobile ? Text.AlignHCenter : Text.AlignLeft - verticalAlignment: Kirigami.Settings.isMobile ? Text.AlignVCenter : Text.AlignTop + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter wrapMode: Text.WordWrap level: 3 - opacity: 0.6 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 } } } } }