mirror of
https://gitgud.io/wackyideas/aerothemeplasma.git
synced 2024-08-15 00:43:43 +00:00
Very early KDE 6 release.
This commit is contained in:
parent
7cc4ccabbc
commit
686046d4f7
6272 changed files with 140920 additions and 529657 deletions
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
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 as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
import org.kde.plasma.plasmoid 2.0
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.quickcharts 1.0 as Charts
|
||||
|
||||
import "global"
|
||||
|
||||
MouseArea {
|
||||
id: compactRoot
|
||||
|
||||
Layout.minimumWidth: Plasmoid.formFactor === PlasmaCore.Types.Horizontal ? height : Kirigami.Units.iconSizes.small
|
||||
Layout.minimumHeight: Plasmoid.formFactor === PlasmaCore.Types.Vertical ? width : Kirigami.Units.iconSizes.small + 2 * Kirigami.Units.iconSizes.small
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
|
||||
property int activeCount: 0
|
||||
property int unreadCount: 0
|
||||
|
||||
property int jobsCount: 0
|
||||
property int jobsPercentage: 0
|
||||
|
||||
property bool inhibited: false
|
||||
|
||||
property bool wasExpanded: false
|
||||
onPressed: wasExpanded = root.expanded
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
Globals.toggleDoNotDisturbMode();
|
||||
} else {
|
||||
root.expanded = !wasExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
Kirigami.Icon {
|
||||
id: notificationIcon
|
||||
anchors.centerIn: parent
|
||||
// Deliberately rounding the size here rather than letting Kirigami.Icon
|
||||
// do it itself so that children can derive sane sizes from it.
|
||||
width: Kirigami.Units.iconSizes.roundedIconSize(Math.min(parent.width, parent.height))
|
||||
height: width
|
||||
//visible: opacity > 0
|
||||
//active: compactRoot.containsMouse
|
||||
|
||||
source: {
|
||||
let iconName;
|
||||
if(compactRoot.jobsCount > 0) {
|
||||
iconName = "notification-progress-active"
|
||||
} else if (compactRoot.inhibited) {
|
||||
iconName = "notification-disabled";
|
||||
} else if (compactRoot.unreadCount > 0) {
|
||||
iconName = "notification-active";
|
||||
} else {
|
||||
iconName = "notification-inactive"
|
||||
}
|
||||
return iconName;
|
||||
}
|
||||
|
||||
/*Charts.PieChart {
|
||||
id: chart
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
visible: false
|
||||
|
||||
range { from: 0; to: 100; automatic: false }
|
||||
|
||||
valueSources: Charts.SingleValueSource { value: compactRoot.jobsPercentage }
|
||||
colorSource: Charts.SingleValueSource { value: Kirigami.Theme.highlightColor }
|
||||
|
||||
thickness: 5
|
||||
}
|
||||
|
||||
PlasmaComponents3.Label {
|
||||
id: countLabel
|
||||
anchors.centerIn: parent
|
||||
width: Math.round(Math.min(parent.width, parent.height) * (text.length > 1 ? 0.67 : 0.75))
|
||||
height: width
|
||||
fontSizeMode: Text.Fit
|
||||
font.pointSize: 1024
|
||||
font.pixelSize: -1
|
||||
minimumPointSize: 5//Kirigami.Theme.smallFont.pointSize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: compactRoot.unreadCount || ""
|
||||
renderType: Text.QtRendering
|
||||
visible: false
|
||||
}
|
||||
|
||||
PlasmaComponents3.BusyIndicator {
|
||||
id: busyIndicator
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
running: visible
|
||||
}*/
|
||||
}
|
||||
|
||||
/*Kirigami.Icon {
|
||||
id: dndIcon
|
||||
anchors.fill: parent
|
||||
source: "notifications-disabled"
|
||||
opacity: 0
|
||||
scale: 2
|
||||
visible: opacity > 0
|
||||
active: compactRoot.containsMouse
|
||||
}*/
|
||||
|
||||
/*states: [
|
||||
State { // active process
|
||||
when: compactRoot.jobsCount > 0
|
||||
PropertyChanges {
|
||||
target: notificationIcon
|
||||
source: "notification-progress-inactive"
|
||||
}
|
||||
PropertyChanges {
|
||||
target: countLabel
|
||||
text: compactRoot.jobsCount
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: busyIndicator
|
||||
visible: compactRoot.jobsPercentage == 0
|
||||
}
|
||||
PropertyChanges {
|
||||
target: chart
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State { // do not disturb
|
||||
when: compactRoot.inhibited
|
||||
PropertyChanges {
|
||||
target: dndIcon
|
||||
scale: 1
|
||||
opacity: 1
|
||||
}
|
||||
PropertyChanges {
|
||||
target: notificationIcon
|
||||
scale: 0
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
State { // unread notifications
|
||||
name: "UNREAD"
|
||||
when: compactRoot.unreadCount > 0
|
||||
PropertyChanges {
|
||||
target: notificationIcon
|
||||
source: "notification-active"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
to: "*" // any state
|
||||
NumberAnimation {
|
||||
targets: [notificationIcon, dndIcon]
|
||||
properties: "opacity,scale"
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: ""
|
||||
to: "UNREAD"
|
||||
SequentialAnimation {
|
||||
RotationAnimation {
|
||||
target: notificationIcon
|
||||
to: 30
|
||||
easing.type: Easing.InOutQuad
|
||||
duration: Kirigami.Units.longDuration
|
||||
}
|
||||
RotationAnimation {
|
||||
target: notificationIcon
|
||||
to: -30
|
||||
easing.type: Easing.InOutQuad
|
||||
duration: Kirigami.Units.longDuration * 2 // twice the swing distance, keep speed uniform
|
||||
}
|
||||
RotationAnimation {
|
||||
target: notificationIcon
|
||||
to: 0
|
||||
easing.type: Easing.InOutQuad
|
||||
duration: Kirigami.Units.longDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
]*/
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.10
|
||||
import org.kde.kirigami 2.11 as Kirigami
|
||||
|
||||
MouseArea {
|
||||
id: delegate
|
||||
|
||||
property Item contentItem
|
||||
property bool draggable: false
|
||||
signal dismissRequested
|
||||
|
||||
implicitWidth: contentItem ? contentItem.implicitWidth : 0
|
||||
implicitHeight: contentItem ? contentItem.implicitHeight : 0
|
||||
opacity: 1 - Math.min(1, 1.5 * Math.abs(x) / width)
|
||||
|
||||
drag {
|
||||
axis: Drag.XAxis
|
||||
target: draggable && Kirigami.Settings.tabletMode ? this : null
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (Math.abs(x) > width / 2) {
|
||||
delegate.dismissRequested();
|
||||
} else {
|
||||
slideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: slideAnim
|
||||
target: delegate
|
||||
property:"x"
|
||||
to: 0
|
||||
duration: Kirigami.Units.longDuration
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2016, 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
Item {
|
||||
id: area
|
||||
|
||||
signal activated
|
||||
signal contextMenuRequested(var pos)
|
||||
|
||||
required property Item dragParent
|
||||
property int dragPixmapSize: Kirigami.Units.iconSizes.large
|
||||
|
||||
readonly property alias dragging: dragHandler.active
|
||||
readonly property alias hovered: hoverHandler.hovered
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
onTapped: {
|
||||
area.activated();
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: menuTapHandler
|
||||
acceptedButtons: Qt.LeftButton
|
||||
acceptedDevices: PointerDevice.TouchScreen | PointerDevice.Stylus
|
||||
onLongPressed: area.contextMenuRequested(point.position)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.RightButton
|
||||
cursorShape: tapHandler.pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||
anchors.fill: parent
|
||||
onPressed: (mouse) => {
|
||||
area.contextMenuRequested(Qt.point(mouse.x, mouse.y))
|
||||
}
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
|
||||
onActiveChanged: if (active) {
|
||||
area.dragParent.grabToImage(result => {
|
||||
area.dragParent.Drag.imageSource = result.url;
|
||||
area.dragParent.Drag.active = dragHandler.active;
|
||||
}, Qt.size(area.dragPixmapSize, area.dragPixmapSize));
|
||||
} else {
|
||||
area.dragParent.Drag.imageSource = "";
|
||||
area.dragParent.Drag.active = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.8
|
||||
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtras
|
||||
|
||||
import org.kde.kquickcontrolsaddons 2.0 as KQCAddons
|
||||
|
||||
PlasmaExtras.Menu {
|
||||
id: contextMenu
|
||||
|
||||
signal closed
|
||||
|
||||
property QtObject __clipboard: KQCAddons.Clipboard { }
|
||||
|
||||
// can be a Text or TextEdit
|
||||
property Item target
|
||||
|
||||
property string link
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === PlasmaExtras.Menu.Closed) {
|
||||
closed();
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaExtras.MenuItem {
|
||||
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "@action:inmenu", "Copy Link Address")
|
||||
icon: "edit-copy-symbolic"
|
||||
onClicked: __clipboard.content = contextMenu.link
|
||||
visible: contextMenu.link !== ""
|
||||
}
|
||||
|
||||
PlasmaExtras.MenuItem {
|
||||
separator: true
|
||||
visible: contextMenu.link !== ""
|
||||
}
|
||||
|
||||
PlasmaExtras.MenuItem {
|
||||
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "@action:inmenu", "Copy")
|
||||
icon: "edit-copy-symbolic"
|
||||
enabled: typeof target.selectionStart !== "undefined"
|
||||
? target.selectionStart !== target.selectionEnd
|
||||
: (target.text || "").length > 0
|
||||
onClicked: {
|
||||
if (typeof target.copy === "function") {
|
||||
target.copy();
|
||||
} else {
|
||||
__clipboard.content = target.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaExtras.MenuItem {
|
||||
id: selectAllAction
|
||||
icon: "edit-select-all-symbolic"
|
||||
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "@action:inmenu", "Select All")
|
||||
onClicked: target.selectAll()
|
||||
visible: typeof target.selectAll === "function"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,644 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
|
||||
import org.kde.plasma.plasmoid 2.0
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.ksvg 1.0 as KSvg
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtras
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
import org.kde.coreaddons 1.0 as KCoreAddons
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
import org.kde.plasma.private.notifications as Notifications
|
||||
|
||||
import "global"
|
||||
|
||||
PlasmaExtras.Representation {
|
||||
// TODO these should be configurable in the future
|
||||
readonly property int dndMorningHour: 6
|
||||
readonly property int dndEveningHour: 20
|
||||
readonly property var appletInterface: root
|
||||
|
||||
Layout.minimumWidth: Kirigami.Units.iconSizes.small * 12
|
||||
Layout.minimumHeight: Kirigami.Units.iconSizes.small * 12
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small * 18
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small * 24
|
||||
Layout.maximumWidth: Kirigami.Units.iconSizes.small * 80
|
||||
Layout.maximumHeight: Kirigami.Units.iconSizes.small * 40
|
||||
|
||||
Layout.fillHeight: Plasmoid.formFactor === PlasmaCore.Types.Vertical
|
||||
|
||||
collapseMarginsHint: true
|
||||
|
||||
Keys.onDownPressed: dndCheck.forceActiveFocus(Qt.TabFocusReason);
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onExpandedChanged() {
|
||||
if (root.expanded) {
|
||||
list.positionViewAtBeginning();
|
||||
list.currentIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header: PlasmaExtras.PlasmoidHeading {
|
||||
ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
id: header
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
QQC2.CheckBox {
|
||||
id: dndCheck
|
||||
enabled: NotificationManager.Server.valid
|
||||
text: i18n("Do not disturb")
|
||||
icon.name: "notifications-disabled"
|
||||
checkable: true
|
||||
checked: Globals.inhibited
|
||||
|
||||
KeyNavigation.down: list
|
||||
KeyNavigation.tab: list
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PlasmaExtras.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 manually disabled")});
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
PlasmaComponents3.ToolButton {
|
||||
visible: !(Plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)
|
||||
|
||||
Accessible.name: root.clearHistoryAction.text
|
||||
icon.name: "edit-clear-history"
|
||||
enabled: root.clearHistoryAction.visible
|
||||
onClicked: root.clearHistoryAction.trigger()
|
||||
|
||||
PlasmaComponents3.ToolTip {
|
||||
text: parent.Accessible.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaExtras.DescriptiveLabel {
|
||||
Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + 2 * dndCheck.spacing + Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + 2 * dndCheck.spacing + Kirigami.Units.iconSizes.smallMedium : 0
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.PlainText
|
||||
text: {
|
||||
if (!Globals.inhibited) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
|
||||
var inhibitedUntilTime = inhibitedUntil.getTime();
|
||||
var inhibitedByApp = notificationSettings.notificationsInhibitedByApplication;
|
||||
var inhibitedByMirroredScreens = notificationSettings.inhibitNotificationsWhenScreensMirrored
|
||||
&& notificationSettings.screensMirrored;
|
||||
var dateNow = Date.now();
|
||||
|
||||
var sections = [];
|
||||
|
||||
// Show until time if valid but not if too far int he future
|
||||
if (!isNaN(inhibitedUntilTime) && inhibitedUntilTime - dateNow > 0 &&
|
||||
inhibitedUntilTime - dateNow < 100 * 24 * 60 * 60 * 1000 /* 1 year*/) {
|
||||
const endTime = KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat);
|
||||
const lowercaseEndTime = endTime[0] + endTime.slice(1);
|
||||
sections.push(i18nc("Do not disturb until date", "Automatically ends: %1", lowercaseEndTime));
|
||||
}
|
||||
|
||||
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 !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.WheelForwarder {
|
||||
id: wheelForwarder
|
||||
toItem: scrollView.contentItem
|
||||
}
|
||||
|
||||
PlasmaComponents3.ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
background: null
|
||||
focus: true
|
||||
|
||||
contentItem: ListView {
|
||||
id: list
|
||||
width: scrollView.availableWidth
|
||||
focus: true
|
||||
model: root.expanded ? historyModel : null
|
||||
currentIndex: -1
|
||||
|
||||
topMargin: Kirigami.Units.largeSpacing
|
||||
bottomMargin: Kirigami.Units.largeSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
readonly property alias wheelForwarder: wheelForwarder
|
||||
|
||||
KeyNavigation.up: dndCheck
|
||||
|
||||
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: event => { Keys.returnPressed(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]);
|
||||
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: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Home:
|
||||
currentIndex = 0;
|
||||
break;
|
||||
case Qt.Key_End:
|
||||
currentIndex = count - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 PlasmaExtras.Highlight as this is only for indicating keyboard focus
|
||||
highlight: KSvg.FrameSvgItem {
|
||||
imagePath: "widgets/listitem"
|
||||
prefix: "pressed"
|
||||
}
|
||||
|
||||
// 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: ListView.view.width
|
||||
contentItem: delegateLoader
|
||||
|
||||
// NOTE: The following animations replace the Transitions in the ListView
|
||||
// because they don't work when the items change size during the animation
|
||||
// (showing/hiding the show more/show less button) in that case they will
|
||||
// animate to a wrong position and stay there
|
||||
// see https://bugs.kde.org/show_bug.cgi?id=427894 and QTBUG-110366
|
||||
property real oldY: -1
|
||||
property int oldListCount: -1
|
||||
onYChanged: {
|
||||
if (oldY < 0 || oldListCount === list.count) {
|
||||
oldY = y;
|
||||
return;
|
||||
}
|
||||
traslAnim.from = oldY - y;
|
||||
traslAnim.running = true;
|
||||
oldY = y;
|
||||
oldListCount = list.count;
|
||||
}
|
||||
transform: Translate {
|
||||
id: transl
|
||||
}
|
||||
NumberAnimation {
|
||||
id: traslAnim
|
||||
target: transl
|
||||
properties: "y"
|
||||
to: 0
|
||||
duration: Kirigami.Units.longDuration
|
||||
}
|
||||
opacity: 0
|
||||
ListView.onAdd: appearAnim.restart();
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
if (!appearAnim.running) {
|
||||
opacity = 1;
|
||||
}
|
||||
});
|
||||
oldListCount = list.count;
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: appearAnim
|
||||
PropertyAnimation { target: delegate; property: "opacity"; to: 0 }
|
||||
PauseAnimation { duration: Kirigami.Units.longDuration}
|
||||
NumberAnimation {
|
||||
target: delegate
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Kirigami.Units.longDuration
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnimation
|
||||
PropertyAction { target: delegate; property: "ListView.delayRemove"; value: true }
|
||||
ParallelAnimation {
|
||||
NumberAnimation { target: delegate; property: "opacity"; to: 0; duration: Kirigami.Units.longDuration }
|
||||
NumberAnimation {
|
||||
target: transl
|
||||
property: "x"
|
||||
to: list.width - (scrollView.PlasmaComponents3.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 2 : 0)
|
||||
duration: Kirigami.Units.longDuration
|
||||
}
|
||||
}
|
||||
PropertyAction { target: delegate; property: "ListView.delayRemove"; value: false }
|
||||
}
|
||||
|
||||
draggable: !model.isGroup && model.type != NotificationManager.Notifications.JobType
|
||||
|
||||
onDismissRequested: {
|
||||
removeAnimation.start();
|
||||
|
||||
historyModel.close(historyModel.index(index, 0));
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: delegateLoader
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Kirigami.Units.largeSpacing
|
||||
right: parent.right
|
||||
rightMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
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));
|
||||
onConfigureClicked: historyModel.configure(historyModel.index(index, 0))
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notificationDelegate
|
||||
ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
RowLayout {
|
||||
Item {
|
||||
id: groupLineContainer
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
width: Kirigami.Units.iconSizes.small
|
||||
visible: model.isInGroup
|
||||
|
||||
// Not using the Plasma theme's vertical line SVG because we want something thicker
|
||||
// than a hairline, and thickening a thin line SVG does not necessarily look good
|
||||
// with all Plasma themes.
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 3
|
||||
height: parent.height
|
||||
// TODO: use separator color here, once that color role is implemented
|
||||
color: Kirigami.Theme.textColor
|
||||
opacity: 0.2
|
||||
}
|
||||
}
|
||||
|
||||
NotificationItem {
|
||||
Layout.fillWidth: true
|
||||
|
||||
notificationType: model.type
|
||||
|
||||
inGroup: model.isInGroup
|
||||
inHistory: true
|
||||
listViewParent: list
|
||||
|
||||
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: close()
|
||||
|
||||
onDismissClicked: {
|
||||
model.dismissed = false;
|
||||
root.closePlasmoid();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
expire();
|
||||
}
|
||||
onOpenUrl: {
|
||||
Qt.openUrlExternally(url);
|
||||
expire();
|
||||
}
|
||||
onFileActionInvoked: {
|
||||
if (action.objectName === "movetotrash" || action.objectName === "deletefile") {
|
||||
close();
|
||||
} else {
|
||||
expire();
|
||||
}
|
||||
}
|
||||
|
||||
onSuspendJobClicked: historyModel.suspendJob(historyModel.index(index, 0))
|
||||
onResumeJobClicked: historyModel.resumeJob(historyModel.index(index, 0))
|
||||
onKillJobClicked: historyModel.killJob(historyModel.index(index, 0))
|
||||
|
||||
function expire() {
|
||||
if (model.resident) {
|
||||
model.expired = true;
|
||||
} else {
|
||||
historyModel.expire(historyModel.index(index, 0));
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
removeAnimation.start();
|
||||
historyModel.close(historyModel.index(index, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
&& delegate.ListView.nextSection !== delegate.ListView.section
|
||||
onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded)
|
||||
}
|
||||
|
||||
KSvg.SvgItem {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
imagePath: "widgets/line"
|
||||
elementId: "horizontal-line"
|
||||
|
||||
// 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
|
||||
&& !removeAnimation.running
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - (Kirigami.Units.iconSizes.small * 4)
|
||||
|
||||
active: list.count === 0
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: NotificationManager.Server.valid ? noUnreadMessage : notAvailableMessage
|
||||
}
|
||||
|
||||
Component {
|
||||
id: noUnreadMessage
|
||||
|
||||
PlasmaExtras.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
|
||||
iconName: "checkmark"
|
||||
text: i18n("No unread notifications")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notAvailableMessage
|
||||
|
||||
PlasmaExtras.PlaceholderMessage {
|
||||
// Checking valid to avoid creating ServerInfo object if everything is alright
|
||||
readonly property NotificationManager.ServerInfo currentOwner: NotificationManager.Server.currentOwner
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
|
||||
iconName: "notifications-disabled"
|
||||
text: i18n("Notification service not available")
|
||||
explanation: currentOwner && currentOwner.vendor && currentOwner.name
|
||||
? i18nc("Vendor and product name", "Notifications are currently provided by '%1 %2'", currentOwner.vendor, currentOwner.name)
|
||||
: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
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.extras 2.0 as PlasmaExtras
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
|
||||
import org.kde.coreaddons 1.0 as KCoreAddons
|
||||
|
||||
|
||||
GridLayout {
|
||||
id: detailsGrid
|
||||
|
||||
property QtObject jobDetails
|
||||
|
||||
columns: 2
|
||||
rowSpacing: Math.round(Kirigami.Units.smallSpacing / 2)
|
||||
columnSpacing: Kirigami.Units.smallSpacing
|
||||
|
||||
// once you use Layout.column/Layout.row *all* of the items in the Layout have to use them
|
||||
Repeater {
|
||||
model: [1, 2]
|
||||
|
||||
PlasmaComponents3.Label {
|
||||
Layout.column: 0
|
||||
Layout.row: index
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignRight
|
||||
text: jobDetails["descriptionLabel" + modelData] && jobDetails["descriptionValue" + modelData]
|
||||
? i18ndc("plasma_applet_org.kde.plasma.notifications", "Row description, e.g. Source", "%1:", jobDetails["descriptionLabel" + modelData]) : ""
|
||||
font: Kirigami.Theme.smallFont
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: [1, 2]
|
||||
|
||||
PlasmaComponents3.Label {
|
||||
id: descriptionValueLabel
|
||||
Layout.column: 1
|
||||
Layout.row: index
|
||||
Layout.fillWidth: true
|
||||
font: Kirigami.Theme.smallFont
|
||||
elide: Text.ElideMiddle
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
verticalAlignment: Text.AlignTop
|
||||
maximumLineCount: 5
|
||||
visible: text !== ""
|
||||
|
||||
// Only let the label grow, never shrink, to avoid repeatedly resizing the dialog when copying many files
|
||||
onImplicitHeightChanged: {
|
||||
if (implicitHeight > Layout.preferredHeight) {
|
||||
Layout.preferredHeight = implicitHeight;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: bindText()
|
||||
function bindText() {
|
||||
text = Qt.binding(function() {
|
||||
return jobDetails["descriptionValue" + modelData] || "";
|
||||
});
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onPressed: mouse => {
|
||||
// break binding so it doesn't update while the menu is opened
|
||||
descriptionValueLabel.text = descriptionValueLabel.text;
|
||||
descriptionValueMenu.open(mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
|
||||
EditContextMenu {
|
||||
id: descriptionValueMenu
|
||||
target: descriptionValueLabel
|
||||
// defer re-binding until after the "Copy" action in the menu has triggered
|
||||
onClosed: Qt.callLater(descriptionValueLabel.bindText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ["Bytes", "Files", "Directories", "Items"]
|
||||
|
||||
PlasmaComponents3.Label {
|
||||
Layout.column: 1
|
||||
Layout.row: 2 + index
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
var processed = jobDetails["processed" + modelData];
|
||||
var total = jobDetails["total" + modelData];
|
||||
|
||||
if (processed > 0 || total > 1) {
|
||||
if (processed > 0 && total > 0 && processed <= total) {
|
||||
switch(modelData) {
|
||||
case "Bytes":
|
||||
return i18ndc("plasma_applet_org.kde.plasma.notifications", "How many bytes have been copied", "%2 of %1",
|
||||
KCoreAddons.Format.formatByteSize(total),
|
||||
KCoreAddons.Format.formatByteSize(processed))
|
||||
case "Files":
|
||||
return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many files have been copied", "%2 of %1 file", "%2 of %1 files",
|
||||
total, processed);
|
||||
case "Directories":
|
||||
return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many dirs have been copied", "%2 of %1 folder", "%2 of %1 folders",
|
||||
total, processed);
|
||||
case "Items":
|
||||
return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many items (that includes files and dirs) have been copied", "%2 of %1 item", "%2 of %1 items",
|
||||
total, processed);
|
||||
}
|
||||
} else {
|
||||
switch(modelData) {
|
||||
case "Bytes":
|
||||
return KCoreAddons.Format.formatByteSize(processed || total)
|
||||
case "Files":
|
||||
return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 file", "%1 files", (processed || total));
|
||||
case "Directories":
|
||||
return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 folder", "%1 folders", (processed || total));
|
||||
case "Items":
|
||||
return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 item", "%1 items", (processed || total));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
font: Kirigami.Theme.smallFont
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents3.Label {
|
||||
Layout.column: 1
|
||||
Layout.row: 2 + 4
|
||||
Layout.fillWidth: true
|
||||
text: jobDetails.speed > 0 ? i18ndc("plasma_applet_org.kde.plasma.notifications", "Bytes per second", "%1/s",
|
||||
KCoreAddons.Format.formatByteSize(jobDetails.speed)) : ""
|
||||
font: Kirigami.Theme.smallFont
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,317 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.8
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQml 2.15
|
||||
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
|
||||
import org.kde.plasma.private.notifications 2.0 as Notifications
|
||||
|
||||
ColumnLayout {
|
||||
id: jobItem
|
||||
|
||||
property int jobState
|
||||
property int jobError
|
||||
|
||||
property alias percentage: progressBar.value
|
||||
property alias suspendable: suspendButton.visible
|
||||
property alias killable: killButton.visible
|
||||
|
||||
property QtObject jobDetails
|
||||
|
||||
readonly property int totalFiles: jobItem.jobDetails && jobItem.jobDetails.totalFiles || 0
|
||||
readonly property var url: {
|
||||
if (jobItem.jobState !== NotificationManager.Notifications.JobStateStopped
|
||||
|| jobItem.jobError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For a single file show actions for it
|
||||
// Otherwise the destination folder all of them were copied into
|
||||
const url = totalFiles === 1 ? jobItem.jobDetails.descriptionUrl
|
||||
: jobItem.jobDetails.destUrl;
|
||||
|
||||
// Don't offer opening files in Trash
|
||||
if (url && url.toString().startsWith("trash:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
property alias iconContainerItem: jobDragIconItem.parent
|
||||
|
||||
readonly property alias dragging: jobDragArea.dragging
|
||||
readonly property alias menuOpen: otherFileActionsMenu.visible
|
||||
|
||||
signal suspendJobClicked
|
||||
signal resumeJobClicked
|
||||
signal killJobClicked
|
||||
|
||||
signal openUrl(string url)
|
||||
signal fileActionInvoked(QtObject action)
|
||||
|
||||
spacing: 0
|
||||
|
||||
Notifications.FileInfo {
|
||||
id: fileInfo
|
||||
url: jobItem.totalFiles === 1 && jobItem.url ? jobItem.url : ""
|
||||
}
|
||||
|
||||
// This item is parented to the NotificationItem iconContainer
|
||||
Item {
|
||||
id: jobDragIconItem
|
||||
readonly property bool shown: jobDragIcon.valid
|
||||
width: parent ? parent.width : 0
|
||||
height: parent ? parent.height : 0
|
||||
visible: shown
|
||||
|
||||
Binding {
|
||||
target: jobDragIconItem.parent
|
||||
property: "visible"
|
||||
value: true
|
||||
when: jobDragIconItem.shown
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
id: jobDragIcon
|
||||
|
||||
anchors.fill: parent
|
||||
active: jobDragArea.hovered
|
||||
opacity: busyIndicator.running ? 0.6 : 1
|
||||
source: !fileInfo.error ? fileInfo.iconName : ""
|
||||
|
||||
Drag.dragType: Drag.Automatic
|
||||
Drag.mimeData: {
|
||||
"text/uri-list": [jobItem.url ?? ""]
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
DraggableFileArea {
|
||||
id: jobDragArea
|
||||
anchors.fill: parent
|
||||
|
||||
dragParent: jobDragIcon
|
||||
|
||||
onActivated: jobItem.openUrl(jobItem.url)
|
||||
onContextMenuRequested: (pos) => {
|
||||
// avoid menu button glowing if we didn't actually press it
|
||||
otherFileActionsButton.checked = false;
|
||||
|
||||
otherFileActionsMenu.visualParent = this;
|
||||
otherFileActionsMenu.open(pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents3.BusyIndicator {
|
||||
id: busyIndicator
|
||||
anchors.centerIn: parent
|
||||
running: fileInfo.busy && !delayBusyTimer.running
|
||||
visible: running
|
||||
|
||||
// Avoid briefly flashing the busy indicator
|
||||
Timer {
|
||||
id: delayBusyTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
running: fileInfo.busy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: progressRow
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
PlasmaComponents3.ProgressBar {
|
||||
id: progressBar
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 100
|
||||
// TODO do we actually need the window visible check? perhaps I do because it can be in popup or expanded plasmoid
|
||||
indeterminate: visible && Window.window && Window.window.visible && percentage < 1
|
||||
&& jobItem.jobState === NotificationManager.Notifications.JobStateRunning
|
||||
// is this too annoying?
|
||||
&& (jobItem.jobDetails.processedBytes === 0 || jobItem.jobDetails.totalBytes === 0)
|
||||
&& jobItem.jobDetails.processedFiles === 0
|
||||
//&& jobItem.jobDetails.processedDirectories === 0
|
||||
}
|
||||
|
||||
PlasmaComponents3.Label {
|
||||
id: progressText
|
||||
|
||||
visible: !progressBar.indeterminate
|
||||
// the || "0" is a workaround for the fact that 0 as number is falsey, and is wrongly considered a missing argument
|
||||
// BUG: 451807
|
||||
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "Percentage of a job", "%1%", jobItem.percentage || "0")
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
|
||||
ToolButton {
|
||||
id: suspendButton
|
||||
buttonIcon: "pause"
|
||||
onClicked: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended ? jobItem.resumeJobClicked()
|
||||
: jobItem.suspendJobClicked()
|
||||
|
||||
checkable: true
|
||||
checked: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended
|
||||
largeSize: true
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: killButton
|
||||
buttonIcon: "stop"
|
||||
onClicked: jobItem.killJobClicked()
|
||||
largeSize: true
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: expandButton
|
||||
buttonIcon: checked ? "collapse" : "expand"//(LayoutMirroring.enabled ? "arrow-left" : "arrow-right")
|
||||
checkable: true
|
||||
visible: jobItem.jobDetails && jobItem.jobDetails.hasDetails
|
||||
onClicked: checked = !checked
|
||||
largeSize: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
//Layout.preferredHeight: item ? item.implicitHeight + Kirigami.Units.smallSpacing : 0
|
||||
//active:
|
||||
// Loader doesn't reset its height when unloaded, just hide it altogether
|
||||
visible: expandButton.checked
|
||||
sourceComponent: JobDetails {
|
||||
jobDetails: jobItem.jobDetails
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: jobActionsRow
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
// We want the actions to be right-aligned but Row also reverses
|
||||
// the order of items, so we put them in reverse order
|
||||
layoutDirection: Qt.RightToLeft
|
||||
visible: jobItem.url && jobItem.url.toString() !== "" && !fileInfo.error
|
||||
|
||||
PlasmaComponents3.Button {
|
||||
id: otherFileActionsButton
|
||||
height: Math.max(implicitHeight, openButton.implicitHeight)
|
||||
icon.name: "application-menu"
|
||||
checkable: true
|
||||
text: openButton.visible ? "" : Accessible.name
|
||||
Accessible.name: i18nd("plasma_applet_org.kde.plasma.notifications", "More Options…")
|
||||
onPressedChanged: {
|
||||
if (pressed) {
|
||||
checked = Qt.binding(function() {
|
||||
return otherFileActionsMenu.visible;
|
||||
});
|
||||
otherFileActionsMenu.visualParent = this;
|
||||
// -1 tells it to "align bottom left of visualParent (this)"
|
||||
otherFileActionsMenu.open(-1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents3.ToolTip {
|
||||
text: parent.Accessible.name
|
||||
enabled: parent.text === ""
|
||||
}
|
||||
|
||||
Notifications.FileMenu {
|
||||
id: otherFileActionsMenu
|
||||
url: jobItem.url || ""
|
||||
onActionTriggered: jobItem.fileActionInvoked(action)
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents3.Button {
|
||||
id: openButton
|
||||
width: Math.min(implicitWidth, jobItem.width - otherFileActionsButton.width - jobActionsRow.spacing)
|
||||
height: Math.max(implicitHeight, otherFileActionsButton.implicitHeight)
|
||||
text: i18nd("plasma_applet_org.kde.plasma.notifications", "Open")
|
||||
onClicked: jobItem.openUrl(jobItem.url)
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: jobItem.jobDetails && jobItem.jobDetails.totalFiles !== 1
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
text: i18nd("plasma_applet_org.kde.plasma.notifications", "Open Containing Folder")
|
||||
icon.name: "folder-open"
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: fileInfo.openAction
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
text: fileInfo.openAction.text
|
||||
icon.name: fileInfo.openActionIconName
|
||||
visible: fileInfo.openAction.enabled
|
||||
onClicked: {
|
||||
fileInfo.openAction.trigger();
|
||||
jobItem.fileActionInvoked(fileInfo.openAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: jobItem.jobState === NotificationManager.Notifications.JobStateRunning
|
||||
PropertyChanges {
|
||||
target: suspendButton
|
||||
// Explicitly set it to false so it unchecks when pausing from applet
|
||||
// and then the job unpauses programmatically elsewhere.
|
||||
checked: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended
|
||||
PropertyChanges {
|
||||
target: suspendButton
|
||||
checked: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: progressBar
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: jobItem.jobState === NotificationManager.Notifications.JobStateStopped
|
||||
PropertyChanges {
|
||||
target: progressRow
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: expandButton
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
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 as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
|
||||
import org.kde.coreaddons 1.0 as KCoreAddons
|
||||
|
||||
import org.kde.quickcharts 1.0 as Charts
|
||||
|
||||
import "global"
|
||||
|
||||
RowLayout {
|
||||
id: notificationHeading
|
||||
|
||||
readonly property bool hasIcon: applicationIconItem.visible
|
||||
property bool inGroup
|
||||
property bool inHistory
|
||||
property int notificationType
|
||||
|
||||
property var applicationIconSource
|
||||
property string applicationName
|
||||
property string originName
|
||||
|
||||
property string configureActionLabel
|
||||
|
||||
property alias configurable: configureButton.visible
|
||||
property alias dismissable: dismissButton.visible
|
||||
property bool dismissed
|
||||
property string closeButtonTooltip: ""//closeButtonToolTip.text
|
||||
property alias closable: closeButton.visible
|
||||
|
||||
property var time
|
||||
|
||||
property int jobState
|
||||
property QtObject jobDetails
|
||||
property int urgency
|
||||
|
||||
property real timeout: 5000
|
||||
property real remainingTime: 0
|
||||
|
||||
signal configureClicked
|
||||
signal dismissClicked
|
||||
signal closeClicked
|
||||
|
||||
// notification created/updated time changed
|
||||
onTimeChanged: updateAgoText()
|
||||
|
||||
function updateAgoText() {
|
||||
ageLabel.agoText = ageLabel.generateAgoText();
|
||||
}
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Layout.preferredHeight: Math.max(applicationNameLabel.implicitHeight, Kirigami.Units.iconSizes.smallMedium)
|
||||
|
||||
|
||||
Component.onCompleted: updateAgoText()
|
||||
|
||||
Connections {
|
||||
target: Globals
|
||||
// clock time changed
|
||||
function onTimeChanged() {
|
||||
notificationHeading.updateAgoText()
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
id: applicationIconItem
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing/2
|
||||
source: notificationHeading.applicationIconSource
|
||||
visible: valid
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
id: applicationNameLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: applicationIconItem.visible ? Kirigami.Units.smallSpacing : 0
|
||||
level: 3
|
||||
color: notificationPopup.urgency === NotificationManager.Notifications.CriticalUrgency ? "#9d3939" : "#1d3287"
|
||||
type: notificationPopup.urgency === NotificationManager.Notifications.CriticalUrgency ? Kirigami.Heading.Type.Primary : Kirigami.Heading.Type.Normal
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 2
|
||||
text: notificationHeading.applicationName + (notificationHeading.originName ? " · " + notificationHeading.originName : "")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: spacer
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
id: ageLabel
|
||||
|
||||
// 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 !== ""
|
||||
level: 5
|
||||
opacity: 0.9
|
||||
wrapMode: Text.NoWrap
|
||||
text: generateRemainingText() || agoText
|
||||
|
||||
function generateAgoText() {
|
||||
if (!time || isNaN(time.getTime())
|
||||
|| notificationHeading.jobState === NotificationManager.Notifications.JobStateRunning
|
||||
|| notificationHeading.jobState === NotificationManager.Notifications.JobStateSuspended) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var deltaMinutes = Math.floor((Date.now() - time.getTime()) / 1000 / 60);
|
||||
if (deltaMinutes < 1) {
|
||||
// "Just now" is implied by
|
||||
return notificationHeading.inHistory
|
||||
? i18ndc("plasma_applet_org.kde.plasma.notifications", "Notification was added less than a minute ago, keep short", "Just now")
|
||||
: "";
|
||||
}
|
||||
|
||||
// Received less than an hour ago, show relative minutes
|
||||
if (deltaMinutes < 60) {
|
||||
return i18ndcp("plasma_applet_org.kde.plasma.notifications", "Notification was added minutes ago, keep short", "%1 min ago", "%1 min 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(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, Locale.ShortFormat);
|
||||
}
|
||||
|
||||
function generateRemainingText() {
|
||||
if (notificationHeading.notificationType !== NotificationManager.Notifications.JobType
|
||||
|| notificationHeading.jobState !== NotificationManager.Notifications.JobStateRunning) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var details = notificationHeading.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 i18ndcp("plasma_applet_org.kde.plasma.notifications", "seconds remaining, keep short",
|
||||
"%1 s remaining", "%1 s remaining", Math.round(eta));
|
||||
}
|
||||
if (eta < 60 * 60) {// 1 hour
|
||||
return i18ndcp("plasma_applet_org.kde.plasma.notifications", "minutes remaining, keep short",
|
||||
"%1 min remaining", "%1 min 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 i18ndcp("plasma_applet_org.kde.plasma.notifications", "hours remaining, keep short",
|
||||
"%1 h remaining", "%1 h remaining",
|
||||
Math.round(eta / 60 / 60));
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/*PlasmaCore.ToolTipArea {
|
||||
anchors.fill: parent
|
||||
active: ageLabel.agoText !== ""
|
||||
subText: notificationHeading.time ? notificationHeading.time.toLocaleString(Qt.locale(), Locale.LongFormat) : ""
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
ToolButton {
|
||||
id: configureButton
|
||||
buttonIcon: "settings"
|
||||
visible: false
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
//Layout.topMargin: -Kirigami.Units.smallSpacing / 2
|
||||
|
||||
//display: PlasmaComponents3.AbstractButton.IconOnly
|
||||
//text: notificationHeading.configureActionLabel || i18nd("plasma_applet_org.kde.plasma.notifications", "Configure")
|
||||
//Accessible.description: applicationNameLabel.text
|
||||
|
||||
onClicked: (mouse) => { notificationHeading.configureClicked(); }
|
||||
|
||||
/*PlasmaComponents3.ToolTip {
|
||||
text: parent.text
|
||||
}*/
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: dismissButton
|
||||
buttonIcon: notificationHeading.dismissed ? "restore" : "minimize"
|
||||
visible: false
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.rightMargin: closeButton.visible ? 0 : Kirigami.Units.smallSpacing / 2
|
||||
//Layout.topMargin: -Kirigami.Units.smallSpacing
|
||||
//display: PlasmaComponents3.AbstractButton.IconOnly
|
||||
/*text: notificationHeading.dismissed
|
||||
? i18ndc("plasma_applet_org.kde.plasma.notifications", "Opposite of minimize", "Restore")
|
||||
: i18nd("plasma_applet_org.kde.plasma.notifications", "Minimize")
|
||||
Accessible.description: applicationNameLabel.text*/
|
||||
|
||||
onClicked: (mouse) => { notificationHeading.dismissClicked(); }
|
||||
|
||||
/*PlasmaComponents3.ToolTip {
|
||||
text: parent.text
|
||||
}*/
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: closeButton
|
||||
//visible: false
|
||||
buttonIcon: "close"
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing / 2
|
||||
onClicked: (mouse) => { notificationHeading.closeClicked(); }
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: notificationHeading.inGroup
|
||||
PropertyChanges {
|
||||
target: applicationIconItem
|
||||
source: ""
|
||||
}
|
||||
PropertyChanges {
|
||||
target: applicationNameLabel
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
}
|
|
@ -0,0 +1,473 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
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.components 3.0 as PlasmaComponents3
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtras
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
|
||||
import org.kde.plasma.private.notifications 2.0 as Notifications
|
||||
|
||||
ColumnLayout {
|
||||
id: notificationItem
|
||||
|
||||
property int maximumLineCount: 0
|
||||
property alias bodyCursorShape: bodyLabel.cursorShape
|
||||
|
||||
property int notificationType
|
||||
property int urgency
|
||||
|
||||
property bool inGroup: false
|
||||
property bool inHistory: false
|
||||
property ListView listViewParent: null
|
||||
|
||||
property alias applicationIconSource: notificationHeading.applicationIconSource
|
||||
property alias applicationName: notificationHeading.applicationName
|
||||
property alias originName: notificationHeading.originName
|
||||
|
||||
property string summary
|
||||
property alias time: notificationHeading.time
|
||||
|
||||
property alias configurable: notificationHeading.configurable
|
||||
property alias dismissable: notificationHeading.dismissable
|
||||
property alias dismissed: notificationHeading.dismissed
|
||||
property alias closable: notificationHeading.closable
|
||||
|
||||
// This isn't an alias because TextEdit RichText adds some HTML tags to it
|
||||
property string body
|
||||
property string accessibleDescription
|
||||
property var icon
|
||||
property var urls: []
|
||||
|
||||
property int jobState
|
||||
property int percentage
|
||||
property int jobError: 0
|
||||
property bool suspendable
|
||||
property bool killable
|
||||
|
||||
property QtObject jobDetails
|
||||
|
||||
property alias configureActionLabel: notificationHeading.configureActionLabel
|
||||
property var actionNames: []
|
||||
property var actionLabels: []
|
||||
|
||||
property bool hasReplyAction
|
||||
property string replyActionLabel
|
||||
property string replyPlaceholderText
|
||||
property string replySubmitButtonText
|
||||
property string replySubmitButtonIconName
|
||||
|
||||
property int headingLeftPadding: 0
|
||||
property int headingRightPadding: 0
|
||||
|
||||
property int thumbnailLeftPadding: 0
|
||||
property int thumbnailRightPadding: 0
|
||||
property int thumbnailTopPadding: 0
|
||||
property int thumbnailBottomPadding: 0
|
||||
|
||||
property alias timeout: notificationHeading.timeout
|
||||
property alias remainingTime: notificationHeading.remainingTime
|
||||
|
||||
readonly property bool menuOpen: bodyLabel.contextMenu !== null
|
||||
|| Boolean(thumbnailStripLoader.item?.menuOpen || jobLoader.item?.menuOpen)
|
||||
|
||||
readonly property bool dragging: Boolean(thumbnailStripLoader.item?.dragging || jobLoader.item?.dragging)
|
||||
property bool replying: false
|
||||
readonly property bool hasPendingReply: replyLoader.item?.text !== ""
|
||||
readonly property alias headerHeight: headingElement.height
|
||||
property int extraSpaceForCriticalNotificationLine: 0
|
||||
|
||||
signal bodyClicked
|
||||
signal closeClicked
|
||||
signal configureClicked
|
||||
signal dismissClicked
|
||||
signal actionInvoked(string actionName)
|
||||
signal replied(string text)
|
||||
signal openUrl(string url)
|
||||
signal fileActionInvoked(QtObject action)
|
||||
signal forceActiveFocusRequested
|
||||
|
||||
signal suspendJobClicked
|
||||
signal resumeJobClicked
|
||||
signal killJobClicked
|
||||
|
||||
spacing: 0//Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Theme.colorSet: notificationItem.inHistory ? Kirigami.Theme.Header : Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
// Header
|
||||
Item {
|
||||
id: headingElement
|
||||
Layout.fillWidth: !notificationItem.inGroup
|
||||
Layout.preferredHeight: notificationHeading.implicitHeight
|
||||
Layout.preferredWidth: notificationHeading.implicitWidth
|
||||
Layout.alignment: notificationItem.inGroup && summaryLabel.lineCount > 1 ? Qt.AlignTop : 0
|
||||
Layout.topMargin: notificationItem.inGroup && summaryLabel.lineCount > 1 ? Math.max(0, (summaryLabelTextMetrics.height - Layout.preferredHeight) / 2) : Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
//Layout.bottomMargin: notificationItem.inGroup ? 0 : -parent.spacing
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing/2
|
||||
|
||||
|
||||
|
||||
/*PlasmaExtras.PlasmoidHeading {
|
||||
topInset: 0
|
||||
anchors.fill: parent
|
||||
visible: !notificationItem.inHistory
|
||||
opacity: 0
|
||||
|
||||
// HACK PlasmoidHeading is a QQC2 Control which accepts left mouse button by default,
|
||||
// which breaks the popup default action mouse handler, cf. QTBUG-89785
|
||||
Component.onCompleted: Notifications.InputDisabler.makeTransparentForInput(this)
|
||||
}*/
|
||||
|
||||
NotificationHeader {
|
||||
id: notificationHeading
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: notificationItem.headingLeftPadding
|
||||
rightMargin: notificationItem.headingRightPadding
|
||||
}
|
||||
|
||||
inGroup: notificationItem.inGroup
|
||||
inHistory: notificationItem.inHistory
|
||||
urgency: notificationItem.urgency
|
||||
|
||||
notificationType: notificationItem.notificationType
|
||||
jobState: notificationItem.jobState
|
||||
jobDetails: notificationItem.jobDetails
|
||||
|
||||
onConfigureClicked: notificationItem.configureClicked()
|
||||
onDismissClicked: notificationItem.dismissClicked()
|
||||
onCloseClicked: notificationItem.closeClicked()
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else that goes below the header
|
||||
// This is its own ColumnLayout-within-a-ColumnLayout because it lets us set
|
||||
// the left margin once rather than several times, in each of its children
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
Layout.leftMargin: (notificationItem.inGroup || !notificationItem.inHistory ? 0 : notificationItem.spacing) + notificationHeading.hasIcon ? (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing) : 0
|
||||
Layout.topMargin: notificationItem.inHistory ? 0 : -Kirigami.Units.smallSpacing
|
||||
|
||||
Accessible.role: notificationItem.inHistory ? Accessible.NoRole : Accessible.Notification
|
||||
Accessible.name: summaryLabel.text
|
||||
Accessible.description: notificationItem.accessibleDescription
|
||||
|
||||
// Notification body
|
||||
RowLayout {
|
||||
id: summaryRow
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: notificationItem.inGroup ? parent.right : iconContainer.left
|
||||
rightMargin: notificationItem.inGroup ? 0 : notificationItem.spacing
|
||||
}
|
||||
visible: summaryLabel.text !== ""
|
||||
|
||||
Kirigami.Heading {
|
||||
id: summaryLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
Layout.topMargin: notificationItem.inGroup && lineCount > 1 ? Math.max(0, (headingElement.Layout.preferredHeight - summaryLabelTextMetrics.height) / 2) : 0
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
textFormat: Text.PlainText
|
||||
maximumLineCount: 3
|
||||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideRight
|
||||
opacity: 0.66
|
||||
level: 5
|
||||
color: "black"
|
||||
// Give it a bit more visual prominence than the app name in the header
|
||||
//type: Kirigami.Heading.Type.Primary
|
||||
text: {
|
||||
if (notificationItem.notificationType === NotificationManager.Notifications.JobType) {
|
||||
if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) {
|
||||
if (notificationItem.summary) {
|
||||
return i18ndc("plasma_applet_org.kde.plasma.notifications", "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 i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary);
|
||||
} else {
|
||||
return i18nd("plasma_applet_org.kde.plasma.notifications", "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);
|
||||
} else {
|
||||
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 "";
|
||||
}
|
||||
visible: text !== ""
|
||||
|
||||
TextMetrics {
|
||||
id: summaryLabelTextMetrics
|
||||
font: summaryLabel.font
|
||||
text: summaryLabel.text
|
||||
}
|
||||
}
|
||||
|
||||
// inGroup headerItem is reparented here
|
||||
}
|
||||
|
||||
SelectableLabel {
|
||||
id: bodyLabel
|
||||
|
||||
readonly property real maximumHeight: Kirigami.Units.iconSizes.small * notificationItem.maximumLineCount
|
||||
readonly property bool truncated: notificationItem.maximumLineCount > 0 && bodyLabel.implicitHeight > maximumHeight
|
||||
|
||||
height: truncated ? maximumHeight : implicitHeight
|
||||
anchors {
|
||||
top: summaryRow.bottom
|
||||
topMargin: summaryRow.visible && notificationItem.inGroup && iconContainer.visible ? notificationItem.spacing : 0
|
||||
left: parent.left
|
||||
right: iconContainer.left
|
||||
rightMargin: iconContainer.visible ? notificationItem.spacing : 0
|
||||
}
|
||||
|
||||
listViewParent: notificationItem.listViewParent
|
||||
// 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: "<style>a { color: " + Kirigami.Theme.linkColor + "; }</style>" + notificationItem.body
|
||||
|
||||
// Cannot do text !== "" because RichText adds some HTML tags even when empty
|
||||
visible: notificationItem.body !== ""
|
||||
onClicked: notificationItem.bodyClicked()
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: iconContainer
|
||||
|
||||
width: visible ? iconItem.width : 0
|
||||
height: visible ? Math.max(iconItem.height + notificationItem.spacing * 2, bodyLabel.height + bodyLabel.anchors.topMargin + (notificationItem.inGroup ? 0 : summaryRow.implicitHeight)) : 0
|
||||
anchors {
|
||||
top: notificationItem.inGroup ? bodyLabel.top : parent.top
|
||||
right: parent.right
|
||||
}
|
||||
visible: iconItem.shouldBeShown
|
||||
|
||||
Kirigami.Icon {
|
||||
id: iconItem
|
||||
|
||||
width: Kirigami.Units.iconSizes.large
|
||||
height: Kirigami.Units.iconSizes.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// don't show two identical icons
|
||||
readonly property bool shouldBeShown: valid && source != notificationItem.applicationIconSource
|
||||
|
||||
smooth: true
|
||||
source: notificationItem.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Job progress reporting
|
||||
Loader {
|
||||
id: jobLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? item.implicitHeight : 0
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: notificationHeading.hasIcon ? (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing) : 0
|
||||
active: notificationItem.notificationType === NotificationManager.Notifications.JobType
|
||||
visible: active
|
||||
sourceComponent: JobItem {
|
||||
iconContainerItem: iconContainer
|
||||
|
||||
jobState: notificationItem.jobState
|
||||
jobError: notificationItem.jobError
|
||||
percentage: notificationItem.percentage
|
||||
suspendable: notificationItem.suspendable
|
||||
killable: notificationItem.killable
|
||||
|
||||
jobDetails: notificationItem.jobDetails
|
||||
|
||||
onSuspendJobClicked: notificationItem.suspendJobClicked()
|
||||
onResumeJobClicked: notificationItem.resumeJobClicked()
|
||||
onKillJobClicked: notificationItem.killJobClicked()
|
||||
|
||||
onOpenUrl: notificationItem.openUrl(url)
|
||||
onFileActionInvoked: notificationItem.fileActionInvoked(action)
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
Item {
|
||||
id: actionContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.rightMargin: -Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: notificationHeading.hasIcon ? (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing) : 0
|
||||
visible: actionRepeater.count > 0 && actionFlow.parent === this
|
||||
|
||||
// Notification actions
|
||||
Flow { // it's a Flow so it can wrap if too long
|
||||
id: actionFlow
|
||||
// For a cleaner look, if there is a thumbnail, puts the actions next to the thumbnail strip's menu button
|
||||
parent: thumbnailStripLoader.item?.actionContainer ?? actionContainer
|
||||
width: parent.width
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
layoutDirection: Qt.RightToLeft
|
||||
enabled: !replyLoader.active
|
||||
opacity: replyLoader.active ? 0 : 1
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: actionRepeater
|
||||
|
||||
model: {
|
||||
var buttons = [];
|
||||
var actionNames = (notificationItem.actionNames || []);
|
||||
var actionLabels = (notificationItem.actionLabels || []);
|
||||
// HACK We want the actions to be right-aligned but Flow also reverses
|
||||
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;
|
||||
}
|
||||
|
||||
PlasmaComponents3.ToolButton {
|
||||
flat: false
|
||||
// why does it spit "cannot assign undefined to string" when a notification becomes expired?
|
||||
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 ? item.implicitHeight : 0
|
||||
// When there is only one action and it is a reply action, show text field right away
|
||||
active: notificationItem.replying || (notificationItem.hasReplyAction && (notificationItem.actionNames || []).length === 0)
|
||||
visible: active
|
||||
opacity: active ? 1 : 0
|
||||
x: active ? 0 : parent.width
|
||||
/*Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}*/
|
||||
|
||||
function beginReply() {
|
||||
notificationItem.replying = true;
|
||||
|
||||
notificationItem.forceActiveFocusRequested();
|
||||
replyLoader.item.activate();
|
||||
}
|
||||
|
||||
sourceComponent: NotificationReplyField {
|
||||
placeholderText: notificationItem.replyPlaceholderText
|
||||
buttonIconName: notificationItem.replySubmitButtonIconName
|
||||
buttonText: notificationItem.replySubmitButtonText
|
||||
onReplied: notificationItem.replied(text)
|
||||
|
||||
replying: notificationItem.replying
|
||||
onBeginReplyRequested: replyLoader.beginReply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thumbnails
|
||||
Loader {
|
||||
id: thumbnailStripLoader
|
||||
Layout.leftMargin: notificationItem.thumbnailLeftPadding
|
||||
Layout.rightMargin: notificationItem.thumbnailRightPadding
|
||||
// no change in Layout.topMargin to keep spacing to notification text consistent
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: notificationItem.thumbnailBottomPadding
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? item.implicitHeight : 0
|
||||
active: notificationItem.urls.length > 0
|
||||
visible: active
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: notificationItem.inGroup
|
||||
PropertyChanges {
|
||||
target: headingElement
|
||||
parent: summaryRow
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: summaryRow
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: summaryLabel
|
||||
visible: true
|
||||
}
|
||||
|
||||
/*PropertyChanges {
|
||||
target: bodyLabel.Label
|
||||
alignment: Qt.AlignTop
|
||||
}*/
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
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.kquickcontrolsaddons 2.0 as KQuickAddons
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.ksvg as KSvg
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
|
||||
import ".."
|
||||
|
||||
PlasmaCore.Dialog {
|
||||
id: notificationPopup
|
||||
|
||||
property int popupWidth
|
||||
|
||||
property alias notificationType: notificationItem.notificationType
|
||||
|
||||
property alias applicationName: notificationItem.applicationName
|
||||
property alias applicationIconSource: notificationItem.applicationIconSource
|
||||
property alias originName: notificationItem.originName
|
||||
|
||||
property alias time: notificationItem.time
|
||||
|
||||
property alias summary: notificationItem.summary
|
||||
property alias body: notificationItem.body
|
||||
property alias accessibleDescription: notificationItem.accessibleDescription
|
||||
property alias icon: notificationItem.icon
|
||||
property alias urls: notificationItem.urls
|
||||
|
||||
property int urgency
|
||||
property int timeout
|
||||
property int dismissTimeout
|
||||
|
||||
property alias jobState: notificationItem.jobState
|
||||
property alias percentage: notificationItem.percentage
|
||||
property alias jobError: notificationItem.jobError
|
||||
property alias suspendable: notificationItem.suspendable
|
||||
property alias killable: notificationItem.killable
|
||||
property alias jobDetails: notificationItem.jobDetails
|
||||
|
||||
property alias configureActionLabel: notificationItem.configureActionLabel
|
||||
property alias configurable: notificationItem.configurable
|
||||
property alias dismissable: notificationItem.dismissable
|
||||
property alias closable: notificationItem.closable
|
||||
|
||||
property bool hasDefaultAction
|
||||
property var defaultActionFallbackWindowIdx
|
||||
property alias actionNames: notificationItem.actionNames
|
||||
property alias actionLabels: notificationItem.actionLabels
|
||||
|
||||
property alias hasReplyAction: notificationItem.hasReplyAction
|
||||
property alias replyActionLabel: notificationItem.replyActionLabel
|
||||
property alias replyPlaceholderText: notificationItem.replyPlaceholderText
|
||||
property alias replySubmitButtonText: notificationItem.replySubmitButtonText
|
||||
property alias replySubmitButtonIconName: notificationItem.replySubmitButtonIconName
|
||||
|
||||
signal configureClicked
|
||||
signal dismissClicked
|
||||
signal closeClicked
|
||||
|
||||
signal defaultActionInvoked
|
||||
signal actionInvoked(string actionName)
|
||||
signal replied(string text)
|
||||
signal openUrl(string url)
|
||||
signal fileActionInvoked(QtObject action)
|
||||
signal forceActiveFocusRequested
|
||||
|
||||
signal expired
|
||||
signal hoverEntered
|
||||
signal hoverExited
|
||||
|
||||
signal suspendJobClicked
|
||||
signal resumeJobClicked
|
||||
signal killJobClicked
|
||||
|
||||
property int defaultTimeout: 5000
|
||||
readonly property int effectiveTimeout: {
|
||||
if (timeout === -1) {
|
||||
return defaultTimeout;
|
||||
}
|
||||
if (dismissTimeout) {
|
||||
return dismissTimeout;
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
backgroundHints: "SolidBackground"
|
||||
location: PlasmaCore.Types.Floating
|
||||
// On wayland we need focus to copy to the clipboard, we change on mouse interaction until the cursor leaves
|
||||
flags: notificationItem.replying || focusListener.wantsFocus ? 0 : Qt.WindowDoesNotAcceptFocus
|
||||
|
||||
visible: false
|
||||
|
||||
KSvg.FrameSvgItem {
|
||||
id: solidBg
|
||||
visible: false
|
||||
imagePath: "solid/widgets/tooltip"
|
||||
}
|
||||
KSvg.FrameSvgItem {
|
||||
id: dialogSvg
|
||||
visible: false
|
||||
imagePath: "solid/dialogs/background"
|
||||
}
|
||||
|
||||
// When notification is updated, restart hide timer
|
||||
onTimeChanged: {
|
||||
if (timer.running) {
|
||||
timer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
mainItem: KQuickAddons.MouseEventListener {
|
||||
id: focusListener
|
||||
property bool wantsFocus: false
|
||||
|
||||
width: notificationPopup.popupWidth
|
||||
height: notificationItem.implicitHeight + notificationItem.y + Kirigami.Units.smallSpacing*3
|
||||
|
||||
acceptedButtons: Qt.AllButtons
|
||||
hoverEnabled: true
|
||||
onPressed: wantsFocus = true
|
||||
onContainsMouseChanged: wantsFocus = wantsFocus && containsMouse
|
||||
|
||||
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onEntered: {
|
||||
if (notificationPopup.hasDefaultAction && !notificationItem.dragging) {
|
||||
dragActivationTimer.start();
|
||||
} else {
|
||||
drag.accepted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dragActivationTimer
|
||||
interval: 250 // same as Task Manager
|
||||
repeat: false
|
||||
onTriggered: notificationPopup.defaultActionInvoked()
|
||||
}
|
||||
|
||||
// Visual flourish for critical notifications to make them stand out more
|
||||
/*Rectangle {
|
||||
id: criticalNotificationLine
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
// Subtract bottom margin that header sets which is not a part of
|
||||
// its height, and also the PlasmoidHeading's bottom line
|
||||
topMargin: notificationItem.headerHeight - notificationItem.spacing - 1
|
||||
bottom: parent.bottom
|
||||
bottomMargin: -notificationPopup.margins.bottom
|
||||
left: parent.left
|
||||
leftMargin: -notificationPopup.margins.left
|
||||
}
|
||||
implicitWidth: 4
|
||||
|
||||
visible: notificationPopup.urgency === NotificationManager.Notifications.CriticalUrgency
|
||||
|
||||
color: Kirigami.Theme.neutralTextColor
|
||||
}*/
|
||||
|
||||
Rectangle {
|
||||
id: backgroundRect
|
||||
anchors {
|
||||
fill: parent
|
||||
|
||||
|
||||
}
|
||||
z: 0
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#ffffff" }
|
||||
GradientStop { position: 1.0; color: "#d3d1d2" }
|
||||
}
|
||||
}
|
||||
DraggableDelegate {
|
||||
id: area
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing*2
|
||||
hoverEnabled: true
|
||||
draggable: notificationItem.notificationType != NotificationManager.Notifications.JobType
|
||||
onDismissRequested: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
|
||||
|
||||
cursorShape: hasDefaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: {
|
||||
let buttons = Qt.MiddleButton;
|
||||
if (hasDefaultAction || draggable) {
|
||||
buttons |= Qt.LeftButton;
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
// NOTE "mouse" can be null when faked by the SelectableLabel
|
||||
if (mouse && mouse.button === Qt.MiddleButton) {
|
||||
if (notificationItem.closable) {
|
||||
notificationItem.closeClicked();
|
||||
}
|
||||
} else if (hasDefaultAction) {
|
||||
notificationPopup.defaultActionInvoked();
|
||||
}
|
||||
}
|
||||
onEntered: notificationPopup.hoverEntered()
|
||||
onExited: notificationPopup.hoverExited()
|
||||
|
||||
LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
interval: notificationPopup.effectiveTimeout
|
||||
running: {
|
||||
if (!notificationPopup.visible) {
|
||||
return false;
|
||||
}
|
||||
if (area.containsMouse) {
|
||||
return false;
|
||||
}
|
||||
if (interval <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (notificationItem.dragging || notificationItem.menuOpen) {
|
||||
return false;
|
||||
}
|
||||
if (notificationItem.replying
|
||||
&& (notificationPopup.active || notificationItem.hasPendingReply)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
onTriggered: {
|
||||
if (notificationPopup.dismissTimeout) {
|
||||
notificationPopup.dismissClicked();
|
||||
} else {
|
||||
notificationPopup.expired();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: notificationItem
|
||||
property: "remainingTime"
|
||||
from: timer.interval
|
||||
to: 0
|
||||
duration: timer.interval
|
||||
running: timer.running && Kirigami.Units.longDuration > 1
|
||||
}
|
||||
|
||||
|
||||
NotificationItem {
|
||||
id: notificationItem
|
||||
// let the item bleed into the dialog margins so the close button margins cancel out
|
||||
y: closable || dismissable || configurable ? -notificationPopup.margins.top : 0
|
||||
headingRightPadding: -notificationPopup.margins.right
|
||||
width: parent.width
|
||||
maximumLineCount: 8
|
||||
bodyCursorShape: notificationPopup.hasDefaultAction ? Qt.PointingHandCursor : 0
|
||||
|
||||
opacity: {
|
||||
if(focusListener.containsMouse) return 1;
|
||||
if(notificationItem.remainingTime < effectiveTimeout / 4) {
|
||||
return (4 * notificationItem.remainingTime / effectiveTimeout);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
thumbnailLeftPadding: -notificationPopup.margins.left
|
||||
thumbnailRightPadding: -notificationPopup.margins.right
|
||||
thumbnailTopPadding: -notificationPopup.margins.top
|
||||
thumbnailBottomPadding: -notificationPopup.margins.bottom
|
||||
|
||||
urgency: notificationPopup.urgency
|
||||
extraSpaceForCriticalNotificationLine: 0//criticalNotificationLine.visible ? criticalNotificationLine.implicitWidth : 0
|
||||
|
||||
timeout: timer.running ? timer.interval : 0
|
||||
|
||||
closable: true
|
||||
|
||||
onBodyClicked: {
|
||||
if (area.acceptedButtons & Qt.LeftButton) {
|
||||
area.clicked(null /*mouse*/);
|
||||
}
|
||||
}
|
||||
onCloseClicked: notificationPopup.closeClicked()
|
||||
onDismissClicked: notificationPopup.dismissClicked()
|
||||
onConfigureClicked: notificationPopup.configureClicked()
|
||||
onActionInvoked: actionName => notificationPopup.actionInvoked(actionName)
|
||||
onReplied: text => notificationPopup.replied(text)
|
||||
onOpenUrl: url => notificationPopup.openUrl(url)
|
||||
onFileActionInvoked: action => notificationPopup.fileActionInvoked(action)
|
||||
onForceActiveFocusRequested: notificationPopup.forceActiveFocusRequested()
|
||||
|
||||
onSuspendJobClicked: notificationPopup.suspendJobClicked()
|
||||
onResumeJobClicked: notificationPopup.resumeJobClicked()
|
||||
onKillJobClicked: notificationPopup.killJobClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@broulik.de>
|
||||
|
||||
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.components 3.0 as PlasmaComponents3
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
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: Kirigami.Units.smallSpacing
|
||||
|
||||
function activate() {
|
||||
replyTextField.forceActiveFocus();
|
||||
}
|
||||
|
||||
PlasmaComponents3.TextField {
|
||||
id: replyTextField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: replyRow.placeholderText
|
||||
|| i18ndc("plasma_applet_org.kde.plasma.notifications", "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
|
||||
|| i18ndc("plasma_applet_org.kde.plasma.notifications", "@action:button", "Send")
|
||||
enabled: replyTextField.length > 0
|
||||
onClicked: replyRow.replied(replyTextField.text)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2011 Marco Martin <notmart@gmail.com>
|
||||
SPDX-FileCopyrightText: 2014, 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts 1.1
|
||||
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
import org.kde.plasma.private.notifications as Notifications
|
||||
|
||||
PlasmaComponents3.ScrollView {
|
||||
id: bodyTextContainer
|
||||
|
||||
property alias text: bodyText.text
|
||||
|
||||
property int cursorShape
|
||||
|
||||
property QtObject contextMenu: null
|
||||
property ListView listViewParent: null
|
||||
|
||||
signal clicked(var mouse)
|
||||
signal linkActivated(string link)
|
||||
|
||||
leftPadding: mirrored && !Kirigami.Settings.isMobile ? PlasmaComponents3.ScrollBar.vertical.width : 0
|
||||
rightPadding: !mirrored && !Kirigami.Settings.isMobile ? PlasmaComponents3.ScrollBar.vertical.width : 0
|
||||
|
||||
PlasmaComponents3.TextArea {
|
||||
id: bodyText
|
||||
enabled: !Kirigami.Settings.isMobile
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
background: null
|
||||
color: Kirigami.Theme.textColor
|
||||
|
||||
// Selectable only when we are in desktop mode
|
||||
selectByMouse: !Kirigami.Settings.tabletMode
|
||||
|
||||
readOnly: true
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: TextEdit.RichText
|
||||
|
||||
onLinkActivated: bodyTextContainer.linkActivated(link)
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: bodyTextContainer.clicked(null)
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
cursorShape: {
|
||||
if (bodyText.hoveredLink) {
|
||||
return Qt.PointingHandCursor;
|
||||
} else if (bodyText.selectionStart !== bodyText.selectionEnd) {
|
||||
return Qt.IBeamCursor;
|
||||
} else {
|
||||
return bodyTextContainer.cursorShape || Qt.IBeamCursor;
|
||||
}
|
||||
}
|
||||
onTapped: eventPoint => {
|
||||
contextMenu = contextMenuComponent.createObject(bodyText);
|
||||
contextMenu.link = bodyText.linkAt(eventPoint.position.x, eventPoint.position.y);
|
||||
|
||||
contextMenu.closed.connect(function() {
|
||||
contextMenu.destroy();
|
||||
contextMenu = null;
|
||||
});
|
||||
contextMenu.open(eventPoint.position.x, eventPoint.position.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: contextMenuComponent
|
||||
|
||||
EditContextMenu {
|
||||
target: bodyText
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: if (bodyTextContainer.listViewParent !== null) {
|
||||
bodyTextContainer.listViewParent.wheelForwarder.interceptWheelEvent(bodyText);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Layouts 1.1
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
import org.kde.kquickcontrolsaddons 2.0 as KQCAddons
|
||||
|
||||
import org.kde.plasma.private.notifications 2.0 as Notifications
|
||||
|
||||
import "global"
|
||||
|
||||
Item {
|
||||
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
|
||||
readonly property alias dragging: dragArea.dragging
|
||||
|
||||
property int leftPadding: 0
|
||||
property int rightPadding: 0
|
||||
property int topPadding: 0
|
||||
property int bottomPadding: 0
|
||||
|
||||
property alias actionContainer: thumbnailActionContainer
|
||||
|
||||
signal openUrl(string url)
|
||||
signal fileActionInvoked(QtObject action)
|
||||
|
||||
implicitHeight: Math.max(thumbnailActionRow.implicitHeight + 2 * thumbnailActionRow.anchors.topMargin,
|
||||
Math.round(Math.min(width / 3, width / thumbnailer.ratio)))
|
||||
+ topPadding + bottomPadding
|
||||
|
||||
Notifications.FileMenu {
|
||||
id: fileMenu
|
||||
url: thumbnailer.url
|
||||
visualParent: menuButton
|
||||
onActionTriggered: thumbnailArea.fileActionInvoked(action)
|
||||
}
|
||||
|
||||
Notifications.Thumbnailer {
|
||||
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
|
||||
// Also use popupWidth instead of our width to ensure it is fixed and doesn't
|
||||
// change temporarily during (re)layouting
|
||||
size: Qt.size(Globals.popupWidth, Globals.popupWidth)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
DraggableFileArea {
|
||||
id: dragArea
|
||||
anchors.fill: parent
|
||||
dragParent: previewIcon
|
||||
dragPixmapSize: previewIcon.height
|
||||
|
||||
onActivated: thumbnailArea.openUrl(thumbnailer.url)
|
||||
onContextMenuRequested: (pos) => {
|
||||
// avoid menu button glowing if we didn't actually press it
|
||||
menuButton.checked = false;
|
||||
|
||||
fileMenu.visualParent = this;
|
||||
fileMenu.open(pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
|
||||
KQCAddons.QPixmapItem {
|
||||
id: previewPixmap
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: thumbnailArea.leftPadding
|
||||
rightMargin: thumbnailArea.rightPadding
|
||||
topMargin: thumbnailArea.topPadding
|
||||
bottomMargin: thumbnailArea.bottomPadding
|
||||
}
|
||||
pixmap: thumbnailer.pixmap
|
||||
smooth: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
Kirigami.Icon {
|
||||
id: previewIcon
|
||||
anchors.centerIn: parent
|
||||
width: height
|
||||
height: Kirigami.Units.iconSizes.roundedIconSize(parent.height)
|
||||
active: dragArea.hovered
|
||||
source: !thumbnailer.busy && !thumbnailer.hasPreview ? thumbnailer.iconName : ""
|
||||
|
||||
Drag.dragType: Drag.Automatic
|
||||
Drag.mimeData: {
|
||||
"text/uri-list": [thumbnailer.url],
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents3.BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: thumbnailer.busy
|
||||
visible: thumbnailer.busy
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: thumbnailActionRow
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
margins: Kirigami.Units.smallSpacing
|
||||
}
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Item {
|
||||
id: thumbnailActionContainer
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
// actionFlow is reparented here
|
||||
}
|
||||
|
||||
PlasmaComponents3.Button {
|
||||
id: menuButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
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: i18nd("plasma_applet_org.kde.plasma.notifications", "More Options…")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQml.Models 2.2
|
||||
|
||||
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||
// for Highlight
|
||||
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.kquickcontrolsaddons 2.0 as KQuickControlsAddons
|
||||
*/
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.15
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.plasmoid 2.0
|
||||
import org.kde.ksvg 1.0 as KSvg
|
||||
import org.kde.kirigami 2.5 as Kirigami // For Settings.tabletMode
|
||||
|
||||
MouseArea {
|
||||
id: toolButton
|
||||
|
||||
Layout.maximumWidth: largeSize ? Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing : Kirigami.Units.iconSizes.small+1;
|
||||
Layout.maximumHeight: largeSize ? Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing : Kirigami.Units.iconSizes.small;
|
||||
Layout.preferredWidth: largeSize ? Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing : Kirigami.Units.iconSizes.small+1;
|
||||
Layout.preferredHeight: largeSize ? Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing : Kirigami.Units.iconSizes.small;
|
||||
width: largeSize ? Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing : Kirigami.Units.iconSizes.small+1;
|
||||
height: largeSize ? Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing : Kirigami.Units.iconSizes.small
|
||||
|
||||
//signal clicked
|
||||
property string buttonIcon: ""
|
||||
property bool checkable: false
|
||||
property bool checked: false
|
||||
property bool largeSize: false
|
||||
|
||||
hoverEnabled: true
|
||||
KSvg.FrameSvgItem {
|
||||
id: normalButton
|
||||
imagePath: Qt.resolvedUrl("svgs/button.svgz")
|
||||
anchors.fill: parent
|
||||
prefix: {
|
||||
if(parent.containsPress || (checkable && checked)) return "toolbutton-pressed";
|
||||
else return "toolbutton-hover";
|
||||
}
|
||||
visible: parent.containsMouse || (checkable && checked)
|
||||
}
|
||||
|
||||
KSvg.SvgItem {
|
||||
id: buttonIconSvg
|
||||
imagePath: Qt.resolvedUrl("svgs/icons.svg");
|
||||
elementId: buttonIcon
|
||||
width: largeSize ? Kirigami.Units.iconSizes.small : 10;
|
||||
height: largeSize ? Kirigami.Units.iconSizes.small : 10;
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,720 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
pragma Singleton
|
||||
import QtQuick 2.8
|
||||
import QtQuick.Window 2.12
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQml 2.15
|
||||
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.plasma5support 2.0 as P5Support
|
||||
import org.kde.kquickcontrolsaddons 2.0
|
||||
import org.kde.kirigami 2.11 as Kirigami
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
import org.kde.taskmanager 0.1 as TaskManager
|
||||
|
||||
import org.kde.plasma.private.notifications 2.0 as Notifications
|
||||
|
||||
import ".."
|
||||
|
||||
// This singleton object contains stuff shared between all notification plasmoids, namely:
|
||||
// - Popup creation and placement
|
||||
// - Do not disturb mode
|
||||
QtObject {
|
||||
id: globals
|
||||
|
||||
// Listened to by "ago" label in NotificationHeader to update all of them in unison
|
||||
signal timeChanged
|
||||
|
||||
property bool inhibited: false
|
||||
|
||||
onInhibitedChanged: {
|
||||
var pa = pulseAudio.item;
|
||||
if (!pa) {
|
||||
return;
|
||||
}
|
||||
|
||||
var stream = pa.notificationStream;
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inhibited) {
|
||||
// Only remember that we muted if previously not muted.
|
||||
if (!stream.muted) {
|
||||
notificationSettings.notificationSoundsInhibited = true;
|
||||
stream.mute();
|
||||
}
|
||||
} else {
|
||||
// Only unmute if we previously muted it.
|
||||
if (notificationSettings.notificationSoundsInhibited) {
|
||||
stream.unmute();
|
||||
}
|
||||
notificationSettings.notificationSoundsInhibited = false;
|
||||
}
|
||||
notificationSettings.save();
|
||||
}
|
||||
|
||||
// Some parts of the code rely on plasmoid and since we're in a singleton here
|
||||
// this is named "plasmoid"
|
||||
property var plasmoidItem: null
|
||||
property var plasmoid: null
|
||||
|
||||
// HACK When a plasmoid is destroyed, QML sets its value to "null" in the Array
|
||||
// so we then remove it so we have a working "plasmoid" again
|
||||
onPlasmoidChanged: {
|
||||
if (!plasmoid) {
|
||||
// this doesn't Q_EMIT a change, only in ratePlasmoids() it will detect the change
|
||||
plasmoidItems.splice(0, 1); // remove first
|
||||
ratePlasmoids();
|
||||
}
|
||||
}
|
||||
|
||||
// all notification plasmoids
|
||||
property var plasmoidItems: []
|
||||
|
||||
property int popupLocation: {
|
||||
// if we are on mobile, we can ignore the settings totally and just
|
||||
// align it to top center
|
||||
if (Kirigami.Settings.isMobile) {
|
||||
return Qt.AlignTop | Qt.AlignHCenter;
|
||||
}
|
||||
|
||||
switch (notificationSettings.popupPosition) {
|
||||
// Auto-determine location based on plasmoid location
|
||||
case NotificationManager.Settings.CloseToWidget:
|
||||
if (!plasmoid) {
|
||||
return Qt.AlignBottom | Qt.AlignRight; // just in case
|
||||
}
|
||||
|
||||
var alignment = 0;
|
||||
// NOTE this is our "plasmoid" property from above, don't port this to Plasmoid attached property!
|
||||
if (plasmoid.location === PlasmaCore.Types.LeftEdge) {
|
||||
alignment |= Qt.AlignLeft;
|
||||
} else if (plasmoid.location === PlasmaCore.Types.RightEdge) {
|
||||
alignment |= Qt.AlignRight;
|
||||
// No horizontal alignment flag has it place it left or right depending on
|
||||
// which half of the *panel* the notification plasmoid is in
|
||||
}
|
||||
|
||||
if (plasmoid.location === PlasmaCore.Types.TopEdge) {
|
||||
alignment |= Qt.AlignTop;
|
||||
} else if (plasmoid.location === PlasmaCore.Types.BottomEdge) {
|
||||
alignment |= Qt.AlignBottom;
|
||||
// No vertical alignment flag has it place it top or bottom edge depending on
|
||||
// which half of the *screen* the notification plasmoid is in
|
||||
}
|
||||
return alignment;
|
||||
|
||||
case NotificationManager.Settings.TopLeft:
|
||||
return Qt.AlignTop | Qt.AlignLeft;
|
||||
case NotificationManager.Settings.TopCenter:
|
||||
return Qt.AlignTop | Qt.AlignHCenter;
|
||||
case NotificationManager.Settings.TopRight:
|
||||
return Qt.AlignTop | Qt.AlignRight;
|
||||
case NotificationManager.Settings.BottomLeft:
|
||||
return Qt.AlignBottom | Qt.AlignLeft;
|
||||
case NotificationManager.Settings.BottomCenter:
|
||||
return Qt.AlignBottom | Qt.AlignHCenter;
|
||||
case NotificationManager.Settings.BottomRight:
|
||||
return Qt.AlignBottom | Qt.AlignRight;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property rect screenRect: {
|
||||
if (!plasmoid) {
|
||||
return Qt.rect(0, 0, -1, -1);
|
||||
}
|
||||
|
||||
const containment = plasmoid.containment;
|
||||
// NOTE this is our "plasmoid" property from above, don't port this to Plasmoid attached property!
|
||||
let rect = Qt.rect(containment.screenGeometry.x + containment.availableScreenRect.x,
|
||||
containment.screenGeometry.y + containment.availableScreenRect.y,
|
||||
containment.availableScreenRect.width,
|
||||
containment.availableScreenRect.height);
|
||||
|
||||
// When no explicit screen corner is configured,
|
||||
// restrict notification popup position by horizontal panel width
|
||||
if (visualParent && notificationSettings.popupPosition === NotificationManager.Settings.CloseToWidget
|
||||
&& plasmoid.formFactor === PlasmaCore.Types.Horizontal) {
|
||||
const visualParentWindow = visualParent.Window.window;
|
||||
if (visualParentWindow) {
|
||||
const left = Math.max(rect.left, visualParentWindow.x);
|
||||
const right = Math.min(rect.right, visualParentWindow.x + visualParentWindow.width);
|
||||
rect = Qt.rect(left, rect.y, right - left, rect.height);
|
||||
}
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
onScreenRectChanged: repositionTimer.start()
|
||||
|
||||
readonly property Item visualParent: {
|
||||
if (!plasmoidItem) {
|
||||
return null;
|
||||
}
|
||||
// NOTE this is our "plasmoid" property from above, don't port this to Plasmoid attached property!
|
||||
return (plasmoid && plasmoid.systemTrayRepresentation)
|
||||
|| plasmoidItem.compactRepresentationItem
|
||||
|| plasmoidItem.fullRepresentationItem;
|
||||
}
|
||||
onVisualParentChanged: positionPopups()
|
||||
|
||||
property QtObject obstructingDialog: null
|
||||
readonly property QtObject focusDialog: plasmoid ? plasmoid.focussedPlasmaDialog : null
|
||||
onFocusDialogChanged: {
|
||||
if (focusDialog && !(focusDialog instanceof NotificationPopup)) {
|
||||
// keep around the last focusDialog so notifications don't jump around if there is an open but unfocused (eg pinned) Plasma dialog
|
||||
// and exclude NotificationPopups so that notifications don't jump down on close when the focusDialog becomes NotificationPopup
|
||||
obstructingDialog = focusDialog;
|
||||
}
|
||||
positionPopups()
|
||||
}
|
||||
|
||||
// The raw width of the popup's content item, the Dialog itself adds some margins
|
||||
// Make it wider when on the top or the bottom center, since there's more horizontal
|
||||
// space available without looking weird
|
||||
// On mobile however we don't really want to have larger notifications
|
||||
property int popupWidth: (popupLocation & Qt.AlignHCenter) && !Kirigami.Settings.isMobile ? Kirigami.Units.iconSizes.small * 22 : Kirigami.Units.iconSizes.small * 18
|
||||
property int popupEdgeDistance: Kirigami.Units.iconSizes.small * 2
|
||||
// Reduce spacing between popups when centered so the stack doesn't intrude into the
|
||||
// view as much
|
||||
property int popupSpacing: (popupLocation & Qt.AlignHCenter) && !Kirigami.Settings.isMobile ? Kirigami.Units.smallSpacing : Kirigami.Units.iconSizes.small
|
||||
|
||||
// How much vertical screen real estate the notification popups may consume
|
||||
readonly property real popupMaximumScreenFill: 0.8
|
||||
|
||||
onPopupLocationChanged: Qt.callLater(positionPopups)
|
||||
|
||||
Component.onCompleted: checkInhibition()
|
||||
|
||||
function adopt(plasmoid) {
|
||||
// this doesn't Q_EMIT a change, only in ratePlasmoids() it will detect the change
|
||||
globals.plasmoidItems.push(plasmoid);
|
||||
ratePlasmoids();
|
||||
}
|
||||
|
||||
// Sorts plasmoids based on a heuristic to find a suitable plasmoid to follow when placing popups
|
||||
function ratePlasmoids() {
|
||||
var plasmoidScore = function(plasmoidItem) {
|
||||
if (!plasmoidItem || plasmoidItem.plasmoid) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const plasmoid = plasmoidItem.plasmoid;
|
||||
var score = 0;
|
||||
|
||||
// Prefer plasmoids in a panel, prefer horizontal panels over vertical ones
|
||||
// NOTE this is our "plasmoid" property from above, don't port this to Plasmoid attached property!
|
||||
if (plasmoid.location === PlasmaCore.Types.LeftEdge
|
||||
|| plasmoid.location === PlasmaCore.Types.RightEdge) {
|
||||
score += 1;
|
||||
} else if (plasmoid.location === PlasmaCore.Types.TopEdge
|
||||
|| plasmoid.location === PlasmaCore.Types.BottomEdge) {
|
||||
score += 2;
|
||||
}
|
||||
|
||||
// Prefer iconified plasmoids
|
||||
if (!plasmoidItem.expanded) {
|
||||
++score;
|
||||
}
|
||||
|
||||
// Prefer plasmoids on primary screen
|
||||
if (plasmoid && plasmoid.containment.screen === 0) {
|
||||
++score;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
var newPlasmoidItems = plasmoidItems;
|
||||
newPlasmoidItems.sort(function (a, b) {
|
||||
var scoreA = plasmoidScore(a);
|
||||
var scoreB = plasmoidScore(b);
|
||||
// Sort descending by score
|
||||
if (scoreA < scoreB) {
|
||||
return 1;
|
||||
} else if (scoreA > scoreB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
globals.plasmoidItems = newPlasmoidItems;
|
||||
globals.plasmoidItem = newPlasmoidItems[0];
|
||||
globals.plasmoid = globals.plasmoidItem.plasmoid;
|
||||
}
|
||||
|
||||
function checkInhibition() {
|
||||
globals.inhibited = Qt.binding(function() {
|
||||
var inhibited = false;
|
||||
|
||||
if (!NotificationManager.Server.valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
|
||||
if (!isNaN(inhibitedUntil.getTime())) {
|
||||
inhibited |= (Date.now() < inhibitedUntil.getTime());
|
||||
}
|
||||
|
||||
if (notificationSettings.notificationsInhibitedByApplication) {
|
||||
inhibited |= true;
|
||||
}
|
||||
|
||||
if (notificationSettings.inhibitNotificationsWhenScreensMirrored) {
|
||||
inhibited |= notificationSettings.screensMirrored;
|
||||
}
|
||||
|
||||
return inhibited;
|
||||
});
|
||||
}
|
||||
|
||||
function revokeInhibitions() {
|
||||
notificationSettings.notificationsInhibitedUntil = undefined;
|
||||
notificationSettings.revokeApplicationInhibitions();
|
||||
// overrules current mirrored screen setup, updates again when screen configuration changes
|
||||
notificationSettings.screensMirrored = false;
|
||||
|
||||
notificationSettings.save();
|
||||
}
|
||||
|
||||
function rectIntersect(rect1 /*dialog*/, rect2 /*popup*/) {
|
||||
return rect1.x < rect2.x + rect2.width
|
||||
&& rect2.x < rect1.x + rect1.width
|
||||
&& rect1.y < rect2.y + rect2.height
|
||||
&& rect2.y < rect1.y + rect1.height;
|
||||
}
|
||||
|
||||
function positionPopups() {
|
||||
if (!plasmoid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screenRect = globals.screenRect;
|
||||
if (screenRect.width <= 0 || screenRect.height <= 0) {
|
||||
return;
|
||||
}
|
||||
if (!globals.visualParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let effectivePopupLocation = popupLocation;
|
||||
|
||||
const visualParent = globals.visualParent;
|
||||
const visualParentWindow = visualParent.Window.window;
|
||||
|
||||
// When no horizontal alignment is specified, place it depending on which half of the *panel*
|
||||
// the notification plasmoid is in
|
||||
if (visualParentWindow) {
|
||||
if (!(effectivePopupLocation & (Qt.AlignLeft | Qt.AlignHCenter | Qt.AlignRight))) {
|
||||
const iconHCenter = visualParent.mapToItem(null /*mapToScene*/, 0, 0).x + visualParent.width / 2;
|
||||
|
||||
if (iconHCenter < visualParentWindow.width / 2) {
|
||||
effectivePopupLocation |= Qt.AlignLeft;
|
||||
} else {
|
||||
effectivePopupLocation |= Qt.AlignRight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When no vertical alignment is specified, place it depending on which half of the *screen*
|
||||
// the notification plasmoid is in
|
||||
if (!(effectivePopupLocation & (Qt.AlignTop | Qt.AlignBottom))) {
|
||||
const screenVCenter = screenRect.y + screenRect.height / 2;
|
||||
const iconVCenter = visualParent.mapToGlobal(0, visualParent.height / 2).y;
|
||||
|
||||
if (iconVCenter < screenVCenter) {
|
||||
effectivePopupLocation |= Qt.AlignTop;
|
||||
} else {
|
||||
effectivePopupLocation |= Qt.AlignBottom;
|
||||
}
|
||||
}
|
||||
|
||||
let y = screenRect.y;
|
||||
if (effectivePopupLocation & Qt.AlignBottom) {
|
||||
y += screenRect.height - popupEdgeDistance;
|
||||
} else {
|
||||
y += popupEdgeDistance;
|
||||
}
|
||||
|
||||
for (var i = 0; i < popupInstantiator.count; ++i) {
|
||||
let popup = popupInstantiator.objectAt(i);
|
||||
if (!popup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Popup width is fixed, so don't rely on the actual window size
|
||||
var popupEffectiveWidth = popupWidth + popup.margins.left + popup.margins.right;
|
||||
|
||||
const leftMostX = screenRect.x + popupEdgeDistance;
|
||||
const rightMostX = screenRect.x + screenRect.width - popupEdgeDistance - popupEffectiveWidth;
|
||||
|
||||
// If available screen rect is narrower than the popup, center it in the available rect
|
||||
if (screenRect.width < popupEffectiveWidth || effectivePopupLocation & Qt.AlignHCenter) {
|
||||
popup.x = screenRect.x + (screenRect.width - popupEffectiveWidth) / 2
|
||||
} else if (effectivePopupLocation & Qt.AlignLeft) {
|
||||
popup.x = leftMostX;
|
||||
} else if (effectivePopupLocation & Qt.AlignRight) {
|
||||
popup.x = rightMostX;
|
||||
}
|
||||
|
||||
if (effectivePopupLocation & Qt.AlignTop) {
|
||||
// We want to calculate the new position based on its original target position to avoid positioning it and then
|
||||
// positioning it again, hence the temporary Qt.rect with explicit "y" and not just the popup as a whole
|
||||
if (obstructingDialog && obstructingDialog.visible
|
||||
&& rectIntersect(obstructingDialog, Qt.rect(popup.x, y, popup.width, popup.height))) {
|
||||
y = obstructingDialog.y + obstructingDialog.height + popupEdgeDistance;
|
||||
}
|
||||
popup.y = y;
|
||||
// If the popup isn't ready yet, ignore its occupied space for now.
|
||||
// We'll reposition everything in onHeightChanged eventually.
|
||||
y += popup.height + (popup.height > 0 ? popupSpacing : 0);
|
||||
} else {
|
||||
y -= popup.height;
|
||||
if (obstructingDialog && obstructingDialog.visible
|
||||
&& rectIntersect(obstructingDialog, Qt.rect(popup.x, y, popup.width, popup.height))) {
|
||||
y = obstructingDialog.y - popup.height - popupEdgeDistance;
|
||||
}
|
||||
popup.y = y;
|
||||
if (popup.height > 0) {
|
||||
y -= popupSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
// don't let notifications take more than popupMaximumScreenFill of the screen
|
||||
var visible = true;
|
||||
if (i > 0) { // however always show at least one popup
|
||||
if (effectivePopupLocation & Qt.AlignTop) {
|
||||
visible = (popup.y + popup.height < screenRect.y + (screenRect.height * popupMaximumScreenFill));
|
||||
} else {
|
||||
visible = (popup.y > screenRect.y + (screenRect.height * (1 - popupMaximumScreenFill)));
|
||||
}
|
||||
}
|
||||
|
||||
popup.visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject popupNotificationsModel: NotificationManager.Notifications {
|
||||
limit: plasmoid ? (Math.ceil(globals.screenRect.height / (Kirigami.Units.iconSizes.small * 4))) : 0
|
||||
showExpired: false
|
||||
showDismissed: false
|
||||
blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications
|
||||
blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices
|
||||
whitelistedDesktopEntries: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedApplications : []
|
||||
whitelistedNotifyRcNames: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedServices : []
|
||||
showJobs: notificationSettings.jobsInNotifications
|
||||
sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
|
||||
sortOrder: Qt.AscendingOrder
|
||||
groupMode: NotificationManager.Notifications.GroupDisabled
|
||||
window: visualParent ? visualParent.Window.window : null
|
||||
urgencies: {
|
||||
var urgencies = 0;
|
||||
|
||||
// Critical always except in do not disturb mode when disabled in settings
|
||||
if (!globals.inhibited || notificationSettings.criticalPopupsInDoNotDisturbMode) {
|
||||
urgencies |= NotificationManager.Notifications.CriticalUrgency;
|
||||
}
|
||||
|
||||
// Normal only when not in do not disturb mode
|
||||
if (!globals.inhibited) {
|
||||
urgencies |= NotificationManager.Notifications.NormalUrgency;
|
||||
}
|
||||
|
||||
// Low only when enabled in settings and not in do not disturb mode
|
||||
if (!globals.inhibited && notificationSettings.lowPriorityPopups) {
|
||||
urgencies |=NotificationManager.Notifications.LowUrgency;
|
||||
}
|
||||
|
||||
return urgencies;
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject notificationSettings: NotificationManager.Settings {
|
||||
onNotificationsInhibitedUntilChanged: globals.checkInhibition()
|
||||
}
|
||||
|
||||
property QtObject tasksModel: TaskManager.TasksModel {
|
||||
groupMode: TaskManager.TasksModel.GroupApplications
|
||||
groupInline: false
|
||||
}
|
||||
|
||||
// This periodically checks whether do not disturb mode timed out and updates the "minutes ago" labels
|
||||
property QtObject timeSource: P5Support.DataSource {
|
||||
engine: "time"
|
||||
connectedSources: ["Local"]
|
||||
interval: 60000 // 1 min
|
||||
intervalAlignment: P5Support.Types.AlignToMinute
|
||||
onDataChanged: {
|
||||
checkInhibition();
|
||||
globals.timeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
property Instantiator popupInstantiator: Instantiator {
|
||||
model: popupNotificationsModel
|
||||
delegate: NotificationPopup {
|
||||
id: popup
|
||||
// so Instantiator can access that after the model row is gone
|
||||
readonly property var notificationId: model.notificationId
|
||||
|
||||
popupWidth: globals.popupWidth
|
||||
type: model.urgency === NotificationManager.Notifications.CriticalUrgency
|
||||
|| (model.urgency === NotificationManager.Notifications.NormalUrgency && notificationSettings.keepNormalAlwaysOnTop)
|
||||
? PlasmaCore.Dialog.CriticalNotification : PlasmaCore.Dialog.Notification
|
||||
|
||||
notificationType: model.type
|
||||
|
||||
applicationName: model.applicationName
|
||||
applicationIconSource: model.applicationIconName
|
||||
originName: model.originName || ""
|
||||
|
||||
time: model.updated || model.created
|
||||
|
||||
configurable: model.configurable
|
||||
// For running jobs instead of offering a "close" button that might lead the user to
|
||||
// think that will cancel the job, we offer a "dismiss" button that hides it in the history
|
||||
dismissable: model.type === NotificationManager.Notifications.JobType
|
||||
&& model.jobState !== NotificationManager.Notifications.JobStateStopped
|
||||
// TODO would be nice to be able to "pin" jobs when they autohide
|
||||
&& notificationSettings.permanentJobPopups
|
||||
closable: model.closable
|
||||
|
||||
summary: model.summary
|
||||
body: model.body || ""
|
||||
accessibleDescription: model.accessibleDescription
|
||||
icon: model.image || model.iconName
|
||||
hasDefaultAction: model.hasDefaultAction || false
|
||||
timeout: model.timeout
|
||||
// Increase default timeout for notifications with a URL so you have enough time
|
||||
// to interact with the thumbnail or bring the window to the front where you want to drag it into
|
||||
defaultTimeout: notificationSettings.popupTimeout + (model.urls && model.urls.length > 0 ? 5000 : 0)
|
||||
// When configured to not keep jobs open permanently, we autodismiss them after the standard timeout
|
||||
dismissTimeout: !notificationSettings.permanentJobPopups
|
||||
&& model.type === NotificationManager.Notifications.JobType
|
||||
&& model.jobState !== NotificationManager.Notifications.JobStateStopped
|
||||
? defaultTimeout : 0
|
||||
|
||||
urls: model.urls || []
|
||||
urgency: model.urgency || NotificationManager.Notifications.NormalUrgency
|
||||
|
||||
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 || ""
|
||||
actionNames: model.actionNames
|
||||
actionLabels: model.actionLabels
|
||||
|
||||
hasReplyAction: model.hasReplyAction || false
|
||||
replyActionLabel: model.replyActionLabel || ""
|
||||
replyPlaceholderText: model.replyPlaceholderText || ""
|
||||
replySubmitButtonText: model.replySubmitButtonText || ""
|
||||
replySubmitButtonIconName: model.replySubmitButtonIconName || ""
|
||||
|
||||
onExpired: {
|
||||
if (model.resident) {
|
||||
// When resident, only mark it as expired so the popup disappears
|
||||
// but don't actually invalidate the notification
|
||||
model.expired = true;
|
||||
} else {
|
||||
popupNotificationsModel.expire(popupNotificationsModel.index(index, 0))
|
||||
}
|
||||
}
|
||||
onHoverEntered: model.read = true
|
||||
// explicit close, even when resident
|
||||
onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
|
||||
onDismissClicked: model.dismissed = true
|
||||
onConfigureClicked: popupNotificationsModel.configure(popupNotificationsModel.index(index, 0))
|
||||
onDefaultActionInvoked: {
|
||||
if (defaultActionFallbackWindowIdx) {
|
||||
if (!defaultActionFallbackWindowIdx.valid) {
|
||||
console.warn("Failed fallback notification activation as window no longer exists");
|
||||
return;
|
||||
}
|
||||
|
||||
// When it's a group, activate the window highest in stacking order (presumably last used)
|
||||
if (tasksModel.data(defaultActionFallbackWindowIdx, TaskManager.AbstractTasksModel.IsGroupParent)) {
|
||||
let highestStacking = -1;
|
||||
let highestIdx = undefined;
|
||||
|
||||
for (let i = 0; i < tasksModel.rowCount(defaultActionFallbackWindowIdx); ++i) {
|
||||
const idx = tasksModel.index(i, 0, defaultActionFallbackWindowIdx);
|
||||
|
||||
const stacking = tasksModel.data(idx, TaskManager.AbstractTasksModel.StackingOrder);
|
||||
|
||||
if (stacking > highestStacking) {
|
||||
highestStacking = stacking;
|
||||
highestIdx = tasksModel.makePersistentModelIndex(defaultActionFallbackWindowIdx.row, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (highestIdx && highestIdx.valid) {
|
||||
tasksModel.requestActivate(highestIdx);
|
||||
if (!model.resident) {
|
||||
popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
|
||||
}
|
||||
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
tasksModel.requestActivate(defaultActionFallbackWindowIdx);
|
||||
if (!model.resident) {
|
||||
popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const behavior = model.resident ? NotificationManager.Notifications.None : NotificationManager.Notifications.Close;
|
||||
popupNotificationsModel.invokeDefaultAction(popupNotificationsModel.index(index, 0), behavior)
|
||||
}
|
||||
onActionInvoked: actionName => {
|
||||
const behavior = model.resident ? NotificationManager.Notifications.None : NotificationManager.Notifications.Close;
|
||||
popupNotificationsModel.invokeAction(popupNotificationsModel.index(index, 0), actionName, behavior)
|
||||
}
|
||||
onReplied: {
|
||||
const behavior = model.resident ? NotificationManager.Notifications.None : NotificationManager.Notifications.Close;
|
||||
popupNotificationsModel.reply(popupNotificationsModel.index(index, 0), text, behavior);
|
||||
}
|
||||
onOpenUrl: url => {
|
||||
Qt.openUrlExternally(url);
|
||||
// Client isn't informed of this action, so we always hide the popup
|
||||
if (model.resident) {
|
||||
model.expired = true;
|
||||
} else {
|
||||
popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
|
||||
}
|
||||
}
|
||||
onFileActionInvoked: action => {
|
||||
if (!model.resident
|
||||
|| (action.objectName === "movetotrash" || action.objectName === "deletefile")) {
|
||||
popupNotificationsModel.close(popupNotificationsModel.index(index, 0));
|
||||
} else {
|
||||
model.expired = true;
|
||||
}
|
||||
}
|
||||
onForceActiveFocusRequested: {
|
||||
// NOTE this is our "plasmoid" property from above, don't port this to Plasmoid attached property!
|
||||
plasmoid.forceActivateWindow(popup);
|
||||
}
|
||||
|
||||
onSuspendJobClicked: popupNotificationsModel.suspendJob(popupNotificationsModel.index(index, 0))
|
||||
onResumeJobClicked: popupNotificationsModel.resumeJob(popupNotificationsModel.index(index, 0))
|
||||
onKillJobClicked: popupNotificationsModel.killJob(popupNotificationsModel.index(index, 0))
|
||||
|
||||
// popup width is fixed
|
||||
onHeightChanged: positionPopups()
|
||||
|
||||
Component.onCompleted: {
|
||||
if (model.type === NotificationManager.Notifications.NotificationType && model.desktopEntry) {
|
||||
// Register apps that were seen spawning a popup so they can be configured later
|
||||
// Apps with notifyrc can already be configured anyway
|
||||
if (!model.notifyRcName) {
|
||||
notificationSettings.registerKnownApplication(model.desktopEntry);
|
||||
notificationSettings.save();
|
||||
}
|
||||
|
||||
// If there is no default action, check if there is a window we could activate instead
|
||||
if (!model.hasDefaultAction) {
|
||||
for (let i = 0; i < tasksModel.rowCount(); ++i) {
|
||||
const idx = tasksModel.index(i, 0);
|
||||
|
||||
const appId = tasksModel.data(idx, TaskManager.AbstractTasksModel.AppId);
|
||||
if (appId === model.desktopEntry + ".desktop") {
|
||||
// Takes a row number, not a QModelIndex
|
||||
defaultActionFallbackWindowIdx = tasksModel.makePersistentModelIndex(i);
|
||||
hasDefaultAction = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the model that we're handling the timeout now
|
||||
popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0));
|
||||
}
|
||||
}
|
||||
onObjectAdded: (index, object) => {
|
||||
positionPopups();
|
||||
object.visible = true;
|
||||
}
|
||||
onObjectRemoved: (index, object) => {
|
||||
var notificationId = object.notificationId
|
||||
// Popup might have been destroyed because of a filter change, tell the model to do the timeout work for us again
|
||||
// cannot use QModelIndex here as the model row is already gone
|
||||
popupNotificationsModel.startTimeout(notificationId);
|
||||
|
||||
positionPopups();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO use pulseaudio-qt for this once it becomes a framework
|
||||
property QtObject pulseAudio: Loader {
|
||||
source: "PulseAudio.qml"
|
||||
}
|
||||
|
||||
// Normally popups are repositioned through Qt.callLater but in case of e.g. screen geometry changes we want to compress that
|
||||
property Timer repositionTimer: Timer {
|
||||
interval: 250
|
||||
onTriggered: positionPopups()
|
||||
}
|
||||
|
||||
// Tracks the visual parent's window since mapToItem cannot signal
|
||||
// so that when user resizes panel we reposition the popups live
|
||||
property Connections visualParentWindowConnections: Connections {
|
||||
target: visualParent ? visualParent.Window.window : null
|
||||
function onXChanged() {
|
||||
repositionTimer.start();
|
||||
}
|
||||
function onYChanged() {
|
||||
repositionTimer.start();
|
||||
}
|
||||
function onWidthChanged() {
|
||||
repositionTimer.start();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
repositionTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
// Keeps the Inhibited property on DBus in sync with our inhibition handling
|
||||
property Binding serverInhibitedBinding: Binding {
|
||||
target: NotificationManager.Server
|
||||
property: "inhibited"
|
||||
value: globals.inhibited
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
|
||||
function toggleDoNotDisturbMode() {
|
||||
var oldInhibited = globals.inhibited;
|
||||
if (oldInhibited) {
|
||||
globals.revokeInhibitions();
|
||||
} else {
|
||||
// Effectively "in a year" is "until turned off"
|
||||
var d = new Date();
|
||||
d.setFullYear(d.getFullYear() + 1);
|
||||
notificationSettings.notificationsInhibitedUntil = d;
|
||||
notificationSettings.save();
|
||||
}
|
||||
|
||||
checkInhibition();
|
||||
|
||||
if (globals.inhibited !== oldInhibited) {
|
||||
shortcuts.showDoNotDisturbOsd(globals.inhibited);
|
||||
}
|
||||
}
|
||||
|
||||
property Notifications.GlobalShortcuts shortcuts: Notifications.GlobalShortcuts {
|
||||
onToggleDoNotDisturbTriggered: globals.toggleDoNotDisturbMode()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2017, 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.2
|
||||
|
||||
import org.kde.plasma.private.volume 0.1
|
||||
|
||||
QtObject {
|
||||
id: pulseAudio
|
||||
|
||||
readonly property string notificationStreamId: "sink-input-by-media-role:event"
|
||||
|
||||
property QtObject notificationStream
|
||||
|
||||
property QtObject instantiator: Instantiator {
|
||||
model: StreamRestoreModel {}
|
||||
|
||||
delegate: QtObject {
|
||||
readonly property string name: Name
|
||||
readonly property bool muted: Muted
|
||||
|
||||
function mute() {
|
||||
Muted = true
|
||||
}
|
||||
function unmute() {
|
||||
Muted = false
|
||||
}
|
||||
}
|
||||
|
||||
onObjectAdded: (index, object) => {
|
||||
if (object.name === notificationStreamId) {
|
||||
notificationStream = object;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
singleton Globals 1.0 Globals.qml
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.8
|
||||
import QtQml 2.15
|
||||
|
||||
import org.kde.plasma.plasmoid 2.0
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.kquickcontrolsaddons 2.0 // For KCMShell
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.ksvg 1.0 as KSvg
|
||||
|
||||
import org.kde.coreaddons 1.0 as KCoreAddons
|
||||
import org.kde.kcmutils as KCMUtils
|
||||
import org.kde.config as KConfig
|
||||
|
||||
import org.kde.notificationmanager as NotificationManager
|
||||
|
||||
import org.kde.plasma.private.notifications 2.0 as Notifications
|
||||
|
||||
import "global"
|
||||
|
||||
PlasmoidItem {
|
||||
id: root
|
||||
|
||||
property alias clearHistoryAction: clearHistory
|
||||
|
||||
readonly property int unreadCount: Math.min(99, historyModel.unreadNotificationsCount)
|
||||
|
||||
readonly property bool inhibitedOrBroken: Globals.inhibited || !NotificationManager.Server.valid
|
||||
|
||||
readonly property bool inPanel: (Plasmoid.location === PlasmaCore.Types.TopEdge
|
||||
|| Plasmoid.location === PlasmaCore.Types.RightEdge
|
||||
|| Plasmoid.location === PlasmaCore.Types.BottomEdge
|
||||
|| Plasmoid.location === PlasmaCore.Types.LeftEdge)
|
||||
|
||||
readonly property int effectiveStatus: historyModel.activeJobsCount > 0
|
||||
|| historyModel.unreadNotificationsCount > 0
|
||||
|| Globals.inhibited ? PlasmaCore.Types.ActiveStatus
|
||||
: PlasmaCore.Types.PassiveStatus
|
||||
onEffectiveStatusChanged: {
|
||||
if (effectiveStatus === PlasmaCore.Types.PassiveStatus) {
|
||||
// HACK System Tray only lets applets self-hide when in Active state
|
||||
// When we clear the notifications, the status is updated right away
|
||||
// as a result of model signals, and when we then try to collapse
|
||||
// the popup isn't hidden.
|
||||
Qt.callLater(function() {
|
||||
Plasmoid.status = effectiveStatus;
|
||||
});
|
||||
} else {
|
||||
Plasmoid.status = effectiveStatus;
|
||||
}
|
||||
}
|
||||
|
||||
Plasmoid.status: effectiveStatus
|
||||
|
||||
|
||||
toolTipMainText: "Action Center"
|
||||
toolTipSubText: {
|
||||
var lines = [];
|
||||
|
||||
if (jobAggregator.count > 0) {
|
||||
let description = i18np("%1 running job", "%1 running jobs", jobAggregator.count);
|
||||
|
||||
if (jobAggregator.summary) {
|
||||
if (jobAggregator.percentage > 0) {
|
||||
description = i18nc("Job title (percentage)", "%1 (%2%)", jobAggregator.summary, jobAggregator.percentage);
|
||||
} else {
|
||||
description = jobAggregator.summary;
|
||||
}
|
||||
} else if (jobAggregator.percentage > 0) {
|
||||
description = i18np("%1 running job (%2%)", "%1 running jobs (%2%)", jobAggregator.count, jobAggregator.percentage);
|
||||
}
|
||||
|
||||
lines.push(description);
|
||||
}
|
||||
|
||||
if (!NotificationManager.Server.valid) {
|
||||
lines.push(i18n("Notification service not available"));
|
||||
} else {
|
||||
// Any notification that is newer than "lastRead" is "unread"
|
||||
// since it doesn't know the popup is on screen which makes the user see it
|
||||
var actualUnread = historyModel.unreadNotificationsCount - Globals.popupNotificationsModel.activeNotificationsCount;
|
||||
if (actualUnread > 0) {
|
||||
lines.push(i18np("%1 unread notification", "%1 unread notifications", actualUnread));
|
||||
}
|
||||
|
||||
if (Globals.inhibited) {
|
||||
var inhibitedUntil = notificationSettings.notificationsInhibitedUntil
|
||||
var inhibitedUntilTime = inhibitedUntil.getTime();
|
||||
var inhibitedUntilValid = !isNaN(inhibitedUntilTime);
|
||||
var dateNow = Date.now();
|
||||
|
||||
// Show until time if valid but not if too far in the future
|
||||
// TODO check app inhibition, too
|
||||
if (inhibitedUntilValid && inhibitedUntilTime - dateNow > 0
|
||||
&& inhibitedUntilTime - dateNow < 100 * 24 * 60 * 60 * 1000 /* 100 days*/) {
|
||||
lines.push(i18n("Do not disturb until %1; middle-click to exit now",
|
||||
KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat)));
|
||||
} else {
|
||||
lines.push(i18n("Do not disturb mode active; middle-click to exit"));
|
||||
}
|
||||
} else {
|
||||
if (lines.length === 0) {
|
||||
lines.push(i18n("No unread notifications"));
|
||||
}
|
||||
lines.push(i18n("Middle-click to enter do not disturb mode"));
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
Plasmoid.title: "Open Action Center"
|
||||
// Even though the actual icon is drawn with custom code in CompactRepresentation,
|
||||
// set the icon property here anyway because it's useful in other contexts
|
||||
Plasmoid.icon: {
|
||||
let iconName;
|
||||
if (root.inhibitedOrBroken) {
|
||||
iconName = "notifications-disabled";
|
||||
} else if (root.unreadCount > 0) {
|
||||
iconName = "notification-active";
|
||||
} else {
|
||||
iconName = "notification-inactive"
|
||||
}
|
||||
// "active jobs" state not included here since that requires custom painting,
|
||||
// and not just a simple icon
|
||||
|
||||
if (inPanel) {
|
||||
return symbolicizeIconName(iconName);
|
||||
}
|
||||
|
||||
return iconName;
|
||||
}
|
||||
|
||||
switchWidth: Kirigami.Units.iconSizes.small * 14
|
||||
// This is to let the plasmoid expand in a vertical panel for a "sidebar" notification panel
|
||||
// The CompactRepresentation size is limited to not have the notification icon grow gigantic
|
||||
// but it should still switch over to full rep once there's enough width (disregarding the limited height)
|
||||
switchHeight: Plasmoid.formFactor === PlasmaCore.Types.Vertical ? 1 : Kirigami.Units.iconSizes.small * 10
|
||||
|
||||
onExpandedChanged: expanded => {
|
||||
if (!expanded) {
|
||||
historyModel.lastRead = undefined; // reset to now
|
||||
historyModel.collapseAllGroups();
|
||||
}
|
||||
}
|
||||
|
||||
compactRepresentation: CompactRepresentation {
|
||||
activeCount: Globals.popupNotificationsModel.activeNotificationsCount
|
||||
unreadCount: root.unreadCount
|
||||
|
||||
jobsCount: historyModel.activeJobsCount
|
||||
jobsPercentage: historyModel.jobsPercentage
|
||||
|
||||
inhibited: root.inhibitedOrBroken
|
||||
}
|
||||
|
||||
fullRepresentation: FullRepresentation {
|
||||
|
||||
}
|
||||
|
||||
NotificationManager.Settings {
|
||||
id: notificationSettings
|
||||
}
|
||||
|
||||
NotificationManager.Notifications {
|
||||
id: historyModel
|
||||
showExpired: true
|
||||
showDismissed: true
|
||||
showJobs: notificationSettings.jobsInNotifications
|
||||
sortMode: NotificationManager.Notifications.SortByDate
|
||||
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;
|
||||
}
|
||||
|
||||
onCountChanged: count => {
|
||||
if (count === 0) {
|
||||
closePlasmoid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.JobAggregator {
|
||||
id: jobAggregator
|
||||
sourceModel: NotificationManager.Notifications {
|
||||
id: jobAggregatorModel
|
||||
showExpired: historyModel.showExpired
|
||||
showDismissed: historyModel.showDismissed
|
||||
showJobs: historyModel.showJobs
|
||||
showNotifications: false
|
||||
blacklistedDesktopEntries: historyModel.blacklistedDesktopEntries
|
||||
blacklistedNotifyRcNames: historyModel.blacklistedNotifyRcNames
|
||||
urgencies: historyModel.urgencies
|
||||
}
|
||||
}
|
||||
|
||||
function closePlasmoid() {
|
||||
if (root.hideOnWindowDeactivate) {
|
||||
root.expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
function action_configure() {
|
||||
KCMUtils.KCMLauncher.openSystemSettings("kcm_notifications");
|
||||
}
|
||||
|
||||
function symbolicizeIconName(iconName) {
|
||||
const symbolicSuffix = "-symbolic";
|
||||
if (iconName.endsWith(symbolicSuffix)) {
|
||||
return iconName;
|
||||
}
|
||||
|
||||
return iconName + symbolicSuffix;
|
||||
}
|
||||
|
||||
Plasmoid.contextualActions: [
|
||||
PlasmaCore.Action {
|
||||
id: clearHistory
|
||||
text: i18n("Clear All Notifications")
|
||||
icon.name: "edit-clear-history"
|
||||
visible: historyModel.expiredNotificationsCount > 0
|
||||
onTriggered: {
|
||||
historyModel.clear(NotificationManager.Notifications.ClearExpired);
|
||||
// clear is async,
|
||||
historyModel.countChanged.connect(closeWhenCleared)
|
||||
}
|
||||
function closeWhenCleared() {
|
||||
if (historyModel.count === 0) {
|
||||
closePlasmoid();
|
||||
}
|
||||
historyModel.countChanged.disconnect(closeWhenCleared)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
PlasmaCore.Action {
|
||||
id: configureAction
|
||||
text: i18n("&Configure Event Notifications and Actions…")
|
||||
icon.name: "configure"
|
||||
visible: KConfig.KAuthorized.authorizeControlModule("kcm_notifications");
|
||||
onTriggered: KCMUtils.KCMLauncher.openSystemSettings("kcm_notifications");
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Globals.adopt(root);
|
||||
|
||||
// The applet's config window has nothing in it, so let's make the header's
|
||||
// "Configure" button open the KCM instead, like we do in the Bluetooth
|
||||
// and Networks applets
|
||||
Plasmoid.setInternalAction("configure", configureAction)
|
||||
}
|
||||
}
|
BIN
plasma/plasmoids/org.kde.plasma.notifications/contents/ui/svgs/button.svgz
Executable file
BIN
plasma/plasmoids/org.kde.plasma.notifications/contents/ui/svgs/button.svgz
Executable file
Binary file not shown.
146
plasma/plasmoids/org.kde.plasma.notifications/contents/ui/svgs/icons.svg
Executable file
146
plasma/plasmoids/org.kde.plasma.notifications/contents/ui/svgs/icons.svg
Executable file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 88 KiB |
213
plasma/plasmoids/org.kde.plasma.notifications/metadata.json
Normal file
213
plasma/plasmoids/org.kde.plasma.notifications/metadata.json
Normal file
|
@ -0,0 +1,213 @@
|
|||
{
|
||||
"KPackageStructure": "Plasma/Applet",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "kde@privat.broulik.de",
|
||||
"Name": "Kai Uwe Broulik",
|
||||
"Name[ar]": "Kai Uwe Broulik",
|
||||
"Name[ast]": "Kai Uwe Broulik",
|
||||
"Name[az]": "Kai Uwe Broulik",
|
||||
"Name[be]": "Kai Uwe Broulik",
|
||||
"Name[bg]": "Kai Uwe Broulik",
|
||||
"Name[ca@valencia]": "Kai Uwe Broulik",
|
||||
"Name[ca]": "Kai Uwe Broulik",
|
||||
"Name[cs]": "Kai Uwe Broulik",
|
||||
"Name[da]": "Kai Uwe Broulik",
|
||||
"Name[de]": "Kai Uwe Broulik",
|
||||
"Name[en_GB]": "Kai Uwe Broulik",
|
||||
"Name[eo]": "Kai Uwe Broulik",
|
||||
"Name[es]": "Kai Uwe Broulik",
|
||||
"Name[et]": "Kai Uwe Broulik",
|
||||
"Name[eu]": "Kai Uwe Broulik",
|
||||
"Name[fi]": "Kai Uwe Broulik",
|
||||
"Name[fr]": "Kai Uwe Broulik",
|
||||
"Name[gl]": "Kai Uwe Broulik",
|
||||
"Name[he]": "קאי אווה ברוליק",
|
||||
"Name[hi]": "काई उवे ब्रोलिक",
|
||||
"Name[hu]": "Kai Uwe Broulik",
|
||||
"Name[ia]": "Kai Uwe Broulik",
|
||||
"Name[id]": "Kai Uwe Broulik",
|
||||
"Name[is]": "Kai Uwe Broulik",
|
||||
"Name[it]": "Kai Uwe Broulik",
|
||||
"Name[ja]": "Kai Uwe Broulik",
|
||||
"Name[ka]": "კაი უვე ბროულიკი",
|
||||
"Name[ko]": "Kai Uwe Broulik",
|
||||
"Name[lt]": "Kai Uwe Broulik",
|
||||
"Name[nl]": "Kai Uwe Broulik",
|
||||
"Name[nn]": "Kai Uwe Broulik",
|
||||
"Name[pl]": "Kai Uwe Broulik",
|
||||
"Name[pt]": "Kai Uwe Broulik",
|
||||
"Name[pt_BR]": "Kai Uwe Broulik",
|
||||
"Name[ro]": "Kai Uwe Broulik",
|
||||
"Name[ru]": "Kai Uwe Broulik",
|
||||
"Name[sa]": "कै उवे ब्रौलिक्",
|
||||
"Name[sk]": "Kai Uwe Broulik",
|
||||
"Name[sl]": "Kai Uwe Broulik",
|
||||
"Name[sv]": "Kai Uwe Broulik",
|
||||
"Name[ta]": "காய் ஊவே புரோலிக்",
|
||||
"Name[tr]": "Kai Uwe Broulik",
|
||||
"Name[uk]": "Kai Uwe Broulik",
|
||||
"Name[vi]": "Kai Uwe Broulik",
|
||||
"Name[x-test]": "xxKai Uwe Broulikxx",
|
||||
"Name[zh_CN]": "Kai Uwe Broulik",
|
||||
"Name[zh_TW]": "Kai Uwe Broulik"
|
||||
}
|
||||
],
|
||||
"BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=plasmashell&component=Notifications",
|
||||
"Category": "Tasks",
|
||||
"Description": "Display notifications and jobs",
|
||||
"Description[ar]": "اعرض الإخطارات والمهام",
|
||||
"Description[az]": "Bildirişləri və əməlləri göstərmək",
|
||||
"Description[be]": "Паказ апавяшчэнняў і задач",
|
||||
"Description[bg]": "Показване на известия и задачи",
|
||||
"Description[ca@valencia]": "Mostra les notificacions i els treballs",
|
||||
"Description[ca]": "Mostra les notificacions i els treballs",
|
||||
"Description[cs]": "Upozornění a úlohy",
|
||||
"Description[da]": "Vis bekendtgørelser og job",
|
||||
"Description[de]": "Benachrichtigungen und Aktionen anzeigen",
|
||||
"Description[en_GB]": "Display notifications and jobs",
|
||||
"Description[eo]": "Ekranaj atentigoj kaj taskoj",
|
||||
"Description[es]": "Mostrar notificaciones y tareas",
|
||||
"Description[et]": "Märguannete ja tööde näitamine",
|
||||
"Description[eu]": "Azaldu jakinarazpenak eta atazak",
|
||||
"Description[fi]": "Näytä ilmoituksia ja töitä",
|
||||
"Description[fr]": "Afficher les notifications et les tâches",
|
||||
"Description[gl]": "Mostra notificacións e tarefas.",
|
||||
"Description[he]": "הצגת התראות ומשימות",
|
||||
"Description[hi]": "सूचनाएं और कार्य प्रदर्शित करें",
|
||||
"Description[hu]": "Értesítések és feladatok megjelenítése",
|
||||
"Description[ia]": "Monstra notificationes e labores",
|
||||
"Description[id]": "Tampilan notifikasi dan tugas",
|
||||
"Description[is]": "Birting tilkynninga og verka",
|
||||
"Description[it]": "Visualizza le notifiche ed i processi",
|
||||
"Description[ja]": "通知とジョブを表示します",
|
||||
"Description[ka]": "შეტყობინებებისა და ამოცანების ჩვენება",
|
||||
"Description[ko]": "알림과 작업 표시",
|
||||
"Description[lt]": "Rodyti pranešimus ir darbus",
|
||||
"Description[nl]": "Meldingen en taken tonen",
|
||||
"Description[nn]": "Vis varslingar og jobbar",
|
||||
"Description[pa]": "ਨੋਟੀਫਿਕੇਸ਼ਨ ਤੇ ਜਾਬ ਵੇਖੋ",
|
||||
"Description[pl]": "Wyświetla powiadomienia i zadania",
|
||||
"Description[pt]": "Mostrar as notificações e tarefas",
|
||||
"Description[pt_BR]": "Exibe notificações e tarefas",
|
||||
"Description[ro]": "Afișează notificări și sarcini",
|
||||
"Description[ru]": "Уведомления и задания",
|
||||
"Description[sa]": "सूचनाः कार्याणि च प्रदर्शयन्तु",
|
||||
"Description[sk]": "Zobrazenie upozornení a úloh",
|
||||
"Description[sl]": "Prikaz obvestil in poslov",
|
||||
"Description[sv]": "Visa underrättelser och jobb",
|
||||
"Description[ta]": "அறிவிப்புகள் மற்றும் பணிகளை காட்டும்",
|
||||
"Description[tr]": "Görevleri ve bildirimleri görüntüleyin",
|
||||
"Description[uk]": "Показ сповіщень і завдань",
|
||||
"Description[vi]": "Hiển thị thông báo và công việc",
|
||||
"Description[x-test]": "xxDisplay notifications and jobsxx",
|
||||
"Description[zh_CN]": "显示通知和任务",
|
||||
"Description[zh_TW]": "顯示通知與工作",
|
||||
"EnabledByDefault": true,
|
||||
"FormFactors": [
|
||||
"desktop"
|
||||
],
|
||||
"Icon": "preferences-desktop-notification-bell",
|
||||
"Id": "org.kde.plasma.notifications",
|
||||
"License": "GPL-2.0+",
|
||||
"Name": "Notifications",
|
||||
"Name[ar]": "إخطارات",
|
||||
"Name[ast]": "Avisos",
|
||||
"Name[az]": "Bildirilşlər",
|
||||
"Name[be@latin]": "Infarmavańnie",
|
||||
"Name[be]": "Апавяшчэнні",
|
||||
"Name[bg]": "Известия",
|
||||
"Name[bn]": "বিজ্ঞপ্তি",
|
||||
"Name[bn_IN]": "সূচনাবার্তা",
|
||||
"Name[br]": "Kemenn",
|
||||
"Name[bs]": "Obavještenja",
|
||||
"Name[ca@valencia]": "Notificacions",
|
||||
"Name[ca]": "Notificacions",
|
||||
"Name[cs]": "Upozornění",
|
||||
"Name[csb]": "Dôwanié wiédzë",
|
||||
"Name[da]": "Bekendtgørelser",
|
||||
"Name[de]": "Benachrichtigungen",
|
||||
"Name[el]": "Ειδοποιήσεις",
|
||||
"Name[en_GB]": "Notifications",
|
||||
"Name[eo]": "Atentigoj",
|
||||
"Name[es]": "Notificaciones",
|
||||
"Name[et]": "Märguanded",
|
||||
"Name[eu]": "Jakinarazpenak",
|
||||
"Name[fa]": "اخطارها",
|
||||
"Name[fi]": "Ilmoitukset",
|
||||
"Name[fr]": "Notifications",
|
||||
"Name[fy]": "Notifikaasjes",
|
||||
"Name[ga]": "Fógairt",
|
||||
"Name[gl]": "Notificacións",
|
||||
"Name[gu]": "નોંધણીઓ",
|
||||
"Name[he]": "התראות",
|
||||
"Name[hi]": "सूचनाएँ",
|
||||
"Name[hne]": "सूचना मन ल",
|
||||
"Name[hr]": "Obavijesti",
|
||||
"Name[hsb]": "Zdźělenki",
|
||||
"Name[hu]": "Rendszerüzenetek",
|
||||
"Name[ia]": "Notificationes",
|
||||
"Name[id]": "Notifikasi",
|
||||
"Name[is]": "Tilkynningar",
|
||||
"Name[it]": "Notifiche",
|
||||
"Name[ja]": "通知",
|
||||
"Name[ka]": "გაფრთხილებები",
|
||||
"Name[kk]": "Құлақтандыру",
|
||||
"Name[km]": "សេចក្តីជូនដំណឹង",
|
||||
"Name[kn]": "ಸೂಚನೆಗಳು",
|
||||
"Name[ko]": "알림",
|
||||
"Name[ku]": "Agahdarî",
|
||||
"Name[lt]": "Pranešimai",
|
||||
"Name[lv]": "Paziņojumi",
|
||||
"Name[mai]": "सूचनासभ",
|
||||
"Name[mk]": "Известувања",
|
||||
"Name[ml]": "അറിയിപ്പുകള്",
|
||||
"Name[mr]": "सूचना",
|
||||
"Name[ms]": "Pemberitahuan",
|
||||
"Name[nb]": "Varslinger",
|
||||
"Name[nds]": "Bescheden",
|
||||
"Name[ne]": "सूचना",
|
||||
"Name[nl]": "Meldingen",
|
||||
"Name[nn]": "Varslingar",
|
||||
"Name[oc]": "Notificacions",
|
||||
"Name[or]": "ବିଜ୍ଞପ୍ତି",
|
||||
"Name[pa]": "ਨੋਟੀਫਿਕੇਸ਼ਨ",
|
||||
"Name[pl]": "Powiadomienia",
|
||||
"Name[pt]": "Notificações",
|
||||
"Name[pt_BR]": "Notificações",
|
||||
"Name[ro]": "Notificări",
|
||||
"Name[ru]": "Уведомления",
|
||||
"Name[sa]": "अधिसूचना",
|
||||
"Name[se]": "Dieđáhusat",
|
||||
"Name[si]": "දැනුම් දීම්",
|
||||
"Name[sk]": "Upozornenia",
|
||||
"Name[sl]": "Obvestila",
|
||||
"Name[sr@ijekavian]": "обавјештења",
|
||||
"Name[sr@ijekavianlatin]": "obavještenja",
|
||||
"Name[sr@latin]": "obaveštenja",
|
||||
"Name[sr]": "обавештења",
|
||||
"Name[sv]": "Underrättelser",
|
||||
"Name[ta]": "அறிவிப்புகள்",
|
||||
"Name[te]": "నోటీసులు",
|
||||
"Name[tg]": "Огоҳиҳо",
|
||||
"Name[th]": "การแจ้งให้ทราบต่าง ๆ",
|
||||
"Name[tr]": "Bildirimler",
|
||||
"Name[ug]": "ئۇقتۇرۇشلار",
|
||||
"Name[uk]": "Сповіщення",
|
||||
"Name[uz@cyrillic]": "Хабарномалар",
|
||||
"Name[uz]": "Xabarnomalar",
|
||||
"Name[vi]": "Thông báo",
|
||||
"Name[wa]": "Notifiaedjes",
|
||||
"Name[x-test]": "xxNotificationsxx",
|
||||
"Name[zh_CN]": "通知",
|
||||
"Name[zh_TW]": "通知",
|
||||
"Website": "https://kde.org/plasma-desktop/"
|
||||
},
|
||||
"X-KDE-ParentApp": "org.kde.plasmashell",
|
||||
"X-Plasma-API-Minimum-Version": "6.0",
|
||||
"X-Plasma-NotificationAreaCategory": "SystemServices",
|
||||
"X-Plasma-Provides": [
|
||||
"org.kde.plasma.notifications"
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue