1
0
mirror of https://invent.kde.org/system/dolphin synced 2024-06-30 15:36:30 +00:00

Improve Filelight installation UX

Before this commit pressing the free space button when Filelight
is not installed would show a singular action called "Install
Filelight to View Disk Usage Statistics…". Pressing this button
would open the store page for Filelight. This is an okay user
experience, but we can do better.

This commit makes it so pressing the free space button when
Filelight is not installed shows an attractive UI that makes clear
that freeing up disk space can be accomplished nicely by installing
Filelight. The "Install Filelight" button on this UI is connected
to PackageKit directly, so we do not need to show a separate store
like Discover and instead trigger ann installation right then and
there.
Installation failure or success is then shown within Dolphin as a
passive notification.

CCBUG: 477739
This commit is contained in:
Felix Ernst 2024-05-29 13:23:08 +02:00 committed by Felix Ernst
parent 97abdaaa90
commit d21984ba5b
11 changed files with 269 additions and 8 deletions

View File

@ -103,7 +103,7 @@ find_package(Phonon4Qt6 CONFIG REQUIRED)
find_package(PackageKitQt6)
set_package_properties(PackageKitQt6
PROPERTIES DESCRIPTION "Software Manager integration"
TYPE OPTIONAL
TYPE RECOMMENDED
PURPOSE "Used in the service menu installer"
)
if(PackageKitQt6_FOUND)

View File

