diff --git a/CMakeLists.txt b/CMakeLists.txt index a690883d4..b8bb95911 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,206 +1,212 @@ cmake_minimum_required(VERSION 3.0) project(plasma-workspace) set(PROJECT_VERSION "5.17.80") set(PROJECT_VERSION_MAJOR 5) set(QT_MIN_VERSION "5.12.0") set(KF5_MIN_VERSION "5.62.0") set(INSTALL_SDDM_THEME TRUE) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Widgets Quick QuickWidgets Concurrent Test Network) find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMMarkNonGuiExecutable) include(CMakePackageConfigHelpers) include(WriteBasicConfigVersionFile) include(CheckIncludeFiles) include(FeatureSummary) include(ECMOptionalAddSubdirectory) include(ECMQtDeclareLoggingCategory) include(ECMQueryQmake) include(KDEPackageAppTemplates) include(KDEClangFormat) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Plasma DocTools Runner NotifyConfig Su NewStuff Wallet IdleTime Declarative I18n KCMUtils TextWidgets KDELibs4Support Crash GlobalAccel DBusAddons Wayland CoreAddons People ActivitiesStats) find_package(KDED CONFIG REQUIRED) find_package(KF5NetworkManagerQt ${KF5_MIN_VERSION}) set_package_properties(KF5NetworkManagerQt PROPERTIES DESCRIPTION "Qt wrapper for NetworkManager API" TYPE OPTIONAL PURPOSE "Needed by geolocation data engine." ) find_package(KF5Kirigami2 ${KF5_MIN_VERSION} CONFIG) set_package_properties(KF5Kirigami2 PROPERTIES DESCRIPTION "A QtQuick based components set" TYPE RUNTIME ) +find_package(KF5QuickCharts ${KF5_MIN_VERSION} CONFIG) +set_package_properties(KF5QuickCharts PROPERTIES + DESCRIPTION "Used for rendering charts" + TYPE RUNTIME +) + # WARNING PlasmaQuick provides unversioned CMake config find_package(KUserFeedback) find_package(KF5 REQUIRED COMPONENTS PlasmaQuick) find_package(KF5 REQUIRED COMPONENTS SysGuard) find_package(KF5 REQUIRED COMPONENTS Package) find_package(KF5Baloo) set_package_properties(KF5Baloo PROPERTIES DESCRIPTION "File Searching" TYPE RECOMMENDED PURPOSE "Needed for the File Search runner." ) find_package(KF5TextEditor) find_package(KWinDBusInterface CONFIG REQUIRED) find_package(KF5Screen CONFIG REQUIRED) find_package(KScreenLocker 5.13.80 REQUIRED) find_package(ScreenSaverDBusInterface CONFIG REQUIRED) find_package(KF5Holidays) set_package_properties(KF5Holidays PROPERTIES DESCRIPTION "Holidays provider for Plasma calendar" TYPE OPTIONAL PURPOSE "Needed to for holidays plugin for Plasma Calendar." ) find_package(Phonon4Qt5 4.6.60 REQUIRED NO_MODULE) set_package_properties(Phonon4Qt5 PROPERTIES DESCRIPTION "Qt-based audio library" TYPE REQUIRED) find_package(KF5Activities ${KF5_MIN_VERSION}) set_package_properties(KF5Activities PROPERTIES DESCRIPTION "management of Plasma activities" TYPE OPTIONAL PURPOSE "Needed by activity related plasmoids." ) find_package(ZLIB) set_package_properties(ZLIB PROPERTIES DESCRIPTION "Support for gzip compressed files and data streams" URL "https://www.zlib.net" TYPE REQUIRED ) find_package(X11) set_package_properties(X11 PROPERTIES DESCRIPTION "X11 libraries" URL "https://www.x.org" TYPE OPTIONAL PURPOSE "Required for building the X11 based workspace") if(X11_FOUND) find_package(XCB MODULE REQUIRED COMPONENTS XCB RANDR) set_package_properties(XCB PROPERTIES TYPE REQUIRED) if(NOT X11_SM_FOUND) message(FATAL_ERROR "\nThe X11 Session Management (SM) development package could not be found.\nPlease install libSM.\n") endif(NOT X11_SM_FOUND) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS X11Extras) endif() if(X11_FOUND AND XCB_XCB_FOUND) set(HAVE_X11 1) endif() find_package(AppStreamQt 0.10.6) set_package_properties(AppStreamQt PROPERTIES DESCRIPTION "Access metadata for listing available software" URL "https://www.freedesktop.org/wiki/Distributions/AppStream/" TYPE OPTIONAL ) if(${AppStreamQt_FOUND}) set(HAVE_APPSTREAMQT true) endif() include(ConfigureChecks.cmake) include_directories("${CMAKE_CURRENT_BINARY_DIR}") configure_file(config-workspace.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-workspace.h) configure_file(config-unix.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-unix.h ) configure_file(config-X11.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-X11.h) configure_file(config-appstream.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-appstream.h ) add_subdirectory(login-sessions) plasma_install_package(lookandfeel org.kde.breeze.desktop look-and-feel lookandfeel) if (INSTALL_SDDM_THEME) configure_file(sddm-theme/theme.conf.cmake ${CMAKE_CURRENT_BINARY_DIR}/sddm-theme/theme.conf) # Install the login theme into the SDDM directory # Longer term we need to look at making SDDM load from look and feel somehow.. and allow copying at runtime #NOTE this trailing slash is important to rename the directory install(DIRECTORY sddm-theme/ DESTINATION ${KDE_INSTALL_FULL_DATADIR}/sddm/themes/breeze PATTERN "README.txt" EXCLUDE PATTERN "components" EXCLUDE PATTERN "dummydata" EXCLUDE PATTERN "theme.conf.cmake" EXCLUDE) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/sddm-theme/theme.conf DESTINATION ${KDE_INSTALL_FULL_DATADIR}/sddm/themes/breeze) install(DIRECTORY lookandfeel/contents/components DESTINATION ${KDE_INSTALL_FULL_DATADIR}/sddm/themes/breeze PATTERN "README.txt" EXCLUDE) endif() add_definitions(-DQT_NO_URL_CAST_FROM_STRING) # locate qdbus in the Qt path because not every distro makes a symlink at /usr/bin/qdbus query_qmake(QtBinariesDir QT_INSTALL_BINS) add_subdirectory(doc) add_subdirectory(libkworkspace) add_subdirectory(libdbusmenuqt) add_subdirectory(appmenu) add_subdirectory(libtaskmanager) add_subdirectory(libnotificationmanager) add_subdirectory(libcolorcorrect) add_subdirectory(components) add_subdirectory(plasma-windowed) add_subdirectory(shell) add_subdirectory(freespacenotifier) add_subdirectory(klipper) add_subdirectory(krunner) add_subdirectory(ksmserver) add_subdirectory(logout-greeter) add_subdirectory(ksplash) add_subdirectory(systemmonitor) add_subdirectory(statusnotifierwatcher) add_subdirectory(startkde) add_subdirectory(themes) add_subdirectory(kcms) add_subdirectory(containmentactions) add_subdirectory(runners) add_subdirectory(applets) add_subdirectory(dataengines) add_subdirectory(wallpapers) add_subdirectory(kioslave) add_subdirectory(ktimezoned) add_subdirectory(menu) add_subdirectory(phonon) # This ensures pressing the eject button on a CD drive ejects the disc # It listens to the Solid::OpticalDrive::ejectPressed signal that is only # supported by Solid in the HAL backend and does nothing with UDev if(CMAKE_SYSTEM_NAME MATCHES FreeBSD) add_subdirectory(solidautoeject) endif() ecm_optional_add_subdirectory(xembed-sni-proxy) ecm_optional_add_subdirectory(gmenu-dbusmenu-proxy) add_subdirectory(soliduiserver) if(KF5Holidays_FOUND) add_subdirectory(plasmacalendarintegration) endif() add_subdirectory(templates) install(FILES plasma-workspace.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) # add clang-format target for all our real source files file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h) kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) diff --git a/applets/notifications/package/contents/ui/NotificationHeader.qml b/applets/notifications/package/contents/ui/NotificationHeader.qml index 04f6fbf53..5049d3db9 100644 --- a/applets/notifications/package/contents/ui/NotificationHeader.qml +++ b/applets/notifications/package/contents/ui/NotificationHeader.qml @@ -1,221 +1,252 @@ /* * Copyright 2018-2019 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ import QtQuick 2.8 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.notificationmanager 1.0 as NotificationManager import org.kde.kcoreaddons 1.0 as KCoreAddons +import org.kde.quickcharts 1.0 as Charts + import "global" RowLayout { id: notificationHeading property bool inGroup 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 alias closeButtonTooltip: closeButton.tooltip property alias closable: closeButton.visible property var time property int jobState property QtObject jobDetails + 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: units.smallSpacing Layout.preferredHeight: Math.max(applicationNameLabel.implicitHeight, units.iconSizes.small) Component.onCompleted: updateAgoText() Connections { target: Globals // clock time changed onTimeChanged: notificationHeading.updateAgoText() } PlasmaCore.IconItem { id: applicationIconItem Layout.preferredWidth: units.iconSizes.small Layout.preferredHeight: units.iconSizes.small source: notificationHeading.applicationIconSource usesPlasmaTheme: false visible: valid } PlasmaExtras.DescriptiveLabel { id: applicationNameLabel Layout.fillWidth: true textFormat: Text.PlainText elide: Text.ElideLeft text: notificationHeading.applicationName + (notificationHeading.originName ? " ยท " + notificationHeading.originName : "") } 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: generateRemainingText() || agoText Layout.rightMargin: -notificationHeading.spacing // the ToolButton's margins are enough function generateAgoText() { if (!time || isNaN(time.getTime()) || notificationHeading.jobState === NotificationManager.Notifications.JobStateRunning) { return ""; } 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 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) { 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 shoing 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) : "" } } RowLayout { id: headerButtonsRow spacing: 0 PlasmaComponents.ToolButton { id: configureButton tooltip: notificationHeading.configureActionLabel || i18nd("plasma_applet_org.kde.plasma.notifications", "Configure") iconSource: "configure" visible: false onClicked: notificationHeading.configureClicked() } PlasmaComponents.ToolButton { id: dismissButton tooltip: notificationHeading.dismissed ? i18ndc("plasma_applet_org.kde.plasma.notifications", "Opposite of minimize", "Restore") : i18nd("plasma_applet_org.kde.plasma.notifications", "Minimize") iconSource: notificationHeading.dismissed ? "window-restore" : "window-minimize" visible: false onClicked: notificationHeading.dismissClicked() } PlasmaComponents.ToolButton { id: closeButton tooltip: i18nd("plasma_applet_org.kde.plasma.notifications", "Close") - iconSource: "window-close" visible: false onClicked: notificationHeading.closeClicked() + + PlasmaCore.IconItem { + anchors.fill: parent + anchors.margins: units.smallSpacing + units.devicePixelRatio * 2 + source: "window-close" + roundToIconSize: false + active: closeButton.hovered + + Charts.PieChart { + anchors.fill: parent + anchors.margins: -units.devicePixelRatio + + opacity: (notificationHeading.remainingTime > 0 && notificationHeading.remainingTime < notificationHeading.timeout) ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: units.longDuration } + } + + range { from: 0; to: notificationHeading.timeout; automatic: false } + + valueSources: Charts.SingleValueSource { value: notificationHeading.timeout - notificationHeading.remainingTime } + colorSource: Charts.SingleValueSource { value: "transparent" } + + backgroundColor: theme.highlightColor + + thickness: units.devicePixelRatio * 5 + } + } } } states: [ State { when: notificationHeading.inGroup PropertyChanges { target: applicationIconItem source: "" } PropertyChanges { target: applicationNameLabel visible: false } } ] } diff --git a/applets/notifications/package/contents/ui/NotificationItem.qml b/applets/notifications/package/contents/ui/NotificationItem.qml index f276bea19..ac262b0a3 100644 --- a/applets/notifications/package/contents/ui/NotificationItem.qml +++ b/applets/notifications/package/contents/ui/NotificationItem.qml @@ -1,365 +1,368 @@ /* * Copyright 2018-2019 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ import QtQuick 2.8 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 as KQCAddons import org.kde.notificationmanager 1.0 as NotificationManager ColumnLayout { id: notificationItem property bool hovered: false property int maximumLineCount: 0 property alias bodyCursorShape: bodyLabel.cursorShape property int notificationType property bool inGroup: false 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 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 bool showDetails property alias configureActionLabel: notificationHeading.configureActionLabel property var actionNames: [] property var actionLabels: [] 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 || (thumbnailStripLoader.item && thumbnailStripLoader.item.menuOpen) || (jobLoader.item && jobLoader.item.menuOpen) readonly property bool dragging: thumbnailStripLoader.item && thumbnailStripLoader.item.dragging signal bodyClicked(var mouse) signal closeClicked signal configureClicked signal dismissClicked signal actionInvoked(string actionName) signal openUrl(string url) signal fileActionInvoked signal suspendJobClicked signal resumeJobClicked signal killJobClicked spacing: units.smallSpacing NotificationHeader { id: notificationHeading Layout.fillWidth: true Layout.leftMargin: notificationItem.headingLeftPadding Layout.rightMargin: notificationItem.headingRightPadding inGroup: notificationItem.inGroup notificationType: notificationItem.notificationType jobState: notificationItem.jobState jobDetails: notificationItem.jobDetails onConfigureClicked: notificationItem.configureClicked() onDismissClicked: notificationItem.dismissClicked() onCloseClicked: notificationItem.closeClicked() } RowLayout { id: defaultHeaderContainer Layout.fillWidth: true } // Notification body RowLayout { id: bodyRow Layout.fillWidth: true spacing: units.smallSpacing ColumnLayout { Layout.fillWidth: true spacing: 0 RowLayout { id: summaryRow Layout.fillWidth: true visible: summaryLabel.text !== "" PlasmaExtras.Heading { id: summaryLabel Layout.fillWidth: true Layout.preferredHeight: implicitHeight textFormat: Text.PlainText maximumLineCount: 3 wrapMode: Text.WordWrap elide: Text.ElideRight level: 4 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 !== "" } // inGroup headerItem is reparented here } RowLayout { id: bodyTextRow Layout.fillWidth: true spacing: units.smallSpacing SelectableLabel { id: bodyLabel // FIXME how to assign this via State? target: bodyLabel.Layout doesn't work and just assigning the property doesn't either Layout.alignment: notificationItem.inGroup ? Qt.AlignTop : Qt.AlignVCenter Layout.fillWidth: true Layout.maximumHeight: notificationItem.maximumLineCount > 0 ? (theme.mSize(font).height * notificationItem.maximumLineCount) : -1 text: notificationItem.body // Cannot do text !== "" because RichText adds some HTML tags even when empty visible: notificationItem.body !== "" onClicked: notificationItem.bodyClicked(mouse) onLinkActivated: Qt.openUrlExternally(link) } // inGroup iconContainer is reparented here } } Item { id: iconContainer Layout.preferredWidth: units.iconSizes.large Layout.preferredHeight: units.iconSizes.large visible: iconItem.active || imageItem.active PlasmaCore.IconItem { id: iconItem // don't show two identical icons readonly property bool active: valid && source != notificationItem.applicationIconSource anchors.fill: parent usesPlasmaTheme: false smooth: true source: { var icon = notificationItem.icon; if (typeof icon !== "string") { // displayed by QImageItem below return ""; } // don't show a generic "info" icon since this is a notification already if (icon === "dialog-information") { return ""; } return icon; } visible: active } KQCAddons.QImageItem { id: imageItem readonly property bool active: !null && nativeWidth > 0 anchors.fill: parent smooth: true fillMode: KQCAddons.QImageItem.PreserveAspectFit visible: active image: typeof notificationItem.icon === "object" ? notificationItem.icon : undefined } } } // Job progress reporting Loader { id: jobLoader Layout.fillWidth: true active: notificationItem.notificationType === NotificationManager.Notifications.JobType visible: active sourceComponent: JobItem { jobState: notificationItem.jobState jobError: notificationItem.jobError percentage: notificationItem.percentage suspendable: notificationItem.suspendable killable: notificationItem.killable jobDetails: notificationItem.jobDetails showDetails: notificationItem.showDetails onSuspendJobClicked: notificationItem.suspendJobClicked() onResumeJobClicked: notificationItem.resumeJobClicked() onKillJobClicked: notificationItem.killJobClicked() onOpenUrl: notificationItem.openUrl(url) onFileActionInvoked: notificationItem.fileActionInvoked() hovered: notificationItem.hovered } } RowLayout { Layout.fillWidth: true visible: actionRepeater.count > 0 // Notification actions Flow { // it's a Flow so it can wrap if too long Layout.fillWidth: true spacing: units.smallSpacing layoutDirection: Qt.RightToLeft Repeater { id: actionRepeater model: { var buttons = []; // HACK We want the actions to be right-aligned but Flow also reverses var actionNames = (notificationItem.actionNames || []).reverse(); var actionLabels = (notificationItem.actionLabels || []).reverse(); for (var i = 0; i < actionNames.length; ++i) { buttons.push({ actionName: actionNames[i], label: actionLabels[i] }); } return buttons; } PlasmaComponents.ToolButton { flat: false // why does it spit "cannot assign undefined to string" when a notification becomes expired? text: modelData.label || "" Layout.preferredWidth: minimumWidth onClicked: notificationItem.actionInvoked(modelData.actionName) } } } } // 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 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() } } states: [ State { when: notificationItem.inGroup PropertyChanges { target: notificationHeading parent: summaryRow } PropertyChanges { target: summaryRow visible: true } PropertyChanges { target: summaryLabel visible: true } /*PropertyChanges { target: bodyLabel.Label alignment: Qt.AlignTop }*/ PropertyChanges { target: iconContainer parent: bodyTextRow } } ] } diff --git a/applets/notifications/package/contents/ui/NotificationPopup.qml b/applets/notifications/package/contents/ui/NotificationPopup.qml index 2f6b2a259..114bca963 100644 --- a/applets/notifications/package/contents/ui/NotificationPopup.qml +++ b/applets/notifications/package/contents/ui/NotificationPopup.qml @@ -1,220 +1,194 @@ /* * Copyright 2019 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ import QtQuick 2.8 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as Components import org.kde.notificationmanager 1.0 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 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 alias actionNames: notificationItem.actionNames property alias actionLabels: notificationItem.actionLabels signal configureClicked signal dismissClicked signal closeClicked signal defaultActionInvoked signal actionInvoked(string actionName) signal openUrl(string url) signal fileActionInvoked 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; } location: PlasmaCore.Types.Floating flags: Qt.WindowDoesNotAcceptFocus visible: false // When notification is updated, restart hide timer onTimeChanged: { if (timer.running) { timer.restart(); } } mainItem: Item { width: notificationPopup.popupWidth height: notificationItem.implicitHeight + notificationItem.y DraggableDelegate { id: area width: parent.width height: parent.height hoverEnabled: true draggable: notificationItem.notificationType != NotificationManager.Notifications.JobType onDismissRequested: popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) cursorShape: hasDefaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor acceptedButtons: hasDefaultAction || draggable ? Qt.LeftButton : Qt.NoButton onClicked: { 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: notificationPopup.visible && !area.containsMouse && interval > 0 && !notificationItem.dragging && !notificationItem.menuOpen onTriggered: { if (notificationPopup.dismissTimeout) { notificationPopup.dismissClicked(); } else { notificationPopup.expired(); } } } - Timer { - id: timeoutIndicatorDelayTimer - // only show indicator for the last ten seconds of timeout - readonly property int remainingTimeout: 10000 - interval: Math.max(0, timer.interval - remainingTimeout) - running: interval > 0 && timer.running - } - - Rectangle { - id: timeoutIndicatorRect - anchors { - right: parent.right - rightMargin: -notificationPopup.margins.right - bottom: parent.bottom - bottomMargin: -notificationPopup.margins.bottom - } - width: units.devicePixelRatio * 3 - color: theme.highlightColor - opacity: timeoutIndicatorAnimation.running ? 0.6 : 0 - visible: units.longDuration > 1 - Behavior on opacity { - NumberAnimation { - duration: units.longDuration - } - } - - NumberAnimation { - id: timeoutIndicatorAnimation - target: timeoutIndicatorRect - property: "height" - from: area.height + notificationPopup.margins.top + notificationPopup.margins.bottom - to: 0 - duration: Math.min(timer.interval, timeoutIndicatorDelayTimer.remainingTimeout) - running: timer.running && !timeoutIndicatorDelayTimer.running && units.longDuration > 1 - } + NumberAnimation { + target: notificationItem + property: "remainingTime" + from: timer.interval + to: 0 + duration: timer.interval + running: timer.running && 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 hovered: area.containsMouse maximumLineCount: 8 bodyCursorShape: notificationPopup.hasDefaultAction ? Qt.PointingHandCursor : 0 thumbnailLeftPadding: -notificationPopup.margins.left thumbnailRightPadding: -notificationPopup.margins.right thumbnailTopPadding: -notificationPopup.margins.top thumbnailBottomPadding: -notificationPopup.margins.bottom + timeout: timer.running ? timer.interval : 0 + closable: true onBodyClicked: { if (area.acceptedButtons & mouse.button) { area.clicked(null /*mouse*/); } } onCloseClicked: notificationPopup.closeClicked() onDismissClicked: notificationPopup.dismissClicked() onConfigureClicked: notificationPopup.configureClicked() onActionInvoked: notificationPopup.actionInvoked(actionName) onOpenUrl: notificationPopup.openUrl(url) onFileActionInvoked: notificationPopup.fileActionInvoked() onSuspendJobClicked: notificationPopup.suspendJobClicked() onResumeJobClicked: notificationPopup.resumeJobClicked() onKillJobClicked: notificationPopup.killJobClicked() } } } }