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

Merge branch 'improve_filelight_installation_ux' into 'master'

Improve Filelight installation UX

See merge request system/dolphin!783
This commit is contained in:
Felix Ernst 2024-06-25 15:30:55 +00:00
commit bd449328bc
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;