@ -1,5 +1,6 @@
include(ECMAddAppIcon)
set(FILELIGHT_PACKAGE_NAME "filelight")
configure_file(config-dolphin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-dolphin.h)
add_definitions(
@ -273,6 +274,7 @@ target_sources(dolphinstatic PRIVATE
dolphincontextmenu.cpp
dolphinnavigatorswidgetaction.cpp
dolphintabbar.cpp
dolphinpackagemanager.cpp
dolphinplacesmodelsingleton.cpp
dolphinrecenttabsmenu.cpp
dolphintabpage.cpp
@ -333,6 +335,7 @@ target_sources(dolphinstatic PRIVATE
dolphincontextmenu.h
dolphinnavigatorswidgetaction.h
dolphintabbar.h
dolphinpackagemanager.h
dolphinplacesmodelsingleton.h
dolphinrecenttabsmenu.h
dolphintabpage.h
@ -458,6 +461,13 @@ if (HAVE_PLASMA_ACTIVITIES)
)
endif()
if(HAVE_PACKAGEKIT)
target_link_libraries(
dolphinstatic
PK::packagekitqt6
)
endif()
if (HAVE_KUSERFEEDBACK)
target_link_libraries(
dolphinstatic

View File

@ -1,6 +1,10 @@
/** Set whether to build Dolphin with support for these technologies or not. */
#cmakedefine01 HAVE_BALOO
#cmakedefine01 HAVE_PLASMA_ACTIVITIES
#cmakedefine01 HAVE_KUSERFEEDBACK
#cmakedefine01 HAVE_PACKAGEKIT
#cmakedefine01 HAVE_TERMINAL
#cmakedefine01 HAVE_X11
/** The name of the KDE Filelight package. */
#cmakedefine FILELIGHT_PACKAGE_NAME "@FILELIGHT_PACKAGE_NAME@"

View File

@ -0,0 +1,98 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "dolphinpackagemanager.h"
#include <KLocalizedString>
#if HAVE_PACKAGEKIT
#include <PackageKit/Daemon>
#else
#include <QDesktopServices>
#endif
#include <QTimer>
#include <QtAssert>
DolphinPackageManager::DolphinPackageManager(QObject *parent)
: QObject(parent)
{
}
void DolphinPackageManager::install(const QString &packageName, const QUrl &fallBackInstallationPageUrl, std::function<bool()> isPackageInstalledCheck)
{
Q_ASSERT_X(m_packageName.isEmpty(), "install", "Reusing a DolphinPackageManager object has not been implemented and can lead to conflicts.");
if (isPackageInstalledCheck()) {
Q_EMIT success();
return;
}
m_packageName = packageName;
#if HAVE_PACKAGEKIT
Q_UNUSED(fallBackInstallationPageUrl)
PackageKit::Daemon::setHints(PackageKit::Daemon::hints() + QStringList{QStringLiteral("interactive=true")});
const PackageKit::Transaction *resolveTransaction = PackageKit::Daemon::resolve(packageName);
connect(resolveTransaction, &PackageKit::Transaction::errorCode, this, &DolphinPackageManager::slotInstallationFailed);
connect(resolveTransaction, &PackageKit::Transaction::finished, this, [this, packageName]() { // Will be disconnected if we find a package.
slotInstallationFailed(PackageKit::Transaction::ErrorPackageNotFound,
i18nc("@info:shell about system packages", "Could not find package %1.", packageName));
});
connect(resolveTransaction,
&PackageKit::Transaction::package,
this,
[this, resolveTransaction](PackageKit::Transaction::Info /* info */, const QString &packageId) {
disconnect(resolveTransaction, nullptr, this, nullptr); // We only care about the first package.
PackageKit::Transaction *installTransaction = PackageKit::Daemon::installPackage(packageId);
connect(installTransaction,
&PackageKit::Transaction::errorCode,
this,
[installTransaction, this](PackageKit::Transaction::Error error, const QString &details) {
disconnect(installTransaction, nullptr, this, nullptr); // We only want to emit failure() or success() once.
slotInstallationFailed(error, details);
});
connect(installTransaction,
&PackageKit::Transaction::finished,
this,
[installTransaction, this](const PackageKit::Transaction::Exit status, uint /* runtime */) {
disconnect(installTransaction, nullptr, this, nullptr); // We only want to emit failure() or success() once.
if (status == PackageKit::Transaction::ExitSuccess) {
Q_EMIT success();
deleteLater();
} else {
slotInstallationFailed(PackageKit::Transaction::ErrorUnknown,
i18nc("@info %1 is error code",
"Installation exited without reporting success. (%1)",
QMetaEnum::fromType<PackageKit::Transaction::Exit>().valueToKey(status)));
}
});
});
#else
Q_UNUSED(packageName)
QDesktopServices::openUrl(fallBackInstallationPageUrl);
auto waitForSuccess = new QTimer(this);
connect(waitForSuccess, &QTimer::timeout, this, [isPackageInstalledCheck, this, waitForSuccess]() {
if (isPackageInstalledCheck()) {
Q_EMIT success();
deleteLater();
}
});
waitForSuccess->start(3000);
#endif
}
#if HAVE_PACKAGEKIT
void DolphinPackageManager::slotInstallationFailed(PackageKit::Transaction::Error error, const QString &details)
{
Q_EMIT failure(xi18nc("@info:shell %1 is package name, %2 is error message, %3 is error e.g. 'ErrorNoNetwork'",
"Installing <application>%1</application> failed: %2 (%3)<nl/>Please try installing <application>%1</application> manually instead.",
m_packageName,
details,
QMetaEnum::fromType<PackageKit::Transaction::Error>().valueToKey(error)));
deleteLater();
}
#endif

View File

@ -0,0 +1,71 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef DOLPHINPACKAGEMANAGER_H
#define DOLPHINPACKAGEMANAGER_H
#include "config-dolphin.h"
#if HAVE_PACKAGEKIT
#include <PackageKit/Transaction>
#endif
#include <QObject>
/**
* @brief A class providing a simple API to install packages.
*/
class DolphinPackageManager : public QObject
{
Q_OBJECT
public:
explicit DolphinPackageManager(QObject *parent = nullptr);
/**
* @brief Installs a system package.
*
* @param packageName A name that can be resolved to a package.
* @param fallBackInstallationPageUrl This url will be opened if Dolphin was installed without the PackageKit library. A good choice for this parameter
* is an appstream url that will be opened in a software store (like Discover) e.g. "appstream://org.kde.kio.admin".
* The user is then expected to install the package themselves and success() will be emitted once that finished.
* @param isPackageInstalledCheck A function that can be regularly checked to determine if the installation was already successful.
*
* Calling this method will lead to either emitting the success() or the failure() signal. Once either is emitted this object will delete itself.
* This generally happens asynchronously with rare exceptions, so connect to the signals before calling this method.
*/
void install(const QString &packageName, const QUrl &fallBackInstallationPageUrl, std::function<bool()> isPackageInstalledCheck);
Q_SIGNALS:
/**
* Emitted when this object assumes that the package was successfully installed.
* This object will delete itself after emitting this signal.
*
* @note This does not always mean that the isPackageInstalledCheck of DolphinPackageManager::install() also passes.
*/
void success();
/**
* Emitted when the installation finally failed.
* This object will delete itself after emitting this signal.
*
* Be sure to show the @p userFacingErrorMessage to the user or they won't know what happened and can't write useful bug reports.
*/
void failure(const QString &userFacingErrorMessage);
#if HAVE_PACKAGEKIT
private Q_SLOTS:
/** Creates a nice user-facing error message from its parameters and then emits failure() and deletes this object. */
void slotInstallationFailed(PackageKit::Transaction::Error error, const QString &details);
#endif
private:
/** The name of the package that is currently being installed. */
QString m_packageName;
};
#endif // DOLPHINPACKAGEMANAGER_H

View File

@ -168,6 +168,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent)
});
connect(m_statusBar, &DolphinStatusBar::stopPressed, this, &DolphinViewContainer::stopDirectoryLoading);
connect(m_statusBar, &DolphinStatusBar::zoomLevelChanged, this, &DolphinViewContainer::slotStatusBarZoomLevelChanged);
connect(m_statusBar, &DolphinStatusBar::showMessage, this, &DolphinViewContainer::showMessage);
m_statusBarTimer = new QTimer(this);
m_statusBarTimer->setSingleShot(true);

