Commit 59d0458b authored by Eike Hein's avatar Eike Hein

Use the new drag handle in the Language KCM

Summary:
This makes the list items draggable.

To do this, the backend model was split up into three models so
the move() method could actually move a row as the new drag handle
expects. This also makes move and remove actions a little bit
faster since there's no proxy abstractions involved. The use of
Plasma.SortFilterModel with JS callbacks for filtering has been
dropped from the QML code.

The lists now use DelegateRecycler to be speedier.

The list of available languages is now guaranteed to be sorted in
a locale-aware manner and case-insensitively, using QCollator.
This likely works better than whatever PSFM was doing with the
Qt::DisplayRole sort role before.

Also drops a stray unused KConfigGroup member from
TranslationsModel.

Reviewers: mart, davidedmundson

Reviewed By: mart

Subscribers: plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D13194
parent 6e824c93
......@@ -21,7 +21,7 @@
import QtQuick 2.1
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.3 as QtControls
import org.kde.kirigami 2.4 as Kirigami
import org.kde.kirigami 2.5 as Kirigami
import org.kde.plasma.core 2.1 as PlasmaCore
import org.kde.kcm 1.2
......@@ -30,15 +30,45 @@ ScrollViewKCM {
ConfigModule.quickHelp: i18n("Language")
PlasmaCore.SortFilterModel {
id: availableLanguagesModel
Component {
id: addLanguageItemComponent
sourceModel: kcm.translationsModel
Kirigami.BasicListItem {
id: languageItem
filterRole: "IsSelected"
filterCallback: function(source_row, value) { return !value; }
property string languageCode: model.LanguageCode
sortRole: "display"
reserveSpaceForIcon: false
label: model.display
checkable: true
onCheckedChanged: {
if (checked) {
addLanguagesSheet.selectedLanguages.push(index);
// There's no property change notification for pushing to an array
// in a var prop, so we can't bind selectedLanguages.length to
// addLanguagesButton.enabled.
addLanguagesButton.enabled = true;
} else {
addLanguagesSheet.selectedLanguages = addLanguagesSheet.selectedLanguages.filter(function(item) { return item !== index });
// There's no property change notification for pushing to an array
// in a var prop, so we can't bind selectedLanguages.length to
// addLanguagesButton.enabled.
if (!addLanguagesSheet.selectedLanguages.length) {
addLanguagesButton.enabled = false;
}
}
}
data: [Connections {
target: addLanguagesSheet
onSheetOpenChanged: languageItem.checked = false
}]
}
}
Kirigami.OverlaySheet {
......@@ -58,37 +88,16 @@ ScrollViewKCM {
onSheetOpenChanged: selectedLanguages = []
ListView {
implicitWidth: 18 * Kirigami.Units.gridUnit
model: availableLanguagesModel
delegate: Kirigami.BasicListItem {
property string languageCode: model.LanguageCode
id: availableLanguagesList
reserveSpaceForIcon: false
label: model.display
implicitWidth: 18 * Kirigami.Units.gridUnit
checkable: true
onCheckedChanged: {
if (checked) {
addLanguagesSheet.selectedLanguages.push(index);
model: kcm.availableTranslationsModel
// There's no property change notification for pushing to an array
// in a var prop, so we can't bind selectedLanguages.length to
// addLanguagesButton.enabled.
addLanguagesButton.enabled = true;
} else {
addLanguagesSheet.selectedLanguages = addLanguagesSheet.selectedLanguages.filter(function(item) { return item !== index });
delegate: Kirigami.DelegateRecycler {
width: parent.width
// There's no property change notification for pushing to an array
// in a var prop, so we can't bind selectedLanguages.length to
// addLanguagesButton.enabled.
if (!addLanguagesSheet.selectedLanguages.length) {
addLanguagesButton.enabled = false;
}
}
}
sourceComponent: addLanguageItemComponent
}
}
......@@ -103,12 +112,12 @@ ScrollViewKCM {
enabled: false
onClicked: {
var langs = kcm.translationsModel.selectedLanguages.slice();
var langs = kcm.selectedTranslationsModel.selectedLanguages.slice();
addLanguagesSheet.selectedLanguages.sort().forEach(function(index) {
langs.push(availableLanguagesModel.get(index).LanguageCode);
langs.push(kcm.availableTranslationsModel.langCodeAt(index));
});
kcm.translationsModel.selectedLanguages = langs;
kcm.selectedTranslationsModel.selectedLanguages = langs;
addLanguagesSheet.sheetOpen = false;
}
......@@ -128,7 +137,7 @@ ScrollViewKCM {
text: i18nc("@info", "There are no languages available on this system.")
visible: !availableLanguagesModel.count
visible: !availableLanguagesList.count
}
Kirigami.InlineMessage {
......@@ -150,10 +159,10 @@ ScrollViewKCM {
text: i18ncp("@info %2 is the language code",
"The translation files for the language with the code '%2' could not be found. The language will be removed from your configuration. If you want to add it back, please install the localization files for it and add the language again.",
"The translation files for the languages with the codes '%2' could not be found. These languages will be removed from your configuration. If you want to add them back, please install the localization files for it and the languages again.",
kcm.translationsModel.missingLanguages.length,
kcm.translationsModel.missingLanguages.join("', '"))
kcm.selectedTranslationsModel.missingLanguages.length,
kcm.selectedTranslationsModel.missingLanguages.join("', '"))
visible: kcm.translationsModel.missingLanguages.length
visible: kcm.selectedTranslationsModel.missingLanguages.length
}
QtControls.Label {
......@@ -165,28 +174,18 @@ ScrollViewKCM {
}
}
view: ListView {
id: languagesList
model: PlasmaCore.SortFilterModel {
sourceModel: kcm.translationsModel
Component {
id: languagesListItemComponent
filterRole: "IsSelected"
filterCallback: function(source_row, value) { return value; }
sortRole: "SelectedPriority"
}
delegate: Kirigami.SwipeListItem {
Kirigami.SwipeListItem {
id: listItem
width: ListView.view.width
contentItem: RowLayout {
width: implicitWidth
height: Math.max(implicitHeight, Kirigami.Units.iconSizes.smallMedium)
anchors.verticalCenter: parent.verticalCenter
Kirigami.ListItemDragHandle {
listItem: listItem
listView: languagesList
onMoveRequested: kcm.selectedTranslationsModel.move(oldIndex, newIndex)
}
Kirigami.Icon {
visible: model.IsMissing
......@@ -218,36 +217,36 @@ ScrollViewKCM {
enabled: !model.IsMissing && index > 0
iconName: "go-top"
tooltip: i18nc("@info:tooltip", "Promote to default")
onTriggered: kcm.translationsModel.moveSelectedLanguage(index, 0)
},
Kirigami.Action {
enabled: !model.IsMissing && index > 0
iconName: "go-up"
tooltip: i18nc("@info:tooltip", "Move up")
onTriggered: kcm.translationsModel.moveSelectedLanguage(index, index - 1)
},
Kirigami.Action {
enabled: !model.IsMissing && index < (languagesList.count - 1)
iconName: "go-down"
tooltip: i18nc("@info:tooltip", "Move down")
onTriggered: kcm.translationsModel.moveSelectedLanguage(index, index + 1)
onTriggered: kcm.selectedTranslationsModel.move(index, 0)
},
Kirigami.Action {
enabled: !model.IsMissing
iconName: "list-remove"
tooltip: i18nc("@info:tooltip", "Remove")
onTriggered: kcm.translationsModel.removeSelectedLanguage(model.LanguageCode)
onTriggered: kcm.selectedTranslationsModel.remove(model.LanguageCode)
}]
}
}
view: ListView {
id: languagesList
model: kcm.selectedTranslationsModel
delegate: Kirigami.DelegateRecycler {
width: languagesList.width
sourceComponent: languagesListItemComponent
}
}
footer: RowLayout {
id: footerLayout
QtControls.Button {
Layout.alignment: Qt.AlignRight
enabled: availableLanguagesModel.count
enabled: availableLanguagesList.count
text: i18nc("@action:button", "Add languages...")
......
......@@ -33,6 +33,8 @@ K_PLUGIN_FACTORY_WITH_JSON(TranslationsFactory, "kcm_translations.json", registe
Translations::Translations(QObject *parent, const QVariantList &args)
: KQuickAddons::ConfigModule(parent, args)
, m_translationsModel(new TranslationsModel(this))
, m_selectedTranslationsModel(new SelectedTranslationsModel(this))
, m_availableTranslationsModel(new AvailableTranslationsModel(this))
, m_everSaved(false)
{
KAboutData *about = new KAboutData("kcm_translations",
......@@ -44,10 +46,13 @@ Translations::Translations(QObject *parent, const QVariantList &args)
m_config = KConfigGroup(KSharedConfig::openConfig(configFile), "Translations");
connect(m_translationsModel, &TranslationsModel::selectedLanguagesChanged,
connect(m_selectedTranslationsModel, &SelectedTranslationsModel::selectedLanguagesChanged,
this, &Translations::selectedLanguagesChanged);
connect(m_translationsModel, &TranslationsModel::missingLanguagesChanged,
connect(m_selectedTranslationsModel, &SelectedTranslationsModel::missingLanguagesChanged,
this, &Translations::missingLanguagesChanged);
connect(m_selectedTranslationsModel, &SelectedTranslationsModel::selectedLanguagesChanged,
m_availableTranslationsModel, &AvailableTranslationsModel::setSelectedLanguages);
}
Translations::~Translations()
......@@ -59,6 +64,16 @@ QAbstractItemModel* Translations::translationsModel() const
return m_translationsModel;
}
QAbstractItemModel* Translations::selectedTranslationsModel() const
{
return m_selectedTranslationsModel;
}
QAbstractItemModel* Translations::availableTranslationsModel() const
{
return m_availableTranslationsModel;
}
bool Translations::everSaved() const
{
return m_everSaved;
......@@ -69,7 +84,7 @@ void Translations::load()
m_configuredLanguages = m_config.readEntry(lcLanguage,
QString()).split(':', QString::SkipEmptyParts);
m_translationsModel->setSelectedLanguages(m_configuredLanguages);
m_selectedTranslationsModel->setSelectedLanguages(m_configuredLanguages);
}
void Translations::save()
......@@ -77,9 +92,10 @@ void Translations::save()
m_everSaved = true;
emit everSavedChanged();
m_configuredLanguages = m_translationsModel->selectedLanguages();
m_configuredLanguages = m_selectedTranslationsModel->selectedLanguages();
for (const QString& lang : m_translationsModel->missingLanguages()) {
const auto missingLanguages = m_selectedTranslationsModel->missingLanguages();
for (const QString& lang : missingLanguages) {
m_configuredLanguages.removeOne(lang);
}
......@@ -88,7 +104,7 @@ void Translations::save()
writeExports();
m_translationsModel->setSelectedLanguages(m_configuredLanguages);
m_selectedTranslationsModel->setSelectedLanguages(m_configuredLanguages);
}
void Translations::defaults()
......@@ -109,17 +125,17 @@ void Translations::defaults()
QStringList languages;
languages << lang;
m_translationsModel->setSelectedLanguages(languages);
m_selectedTranslationsModel->setSelectedLanguages(languages);
}
void Translations::selectedLanguagesChanged()
{
setNeedsSave(m_configuredLanguages != m_translationsModel->selectedLanguages());
setNeedsSave(m_configuredLanguages != m_selectedTranslationsModel->selectedLanguages());
}
void Translations::missingLanguagesChanged()
{
if (m_translationsModel->missingLanguages().count()) {
if (m_selectedTranslationsModel->missingLanguages().count()) {
setNeedsSave(true);
}
}
......
......@@ -25,6 +25,8 @@
#include <KConfigGroup>
class AvailableTranslationsModel;
class SelectedTranslationsModel;
class TranslationsModel;
class QAbstractListModel;
......@@ -34,6 +36,8 @@ class Translations : public KQuickAddons::ConfigModule
Q_OBJECT
Q_PROPERTY(QAbstractItemModel* translationsModel READ translationsModel CONSTANT)
Q_PROPERTY(QAbstractItemModel* selectedTranslationsModel READ selectedTranslationsModel CONSTANT)
Q_PROPERTY(QAbstractItemModel* availableTranslationsModel READ availableTranslationsModel CONSTANT)
Q_PROPERTY(bool everSaved READ everSaved NOTIFY everSavedChanged)
public:
......@@ -41,6 +45,8 @@ class Translations : public KQuickAddons::ConfigModule
~Translations() override;
QAbstractItemModel* translationsModel() const;
QAbstractItemModel* selectedTranslationsModel() const;
QAbstractItemModel* availableTranslationsModel() const;
bool everSaved() const;
......@@ -58,6 +64,8 @@ class Translations : public KQuickAddons::ConfigModule
private:
TranslationsModel *m_translationsModel;
SelectedTranslationsModel *m_selectedTranslationsModel;
AvailableTranslationsModel *m_availableTranslationsModel;
KConfigGroup m_config;
QStringList m_configuredLanguages;
......
......@@ -22,15 +22,21 @@
#include <KLocalizedString>
#include <QCollator>
#include <QDebug>
#include <QLocale>
#include <QMetaEnum>
QStringList TranslationsModel::m_languages = QStringList();
QSet<QString> TranslationsModel::m_installedLanguages = QSet<QString>();
TranslationsModel::TranslationsModel(QObject *parent)
: QAbstractListModel(parent)
{
m_installedLanguages = KLocalizedString::availableDomainTranslations("systemsettings");
m_languages = m_installedLanguages.toList();
if (m_installedLanguages.isEmpty()) {
m_installedLanguages = KLocalizedString::availableDomainTranslations("systemsettings");
m_languages = m_installedLanguages.toList();
}
}
TranslationsModel::~TranslationsModel()
......@@ -60,12 +66,8 @@ QVariant TranslationsModel::data(const QModelIndex &index, int role) const
return languageCodeToName(m_languages.at(index.row()));
} else if (role == LanguageCode) {
return m_languages.at(index.row());
} else if (role == IsSelected) {
return m_selectedLanguages.contains(m_languages.at(index.row()));
} else if (role == SelectedPriority) {
return m_selectedLanguages.indexOf(m_languages.at(index.row()));
} else if (role == IsMissing) {
return m_missingLanguages.contains(m_languages.at(index.row()));
return false;
}
return QVariant();
......@@ -80,13 +82,74 @@ int TranslationsModel::rowCount(const QModelIndex &parent) const
return m_languages.count();
}
QStringList TranslationsModel::selectedLanguages() const
QString TranslationsModel::languageCodeToName(const QString& languageCode) const
{
return m_selectedLanguages;
const QLocale locale(languageCode);
const QString &languageName = locale.nativeLanguageName();
if (languageName.isEmpty()) {
return languageCode;
}
if (languageCode.contains(QStringLiteral("@"))) {
return i18nc("%1 is language name, %2 is language code name", "%1 (%2)", languageName, languageCode);
}
if (locale.name() != languageCode && m_installedLanguages.contains(locale.name())) {
// KDE languageCode got translated by QLocale to a locale code we also have on
// the list. Currently this only happens with pt that gets translated to pt_BR.
if (languageCode == QLatin1String("pt")) {
return QLocale(QStringLiteral("pt_PT")).nativeLanguageName();
}
qWarning() << "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);
}
return languageName;
}
SelectedTranslationsModel::SelectedTranslationsModel(QObject *parent)
: TranslationsModel(parent)
{
}
SelectedTranslationsModel::~SelectedTranslationsModel()
{
}
QVariant SelectedTranslationsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_selectedLanguages.count()) {
return QVariant();
}
if (role == Qt::DisplayRole) {
return languageCodeToName(m_selectedLanguages.at(index.row()));
} else if (role == LanguageCode) {
return m_selectedLanguages.at(index.row());
} else if (role == IsMissing) {
return m_missingLanguages.contains(m_selectedLanguages.at(index.row()));
}
return QVariant();
}
void TranslationsModel::setSelectedLanguages(const QStringList &languages)
int SelectedTranslationsModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_selectedLanguages.count();
}
QStringList SelectedTranslationsModel::selectedLanguages() const
{
return m_selectedLanguages;
}
void SelectedTranslationsModel::setSelectedLanguages(const QStringList &languages)
{
if (m_selectedLanguages != languages) {
QStringList missingLanguages;
......@@ -107,20 +170,19 @@ void TranslationsModel::setSelectedLanguages(const QStringList &languages)
beginResetModel();
m_selectedLanguages = languages;
m_languages = (m_installedLanguages | QSet<QString>::fromList(m_selectedLanguages)).toList();
endResetModel();
emit selectedLanguagesChanged();
emit selectedLanguagesChanged(m_selectedLanguages);
}
}
QStringList TranslationsModel::missingLanguages() const
QStringList SelectedTranslationsModel::missingLanguages() const
{
return m_missingLanguages;
}
void TranslationsModel::moveSelectedLanguage(int from, int to)
void SelectedTranslationsModel::move(int from, int to)
{
if (from >= m_selectedLanguages.count() || to >= m_selectedLanguages.count()) {
return;
......@@ -130,59 +192,97 @@ void TranslationsModel::moveSelectedLanguage(int from, int to)
return;
}
m_selectedLanguages.move(from, to);
const int modelTo = to + (to > from ? 1 : 0);
emit selectedLanguagesChanged();
const bool ok = beginMoveRows(QModelIndex(), from, from, QModelIndex(), modelTo);
auto idx = index(m_languages.indexOf(m_selectedLanguages.at(from)), 0);
if (ok) {
m_selectedLanguages.move(from, to);
if (idx.isValid()) {
emit dataChanged(idx, idx, QVector<int>{SelectedPriority});
endMoveRows();
emit selectedLanguagesChanged(m_selectedLanguages);
}
}
idx = index(m_languages.indexOf(m_selectedLanguages.at(to)), 0);
void SelectedTranslationsModel::remove(const QString &languageCode)
{
if (languageCode.isEmpty()) {
return;
}
if (idx.isValid()) {
emit dataChanged(idx, idx, QVector<int>{SelectedPriority});
int index = m_selectedLanguages.indexOf(languageCode);
if (index < 1) {
return;
}
beginRemoveRows(QModelIndex(), index, index);
m_selectedLanguages.removeAt(index);
endRemoveRows();
emit selectedLanguagesChanged(m_selectedLanguages);
}
void TranslationsModel::removeSelectedLanguage(const QString &languageCode)
AvailableTranslationsModel::AvailableTranslationsModel(QObject *parent)
: TranslationsModel(parent)
{
m_selectedLanguages.removeOne(languageCode);
}
emit selectedLanguagesChanged();
AvailableTranslationsModel::~AvailableTranslationsModel()
{
}
auto idx = index(m_languages.indexOf(languageCode), 0);
QVariant AvailableTranslationsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_availableLanguages.count()) {
return QVariant();
}
if (idx.isValid()) {
emit dataChanged(idx, idx, QVector<int>{IsSelected, SelectedPriority});
if (role == Qt::DisplayRole) {
return languageCodeToName(m_availableLanguages.at(index.row()));
} else if (role == LanguageCode) {
return m_availableLanguages.at(index.row());
} else if (role == IsMissing) {
return false;
}
return QVariant();
}
QString TranslationsModel::languageCodeToName(const QString& languageCode) const
int AvailableTranslationsModel::rowCount(const QModelIndex &parent) const
{
const QLocale locale(languageCode);
const QString &languageName = locale.nativeLanguageName();
if (languageName.isEmpty()) {
return languageCode;
if (parent.isValid()) {
return 0;
}
if (languageCode.contains(QStringLiteral("@"))) {
return i18nc("%1 is language name, %2 is language code name", "%1 (%2)", languageName, languageCode);
}
return m_availableLanguages.count();
}
if (locale.name() != languageCode && m_installedLanguages.contains(locale.name())) {
// KDE languageCode got translated by QLocale to a locale code we also have on
// the list. Currently this only happens with pt that gets translated to pt_BR.
if (languageCode == QLatin1String("pt")) {
return QLocale(QStringLiteral("pt_PT")).nativeLanguageName();
} else {
qWarning() << "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);
}
QString AvailableTranslationsModel::langCodeAt(int row)
{
if (row < 0 || row >= m_availableLanguages.count()) {
return QString();
}
return languageName;
return m_availableLanguages.at(row);
}
void AvailableTranslationsModel::setSelectedLanguages(const QStringList &languages)
{
beginResetModel();
m_availableLanguages = (m_installedLanguages - QSet<QString>::fromList(languages)).toList();
QCollator c;
c.setCaseSensitivity(Qt::CaseInsensitive);
std::sort(m_availableLanguages.begin(), m_availableLanguages.end(),
[this, &c](const QString &a, const QString &b) {
return c.compare(languageCodeToName(a), languageCodeToName(b)) < 0;
});
endResetModel();
}
......@@ -31,14 +31,9 @@ class TranslationsModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(QStringList selectedLanguages READ selectedLanguages WRITE setSelectedLanguages NOTIFY selectedLanguagesChanged)
Q_PROPERTY(QStringList missingLanguages READ missingLanguages NOTIFY missingLanguagesChanged)
public:
enum AdditionalRoles {
LanguageCode = Qt::UserRole + 1,
IsSelected,
SelectedPriority,
IsMissing
};
Q_ENUM(AdditionalRoles)
......@@ -51,30 +46,63 @@ class TranslationsModel : public QAbstractListModel
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
protected:
QString languageCodeToName(const QString& languageCode) const;
static QStringList m_languages;
static QSet<QString> m_installedLanguages;
};
class SelectedTranslationsModel : public TranslationsModel
{
Q_OBJECT
Q_PROPERTY(QStringList selectedLanguages READ selectedLanguages WRITE setSelectedLanguages NOTIFY selectedLanguagesChanged)
Q_PROPERTY(QStringList missingLanguages READ missingLanguages NOTIFY missingLanguagesChanged)
public:
explicit SelectedTranslationsModel(QObject *parent = nullptr);
~SelectedTranslationsModel() override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QStringList selectedLanguages() const;
void setSelectedLanguages(const QStringList &languages);
QStringList missingLanguages() const;