Commit 9bb6ce7e authored by Harald Sitter's avatar Harald Sitter 🏳️‍🌈
Browse files

add on-demand installation support for translations

parent db97ca83
......@@ -154,6 +154,16 @@ if(${AppStreamQt_FOUND})
set(HAVE_APPSTREAMQT true)
endif()
find_package(PackageKitQt5)
set_package_properties(PackageKitQt5
PROPERTIES DESCRIPTION "Software Manager integration"
TYPE OPTIONAL
PURPOSE "Used to install additional language packages on demand"
)
if(PackageKitQt5_FOUND)
set(HAVE_PACKAGEKIT TRUE)
endif()
# Clipboard applet
ecm_find_qmlmodule(org.kde.prison 1.0)
......
......@@ -10,3 +10,5 @@
#define PLASMA_RELATIVE_DATA_INSTALL_DIR "@PLASMA_RELATIVE_DATA_INSTALL_DIR@"
#define WORKSPACE_VERSION_STRING "${PROJECT_VERSION}"
#cmakedefine HAVE_PACKAGEKIT "${HAVE_PACKAGEKIT}"
......@@ -17,6 +17,20 @@ kcmutils_generate_module_data(
kconfig_add_kcfg_files(kcm_translations_PART_SRCS translationssettingsbase.kcfgc GENERATE_MOC)
ecm_qt_declare_logging_category(
kcm_translations_PART_SRCS
HEADER debug.h
IDENTIFIER KCM_TRANSLATIONS
CATEGORY_NAME org.kde.kcm_translations
DESCRIPTION "Translations KCM"
EXPORT kcm_translations
)
ecm_qt_install_logging_categories(
EXPORT kcm_translations
DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}"
)
add_library(kcm_translations MODULE ${kcm_translations_PART_SRCS})
target_link_libraries(kcm_translations
......@@ -25,6 +39,10 @@ target_link_libraries(kcm_translations
KF5::QuickAddons
)
if(HAVE_PACKAGEKIT)
target_link_libraries(kcm_translations PK::packagekitqt5)
endif()
kcoreaddons_desktop_to_json(kcm_translations "kcm_translations.desktop")
########### install files ###############
......
/*
* Copyright (C) 2015 Marco Martin <mart@kde.org>
* Copyright (C) 2018 Eike Hein <hein@kde.org>
* Copyright (C) 2021 Harald Sitter <sitter@kde.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
......@@ -197,13 +198,54 @@ ScrollViewKCM {
onMoveRequested: kcm.selectedTranslationsModel.move(oldIndex, newIndex)
}
QtControls.BusyIndicator {
visible: model.IsInstalling
running: visible
Layout.alignment: Qt.AlignVCenter
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: implicitWidth
QtControls.ToolTip {
text: xi18nc('@info:tooltip/rich',
'Installing missing packages to complete this translation.')
}
}
Kirigami.Icon {
visible: model.IsIncomplete
Layout.alignment: Qt.AlignVCenter
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: implicitWidth
source: "data-warning"
color: Kirigami.Theme.negativeTextColor
MouseArea {
id: area
anchors.fill: parent
hoverEnabled: true
}
QtControls.ToolTip {
visible: area.containsMouse
text: xi18nc('@info:tooltip/rich',
`Not all translations for this language are installed.
Use the <interface>Install Missing Packages</interface> button to download
and install all missing packages.`)
}
}
Kirigami.Icon {
visible: model.IsMissing
Layout.alignment: Qt.AlignVCenter
width: Kirigami.Units.iconSizes.smallMedium
height: width
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: implicitWidth
source: "error"
color: Kirigami.Theme.negativeTextColor
......@@ -223,6 +265,12 @@ ScrollViewKCM {
}
actions: [
Kirigami.Action {
visible: model.IsIncomplete
iconName: "install"
tooltip: i18nc("@info:tooltip", "Install Missing Packages")
onTriggered: kcm.selectedTranslationsModel.completeLanguage(index)
},
Kirigami.Action {
enabled: !model.IsMissing && index > 0
visible: languagesList.count > 1
......
/*
* Copyright (C) 2014 John Layt <john@layt.net>
* Copyright (C) 2018 Eike Hein <hein@kde.org>
* Copyright (C) 2021 Harald Sitter <sitter@kde.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
......@@ -21,11 +22,132 @@
#include "translationsmodel.h"
#include <KLocalizedString>
#include <KOSRelease>
#include <QCollator>
#include <QDebug>
#include <QLocale>
#include <QMetaEnum>
#include <QMetaObject>
#include <QProcess>
#include "config-workspace.h"
#include "debug.h"
#ifdef HAVE_PACKAGEKIT
#include <PackageKit/Daemon>
class LanguageCompleter : public QObject
{
Q_OBJECT
public:
explicit LanguageCompleter(const QStringList &packages, QObject *parent = nullptr)
: QObject(parent)
, m_packages(packages)
{
}
void start()
{
auto transaction = PackageKit::Daemon::resolve(m_packages, PackageKit::Transaction::FilterNotInstalled | PackageKit::Transaction::FilterArch);
connect(transaction,
&PackageKit::Transaction::package,
this,
[this](PackageKit::Transaction::Info info, const QString &packageID, const QString &summary) {
Q_UNUSED(info);
Q_UNUSED(summary);
m_packageIDs << packageID;
});
connect(transaction, &PackageKit::Transaction::errorCode, this, [](PackageKit::Transaction::Error error, const QString &details) {
qCDebug(KCM_TRANSLATIONS) << "resolve error" << error << details;
});
connect(transaction, &PackageKit::Transaction::finished, this, [this](PackageKit::Transaction::Exit status, uint code) {
qCDebug(KCM_TRANSLATIONS) << "resolve finished" << status << code << m_packageIDs;
if (m_packageIDs.size() != m_packages.size()) {
qCWarning(KCM_TRANSLATIONS) << "Not all missing packages managed to resolve!" << m_packages << m_packageIDs;
}
install();
});
}
Q_SIGNALS:
void complete();
private:
void install()
{
auto transaction = PackageKit::Daemon::installPackages(m_packageIDs);
connect(transaction, &PackageKit::Transaction::errorCode, this, [](PackageKit::Transaction::Error error, const QString &details) {
qCDebug(KCM_TRANSLATIONS) << "install error:" << error << details;
});
connect(transaction, &PackageKit::Transaction::finished, this, [this](PackageKit::Transaction::Exit status, uint code) {
qCDebug(KCM_TRANSLATIONS) << "install finished:" << status << code;
Q_EMIT complete();
});
}
const QStringList m_packages;
QStringList m_packageIDs;
};
#endif
class CompletionCheck : public QObject
{
Q_OBJECT
public:
enum class Result { Error, Incomplete, Complete };
template<typename... Args>
static CompletionCheck *create(Args &&... _args);
~CompletionCheck() override = default;
virtual void start() = 0;
Q_SIGNALS:
void finished(Result result, QStringList missingPackages);
protected:
explicit CompletionCheck(const QString &languageCode, QObject *parent = nullptr)
: QObject(parent)
, m_languageCode(languageCode)
{
}
const QString m_languageCode;
private:
Q_DISABLE_COPY_MOVE(CompletionCheck);
};
class UbuntuCompletionCheck : public CompletionCheck
{
public:
using CompletionCheck::CompletionCheck;
void start() override
{
proc.setProgram("/usr/bin/check-language-support");
proc.setArguments({"--language", m_languageCode.left(m_languageCode.indexOf(QLatin1Char('@')))});
connect(&proc, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this] {
const QString output = QString::fromUtf8(proc.readAllStandardOutput().simplified());
// Whenever we don't get packages back simply pretend the language is complete as we can't
// give any useful information on what's wrong anyway.
Q_EMIT finished(output.isEmpty() ? Result::Complete : Result::Incomplete, output.split(QLatin1Char(' ')));
});
proc.start();
}
private:
QProcess proc;
};
template<typename... Args>
CompletionCheck *CompletionCheck::create(Args &&... _args)
{
KOSRelease os;
if (os.id() == QLatin1String("ubuntu") || os.idLike().contains(QLatin1String("ubuntu"))) {
return new UbuntuCompletionCheck(std::forward<Args>(_args)...);
}
return nullptr;
}
QStringList TranslationsModel::m_languages = QStringList();
QSet<QString> TranslationsModel::m_installedLanguages = QSet<QString>();
......@@ -102,7 +224,7 @@ QString TranslationsModel::languageCodeToName(const QString &languageCode) const
return QLocale(QStringLiteral("pt_PT")).nativeLanguageName();
}
qWarning() << "Language code morphed into another existing language code, please report!" << languageCode << locale.name();
qCWarning(KCM_TRANSLATIONS) << "Language code morphed into another existing language code, please report!" << languageCode << locale.name();
return i18nc("%1 is language name, %2 is language code name", "%1 (%2)", languageName, languageCode);
}
......@@ -124,12 +246,17 @@ QVariant SelectedTranslationsModel::data(const QModelIndex &index, int role) con
return QVariant();
}
const QString code = m_selectedLanguages.at(index.row());
if (role == Qt::DisplayRole) {
return languageCodeToName(m_selectedLanguages.at(index.row()));
return languageCodeToName(code);
} else if (role == LanguageCode) {
return m_selectedLanguages.at(index.row());
return code;
} else if (role == IsMissing) {
return m_missingLanguages.contains(m_selectedLanguages.at(index.row()));
return m_missingLanguages.contains(code);
} else if (role == IsIncomplete) {
return m_incompleteLanguagesWithPackages.contains(code);
} else if (role == IsInstalling) {
return m_installingLanguages.contains(code);
}
return QVariant();
......@@ -155,6 +282,7 @@ void SelectedTranslationsModel::setSelectedLanguages(const QStringList &language
QStringList missingLanguages;
for (const QString &lang : languages) {
reloadCompleteness(lang);
if (!m_installedLanguages.contains(lang)) {
missingLanguages << lang;
}
......@@ -182,6 +310,68 @@ QStringList SelectedTranslationsModel::missingLanguages() const
return m_missingLanguages;
}
void SelectedTranslationsModel::reloadCompleteness(const QString &languageCode)
{
auto *check = CompletionCheck::create(languageCode, this);
if (!check) {
return; // no checking support - default to assume complete
}
connect(check, &CompletionCheck::finished, this, [this, languageCode, check](CompletionCheck::Result result, const QStringList &missingPackages) {
check->deleteLater();
const int index = m_selectedLanguages.indexOf(languageCode);
if (index < 0) { // removed since the check was started
return;
}
switch (result) {
case CompletionCheck::Result::Error:
qCWarning(KCM_TRANSLATIONS) << "Failed to get completion status for" << languageCode;
return;
case CompletionCheck::Result::Incomplete: {
// Cache this, we need to modify the data before marking the change on the model.
const bool changed = !m_incompleteLanguagesWithPackages.contains(languageCode);
m_incompleteLanguagesWithPackages[languageCode] = missingPackages;
if (changed) {
Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0), {IsIncomplete});
}
return;
}
case CompletionCheck::Result::Complete:
if (m_incompleteLanguagesWithPackages.remove(languageCode) > 0) {
Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0), {IsIncomplete});
}
return;
}
});
check->start();
}
void SelectedTranslationsModel::completeLanguage(int index)
{
#ifdef HAVE_PACKAGEKIT
const QString code = m_selectedLanguages.at(index);
auto completer = new LanguageCompleter(m_incompleteLanguagesWithPackages.value(code), this);
connect(completer, &LanguageCompleter::complete, this, [this, code] {
sender()->deleteLater();
const int index = m_selectedLanguages.indexOf(code);
if (index < 0) {
return; // entry was probably removed since the install was started
}
m_installingLanguages.removeAll(code);
reloadCompleteness(code);
Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0), {IsInstalling});
});
m_incompleteLanguagesWithPackages.remove(code);
m_installingLanguages << code;
completer->start();
Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0), {IsIncomplete, IsInstalling});
#else
Q_UNUSED(index);
#endif
}
void SelectedTranslationsModel::move(int from, int to)
{
if (from >= m_selectedLanguages.count() || to >= m_selectedLanguages.count()) {
......@@ -285,3 +475,5 @@ void AvailableTranslationsModel::setSelectedLanguages(const QStringList &languag
endResetModel();
}
#include "translationsmodel.moc"
/*
* Copyright (C) 2014 John Layt <john@layt.net>
* Copyright (C) 2018 Eike Hein <hein@kde.org>
* Copyright (C) 2021 Harald Sitter <sitter@kde.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
......@@ -35,6 +36,8 @@ public:
enum AdditionalRoles {
LanguageCode = Qt::UserRole + 1,
IsMissing,
IsIncomplete,
IsInstalling,
};
Q_ENUM(AdditionalRoles)
......@@ -72,6 +75,7 @@ public:
void setSelectedLanguages(const QStringList &languages);
QStringList missingLanguages() const;
Q_INVOKABLE void completeLanguage(int index);
Q_INVOKABLE void move(int from, int to);
Q_INVOKABLE void remove(const QString &languageCode);
......@@ -81,8 +85,13 @@ Q_SIGNALS:
void missingLanguagesChanged() const;
private:
void reloadCompleteness(const QString &languageCode);
QStringList m_selectedLanguages;
QStringList m_missingLanguages;
QHash<QString, QStringList> m_incompleteLanguagesWithPackages;
QStringList m_installingLanguages;
};
class AvailableTranslationsModel : public TranslationsModel
......
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