1 /*
2  * SPDX-FileCopyrightText: 2017~2017 CSSlayer <wengxt@gmail.com>
3  *
4  * SPDX-License-Identifier: GPL-2.0-or-later
5  */
6 
7 #include "model.h"
8 #include <QCollator>
9 #include <QLocale>
10 #include <fcitx-utils/i18n.h>
11 
12 namespace fcitx {
13 namespace kcm {
14 
CategorizedItemModel(QObject * parent)15 CategorizedItemModel::CategorizedItemModel(QObject *parent)
16     : QAbstractItemModel(parent) {}
17 
rowCount(const QModelIndex & parent) const18 int CategorizedItemModel::rowCount(const QModelIndex &parent) const {
19     if (!parent.isValid()) {
20         return listSize();
21     }
22 
23     if (parent.internalId() > 0) {
24         return 0;
25     }
26 
27     if (parent.column() > 0 || parent.row() >= listSize()) {
28         return 0;
29     }
30 
31     return subListSize(parent.row());
32 }
33 
columnCount(const QModelIndex &) const34 int CategorizedItemModel::columnCount(const QModelIndex &) const { return 1; }
35 
parent(const QModelIndex & child) const36 QModelIndex CategorizedItemModel::parent(const QModelIndex &child) const {
37     if (!child.isValid()) {
38         return QModelIndex();
39     }
40 
41     int row = child.internalId();
42     if (row && row - 1 >= listSize()) {
43         return QModelIndex();
44     }
45 
46     return createIndex(row - 1, 0, -1);
47 }
48 
index(int row,int column,const QModelIndex & parent) const49 QModelIndex CategorizedItemModel::index(int row, int column,
50                                         const QModelIndex &parent) const {
51     // return language index
52     if (!parent.isValid()) {
53         if (column > 0 || row >= listSize()) {
54             return QModelIndex();
55         } else {
56             return createIndex(row, column, static_cast<quintptr>(0));
57         }
58     }
59 
60     // return im index
61     if (parent.column() > 0 || parent.row() >= listSize() ||
62         row >= subListSize(parent.row())) {
63         return QModelIndex();
64     }
65 
66     return createIndex(row, column, parent.row() + 1);
67 }
68 
data(const QModelIndex & index,int role) const69 QVariant CategorizedItemModel::data(const QModelIndex &index, int role) const {
70     if (!index.isValid()) {
71         return QVariant();
72     }
73 
74     if (!index.parent().isValid()) {
75         if (index.column() > 0 || index.row() >= listSize()) {
76             return QVariant();
77         }
78 
79         return dataForCategory(index, role);
80     }
81 
82     if (index.column() > 0 || index.parent().column() > 0 ||
83         index.parent().row() >= listSize()) {
84         return QVariant();
85     }
86 
87     if (index.row() >= subListSize(index.parent().row())) {
88         return QVariant();
89     }
90     return dataForItem(index, role);
91 }
92 
languageName(const QString & langCode)93 static QString languageName(const QString &langCode) {
94     if (langCode.isEmpty()) {
95         return _("Unknown");
96     } else if (langCode == "*")
97         return _("Multilingual");
98     else {
99         QLocale locale(langCode);
100         if (locale.language() == QLocale::C) {
101             // return lang code seems to be a better solution other than
102             // indistinguishable "unknown"
103             return langCode;
104         }
105         const bool hasCountry = langCode.indexOf("_") != -1 &&
106                                 locale.country() != QLocale::AnyCountry;
107         QString languageName;
108         if (hasCountry) {
109             languageName = locale.nativeLanguageName();
110         }
111         if (languageName.isEmpty()) {
112             languageName =
113                 D_("iso_639",
114                    QLocale::languageToString(locale.language()).toUtf8());
115         }
116         if (languageName.isEmpty()) {
117             languageName = _("Other");
118         }
119         QString countryName;
120         // QLocale will always assign a default country for us, check if our
121         // lang code
122 
123         if (langCode.indexOf("_") != -1 &&
124             locale.country() != QLocale::AnyCountry) {
125             countryName = locale.nativeCountryName();
126             if (countryName.isEmpty()) {
127                 countryName = QLocale::countryToString(locale.country());
128             }
129         }
130 
131         if (countryName.isEmpty()) {
132             return languageName;
133         } else {
134             return QString(
135                        C_("%1 is language name, %2 is country name", "%1 (%2)"))
136                 .arg(languageName, countryName);
137         }
138     }
139 }
140 
AvailIMModel(QObject * parent)141 AvailIMModel::AvailIMModel(QObject *parent) : CategorizedItemModel(parent) {}
142 
dataForCategory(const QModelIndex & index,int role) const143 QVariant AvailIMModel::dataForCategory(const QModelIndex &index,
144                                        int role) const {
145     switch (role) {
146 
147     case Qt::DisplayRole:
148         return languageName(filteredIMEntryList[index.row()].first);
149 
150     case FcitxLanguageRole:
151         return filteredIMEntryList[index.row()].first;
152 
153     case FcitxIMUniqueNameRole:
154         return QString();
155 
156     case FcitxRowTypeRole:
157         return LanguageType;
158 
159     default:
160         return QVariant();
161     }
162 }
163 
dataForItem(const QModelIndex & index,int role) const164 QVariant AvailIMModel::dataForItem(const QModelIndex &index, int role) const {
165     const FcitxQtInputMethodEntryList &imEntryList =
166         filteredIMEntryList[index.parent().row()].second;
167 
168     const FcitxQtInputMethodEntry &imEntry = imEntryList[index.row()];
169 
170     switch (role) {
171 
172     case Qt::DisplayRole:
173         return imEntry.name();
174 
175     case FcitxRowTypeRole:
176         return IMType;
177 
178     case FcitxIMUniqueNameRole:
179         return imEntry.uniqueName();
180 
181     case FcitxLanguageRole:
182         return imEntry.languageCode();
183     }
184     return QVariant();
185 }
186 
filterIMEntryList(const FcitxQtInputMethodEntryList & imEntryList,const FcitxQtStringKeyValueList & enabledIMList)187 void AvailIMModel::filterIMEntryList(
188     const FcitxQtInputMethodEntryList &imEntryList,
189     const FcitxQtStringKeyValueList &enabledIMList) {
190     beginResetModel();
191 
192     QMap<QString, int> languageMap;
193     filteredIMEntryList.clear();
194 
195     QSet<QString> enabledIMs;
196     for (const auto &item : enabledIMList) {
197         enabledIMs.insert(item.key());
198     }
199 
200     for (const FcitxQtInputMethodEntry &im : imEntryList) {
201         if (enabledIMs.contains(im.uniqueName())) {
202             continue;
203         }
204         int idx;
205         if (!languageMap.contains(im.languageCode())) {
206             idx = filteredIMEntryList.count();
207             languageMap[im.languageCode()] = idx;
208             filteredIMEntryList.append(
209                 QPair<QString, FcitxQtInputMethodEntryList>(
210                     im.languageCode(), FcitxQtInputMethodEntryList()));
211         } else {
212             idx = languageMap[im.languageCode()];
213         }
214         filteredIMEntryList[idx].second.append(im);
215     }
216     endResetModel();
217 }
218 
IMProxyModel(QObject * parent)219 IMProxyModel::IMProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {
220     setDynamicSortFilter(true);
221     sort(0);
222 }
223 
setFilterText(const QString & text)224 void IMProxyModel::setFilterText(const QString &text) {
225     if (filterText_ != text) {
226         filterText_ = text;
227         invalidate();
228     }
229 }
230 
setShowOnlyCurrentLanguage(bool show)231 void IMProxyModel::setShowOnlyCurrentLanguage(bool show) {
232     if (showOnlyCurrentLanguage_ != show) {
233         showOnlyCurrentLanguage_ = show;
234         invalidate();
235     }
236 }
237 
filterIMEntryList(const FcitxQtInputMethodEntryList & imEntryList,const FcitxQtStringKeyValueList & enabledIMList)238 void IMProxyModel::filterIMEntryList(
239     const FcitxQtInputMethodEntryList &imEntryList,
240     const FcitxQtStringKeyValueList &enabledIMList) {
241     languageSet_.clear();
242 
243     QSet<QString> enabledIMs;
244     for (const auto &item : enabledIMList) {
245         enabledIMs.insert(item.key());
246     }
247     for (const FcitxQtInputMethodEntry &im : imEntryList) {
248         if (enabledIMs.contains(im.uniqueName())) {
249             languageSet_.insert(im.languageCode().left(2));
250         }
251     }
252     invalidate();
253 }
254 
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const255 bool IMProxyModel::filterAcceptsRow(int source_row,
256                                     const QModelIndex &source_parent) const {
257     const QModelIndex index =
258         sourceModel()->index(source_row, 0, source_parent);
259 
260     if (index.data(FcitxRowTypeRole) == LanguageType) {
261         return filterLanguage(index);
262     }
263 
264     return filterIM(index);
265 }
266 
filterLanguage(const QModelIndex & index) const267 bool IMProxyModel::filterLanguage(const QModelIndex &index) const {
268     if (!index.isValid()) {
269         return false;
270     }
271 
272     int childCount = index.model()->rowCount(index);
273     if (childCount == 0)
274         return false;
275 
276     for (int i = 0; i < childCount; ++i) {
277         if (filterIM(index.model()->index(i, 0, index))) {
278             return true;
279         }
280     }
281 
282     return false;
283 }
284 
filterIM(const QModelIndex & index) const285 bool IMProxyModel::filterIM(const QModelIndex &index) const {
286     QString uniqueName = index.data(FcitxIMUniqueNameRole).toString();
287     QString name = index.data(Qt::DisplayRole).toString();
288     QString langCode = index.data(FcitxLanguageRole).toString();
289 
290     // Always show keyboard us if we are not searching.
291     if (uniqueName == "keyboard-us" && filterText_.isEmpty()) {
292         return true;
293     }
294 
295     bool flag = true;
296     QString lang = langCode.left(2);
297     bool showOnlyCurrentLanguage =
298         filterText_.isEmpty() && showOnlyCurrentLanguage_;
299 
300     flag =
301         flag && (showOnlyCurrentLanguage
302                      ? !lang.isEmpty() && (QLocale().name().startsWith(lang) ||
303                                            languageSet_.contains(lang))
304                      : true);
305     if (!filterText_.isEmpty()) {
306         flag = flag && (name.contains(filterText_, Qt::CaseInsensitive) ||
307                         uniqueName.contains(filterText_, Qt::CaseInsensitive) ||
308                         langCode.contains(filterText_, Qt::CaseInsensitive) ||
309                         languageName(langCode).contains(filterText_,
310                                                         Qt::CaseInsensitive));
311     }
312     return flag;
313 }
314 
lessThan(const QModelIndex & left,const QModelIndex & right) const315 bool IMProxyModel::lessThan(const QModelIndex &left,
316                             const QModelIndex &right) const {
317     int result = compareCategories(left, right);
318     if (result < 0) {
319         return true;
320     } else if (result > 0) {
321         return false;
322     }
323 
324     QString l = left.data(Qt::DisplayRole).toString();
325     QString r = right.data(Qt::DisplayRole).toString();
326     return QCollator().compare(l, r) < 0;
327 }
328 
compareCategories(const QModelIndex & left,const QModelIndex & right) const329 int IMProxyModel::compareCategories(const QModelIndex &left,
330                                     const QModelIndex &right) const {
331     QString l = left.data(FcitxLanguageRole).toString();
332     QString r = right.data(FcitxLanguageRole).toString();
333 
334     if (l == r)
335         return 0;
336 
337     if (QLocale().name() == l)
338         return -1;
339 
340     if (QLocale().name() == r)
341         return 1;
342 
343     bool fl = QLocale().name().startsWith(l.left(2));
344     bool fr = QLocale().name().startsWith(r.left(2));
345 
346     if (fl == fr) {
347         return l.size() == r.size() ? l.compare(r) : l.size() - r.size();
348     }
349     return fl ? -1 : 1;
350 }
351 
FilteredIMModel(Mode mode,QObject * parent)352 FilteredIMModel::FilteredIMModel(Mode mode, QObject *parent)
353     : QAbstractListModel(parent), mode_(mode) {}
354 
data(const QModelIndex & index,int role) const355 QVariant FilteredIMModel::data(const QModelIndex &index, int role) const {
356     if (!index.isValid() || index.row() >= filteredIMEntryList_.size()) {
357         return QVariant();
358     }
359 
360     const FcitxQtInputMethodEntry &imEntry =
361         filteredIMEntryList_.at(index.row());
362 
363     switch (role) {
364 
365     case Qt::DisplayRole:
366         return imEntry.name();
367 
368     case FcitxRowTypeRole:
369         return IMType;
370 
371     case FcitxIMUniqueNameRole:
372         return imEntry.uniqueName();
373 
374     case FcitxLanguageRole:
375         return imEntry.languageCode();
376 
377     case FcitxIMConfigurableRole:
378         return imEntry.configurable();
379 
380     case FcitxLanguageNameRole:
381         return languageName(imEntry.languageCode());
382 
383     case FcitxIMLayoutRole: {
384         auto iter = std::find_if(enabledIMList_.begin(), enabledIMList_.end(),
385                                  [&imEntry](const FcitxQtStringKeyValue &item) {
386                                      return item.key() == imEntry.uniqueName();
387                                  });
388         if (iter != enabledIMList_.end()) {
389             return iter->value();
390         }
391         return QString();
392     }
393 
394     default:
395         return QVariant();
396     }
397 }
398 
rowCount(const QModelIndex & parent) const399 int FilteredIMModel::rowCount(const QModelIndex &parent) const {
400     if (parent.isValid()) {
401         return 0;
402     }
403 
404     return filteredIMEntryList_.count();
405 }
406 
roleNames() const407 QHash<int, QByteArray> FilteredIMModel::roleNames() const {
408     return {{Qt::DisplayRole, "name"},
409             {FcitxIMUniqueNameRole, "uniqueName"},
410             {FcitxLanguageRole, "languageCode"},
411             {FcitxLanguageNameRole, "language"},
412             {FcitxIMConfigurableRole, "configurable"},
413             {FcitxIMLayoutRole, "layout"}};
414 }
415 
filterIMEntryList(const FcitxQtInputMethodEntryList & imEntryList,const FcitxQtStringKeyValueList & enabledIMList)416 void FilteredIMModel::filterIMEntryList(
417     const FcitxQtInputMethodEntryList &imEntryList,
418     const FcitxQtStringKeyValueList &enabledIMList) {
419     beginResetModel();
420 
421     filteredIMEntryList_.clear();
422     enabledIMList_ = enabledIMList;
423 
424     // We implement this twice for following reasons:
425     // 1. "enabledIMs" is usually very small.
426     // 2. CurrentIM mode need to keep order by enabledIMs.
427     if (mode_ == CurrentIM) {
428         int row = 0;
429         QMap<QString, const FcitxQtInputMethodEntry *> nameMap;
430         for (auto &imEntry : imEntryList) {
431             nameMap.insert(imEntry.uniqueName(), &imEntry);
432         }
433 
434         for (const auto &im : enabledIMList) {
435             if (auto value = nameMap.value(im.key(), nullptr)) {
436                 filteredIMEntryList_.append(*value);
437                 row++;
438             }
439         }
440     } else if (mode_ == AvailIM) {
441         QSet<QString> enabledIMs;
442         for (const auto &item : enabledIMList) {
443             enabledIMs.insert(item.key());
444         }
445 
446         for (const FcitxQtInputMethodEntry &im : imEntryList) {
447             if (enabledIMs.contains(im.uniqueName())) {
448                 continue;
449             }
450             filteredIMEntryList_.append(im);
451         }
452     }
453     endResetModel();
454 }
455 
move(int from,int to)456 void FilteredIMModel::move(int from, int to) {
457     if (from < 0 || from >= filteredIMEntryList_.size() || to < 0 ||
458         to >= filteredIMEntryList_.size()) {
459         return;
460     }
461     beginMoveRows(QModelIndex(), from, from, QModelIndex(),
462                   to > from ? to + 1 : to);
463     filteredIMEntryList_.move(from, to);
464     endMoveRows();
465     Q_EMIT imListChanged(filteredIMEntryList_);
466 }
467 
remove(int idx)468 void FilteredIMModel::remove(int idx) {
469     if (idx < 0 || idx >= filteredIMEntryList_.size()) {
470         return;
471     }
472     beginRemoveRows(QModelIndex(), idx, idx);
473     filteredIMEntryList_.removeAt(idx);
474     endRemoveRows();
475     Q_EMIT imListChanged(filteredIMEntryList_);
476 }
477 
478 } // namespace kcm
479 } // namespace fcitx
480