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