View File

@ -49,8 +49,8 @@ QPair<QString, Qt::SortOrder> sortOrderForUrl(QUrl &url);
/**
* TODO: Move this somewhere global to all KDE apps, not just Dolphin
*/
const int VERTICAL_SPACER_HEIGHT = 12;
const int LAYOUT_SPACING_SMALL = 2;
constexpr int VERTICAL_SPACER_HEIGHT = 12;
constexpr int LAYOUT_SPACING_SMALL = 2;
}
enum Animated { WithAnimation, WithoutAnimation };

View File

@ -70,6 +70,7 @@ DolphinStatusBar::DolphinStatusBar(QWidget *parent)
// Initialize space information
m_spaceInfo = new StatusBarSpaceInfo(contentsContainer);
connect(m_spaceInfo, &StatusBarSpaceInfo::showMessage, this, &DolphinStatusBar::showMessage);
// Initialize progress information
m_stopButton = new QToolButton(contentsContainer);

View File

@ -9,6 +9,8 @@
#include "animatedheightwidget.h"
#include <KMessageWidget>
#include <QTime>
class QUrl;
@ -96,6 +98,11 @@ Q_SIGNALS:
void zoomLevelChanged(int zoomLevel);
/**
* Requests for @p message with the given @p messageType to be shown to the user in a non-modal way.
*/
void showMessage(const QString &message, KMessageWidget::MessageType messageType);
protected:
void contextMenuEvent(QContextMenuEvent *event) override;
void paintEvent(QPaintEvent *paintEvent) override;

View File

