diff --git a/kcms/translations/package/contents/ui/main.qml b/kcms/translations/package/contents/ui/main.qml --- a/kcms/translations/package/contents/ui/main.qml +++ b/kcms/translations/package/contents/ui/main.qml @@ -21,24 +21,54 @@ 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 ScrollViewKCM { id: root 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 @@ 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 @@ 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 @@ text: i18nc("@info", "There are no languages available on this system.") - visible: !availableLanguagesModel.count + visible: !availableLanguagesList.count } Kirigami.InlineMessage { @@ -150,10 +159,10 @@ 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 @@ } } - 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 @@ 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...") diff --git a/kcms/translations/translations.h b/kcms/translations/translations.h --- a/kcms/translations/translations.h +++ b/kcms/translations/translations.h @@ -25,6 +25,8 @@ #include +class AvailableTranslationsModel; +class SelectedTranslationsModel; class TranslationsModel; class QAbstractListModel; @@ -34,13 +36,17 @@ 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: explicit Translations(QObject* parent = 0, const QVariantList &list = QVariantList()); ~Translations() override; QAbstractItemModel* translationsModel() const; + QAbstractItemModel* selectedTranslationsModel() const; + QAbstractItemModel* availableTranslationsModel() const; bool everSaved() const; @@ -58,6 +64,8 @@ private: TranslationsModel *m_translationsModel; + SelectedTranslationsModel *m_selectedTranslationsModel; + AvailableTranslationsModel *m_availableTranslationsModel; KConfigGroup m_config; QStringList m_configuredLanguages; diff --git a/kcms/translations/translations.cpp b/kcms/translations/translations.cpp --- a/kcms/translations/translations.cpp +++ b/kcms/translations/translations.cpp @@ -33,6 +33,8 @@ 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 @@ 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 @@ 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,26 +84,27 @@ m_configuredLanguages = m_config.readEntry(lcLanguage, QString()).split(':', QString::SkipEmptyParts); - m_translationsModel->setSelectedLanguages(m_configuredLanguages); + m_selectedTranslationsModel->setSelectedLanguages(m_configuredLanguages); } 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); } m_config.writeEntry(lcLanguage, m_configuredLanguages.join(QStringLiteral(":")), KConfig::Persistent); m_config.sync(); writeExports(); - m_translationsModel->setSelectedLanguages(m_configuredLanguages); + m_selectedTranslationsModel->setSelectedLanguages(m_configuredLanguages); } void Translations::defaults() @@ -109,17 +125,17 @@ 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); } } diff --git a/kcms/translations/translationsmodel.h b/kcms/translations/translationsmodel.h --- a/kcms/translations/translationsmodel.h +++ b/kcms/translations/translationsmodel.h @@ -31,14 +31,9 @@ { 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 @@ 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 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; - public Q_SLOTS: - void moveSelectedLanguage(int from, int to); - void removeSelectedLanguage(const QString &languageCode); + Q_INVOKABLE void move(int from, int to); + Q_INVOKABLE void remove(const QString &languageCode); Q_SIGNALS: - void selectedLanguagesChanged() const; + void selectedLanguagesChanged(const QStringList &languages) const; void missingLanguagesChanged() const; private: - QString languageCodeToName(const QString& languageCode) const; + QStringList m_selectedLanguages; + QStringList m_missingLanguages; +}; + +class AvailableTranslationsModel : public TranslationsModel +{ + Q_OBJECT - KConfigGroup m_config; + public: + explicit AvailableTranslationsModel(QObject *parent = nullptr); + ~AvailableTranslationsModel() override; - QStringList m_languages; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QSet m_installedLanguages; + Q_INVOKABLE QString langCodeAt(int row); - QStringList m_selectedLanguages; - QStringList m_missingLanguages; + public Q_SLOTS: + void setSelectedLanguages(const QStringList &languages); + + private: + QStringList m_availableLanguages; }; #endif diff --git a/kcms/translations/translationsmodel.cpp b/kcms/translations/translationsmodel.cpp --- a/kcms/translations/translationsmodel.cpp +++ b/kcms/translations/translationsmodel.cpp @@ -22,15 +22,21 @@ #include +#include #include #include #include +QStringList TranslationsModel::m_languages = QStringList(); +QSet TranslationsModel::m_installedLanguages = QSet(); + 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 @@ 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 @@ 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 @@ beginResetModel(); m_selectedLanguages = languages; - m_languages = (m_installedLanguages | QSet::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 @@ 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{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{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{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::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(); }