Commit 4269a3b4 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Address review comments and some more cleanups

* Make dataengines work fully standalone
* Fix panel icon sizing
  There's still an issue with the popup size when resizing the vertical panel smaller so it collapses
* Implement keyboard navigation for the list with focus hacks...
  Delete key closes notifications or groups, Arrow left/right expand/collapse groups, Enter invokes default action, if any
* Fix DND times that are supposed to be hidden showing
  ModelContextMenu doesn't respect "visible" property
* Move "Notifications" header to ListView header so it scrolls away and leaves more room for the notifications in systray popup
* Fix finished jobs in history showing as failed when app is closed
* Don't remember apps that spawned jobs, only for notifications
* Use CriticalNotification window type (patches pending)
parent 1127a19d
......@@ -27,10 +27,7 @@ import org.kde.plasma.components 2.0 as PlasmaComponents
MouseArea {
id: compactRoot
// FIXME figure out a way how to let the compact icon not grow beond iconSizeHints
// but still let it expand eventually for a sidebar
/*readonly property bool inPanel: (plasmoid.location === PlasmaCore.Types.TopEdge
readonly property bool inPanel: (plasmoid.location === PlasmaCore.Types.TopEdge
|| plasmoid.location === PlasmaCore.Types.RightEdge
|| plasmoid.location === PlasmaCore.Types.BottomEdge
|| plasmoid.location === PlasmaCore.Types.LeftEdge)
......@@ -38,8 +35,8 @@ MouseArea {
Layout.minimumWidth: plasmoid.formFactor === PlasmaCore.Types.Horizontal ? height : units.iconSizes.small
Layout.minimumHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? width : (units.iconSizes.small + 2 * theme.mSize(theme.defaultFont).height)
Layout.maximumWidth: -1//inPanel ? units.iconSizeHints.panel : -1
Layout.maximumHeight: inPanel ? units.iconSizeHints.panel : -1*/
Layout.maximumWidth: inPanel ? units.iconSizeHints.panel : -1
Layout.maximumHeight: inPanel ? units.iconSizeHints.panel : -1
property int activeCount: 0
property int unreadCount: 0
......
......@@ -33,21 +33,33 @@ import org.kde.notificationmanager 1.0 as NotificationManager
import "global"
ColumnLayout {
Layout.preferredWidth: units.gridUnit * 18
Layout.preferredHeight: units.gridUnit * 24
ColumnLayout{
// FIXME fix popup size when resizing panel smaller (so it collapses)
//Layout.preferredWidth: units.gridUnit * 18
//Layout.preferredHeight: units.gridUnit * 24
//Layout.minimumWidth: units.gridUnit * 10
//Layout.minimumHeight: units.gridUnit * 15
Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical
spacing: units.smallSpacing
// TODO these should be configurable in the future
readonly property int dndMorningHour: 6
readonly property int dndEveningHour: 20
// HACK forward focus to the list
onActiveFocusChanged: {
if (activeFocus) {
list.forceActiveFocus();
}
}
Connections {
target: plasmoid
onExpandedChanged: {
if (plasmoid.expanded) {
list.positionViewAtBeginning();
list.currentIndex = -1;
}
}
}
......@@ -131,33 +143,39 @@ ColumnLayout {
model.push({date: d, text: i18n("For 4 hours")});
// Until this evening
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"), visible: dndMenu.date.getHours() < dndEveningHour});
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
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"), visible: dndMenu.date.getHours() > dndMorningHour});
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
var d = dndMenu.date;
d.setHours(dndMorningHour);
// wraps around if neccessary
d.setDate(d.getDate() + (7 - d.getDay() + 1));
d.setMinutes(0);
d.setSeconds(0);
// show Friday and Saturday, Sunday is "0" but for that you can use "until tomorrow morning"
model.push({date: d, text: i18n("Until Monday"), visible: dndMenu.date.getDay() >= 5});
if (dndMenu.date.getDay() >= 5) {
d = dndMenu.date;
d.setHours(dndMorningHour);
// wraps around if neccessary
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"
var d = dndMenu.date;
d = dndMenu.date;
// Just set it to one year in the future so we don't need yet another "do not disturb enabled" property
d.setFullYear(d.getFullYear() + 1);
model.push({date: d, text: i18n("Until turned off")});
......@@ -235,24 +253,6 @@ ColumnLayout {
}
}
RowLayout {
Layout.fillWidth: true
PlasmaExtras.Heading {
Layout.fillWidth: true
level: 3
opacity: 0.6
text: list.count === 0 ? i18n("No unread notifications.") : i18n("Notifications")
}
PlasmaComponents.ToolButton {
iconName: "edit-clear-history"
tooltip: i18n("Clear History")
visible: plasmoid.action("clearHistory").visible
onClicked: action_clearHistory()
}
}
// actual notifications
PlasmaExtras.ScrollArea {
Layout.fillWidth: true
......@@ -263,6 +263,74 @@ ColumnLayout {
ListView {
id: list
model: historyModel
currentIndex: -1
Keys.onDeletePressed: {
var idx = historyModel.index(currentIndex, 0);
if (historyModel.data(idx, NotificationManager.Notifications.ClosableRole)) {
historyModel.close(idx);
// TODO would be nice to stay inside the current group when deleting an item
}
}
Keys.onEnterPressed: Keys.onReturnPressed(event)
Keys.onReturnPressed: {
var idx = historyModel.index(currentIndex, 0);
if (historyModel.data(idx, NotificationManager.Notifications.HasDefaultActionRole)) {
historyModel.invokeDefaultAction(idx);
}
}
Keys.onLeftPressed: setGroupExpanded(currentIndex, LayoutMirroring.enabled)
Keys.onRightPressed: setGroupExpanded(currentIndex, !LayoutMirroring.enabled)
function isRowExpanded(row) {
var idx = historyModel.index(row, 0);
return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole);
}
function setGroupExpanded(row, expanded) {
var rowIdx = historyModel.index(row, 0);
var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx);
var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx));
historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole);
// If the current item went away when the group collapsed, scroll to the group heading
if (!persistentRowIdx || !persistentRowIdx.valid) {
if (persistentGroupIdx && persistentGroupIdx.valid) {
list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain);
// When closed via keyboard, also set a sane current index
if (list.currentIndex > -1) {
list.currentIndex = persistentGroupIdx.row;
}
}
}
}
highlightMoveDuration: 0
highlightResizeDuration: 0
// Not using PlasmaComponents.Highlight as this is only for indicating keyboard focus
highlight: PlasmaCore.FrameSvgItem {
imagePath: "widgets/listitem"
prefix: "pressed"
}
header: RowLayout {
width: list.width
PlasmaExtras.Heading {
Layout.fillWidth: true
level: 3
opacity: 0.6
text: list.count === 0 ? i18n("No unread notifications.") : i18n("Notifications")
}
PlasmaComponents.ToolButton {
iconName: "edit-clear-history"
tooltip: i18n("Clear History")
visible: plasmoid.action("clearHistory").visible
onClicked: action_clearHistory()
}
}
add: Transition {
SequentialAnimation {
......@@ -434,16 +502,7 @@ ColumnLayout {
"Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount))
visible: (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded)
&& delegateLoader.ListView.nextSection !== delegateLoader.ListView.section
onClicked: {
// Scroll to the group top if groups are collsped
if (model.isGroupExpanded) {
var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(historyModel.index(model.index, 0)));
model.isGroupExpanded = false;
list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain);
} else {
model.isGroupExpanded = true;
}
}
onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded)
}
PlasmaCore.SvgItem {
......
......@@ -99,7 +99,7 @@ RowLayout {
property string agoText: ""
visible: text !== ""
text: generateRemainingText() || agoText
Layout.rightMargin: 0 // the ToolButton's margins are enough
Layout.rightMargin: -notificationHeading.spacing // the ToolButton's margins are enough
function generateAgoText() {
if (!time || isNaN(time.getTime()) || notificationHeading.jobState === NotificationManager.Notifications.JobStateRunning) {
......@@ -116,8 +116,8 @@ RowLayout {
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) {
// 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, ""));
}
......
......@@ -147,9 +147,11 @@ ColumnLayout {
text: {
if (notificationItem.notificationType === NotificationManager.Notifications.JobType) {
if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) {
return i18nc("Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary);
if (notificationItem.summary) {
return i18nc("Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary);
}
} else if (notificationItem.jobState === NotificationManager.Notifications.JobStateStopped) {
if (notificationItem.error) {
if (notificationItem.jobError) {
if (notificationItem.summary) {
return i18nc("Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary);
} else {
......
......@@ -94,15 +94,8 @@ PlasmaCore.Dialog {
location: PlasmaCore.Types.Floating
type: PlasmaCore.Dialog.Notification
flags: {
var flags = Qt.WindowDoesNotAcceptFocus;
// FIXME this needs support in KWin somehow...
if (urgency === NotificationManager.Notifications.CriticalUrgency) {
flags |= Qt.WindowStaysOnTopHint;
}
return flags;
}
type: urgency === NotificationManager.Notifications.CriticalUrgency ? PlasmaCore.Dialog.CriticalNotification : PlasmaCore.Dialog.Notification
flags: Qt.WindowDoesNotAcceptFocus
visible: false
......@@ -158,7 +151,6 @@ PlasmaCore.Dialog {
bottomMargin: -notificationPopup.margins.bottom
}
width: units.devicePixelRatio * 3
radius: width
color: theme.highlightColor
opacity: timeoutIndicatorAnimation.running ? 0.6 : 0
visible: units.longDuration > 1
......
......@@ -393,7 +393,7 @@ QtObject {
Component.onCompleted: {
// Register apps that were seen spawning a popup so they can be configured later
// Apps with notifyrc can already be configured anyway
if (model.desktopEntry && !model.notifyRcName) {
if (model.type === NotificationManager.Notifications.NotificationType && model.desktopEntry && !model.notifyRcName) {
notificationSettings.registerKnownApplication(model.desktopEntry);
notificationSettings.save();
}
......
......@@ -70,7 +70,10 @@ Item {
}
Plasmoid.switchWidth: units.gridUnit * 14
Plasmoid.switchHeight: units.gridUnit * 10
// 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)
Plasmoid.switchHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? 1 : units.gridUnit * 10
Plasmoid.onExpandedChanged: {
if (!plasmoid.expanded) {
......
......@@ -76,8 +76,8 @@ Plasma::Service* KuiserverEngine::serviceForSource(const QString& source)
void KuiserverEngine::init()
{
m_jobsModel = JobsModel::createJobsModel();
// don't init, applicationjobs engine should just passively listen
//m_jobsModel->init();
// TODO see if this causes any issues when/if other processes are using applicationjobs engine, e.g. Latte Dock
m_jobsModel->init();
connect(m_jobsModel.data(), &Notifications::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
for (int i = first; i <= last; ++i) {
......
......@@ -41,8 +41,17 @@ using namespace NotificationManager;
NotificationsEngine::NotificationsEngine( QObject* parent, const QVariantList& args )
: Plasma::DataEngine( parent, args )
{
init();
}
NotificationsEngine::~NotificationsEngine()
{
}
void NotificationsEngine::init()
{
connect(&Server::self(), &Server::notificationAdded, this, [this](const Notification &notification) {
notificationAdded(notification);
});
......@@ -62,15 +71,8 @@ NotificationsEngine::NotificationsEngine( QObject* parent, const QVariantList& a
removeSource(source);
}
});
}
NotificationsEngine::~NotificationsEngine()
{
}
void NotificationsEngine::init()
{
Server::self().init();
}
void NotificationsEngine::notificationAdded(const Notification &notification)
......
......@@ -5,12 +5,6 @@
<method name="requestView">
<!-- The desktop entry of the application, e.g. "org.kde.dolphin" -->
<arg name="desktopEntry" type="s" direction="in"/>
<!-- The user-visible application name, e.g. "Dolphin".
If not provided, this will be read from the desktopEntry -->
<arg name="appName" type="s" directon="in"/>
<!-- The application icon name, e.g. "system-file-manager".
If not provided, this will be read from the desktopEntry -->
<arg name="appIconName" type="s" direction="in"/>
<!-- 'capabilities' is used as a bit field:
0x0001 means that the user should be able to cancel the job
......@@ -23,7 +17,7 @@
<arg name="trackerPath" type="o" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In4" value="QVariantMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMap"/>
</method>
</interface>
</node>
......@@ -114,7 +114,7 @@ QString JobPrivate::prettyDestUrl() const
return destUrlString;
}
return url.toDisplayString(); // strips password
return url.toDisplayString(QUrl::RemoveUserInfo);
}
void JobPrivate::updateHasDetails()
......
......@@ -185,6 +185,8 @@ QStringList JobsModelPrivate::registeredJobContacts() const
QDBusObjectPath JobsModelPrivate::requestView(const QString &appName, const QString &appIconName, int capabilities)
{
QString desktopEntry;
QVariantMap hints;
QString applicationName = appName;
QString applicationIconName = appIconName;
......@@ -201,18 +203,21 @@ QDBusObjectPath JobsModelPrivate::requestView(const QString &appName, const QStr
applicationIconName = service->icon();
}
return requestView(desktopEntry, applicationName, applicationIconName, capabilities, QVariantMap() /*hints*/);
if (!applicationName.isEmpty()) {
hints.insert(QStringLiteral("application-display-name"), applicationName);
}
if (!applicationIconName.isEmpty()) {
hints.insert(QStringLiteral("application-icon-name"), applicationIconName);
}
return requestView(desktopEntry, capabilities, hints);
}
QDBusObjectPath JobsModelPrivate::requestView(const QString &desktopEntry,
const QString &appName,
const QString &appIconName,
int capabilities,
const QVariantMap &hints)
{
Q_UNUSED(hints); // reserved for future extension)
qCDebug(NOTIFICATIONMANAGER) << "JobView requested by" << desktopEntry << "claiming to be" << appName;
qCDebug(NOTIFICATIONMANAGER) << "JobView requested by" << desktopEntry << "with hints" << hints;
if (!m_highestJobId) {
++m_highestJobId;
......@@ -221,13 +226,26 @@ QDBusObjectPath JobsModelPrivate::requestView(const QString &desktopEntry,
Job *job = new Job(m_highestJobId);
++m_highestJobId;
const QString serviceName = message().service();
QString applicationName = hints.value(QStringLiteral("application-display-name")).toString();
QString applicationIconName = hints.value(QStringLiteral("application-icon-name")).toString();
job->setDesktopEntry(desktopEntry);
job->setApplicationName(appName);
job->setApplicationIconName(appIconName);
KService::Ptr service = KService::serviceByDesktopName(desktopEntry);
if (service) {
if (applicationName.isEmpty()) {
applicationName = service->name();
}
if (applicationIconName.isEmpty()) {
applicationIconName = service->icon();
}
}
job->setApplicationName(applicationName);
job->setApplicationIconName(applicationIconName);
// No application name? Try to figure out the process name using the sender's PID
const QString serviceName = message().service();
if (job->applicationName().isEmpty()) {
qCInfo(NOTIFICATIONMANAGER) << "JobView request from" << serviceName << "didn't contain any identification information, this is an application bug!";
const QString processName = Utils::processNameFromDBusService(connection(), serviceName);
......@@ -390,7 +408,10 @@ void JobsModelPrivate::onServiceUnregistered(const QString &serviceName)
const QList<Job *> jobs = m_jobServices.keys(serviceName);
for (Job *job : jobs) {
// Mark all jobs as failed
// Mark all non-finished jobs as failed
if (job->state() == Notifications::JobStateStopped) {
continue;
}
job->setError(127); // KIO::ERR_SLAVE_DIED
job->setErrorText(i18n("Application closed unexpectedly."));
job->setState(Notifications::JobStateStopped);
......
......@@ -54,8 +54,6 @@ public:
QDBusObjectPath requestView(const QString &appName, const QString &appIconName, int capabilities);
// V2
QDBusObjectPath requestView(const QString &desktopEntry,
const QString &appName,
const QString &appIconName,
int capabilities,
const QVariantMap &hints);
......
......@@ -148,7 +148,7 @@ QImage Notification::Private::decodeNotificationSpecImageHint(const QDBusArgumen
#define SANITY_CHECK(condition) \
if (!(condition)) { \
qWarning() << "Sanity check failed on" << #condition; \
qCWarning(NOTIFICATIONMANAGER) << "Image decoding sanity check failed on" << #condition; \
return QImage(); \
}
......@@ -188,7 +188,7 @@ QImage Notification::Private::decodeNotificationSpecImageHint(const QDBusArgumen
}
}
if (format == QImage::Format_Invalid) {
qWarning() << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")";
qCWarning(NOTIFICATIONMANAGER) << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")";
return QImage();
}
......@@ -197,7 +197,7 @@ QImage Notification::Private::decodeNotificationSpecImageHint(const QDBusArgumen
end = ptr + pixels.length();
for (int y=0; y<height; ++y, ptr += rowStride) {
if (ptr + channels * width > end) {
qWarning() << "Image data is incomplete. y:" << y << "height:" << height;
qCWarning(NOTIFICATIONMANAGER) << "Image data is incomplete. y:" << y << "height:" << height;
break;
}
fcn((QRgb*)image.scanLine(y), ptr, width);
......
......@@ -65,7 +65,7 @@ void NotificationGroupCollapsingProxyModel::setSourceModel(QAbstractItemModel *s
QVariant NotificationGroupCollapsingProxyModel::data(const QModelIndex &index, int role) const
{
switch (role) {
case NotificationManager::Notifications::IsGroupExpandedRole: {
case Notifications::IsGroupExpandedRole: {
if (m_limit > 0) {
// so each item in a group knows whether the group is expanded
const QModelIndex sourceIdx = mapToSource(index);
......@@ -73,7 +73,7 @@ QVariant NotificationGroupCollapsingProxyModel::data(const QModelIndex &index, i
}
return true;
}
case NotificationManager::Notifications::ExpandedGroupChildrenCountRole:
case Notifications::ExpandedGroupChildrenCountRole:
return rowCount(index.parent().isValid() ? index.parent() : index);
}
......
......@@ -258,6 +258,7 @@ public:
ExpiredRole, ///< The notification timed out and closed. Actions on it cannot be invoked anymore.
DismissedRole ///< The notification got temporarily hidden by the user but could still be interacted with.
};
Q_ENUM(Roles)
/**
* The type of model item.
......@@ -478,7 +479,7 @@ public:
Q_INVOKABLE void collapseAllGroups();
QVariant data(const QModelIndex &index, int role/* = Qt::DisplayRole*/) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
......
[Services][spectacle]
# Screenshot notifications are explicit user interactions, always show them
[Applications][org.kde.spectacle]
ShowPopupsInDndMode=true
# Defaults for media players so their track change notifications don't accumulate in the history
......
......@@ -182,7 +182,10 @@ Settings::Settings(const KSharedConfig::Ptr &config, QObject *parent)
this, &Settings::notificationInhibitionApplicationsChanged);
}
Settings::~Settings() = default;
Settings::~Settings()
{
d->config->markAsClean();
}
Settings::NotificationBehaviors Settings::applicationBehavior(const QString &desktopEntry) const
{
......@@ -247,6 +250,8 @@ void Settings::forgetKnownApplication(const QString &desktopEntry)
void Settings::load()
{
d->config->markAsClean();
d->config->reparseConfiguration();
DoNotDisturbSettings::self()->load();
NotificationSettings::self()->load();
JobSettings::self()->load();
......
......@@ -36,14 +36,12 @@ using namespace NotificationManager;
QString Utils::processNameFromDBusService(const QDBusConnection &connection, const QString &serviceName)
{
qDebug() << "gimme service pid for" << serviceName;
QDBusReply<uint> pidReply = connection.interface()->servicePid(serviceName);
if (!pidReply.isValid()) {
return QString();
}
const auto pid = pidReply.value();
qDebug() << "PIDINHIER" << pid;
KSysGuard::Processes procs;
procs.updateOrAddProcess(pid);
......@@ -61,7 +59,6 @@ QModelIndex Utils::mapToModel(const QModelIndex &idx, const QAbstractItemModel *
{
// KModelIndexProxyMapper can only map diferent indices to a single source
// but we have the other way round, a single index that splits into different source models
qDebug() << "map to model" << idx << sourceModel;
QModelIndex resolvedIdx = idx;
while (resolvedIdx.isValid() && resolvedIdx.model() != sourceModel) {
if (auto *proxyModel = qobject_cast<const QAbstractProxyModel *>(resolvedIdx.model())) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment