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,46 @@ 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 { + property string languageCode: model.LanguageCode - filterRole: "IsSelected" - filterCallback: function(source_row, value) { return !value; } + reserveSpaceForIcon: false - sortRole: "display" + 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; + } + } + } + } } Kirigami.OverlaySheet { @@ -58,37 +80,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 +104,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 +129,7 @@ text: i18nc("@info", "There are no languages available on this system.") - visible: !availableLanguagesModel.count + visible: !availableLanguagesList.count } Kirigami.InlineMessage { @@ -153,7 +154,7 @@ kcm.translationsModel.missingLanguages.length, kcm.translationsModel.missingLanguages.join("', '")) - visible: kcm.translationsModel.missingLanguages.length + visible: kcm.selectedTranslationsModel.missingLanguages.length } QtControls.Label { @@ -165,29 +166,24 @@ } } - 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 +214,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,26 @@ 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()) { + for (const QString& lang : m_selectedTranslationsModel->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 +124,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,76 @@ 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(); + } 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); + } + } + + return languageName; } +SelectedTranslationsModel::SelectedTranslationsModel(QObject *parent) + : TranslationsModel(parent) +{ +} -void TranslationsModel::setSelectedLanguages(const QStringList &languages) +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(); + + return TranslationsModel::data(index, role); +} + +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 +172,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 +194,97 @@ return; } - m_selectedLanguages.move(from, to); + int modelTo = to + (to > from ? 1 : 0); - emit selectedLanguagesChanged(); + bool ok = beginMoveRows(QModelIndex(), from, from, QModelIndex(), modelTo); - auto idx = index(m_languages.indexOf(m_selectedLanguages.at(from)), 0); - - if (idx.isValid()) { - emit dataChanged(idx, idx, QVector{SelectedPriority}); - } + if (ok) { + m_selectedLanguages.move(from, to); - idx = index(m_languages.indexOf(m_selectedLanguages.at(to)), 0); + endMoveRows(); - if (idx.isValid()) { - emit dataChanged(idx, idx, QVector{SelectedPriority}); + emit selectedLanguagesChanged(m_selectedLanguages); } } -void TranslationsModel::removeSelectedLanguage(const QString &languageCode) +void SelectedTranslationsModel::remove(const QString &languageCode) { - m_selectedLanguages.removeOne(languageCode); + if (languageCode.isEmpty()) { + return; + } + + int index = m_selectedLanguages.indexOf(languageCode); - emit selectedLanguagesChanged(); + if (index != -1) { + beginRemoveRows(QModelIndex(), index, index); - auto idx = index(m_languages.indexOf(languageCode), 0); + m_selectedLanguages.removeAt(index); - if (idx.isValid()) { - emit dataChanged(idx, idx, QVector{IsSelected, SelectedPriority}); + endRemoveRows(); + + emit selectedLanguagesChanged(m_selectedLanguages); } } -QString TranslationsModel::languageCodeToName(const QString& languageCode) const +AvailableTranslationsModel::AvailableTranslationsModel(QObject *parent) + : TranslationsModel(parent) { - const QLocale locale(languageCode); - const QString &languageName = locale.nativeLanguageName(); +} - if (languageName.isEmpty()) { - return languageCode; +AvailableTranslationsModel::~AvailableTranslationsModel() +{ +} + +QVariant AvailableTranslationsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_availableLanguages.count()) { + return QVariant(); } - if (languageCode.contains(QStringLiteral("@"))) { - return i18nc("%1 is language name, %2 is language code name", "%1 (%2)", languageName, languageCode); + 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; } - 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); - } + return QVariant(); + + return TranslationsModel::data(index, role); +} + +int AvailableTranslationsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; } - return languageName; + return m_availableLanguages.count(); +} + +QString AvailableTranslationsModel::langCodeAt(int row) +{ + if (row < 0 || row >= m_availableLanguages.count()) { + return QString(); + } + + 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(); }