Commit e738abba authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇
Browse files

[Notifications] Show which app will open a file when job finishes

For example, show "Open with Gwenview" with its icon after downloading
an image file, and "Open with..." in case there is no associated application.
While at it also give an icon to the "Open Containing Folder" button.

Also, port it to use a `KIO::mimetypeJob` to make it asynchronous
and work on remote locations. To make for a less jarring transition (icon suddenly
appearing, causing text reflow and the dialog to resize), take an initial
guess about the mimetype using the file extension while the job is running.

Furthermore, hide the "Open" actions in case the job fails which likely means
the file doesn't exist anymore or cannot be accessed, and as as result
probably cannot be opened anyway.
parent 1c0c824c
......@@ -2,6 +2,7 @@ add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.notification
set(notificationapplet_SRCS
notificationapplet.cpp
fileinfo.cpp
filemenu.cpp
globalshortcuts.cpp
texteditclickhandler.cpp
......
/*
Copyright (C) 2021 Kai Uwe Broulik <kde@broulik.de>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "fileinfo.h"
#include <QMimeDatabase>
#include <QTimer>
#include <KApplicationTrader>
#include <KIO/MimeTypeFinderJob>
Application::Application() = default;
Application::Application(const KService::Ptr &service)
: m_service(service)
{
}
QString Application::name() const
{
return m_service ? m_service->name() : QString();
}
QString Application::iconName() const
{
return m_service ? m_service->icon() : QString();
}
bool Application::isValid() const
{
return m_service && m_service->isValid();
}
FileInfo::FileInfo(QObject *parent)
: QObject(parent)
{
}
FileInfo::~FileInfo() = default;
QUrl FileInfo::url() const
{
return m_url;
}
void FileInfo::setUrl(const QUrl &url)
{
if (m_url != url) {
m_url = url;
reload();
emit urlChanged(url);
}
}
bool FileInfo::busy() const
{
return m_busy;
}
void FileInfo::setBusy(bool busy)
{
if (m_busy != busy) {
m_busy = busy;
emit busyChanged(busy);
}
}
int FileInfo::error() const
{
return m_error;
}
void FileInfo::setError(int error)
{
if (m_error != error) {
m_error = error;
emit errorChanged(error);
}
}
QString FileInfo::mimeType() const
{
return m_mimeType;
}
QString FileInfo::iconName() const
{
return m_iconName;
}
Application FileInfo::preferredApplication() const
{
return m_preferredApplication;
}
void FileInfo::reload()
{
if (!m_url.isValid()) {
return;
}
if (m_job) {
m_job->kill();
}
setError(0);
// Do a quick guess by file name while we wait for the job to find the mime type
QString guessedMimeType;
// NOTE using QUrl::path() for API that accepts local files is usually wrong
// but here we really only care about the file name and its extension.
const auto type = QMimeDatabase().mimeTypeForFile(m_url.path(), QMimeDatabase::MatchExtension);
if (!type.isDefault()) {
guessedMimeType = type.name();
}
mimeTypeFound(guessedMimeType);
m_job = new KIO::MimeTypeFinderJob(m_url);
m_job->setAuthenticationPromptEnabled(false);
const QUrl url = m_url;
connect(m_job, &KIO::MimeTypeFinderJob::result, this, [this, url] {
setError(m_job->error());
if (m_job->error()) {
qWarning() << "Failed to determine mime type for" << url << m_job->errorString();
} else {
mimeTypeFound(m_job->mimeType());
}
setBusy(false);
});
setBusy(true);
m_job->start();
}
void FileInfo::mimeTypeFound(const QString &mimeType)
{
if (m_mimeType == mimeType) {
return;
}
m_mimeType = mimeType;
KService::Ptr preferredApp;
if (!mimeType.isEmpty()) {
const auto type = QMimeDatabase().mimeTypeForName(mimeType);
m_iconName = type.iconName();
preferredApp = KApplicationTrader::preferredService(mimeType);
} else {
m_iconName.clear();
}
m_preferredApplication = Application(preferredApp);
emit mimeTypeChanged();
}
/*
Copyright (C) 2021 Kai Uwe Broulik <kde@broulik.de>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <QObject>
#include <QString>
#include <QUrl>
#include <KService>
namespace KIO
{
class MimeTypeFinderJob;
}
class Application
{
Q_GADGET
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString iconName READ iconName CONSTANT)
Q_PROPERTY(bool valid READ isValid CONSTANT)
public:
Application();
explicit Application(const KService::Ptr &service);
QString name() const;
QString iconName() const;
bool isValid() const;
private:
KService::Ptr m_service;
};
Q_DECLARE_METATYPE(Application)
class FileInfo : public QObject
{
Q_OBJECT
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
Q_PROPERTY(int error READ error NOTIFY errorChanged)
Q_PROPERTY(QString mimeType READ mimeType NOTIFY mimeTypeChanged)
Q_PROPERTY(QString iconName READ iconName NOTIFY mimeTypeChanged)
Q_PROPERTY(Application preferredApplication READ preferredApplication NOTIFY mimeTypeChanged)
public:
explicit FileInfo(QObject *parent = nullptr);
~FileInfo() override;
QUrl url() const;
void setUrl(const QUrl &url);
Q_SIGNAL void urlChanged(const QUrl &url);
bool busy() const;
Q_SIGNAL void busyChanged(bool busy);
int error() const;
Q_SIGNAL void errorChanged(bool error);
QString mimeType() const;
Q_SIGNAL void mimeTypeChanged();
QString iconName() const;
Q_SIGNAL void iconNameChanged(const QString &iconName);
Application preferredApplication() const;
Q_SIGNAL void preferredApplicationChanged();
private:
void reload();
void mimeTypeFound(const QString &mimeType);
void setBusy(bool busy);
void setError(int error);
QUrl m_url;
QPointer<KIO::MimeTypeFinderJob> m_job;
bool m_busy = false;
int m_error = 0;
QString m_mimeType;
QString m_iconName;
Application m_preferredApplication;
};
......@@ -39,6 +39,7 @@
#include <Plasma/Containment>
#include <PlasmaQuick/Dialog>
#include "fileinfo.h"
#include "filemenu.h"
#include "globalshortcuts.h"
#include "texteditclickhandler.h"
......@@ -50,6 +51,7 @@ NotificationApplet::NotificationApplet(QObject *parent, const QVariantList &data
static bool s_typesRegistered = false;
if (!s_typesRegistered) {
const char uri[] = "org.kde.plasma.private.notifications";
qmlRegisterType<FileInfo>(uri, 2, 0, "FileInfo");
qmlRegisterType<FileMenu>(uri, 2, 0, "FileMenu");
qmlRegisterType<GlobalShortcuts>(uri, 2, 0, "GlobalShortcuts");
qmlRegisterType<TextEditClickHandler>(uri, 2, 0, "TextEditClickHandler");
......@@ -182,16 +184,6 @@ bool NotificationApplet::isPrimaryScreen(const QRect &rect) const
return rect == screen->geometry();
}
QString NotificationApplet::iconNameForUrl(const QUrl &url) const
{
QMimeType mime = QMimeDatabase().mimeTypeForUrl(url);
if (mime.isDefault()) {
return QString();
}
return mime.iconName();
}
void NotificationApplet::forceActivateWindow(QWindow *window)
{
if (window && window->winId()) {
......
......@@ -61,7 +61,6 @@ public:
Q_INVOKABLE bool isPrimaryScreen(const QRect &rect) const;
Q_INVOKABLE QString iconNameForUrl(const QUrl &url) const;
Q_INVOKABLE void forceActivateWindow(QWindow *window);
signals:
......
......@@ -63,7 +63,7 @@ ColumnLayout {
return url;
}
property alias iconContainerItem: jobDragIcon.parent
property alias iconContainerItem: jobDragIconItem.parent
readonly property alias dragging: jobDragArea.dragging
readonly property alias menuOpen: otherFileActionsMenu.visible
......@@ -77,40 +77,75 @@ ColumnLayout {
spacing: 0
Notifications.FileInfo {
id: fileInfo
url: jobItem.totalFiles === 1 && jobItem.url ? jobItem.url : ""
}
// This item is parented to the NotificationItem iconContainer
PlasmaCore.IconItem {
id: jobDragIcon
Item {
id: jobDragIconItem
readonly property bool shown: jobDragIcon.valid
width: parent ? parent.width : 0
height: parent ? parent.height : 0
usesPlasmaTheme: false
visible: valid
active: jobDragArea.containsMouse
source: jobItem.totalFiles === 1 && jobItem.url ? plasmoid.nativeInterface.iconNameForUrl(jobItem.url) : ""
visible: shown
Binding {
target: jobDragIcon.parent
target: jobDragIconItem.parent
property: "visible"
value: true
when: jobDragIcon.valid
when: jobDragIconItem.shown
restoreMode: Binding.RestoreBinding
}
DraggableFileArea {
id: jobDragArea
PlasmaCore.IconItem {
id: jobDragIcon
anchors.fill: parent
usesPlasmaTheme: false
active: jobDragArea.containsMouse
opacity: busyIndicator.running ? 0.6 : 1
source: !fileInfo.error ? fileInfo.iconName : ""
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
DraggableFileArea {
id: jobDragArea
anchors.fill: parent
hoverEnabled: true
dragParent: jobDragIcon
dragUrl: jobItem.url || ""
dragPixmap: jobDragIcon.source
hoverEnabled: true
dragParent: jobDragIcon
dragUrl: jobItem.url || ""
dragPixmap: jobDragIcon.source
onActivated: jobItem.openUrl(jobItem.url)
onContextMenuRequested: {
// avoid menu button glowing if we didn't actually press it
otherFileActionsButton.checked = false;
onActivated: jobItem.openUrl(jobItem.url)
onContextMenuRequested: {
// avoid menu button glowing if we didn't actually press it
otherFileActionsButton.checked = false;
otherFileActionsMenu.visualParent = this;
otherFileActionsMenu.open(x, y);
otherFileActionsMenu.visualParent = this;
otherFileActionsMenu.open(x, 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
}
}
}
......@@ -182,13 +217,14 @@ ColumnLayout {
}
}
Flow { // it's a Flow so it can wrap if too long
Row {
id: jobActionsRow
Layout.fillWidth: true
spacing: PlasmaCore.Units.smallSpacing
// We want the actions to be right-aligned but Flow also reverses
// 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() !== ""
visible: jobItem.url && jobItem.url.toString() !== "" && !fileInfo.error
PlasmaComponents3.Button {
id: otherFileActionsButton
......@@ -219,12 +255,37 @@ ColumnLayout {
PlasmaComponents3.Button {
id: openButton
width: Math.min(implicitWidth, jobItem.width - otherFileActionsButton.width - jobActionsRow.spacing)
height: Math.max(implicitHeight, otherFileActionsButton.implicitHeight)
// would be nice to have the file icon here?
text: jobItem.jobDetails && jobItem.jobDetails.totalFiles === 1
? i18nd("plasma_applet_org.kde.plasma.notifications", "Open")
: i18nd("plasma_applet_org.kde.plasma.notifications", "Open Containing Folder")
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.preferredApplication.valid
PropertyChanges {
target: openButton
text: i18nd("plasma_applet_org.kde.plasma.notifications", "Open with %1", fileInfo.preferredApplication.name)
icon.name: fileInfo.preferredApplication.iconName
}
},
State {
when: !fileInfo.busy
PropertyChanges {
target: openButton
text: i18nd("plasma_applet_org.kde.plasma.notifications", "Open with...");
icon.name: "system-run"
}
}
]
}
}
......
Supports Markdown
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