Commit 1c4de1d4 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Improvements and cleanups all over the place

- Improve compact reprsentation
- Keep popup open during interaction (context menu, drag)
- Improve "ago" label (e.g. show "Last Sunday")
- Improve group visuals (still pretty much WIP)
- Hide popups when interacted (opened file, triggered file action, etc)
  Closing when having invoked an action isn't implemented yet but is probably neccessary
- Make autohide job popup setting work
- Set critical AlwaysOnTop (requires KWin patch)
- Improve error handling in ThumbnailStrip
  Show file icon if thumbnail generation failed instead of a blank space
- Add heuristic for popup placement and make custom setting work
- Add deviceName and displayApplicationName (for KDE Connect)
  So it can show from which device and which app this notification originally came from
- Improved service discovery (and defaultComponent handling)
  Quite a few notifications, e.g. DrKonqi crashes are in plasma_workspace.notifyrc
  When we get an event like this, show the original app instead
- Make blacklist for popup and history work
- Add "lastRead" property for "unread" handling vs "expired" (latter likely to be dropped)
- Add sortMode so history is strictly sorted by date (it's grouped after all)
- Begin work on do not disturb UI in plasmoid
parent eab83389
......@@ -109,6 +109,7 @@ void FileMenu::open(int x, int y)
QMenu *menu = new QMenu();
menu->setAttribute(Qt::WA_DeleteOnClose, true);
connect(menu, &QMenu::triggered, this, &FileMenu::actionTriggered);
connect(menu, &QMenu::aboutToHide, this, [this] {
m_visible = false;
......
......@@ -22,6 +22,7 @@
#include <QPointer>
#include <QUrl>
class QAction;
class QQuickItem;
class FileMenu : public QObject
......@@ -47,6 +48,8 @@ public:
Q_INVOKABLE void open(int x, int y);
signals:
void actionTriggered(QAction *action);
void urlChanged();
void visualParentChanged();
void visibleChanged();
......@@ -54,6 +57,6 @@ signals:
private:
QUrl m_url;
QPointer<QQuickItem> m_visualParent;
bool m_visible;
bool m_visible = false;
};
......@@ -21,10 +21,12 @@
#include "notificationapplet.h"
#include <QClipboard>
#include <QDrag>
#include <QMimeData>
#include <QQuickItem>
#include <QQuickWindow>
#include <QScreen>
#include <QStyleHints>
#include "filemenu.h"
......@@ -103,6 +105,24 @@ void NotificationApplet::doDrag(QQuickItem *item, const QUrl &url, const QPixmap
emit dragActiveChanged();
}
void NotificationApplet::setSelectionClipboardText(const QString &text)
{
// FIXME KDeclarative Clipboard item uses QClipboard::Mode for "mode"
// which is an enum inaccessible from QML
QGuiApplication::clipboard()->setText(text, QClipboard::Selection);
}
bool NotificationApplet::isPrimaryScreen(const QRect &rect) const
{
QScreen *screen = QGuiApplication::primaryScreen();
if (!screen) {
return false;
}
// HACK
return rect == screen->geometry();
}
K_EXPORT_PLASMA_APPLET_WITH_JSON(icon, NotificationApplet, "metadata.json")
#include "notificationapplet.moc"
......@@ -24,6 +24,8 @@
#include <Plasma/Applet>
class QQuickItem;
class QString;
class QRect;
class NotificationApplet : public Plasma::Applet
{
......@@ -42,6 +44,10 @@ public:
Q_INVOKABLE bool isDrag(int oldX, int oldY, int newX, int newY) const;
Q_INVOKABLE void startDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap);
Q_INVOKABLE void setSelectionClipboardText(const QString &text);
Q_INVOKABLE bool isPrimaryScreen(const QRect &rect) const;
signals:
void dragActiveChanged();
......
......@@ -42,11 +42,13 @@ MouseArea {
Layout.maximumHeight: inPanel ? units.iconSizeHints.panel : -1*/
property int activeCount: 0
property int expiredCount: 0
property int unreadCount: 0
property int jobsCount: 0
property int jobsPercentage: 0
property bool inhibited: false
property bool wasExpanded: false
onPressed: wasExpanded = plasmoid.expanded
onClicked: plasmoid.expanded = !wasExpanded
......@@ -63,9 +65,23 @@ MouseArea {
width: units.roundToIconSize(Math.min(parent.width, parent.height))
height: width
svg: notificationSvg
visible: opacity > 0
elementId: "notification-disabled"
Behavior on scale {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
Item {
id: jobProgressItem
anchors {
......@@ -91,18 +107,11 @@ MouseArea {
}
}
PlasmaComponents.Label {
TightLabel {
id: countLabel
anchors {
fill: parent
margins: units.devicePixelRatio * 2
}
height: undefined
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pointSize: 100
fontSizeMode: Text.Fit
minimumPointSize: 7
anchors.centerIn: parent
font.pointSize: -1
font.pixelSize: Math.round(parent.height * 0.6)
}
PlasmaComponents.BusyIndicator {
......@@ -111,6 +120,52 @@ MouseArea {
visible: false
running: visible
}
PlasmaCore.SvgItem {
id: notificationActiveItem
anchors.fill: parent
svg: notificationSvg
elementId: "notification-active"
opacity: 0
scale: 2
visible: opacity > 0
Behavior on scale {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
}
}
PlasmaCore.IconItem {
id: dndIcon
anchors.fill: parent
source: "face-quiet"
opacity: 0
scale: 2
visible: opacity > 0
Behavior on scale {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
}
states: [
......@@ -135,20 +190,34 @@ MouseArea {
},
State { // active notification
when: compactRoot.activeCount > 0
PropertyChanges {
target: notificationActiveItem
scale: 1
opacity: 1
}
},
State { // do not disturb
when: compactRoot.inhibited
PropertyChanges {
target: dndIcon
scale: 1
opacity: 1
}
PropertyChanges {
target: notificationIcon
elementId: "notification-active";
scale: 0
opacity: 0
}
},
State { // unread notifications
when: compactRoot.expiredCount > 0
when: compactRoot.unreadCount > 0
PropertyChanges {
target: notificationIcon
elementId: "notification-empty"
}
PropertyChanges {
target: countLabel
text: compactRoot.expiredCount
text: compactRoot.unreadCount
}
}
]
......
......@@ -32,8 +32,7 @@ PlasmaComponents.ContextMenu {
property QtObject __clipboard: KQCAddons.Clipboard { }
// FIXME this was supposed to be able to deal with a Text {} item, too
// but it it isn't used for that not, so all of this typeof mess can be removed
// can be a Text or TextEdit
property Item target
property string link
......
......@@ -36,26 +36,31 @@ ColumnLayout {
RowLayout {
Layout.fillWidth: true
MouseArea {
width: dndRow.width + units.gridUnit
height: dndRow.height + units.gridUnit / 2
onClicked: dndCheck.checked = !dndCheck.checked
RowLayout {
id: dndRow
anchors.centerIn: parent
PlasmaComponents.CheckBox {
id: dndCheck
tooltip: i18n("Do not disturb")
}
RowLayout {
id: dndRow
spacing: units.smallSpacing
PlasmaCore.IconItem {
// FIXME proper icon
source: "face-quiet"
Layout.preferredWidth: units.iconSizes.smallMedium
Layout.preferredHeight: units.iconSizes.smallMedium
}
PlasmaCore.IconItem {
// FIXME proper icon
source: "face-quiet"
width: height
Layout.fillHeight: true
}
PlasmaComponents.Label {
text: i18n("Do not disturb:")
}
PlasmaComponents.ComboBox {
Layout.preferredWidth: units.gridUnit * 10 // FIXME
model: [
"Disabled",
"While Okular is active",
"For 1 hour",
"Until logging out",
"Until this evening",
"Until Monday morning"
]
}
}
......@@ -72,14 +77,21 @@ ColumnLayout {
}
RowLayout {
Layout.fillWidth: true
spacing: units.smallSpacing
Layout.leftMargin: units.iconSizes.smallMedium + units.smallSpacing
PlasmaCore.IconItem {
Layout.preferredWidth: units.iconSizes.smallMedium
Layout.preferredHeight: units.iconSizes.smallMedium
source: "okular"
}
PlasmaExtras.DescriptiveLabel {
Layout.fillWidth: true
// TODO
text: "Do not disturb mode is configured to automatically enable between 22:00 and 06:00."
text: i18n("Okular has enabled do not disturb mode: Giving a presentation")
textFormat: Text.PlainText
wrapMode: Text.WordWrap
font: theme.smallestFont
maximumLineCount: 3 // just in case
}
}
......@@ -104,7 +116,7 @@ ColumnLayout {
level: 3
opacity: 0.6
visible: list.count === 0
text: i18n("No missed notifications.")
text: i18n("No unread notifications.")
}
PlasmaExtras.ScrollArea {
......@@ -137,6 +149,14 @@ ColumnLayout {
applicationIconSource: model.applicationIconName
time: model.updated || model.created
configurable: model.configurable
// FIXME close group
onCloseClicked: historyModel.close(historyModel.index(index, 0))
//onDismissClicked: model.dismissed = false
// FIXME don't configure event but just app
onConfigureClicked: historyModel.configure(historyModel.index(index, 0))
}
}
......@@ -148,8 +168,11 @@ ColumnLayout {
notificationType: model.type
headerVisible: !model.isInGroup
applicationName: model.applicationName
applicatonIconSource: model.applicationIconName
deviceName: model.deviceName || ""
time: model.updated || model.created
......@@ -158,11 +181,12 @@ ColumnLayout {
// FIXME make the dismiss button a undismiss button
dismissable: model.type === NotificationManager.Notifications.JobType
&& model.jobState !== NotificationManager.Notifications.JobStateStopped
&& model.dismissed
closable: model.type === NotificationManager.Notifications.NotificationType
|| model.jobState === NotificationManager.Notifications.JobStateStopped
summary: model.summary
body: model.body || "" // TODO
body: model.body || ""
icon: model.image || model.iconName
urls: model.urls || []
......
......@@ -44,11 +44,14 @@ ColumnLayout {
// TOOD make an alias on visible if we're not doing an animation
property bool showDetails
readonly property alias menuOpen: otherFileActionsMenu.visible
signal suspendJobClicked
signal resumeJobClicked
signal killJobClicked
signal openUrl(string url)
signal fileActionInvoked
spacing: 0
......@@ -150,8 +153,9 @@ ColumnLayout {
Notifications.FileMenu {
id: otherFileActionsMenu
url: jobDoneActions.url
url: jobDoneActions.url || ""
visualParent: otherFileActionsButton
onActionTriggered: jobItem.fileActionInvoked()
}
}
......
......@@ -23,14 +23,16 @@ import QtQuick.Layouts 1.1
import org.kde.plasma.core 2.0 as PlasmaCore
// TODO grouping and what not
ColumnLayout {
id: delegate
property alias notificationType: notificationItem.notificationType
property alias headerVisible: notificationItem.headerVisible
property alias applicationName: notificationItem.applicationName
property alias applicatonIconSource: notificationItem.applicationIconSource
property alias deviceName: notificationItem.deviceName
property alias time: notificationItem.time
......
......@@ -37,6 +37,7 @@ RowLayout {
property alias applicationIconSource: applicationIconItem.source
property string applicationName
property string deviceName
property string configureActionLabel
......@@ -53,16 +54,10 @@ RowLayout {
signal dismissClicked
signal closeClicked
onTimeChanged: updateAgoText()
function updateAgoText() {
if (time && !isNaN(time.getTime())) {
var now = new Date();
var deltaMinutes = Math.floor((now.getTime() - time.getTime()) / 1000 / 60);
if (deltaMinutes > 0) {
ageLabel.agoText = i18ncp("Received minutes ago, keep short", "%1 min ago", "%1 min ago", deltaMinutes);
return;
}
}
ageLabel.agoText = "";
ageLabel.agoText = ageLabel.generateAgoText();
}
spacing: units.smallSpacing
......@@ -93,87 +88,118 @@ RowLayout {
Layout.fillWidth: true
textFormat: Text.PlainText
elide: Text.ElideRight
text: notificationHeading.applicationName// + (notificationHeading.deviceName ? " · " + notificationHeading.deviceName : "")
text: notificationHeading.applicationName + (notificationHeading.deviceName ? " · " + notificationHeading.deviceName : "")
}
PlasmaExtras.DescriptiveLabel {
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 !== ""
text: remainingText() || agoText
text: generateRemainingText() || agoText
function generateAgoText() {
if (!time || isNaN(time.getTime())) {
return "";
}
function remainingText() {
var now = new Date();
var deltaMinutes = Math.floor((now.getTime() - time.getTime()) / 1000 / 60);
if (deltaMinutes < 1) {
return "";
}
// Received less than an hour ago, show relative minutes
if (deltaMinutes < 60) {
return i18ncp("Notification was added minutes ago, keep short", "%1 min ago", "%1 min ago", deltaMinutes);
}
// Received less than a day ago, show time, 23 hours so the time isn't ambiguous between today and yesterday
if (deltaMinutes < 60 * 23) {
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.JobStateStopped) {
return;
return "";
}
var details = notificationHeading.jobDetails;
if (!details || !details.speed) {
return;
return "";
}
var remaining = details.totalBytes - details.processedBytes;
if (remaining <= 0) {
return;
return "";
}
var eta = remaining / details.speed;
if (!eta) {
return;
return "";
}
if (eta < 60 * 60) { // 1 hr
if (eta < 60) { // 1 minute
return i18ncp("seconds remaining, keep short",
"%1s remaining", "%1s remaining", Math.round(eta));
}
if (eta < 60 * 60) {// 1 hour
return i18ncp("minutes remaining, keep short",
"%1min remaining", "%1min remaining",
Math.round(eta / 60));
} else {
return i18ncp("seconds remaining, keep short",
"%1s remaining", "%1s remaining", Math.round(eta));
}
}
if (eta < 60 * 60 * 5) { // 5 hours max, if it takes even longer there's no real point in shoing that
return i18ncp("hours remaining, keep short",
"%1h remaining", "%1h remaining",
Math.round(eta / 60 / 60));
}
function updateAgoText() {
return "";
}
PlasmaCore.ToolTipArea {
anchors.fill: parent
active: ageLabel.agoText !== ""
subText: notificationHeading.time ? notificationHeading.time.toLocaleString(Qt.locale(), Locale.LongFormat) : ""
}
}
Item {
width: headerButtonsRow.width
RowLayout {
id: headerButtonsRow
spacing: units.smallSpacing * 2
anchors.verticalCenter: parent.verticalCenter
// These aren't ToolButtons so they can be perfectly aligned
// FIXME fix layout overlap
HeaderButton {
id: configureButton
tooltip: notificationHeading.configureActionLabel || i18n("Configure")
iconSource: "configure"
visible: false
onClicked: notificationHeading.configureClicked()
}
RowLayout {
id: headerButtonsRow
spacing: units.smallSpacing * 2
Layout.leftMargin: units.smallSpacing
// These aren't ToolButtons so they can be perfectly aligned
// FIXME fix layout overlap
HeaderButton {
id: configureButton
tooltip: notificationHeading.configureActionLabel || i18n("Configure")
iconSource: "configure"
visible: false
onClicked: notificationHeading.configureClicked()
}
HeaderButton {
id: dismissButton
tooltip: i18n("Hide")
// FIXME proper icon, perhaps from widgets/configuration-icon
iconSource: "file-zoom-out"
visible: false
onClicked: notificationHeading.dismissClicked()
}
HeaderButton {
id: dismissButton
tooltip: i18n("Hide")
// FIXME proper icon, perhaps from widgets/configuration-icon
iconSource: "file-zoom-out"
visible: false
onClicked: notificationHeading.dismissClicked()
}
HeaderButton {
id: closeButton
tooltip: i18n("Close")
iconSource: "window-close"
visible: false
onClicked: notificationHeading.closeClicked()
}
HeaderButton {