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

Compare commits

...

5 Commits

Author SHA1 Message Date
Felix Ernst
bd449328bc Merge branch 'improve_filelight_installation_ux' into 'master'
Improve Filelight installation UX

See merge request system/dolphin!783
2024-06-25 15:30:55 +00:00
Felix Ernst
242b1b73a6 Show installation progress in the status bar
This commit reuses the progress reporting of the status bar for
the Filelight installation progress.
2024-06-25 15:30:54 +00:00
Felix Ernst
982cb7e58c Show installation progress on install button
Also reopen the menu if it is currently open while the installation
finishes.
2024-06-25 15:30:54 +00:00
Felix Ernst
fb651f0cbc Make DolphinPackageInstaller a KJob
This means developers can use a familiar pattern here.
2024-06-25 15:30:54 +00:00
Felix Ernst
d21984ba5b 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
2024-06-25 15:30:54 +00:00
11 changed files with 323 additions and 9 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
dolphinpackageinstaller.cpp
dolphinplacesmodelsingleton.cpp
dolphinrecenttabsmenu.cpp
dolphintabpage.cpp
@ -333,6 +335,7 @@ target_sources(dolphinstatic PRIVATE
dolphincontextmenu.h
dolphinnavigatorswidgetaction.h
dolphintabbar.h
dolphinpackageinstaller.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,110 @@
/*
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 "dolphinpackageinstaller.h"
#include <KLocalizedString>
#if HAVE_PACKAGEKIT
#include <PackageKit/Daemon>
#else
#include <QDesktopServices>
#endif
#include <QTimer>
#include <QtAssert>
DolphinPackageInstaller::DolphinPackageInstaller(const QString &packageName,
const QUrl &fallBackInstallationPageUrl,
std::function<bool()> isPackageInstalledCheck,
QObject *parent)
: KJob(parent)
, m_packageName{packageName}
, m_fallBackInstallationPageUrl{fallBackInstallationPageUrl}
, m_isPackageInstalledCheck{isPackageInstalledCheck}
{
}
void DolphinPackageInstaller::start()
{
if (m_isPackageInstalledCheck()) {
emitResult();
return;
}
#if HAVE_PACKAGEKIT
PackageKit::Daemon::setHints(PackageKit::Daemon::hints() + QStringList{QStringLiteral("interactive=true")});
const PackageKit::Transaction *resolveTransaction = PackageKit::Daemon::resolve(m_packageName);
connect(resolveTransaction, &PackageKit::Transaction::errorCode, this, &DolphinPackageInstaller::slotInstallationFailed);
connect(resolveTransaction, &PackageKit::Transaction::finished, this, [this]() { // Will be disconnected if we find a package.
slotInstallationFailed(PackageKit::Transaction::ErrorPackageNotFound,
i18nc("@info:shell about system packages", "Could not find package %1.", m_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.
const PackageKit::Transaction *installTransaction = PackageKit::Daemon::installPackage(packageId);
connectTransactionToJobProgress(*installTransaction);
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 a result 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 a result once.
if (status == PackageKit::Transaction::ExitSuccess) {
emitResult();
} 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
QDesktopServices::openUrl(m_fallBackInstallationPageUrl);
auto waitForSuccess = new QTimer(this);
connect(waitForSuccess, &QTimer::timeout, this, [this]() {
if (m_isPackageInstalledCheck()) {
emitResult();
}
});
waitForSuccess->start(3000);
#endif
}
#if HAVE_PACKAGEKIT
void DolphinPackageInstaller::connectTransactionToJobProgress(const PackageKit::Transaction &transaction)
{
connect(&transaction, &PackageKit::Transaction::speedChanged, this, [this, &transaction]() {
emitSpeed(transaction.speed());
});
connect(&transaction, &PackageKit::Transaction::percentageChanged, this, [this, &transaction]() {
setPercent(transaction.percentage());
});
}
void DolphinPackageInstaller::slotInstallationFailed(PackageKit::Transaction::Error error, const QString &details)
{
setErrorString(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)));
setError(error);
emitResult();
}
#endif

View File

@ -0,0 +1,85 @@
/*
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 dolphinpackageinstaller_H
#define dolphinpackageinstaller_H
#include "config-dolphin.h"
#if HAVE_PACKAGEKIT
#include <PackageKit/Transaction>
#endif
#include <KJob>
#include <QUrl>
/**
* @brief A KJob providing simple means to install a package.
*/
class DolphinPackageInstaller : public KJob
{
public:
/**
* @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.filelight.desktop". The user is then expected to install the package themselves and
* KJob::result() will be emitted when it is detected that the installation finished successfully.
* @param isPackageInstalledCheck A function that can be regularly checked to determine if the installation was already successful.
*/
explicit DolphinPackageInstaller(const QString &packageName,
const QUrl &fallBackInstallationPageUrl,
std::function<bool()> isPackageInstalledCheck,
QObject *parent = nullptr);
/**
* @see KJob::start().
* Make sure to connect to the KJob::result() signal and show the KJob::errorString() to users there before calling this.
*/
void start() override;
/** @see KJob::errorString(). */
inline QString errorString() const override
{
return m_errorString;
};
private:
/** @see KJob::errorString(). */
inline void setErrorString(const QString &errorString)
{
m_errorString = errorString;
};
#if HAVE_PACKAGEKIT
/**
* Makes sure progress signals of @p transaction are forwarded to KJob's progress signals.
*/
void connectTransactionToJobProgress(const PackageKit::Transaction &transaction);
private Q_SLOTS:
/** Creates a nice user-facing error message from its parameters and then finishes this job with an @p error. */
void slotInstallationFailed(PackageKit::Transaction::Error error, const QString &details);
#endif
private:
/** The name of the package that is supposed to be installed. */
const QString m_packageName;
/** @see DolphinPackageInstaller::DolphinPackageInstaller(). */
const QUrl m_fallBackInstallationPageUrl;
/** @see DolphinPackageInstaller::DolphinPackageInstaller(). */
const std::function<bool()> m_isPackageInstalledCheck;
/** @see KJob::errorString(). */
QString m_errorString;
};
#endif // dolphinpackageinstaller_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,13 @@ DolphinStatusBar::DolphinStatusBar(QWidget *parent)
// Initialize space information
m_spaceInfo = new StatusBarSpaceInfo(contentsContainer);
connect(m_spaceInfo, &StatusBarSpaceInfo::showMessage, this, &DolphinStatusBar::showMessage);
connect(m_spaceInfo,
&StatusBarSpaceInfo::showInstallationProgress,
this,
[this](const QString &currentlyRunningTaskTitle, int installationProgressPercent) {
showProgress(currentlyRunningTaskTitle, installationProgressPercent, CancelLoading::Disallowed);
});
// 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 "dolphinpackageinstaller.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,87 @@ 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->setMinimumWidth(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"));
QDesktopServices::openUrl(QUrl("https://apps.kde.org/filelight"));
#else
QDesktopServices::openUrl(QUrl("appstream://org.kde.filelight.desktop"));
auto packageInstaller = new DolphinPackageInstaller(
FILELIGHT_PACKAGE_NAME,
QUrl("appstream://org.kde.filelight.desktop"),
[]() {
return KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"));
},
this);
connect(packageInstaller, &KJob::result, this, [this](KJob *job) {
Q_EMIT showInstallationProgress(QString(), 100); // Hides the progress information in the status bar.
if (job->error()) {
Q_EMIT showMessage(job->errorString(), KMessageWidget::Error);
} else {
Q_EMIT showMessage(xi18nc("@info", "<application>Filelight</application> installed successfully."), KMessageWidget::Positive);
if (m_textInfoButton->menu()->isVisible()) {
m_textInfoButton->menu()->hide();
updateMenu();
m_textInfoButton->menu()->show();
}
}
});
const auto installationTaskText{i18nc("@info:status", "Installing Filelight…")};
Q_EMIT showInstallationProgress(installationTaskText, -1);
connect(packageInstaller, &KJob::percentChanged, this, [this, installationTaskText](KJob */* job */, long unsigned int percent) {
if (percent < 100) { // Ignore some weird reported values.
Q_EMIT showInstallationProgress(installationTaskText, percent);
}
});
packageInstaller->start();
#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,14 @@ 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);
void showInstallationProgress(const QString &currentlyRunningTaskTitle, int installationProgressPercent);
protected:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
@ -55,6 +66,7 @@ private:
KCapacityBar *m_capacityBar;
QToolButton *m_textInfoButton;
QMenu *m_buttonMenu;
QWidgetAction *m_installFilelightWidgetAction;
QUrl m_url;
bool m_ready;
bool m_shown;