Very early KDE 6 release.

This commit is contained in:
wackyideas 2024-08-09 03:20:25 +02:00
parent 7cc4ccabbc
commit 686046d4f7
6272 changed files with 140920 additions and 529657 deletions

View file

@ -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
}
}
}
]*/
}

View file

@ -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
}
}

View file

@ -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;
}
}
}

View file

@ -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"
}
}

View file

@ -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)
: ""
}
}
}
}
}

View file

@ -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 !== ""
}
}

View file

@ -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
}
}
]
}

View file

@ -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
}
}
]
}

View file

@ -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
}*/
}
]
}

View file

@ -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()
}
}
}
}

View file

@ -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)
}
}

View file

@ -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);
}
}

View file

@ -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…")
}
}
}
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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;
}
}
}
}

View file

@ -0,0 +1 @@
singleton Globals 1.0 Globals.qml

View file

@ -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)
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 88 KiB

View 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"
]
}