@ -6,6 +6,9 @@
#include "statusbarspaceinfo.h"
#include "config-dolphin.h"
#include "dolphinpackagemanager.h"
#include "global.h"
#include "spaceinfoobserver.h"
#include <KCapacityBar>
@ -16,14 +19,19 @@
#include <QDesktopServices>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QMouseEvent>
#include <QPushButton>
#include <QStorageInfo>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidgetAction>
StatusBarSpaceInfo::StatusBarSpaceInfo(QWidget *parent)
: QWidget(parent)
, m_observer(nullptr)
, m_installFilelightWidgetAction{nullptr}
{
m_capacityBar = new KCapacityBar(KCapacityBar::DrawTextInline, this);
m_textInfoButton = new QToolButton(this);
@ -116,17 +124,68 @@ void StatusBarSpaceInfo::updateMenu()
const KService::Ptr kdiskfree = KService::serviceByDesktopName(QStringLiteral("org.kde.kdf"));
if (!filelight && !kdiskfree) {
QAction *installFilelight =
m_buttonMenu->addAction(QIcon::fromTheme(QStringLiteral("filelight")), i18n("Install Filelight to View Disk Usage Statistics…"));
// Show an UI to install a tool to free up disk space because this is what a user pressing on a "free space" button would want.
if (!m_installFilelightWidgetAction) {
auto containerWidget = new QWidget{this};
containerWidget->setContentsMargins(Dolphin::VERTICAL_SPACER_HEIGHT,
Dolphin::VERTICAL_SPACER_HEIGHT,
Dolphin::VERTICAL_SPACER_HEIGHT,
Dolphin::VERTICAL_SPACER_HEIGHT);
auto vLayout = new QVBoxLayout(containerWidget);
connect(installFilelight, &QAction::triggered, this, [] {
auto installFilelightTitle = new QLabel(i18nc("@title", "Free Up Disk Space"), containerWidget);
installFilelightTitle->setAlignment(Qt::AlignCenter);
installFilelightTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
QFont titleFont{installFilelightTitle->font()};
titleFont.setPointSize(titleFont.pointSize() + 2);
installFilelightTitle->setFont(titleFont);
vLayout->addWidget(installFilelightTitle);
vLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT);
auto installFilelightBody =
// i18n: The new line ("<nl/>") tag is only there to format this text visually pleasing, i.e. to avoid having one very long line.
new QLabel(xi18nc("@title", "<para>Install additional software to view disk usage statistics<nl/>and identify big files and folders.</para>"),
containerWidget);
installFilelightBody->setAlignment(Qt::AlignCenter);
installFilelightBody->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
vLayout->addWidget(installFilelightBody);
vLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT);
auto installFilelightButton =
new QPushButton(QIcon::fromTheme(QStringLiteral("filelight")), i18nc("@action:button", "Install Filelight"), containerWidget);
installFilelightButton->setFixedWidth(std::max(installFilelightButton->sizeHint().width(), installFilelightTitle->sizeHint().width()));
auto buttonLayout = new QHBoxLayout{containerWidget};
buttonLayout->addWidget(installFilelightButton, 0, Qt::AlignHCenter);
vLayout->addLayout(buttonLayout);
// Make sure one Tab press focuses the button after the UI opened.
m_buttonMenu->setFocusProxy(installFilelightButton);
containerWidget->setFocusPolicy(Qt::TabFocus);
containerWidget->setFocusProxy(installFilelightButton);
installFilelightButton->setAccessibleDescription(installFilelightBody->text());
connect(installFilelightButton, &QAbstractButton::clicked, this, [this] {
#ifdef Q_OS_WIN
QDesktopServices::openUrl(QUrl("https://apps.kde.org/filelight"));
#else
QDesktopServices::openUrl(QUrl("appstream://org.kde.filelight.desktop"));
auto packageInstaller = new DolphinPackageManager(this);
connect(packageInstaller, &DolphinPackageManager::failure, this, [this, packageInstaller](const QString &userFacingErrorMessage) {
Q_EMIT showMessage(userFacingErrorMessage, KMessageWidget::Error);
});
connect(packageInstaller, &DolphinPackageManager::success, this, [this, packageInstaller]() {
Q_EMIT showMessage(xi18nc("@info", "<application>Filelight</application> installed successfully."), KMessageWidget::Positive);
});
packageInstaller->install(FILELIGHT_PACKAGE_NAME, QUrl("appstream://org.kde.filelight.desktop"), [](){ return KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"));
});
#endif
});
});
m_installFilelightWidgetAction = new QWidgetAction{this};
m_installFilelightWidgetAction->setDefaultWidget(containerWidget); // transfers ownership of containerWidget
}
m_buttonMenu->addAction(m_installFilelightWidgetAction);
return;
}

View File

@ -6,6 +6,8 @@
#ifndef STATUSBARSPACEINFO_H
#define STATUSBARSPACEINFO_H
#include <KMessageWidget>
#include <QUrl>
#include <QWidget>
@ -14,6 +16,7 @@ class QShowEvent;
class QMenu;
class QMouseEvent;
class QToolButton;
class QWidgetAction;
class KCapacityBar;
@ -40,6 +43,12 @@ public:
void update();
Q_SIGNALS:
/**
* Requests for @p message with the given @p messageType to be shown to the user in a non-modal way.
*/
void showMessage(const QString &message, KMessageWidget::MessageType messageType);
protected:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
@ -55,6 +64,7 @@ private:
KCapacityBar *m_capacityBar;
QToolButton *m_textInfoButton;
QMenu *m_buttonMenu;
QWidgetAction *m_installFilelightWidgetAction;
QUrl m_url;
bool m_ready;
bool m_shown;