1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "tcinputmethod_p.h"
5#include <QtVirtualKeyboard/qvirtualkeyboardinputengine.h>
6#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
7#if QT_CONFIG(cangjie)
8#include "cangjiedictionary.h"
9#include "cangjietable.h"
10#endif
11#if QT_CONFIG(zhuyin)
12#include "zhuyindictionary.h"
13#include "zhuyintable.h"
14#endif
15#include "phrasedictionary.h"
16#include <QLoggingCategory>
17
18#include <QLibraryInfo>
19#include <QFileInfo>
20
21#include <array>
22
23QT_BEGIN_NAMESPACE
24namespace QtVirtualKeyboard {
25
26Q_LOGGING_CATEGORY(lcTCIme, "qt.virtualkeyboard.tcime")
27
28using namespace tcime;
29
30class TCInputMethodPrivate
31{
32 Q_DECLARE_PUBLIC(TCInputMethod)
33public:
34
35 TCInputMethodPrivate(TCInputMethod *q_ptr) :
36 q_ptr(q_ptr),
37 inputMode(QVirtualKeyboardInputEngine::InputMode::Latin),
38 wordDictionary(nullptr),
39 highlightIndex(-1)
40 {}
41
42 bool setCandidates(const QStringList &values, bool highlightDefault)
43 {
44 bool candidatesChanged = candidates != values;
45 candidates = values;
46 highlightIndex = !candidates.isEmpty() && highlightDefault ? 0 : -1;
47 return candidatesChanged;
48 }
49
50 bool clearCandidates()
51 {
52 if (candidates.isEmpty())
53 return false;
54
55 candidates.clear();
56 highlightIndex = -1;
57 return true;
58 }
59
60 QString pickHighlighted() const
61 {
62 return (highlightIndex >= 0 && highlightIndex < candidates.size()) ? candidates[highlightIndex] : QString();
63 }
64
65 void reset()
66 {
67 if (clearCandidates()) {
68 Q_Q(TCInputMethod);
69 emit q->selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
70 emit q->selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: highlightIndex);
71 }
72 input.clear();
73 }
74
75 bool compose(const QChar &c)
76 {
77 bool accept;
78 Q_Q(TCInputMethod);
79 QVirtualKeyboardInputContext *ic = q->inputContext();
80 switch (inputMode)
81 {
82#if QT_CONFIG(cangjie)
83 case QVirtualKeyboardInputEngine::InputMode::Cangjie:
84 accept = composeCangjie(ic, c);
85 break;
86#endif
87#if QT_CONFIG(zhuyin)
88 case QVirtualKeyboardInputEngine::InputMode::Zhuyin:
89 accept = composeZhuyin(ic, c);
90 break;
91#endif
92 default:
93 accept = false;
94 break;
95 }
96 return accept;
97 }
98
99#if QT_CONFIG(cangjie)
100 bool composeCangjie(QVirtualKeyboardInputContext *ic, const QChar &c)
101 {
102 bool accept = false;
103 if (!input.contains(c: QChar(0x91CD)) && CangjieTable::isLetter(c)) {
104 if (input.size() < (cangjieDictionary.simplified() ? CangjieTable::MAX_SIMPLIFIED_CODE_LENGTH : CangjieTable::MAX_CODE_LENGTH)) {
105 input.append(c);
106 ic->setPreeditText(text: input);
107 if (setCandidates(values: wordDictionary->getWords(input), highlightDefault: true)) {
108 Q_Q(TCInputMethod);
109 emit q->selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
110 emit q->selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: highlightIndex);
111 }
112 }
113 accept = true;
114 } else if (c.unicode() == 0x91CD) {
115 if (input.isEmpty()) {
116 input.append(c);
117 ic->setPreeditText(text: input);
118 checkSpecialCharInput();
119 }
120 accept = true;
121 } else if (c.unicode() == 0x96E3) {
122 if (input.size() == 1) {
123 Q_ASSERT(input.at(0).unicode() == 0x91CD);
124 input.append(c);
125 ic->setPreeditText(text: input);
126 checkSpecialCharInput();
127 }
128 accept = true;
129 }
130 return accept;
131 }
132
133 bool checkSpecialCharInput()
134 {
135 if (input.size() == 1 && input.at(i: 0).unicode() == 0x91CD) {
136 static const QStringList specialChars1 = QStringList()
137 << QChar(0xFF01) << QChar(0x2018) << QChar(0x3000) << QChar(0xFF0C)
138 << QChar(0x3001) << QChar(0x3002) << QChar(0xFF0E) << QChar(0xFF1B)
139 << QChar(0xFF1A) << QChar(0xFF1F) << QChar(0x300E) << QChar(0x300F)
140 << QChar(0x3010) << QChar(0x3011) << QChar(0xFE57) << QChar(0x2026)
141 << QChar(0x2025) << QChar(0xFE50) << QChar(0xFE51) << QChar(0xFE52)
142 << QChar(0x00B7) << QChar(0xFE54) << QChar(0x2574) << QChar(0x2027)
143 << QChar(0x2032) << QChar(0x2035) << QChar(0x301E) << QChar(0x301D)
144 << QChar(0x201D) << QChar(0x201C) << QChar(0x2019) << QChar(0xFE55)
145 << QChar(0xFE5D) << QChar(0xFE5E) << QChar(0xFE59) << QChar(0xFE5A)
146 << QChar(0xFE5B) << QChar(0xFE5C) << QChar(0xFE43) << QChar(0xFE44);
147 Q_Q(TCInputMethod);
148 if (setCandidates(values: specialChars1, highlightDefault: true)) {
149 emit q->selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
150 emit q->selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: highlightIndex);
151 }
152 q->inputContext()->setPreeditText(text: candidates[highlightIndex]);
153 return true;
154 } else if (input.size() == 2 && input.at(i: 0).unicode() == 0x91CD && input.at(i: 1).unicode() == 0x96E3) {
155 static const QStringList specialChars2 = QStringList()
156 << QChar(0x3008) << QChar(0x3009) << QChar(0xFE31) << QChar(0x2013)
157 << QChar(0xFF5C) << QChar(0x300C) << QChar(0x300D) << QChar(0xFE40)
158 << QChar(0xFE3F) << QChar(0x2014) << QChar(0xFE3E) << QChar(0xFE3D)
159 << QChar(0x300A) << QChar(0x300B) << QChar(0xFE3B) << QChar(0xFE3C)
160 << QChar(0xFE56) << QChar(0xFE30) << QChar(0xFE39) << QChar(0xFE3A)
161 << QChar(0x3014) << QChar(0x3015) << QChar(0xFE37) << QChar(0xFE38)
162 << QChar(0xFE41) << QChar(0xFE42) << QChar(0xFF5B) << QChar(0xFF5D)
163 << QChar(0xFE35) << QChar(0xFE36) << QChar(0xFF08) << QChar(0xFF09)
164 << QChar(0xFE4F) << QChar(0xFE34) << QChar(0xFE33);
165 Q_Q(TCInputMethod);
166 if (setCandidates(values: specialChars2, highlightDefault: true)) {
167 emit q->selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
168 emit q->selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: highlightIndex);
169 }
170 q->inputContext()->setPreeditText(text: candidates[highlightIndex]);
171 return true;
172 }
173 return false;
174 }
175#endif
176
177#if QT_CONFIG(zhuyin)
178 bool composeZhuyin(QVirtualKeyboardInputContext *ic, const QChar &c)
179 {
180 if (ZhuyinTable::isTone(c)) {
181 if (input.isEmpty())
182 // Tones are accepted only when there's text in composing.
183 return false;
184
185 auto strippedTones = ZhuyinTable::stripTones(input);
186 if (!strippedTones.ok)
187 // Tones cannot be composed if there's no syllables.
188 return false;
189
190 // Replace the original tone with the new tone, but the default tone
191 // character should not be composed into the composing text.
192 QChar tone = strippedTones.pair[1].at(n: 0);
193 if (c == ZhuyinTable::DEFAULT_TONE) {
194 if (tone != ZhuyinTable::DEFAULT_TONE)
195 input.remove(i: input.size() - 1, len: 1);
196 } else {
197 if (tone == ZhuyinTable::DEFAULT_TONE)
198 input.append(c);
199 else
200 input.replace(i: input.size() - 1, len: 1, after: c);
201 }
202 } else if (ZhuyinTable::getInitials(initials: c) > 0) {
203 // Insert the initial or replace the original initial.
204 if (input.isEmpty() || !ZhuyinTable::getInitials(initials: input.at(i: 0)))
205 input.insert(i: 0, c);
206 else
207 input.replace(i: 0, len: 1, after: c);
208 } else if (ZhuyinTable::getFinals(finals: QStringView(&c, 1)) > 0) {
209 // Replace the finals in the decomposed of syllables and tones.
210 std::array<QChar, 4> decomposed = decomposeZhuyin();
211 if (ZhuyinTable::isYiWuYuFinals(c)) {
212 decomposed[1] = c;
213 } else {
214 decomposed[2] = c;
215 }
216
217 // Compose back the text after the finals replacement.
218 input.clear();
219 for (QChar ch : decomposed) {
220 if (!ch.isNull())
221 input.append(c: ch);
222 }
223 } else {
224 return false;
225 }
226
227 ic->setPreeditText(text: input);
228 if (setCandidates(values: wordDictionary->getWords(input), highlightDefault: true)) {
229 Q_Q(TCInputMethod);
230 emit q->selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
231 emit q->selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: highlightIndex);
232 }
233
234 return true;
235 }
236
237 std::array<QChar, 4> decomposeZhuyin()
238 {
239 std::array<QChar, 4> results = {};
240 auto strippedTones = ZhuyinTable::stripTones(input);
241 if (strippedTones.ok) {
242 // Decompose tones.
243 QChar tone = strippedTones.pair[1].at(n: 0);
244 if (tone != ZhuyinTable::DEFAULT_TONE)
245 results[3] = tone;
246
247 // Decompose initials.
248 QStringView syllables = strippedTones.pair[0];
249 if (ZhuyinTable::getInitials(initials: syllables.at(n: 0)) > 0) {
250 results[0] = syllables.at(n: 0);
251 syllables = syllables.mid(pos: 1);
252 }
253
254 // Decompose finals.
255 if (!syllables.isEmpty()) {
256 if (ZhuyinTable::isYiWuYuFinals(c: syllables.at(n: 0))) {
257 results[1] = syllables.at(n: 0);
258 if (syllables.size() > 1)
259 results[2] = syllables.at(n: 1);
260 } else {
261 results[2] = syllables.at(n: 0);
262 }
263 }
264 }
265 return results;
266 }
267#endif
268
269 TCInputMethod *q_ptr;
270 QVirtualKeyboardInputEngine::InputMode inputMode;
271#if QT_CONFIG(cangjie)
272 CangjieDictionary cangjieDictionary;
273#endif
274#if QT_CONFIG(zhuyin)
275 ZhuyinDictionary zhuyinDictionary;
276#endif
277 PhraseDictionary phraseDictionary;
278 WordDictionary *wordDictionary;
279 QString input;
280 QStringList candidates;
281 int highlightIndex;
282};
283
284/*!
285 \class QtVirtualKeyboard::TCInputMethod
286 \internal
287*/
288
289TCInputMethod::TCInputMethod(QObject *parent) :
290 QVirtualKeyboardAbstractInputMethod(parent),
291 d_ptr(new TCInputMethodPrivate(this))
292{
293}
294
295TCInputMethod::~TCInputMethod()
296{
297}
298
299bool TCInputMethod::simplified() const
300{
301#if QT_CONFIG(cangjie)
302 Q_D(const TCInputMethod);
303 return d->cangjieDictionary.simplified();
304#else
305 return false;
306#endif
307}
308
309void TCInputMethod::setSimplified(bool simplified)
310{
311 qCDebug(lcTCIme) << "TCInputMethod::setSimplified(): " << simplified;
312#if QT_CONFIG(cangjie)
313 Q_D(TCInputMethod);
314 if (d->cangjieDictionary.simplified() != simplified) {
315 d->reset();
316 QVirtualKeyboardInputContext *ic = inputContext();
317 if (ic)
318 ic->clear();
319 d->cangjieDictionary.setSimplified(simplified);
320 emit simplifiedChanged();
321 }
322#else
323 Q_UNUSED(simplified);
324#endif
325}
326
327QList<QVirtualKeyboardInputEngine::InputMode> TCInputMethod::inputModes(const QString &locale)
328{
329 Q_UNUSED(locale);
330 return QList<QVirtualKeyboardInputEngine::InputMode>()
331#if QT_CONFIG(zhuyin)
332 << QVirtualKeyboardInputEngine::InputMode::Zhuyin
333#endif
334#if QT_CONFIG(cangjie)
335 << QVirtualKeyboardInputEngine::InputMode::Cangjie
336#endif
337 ;
338}
339
340bool TCInputMethod::setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
341{
342 Q_UNUSED(locale);
343 Q_D(TCInputMethod);
344 if (d->inputMode == inputMode)
345 return true;
346 update();
347 bool result = false;
348 d->inputMode = inputMode;
349 d->wordDictionary = nullptr;
350#if QT_CONFIG(cangjie)
351 if (inputMode == QVirtualKeyboardInputEngine::InputMode::Cangjie) {
352 if (d->cangjieDictionary.isEmpty()) {
353 QString cangjieDictionary(qEnvironmentVariable(varName: "QT_VIRTUALKEYBOARD_CANGJIE_DICTIONARY"));
354 if (!QFileInfo::exists(file: cangjieDictionary)) {
355 cangjieDictionary = QLibraryInfo::path(p: QLibraryInfo::DataPath) + QLatin1String("/qtvirtualkeyboard/tcime/dict_cangjie.dat");
356 if (!QFileInfo::exists(file: cangjieDictionary))
357 cangjieDictionary = QLatin1String(":/qt-project.org/imports/QtQuick/VirtualKeyboard/3rdparty/tcime/data/qt/dict_cangjie.dat");
358 }
359 d->cangjieDictionary.load(fileName: cangjieDictionary);
360 }
361 d->wordDictionary = &d->cangjieDictionary;
362 }
363#endif
364#if QT_CONFIG(zhuyin)
365 if (inputMode == QVirtualKeyboardInputEngine::InputMode::Zhuyin) {
366 if (d->zhuyinDictionary.isEmpty()) {
367 QString zhuyinDictionary(qEnvironmentVariable(varName: "QT_VIRTUALKEYBOARD_ZHUYIN_DICTIONARY"));
368 if (!QFileInfo::exists(file: zhuyinDictionary)) {
369 zhuyinDictionary = QLibraryInfo::path(p: QLibraryInfo::DataPath) + QLatin1String("/qtvirtualkeyboard/tcime/dict_zhuyin.dat");
370 if (!QFileInfo::exists(file: zhuyinDictionary))
371 zhuyinDictionary = QLatin1String(":/qt-project.org/imports/QtQuick/VirtualKeyboard/3rdparty/tcime/data/qt/dict_zhuyin.dat");
372 }
373 d->zhuyinDictionary.load(fileName: zhuyinDictionary);
374 }
375 d->wordDictionary = &d->zhuyinDictionary;
376 }
377#endif
378 result = d->wordDictionary && !d->wordDictionary->isEmpty();
379 if (result && d->phraseDictionary.isEmpty()) {
380 QString phraseDictionary(qEnvironmentVariable(varName: "QT_VIRTUALKEYBOARD_PHRASE_DICTIONARY"));
381 if (!QFileInfo::exists(file: phraseDictionary)) {
382 phraseDictionary = QLibraryInfo::path(p: QLibraryInfo::DataPath) + QLatin1String("/qtvirtualkeyboard/tcime/dict_phrases.dat");
383 if (!QFileInfo::exists(file: phraseDictionary))
384 phraseDictionary = QLatin1String(":/qt-project.org/imports/QtQuick/VirtualKeyboard/3rdparty/tcime/data/qt/dict_phrases.dat");
385 }
386 d->phraseDictionary.load(fileName: phraseDictionary);
387 }
388 if (!result)
389 inputMode = QVirtualKeyboardInputEngine::InputMode::Latin;
390 return result;
391}
392
393bool TCInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase)
394{
395 Q_UNUSED(textCase);
396 return true;
397}
398
399bool TCInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers)
400{
401 Q_UNUSED(key);
402 Q_UNUSED(text);
403 Q_UNUSED(modifiers);
404 Q_D(TCInputMethod);
405 QVirtualKeyboardInputContext *ic = inputContext();
406 bool accept = false;
407 switch (key) {
408 case Qt::Key_Context1:
409 // Do nothing on symbol mode switch
410 accept = true;
411 break;
412
413 case Qt::Key_Enter:
414 case Qt::Key_Return:
415 update();
416 break;
417
418 case Qt::Key_Tab:
419 case Qt::Key_Space:
420 if (!d->input.isEmpty()) {
421 accept = true;
422 if (d->highlightIndex >= 0) {
423 QString finalWord = d->pickHighlighted();
424 d->reset();
425 inputContext()->commit(text: finalWord);
426 if (d->setCandidates(values: d->phraseDictionary.getWords(input: finalWord.left(n: 1)), highlightDefault: false)) {
427 emit selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
428 emit selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: d->highlightIndex);
429 }
430 }
431 } else {
432 update();
433 }
434 break;
435
436 case Qt::Key_Backspace:
437 if (!d->input.isEmpty()) {
438 d->input.remove(i: d->input.size() - 1, len: 1);
439 ic->setPreeditText(text: d->input);
440#if QT_CONFIG(cangjie)
441 if (!d->checkSpecialCharInput()) {
442#endif
443 if (d->setCandidates(values: d->wordDictionary->getWords(input: d->input), highlightDefault: true)) {
444 emit selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
445 emit selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: d->highlightIndex);
446 }
447#if QT_CONFIG(cangjie)
448 }
449#endif
450 accept = true;
451 } else if (d->clearCandidates()) {
452 emit selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
453 emit selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: d->highlightIndex);
454 }
455 break;
456
457 default:
458 if (text.size() == 1)
459 accept = d->compose(c: text.at(i: 0));
460 if (!accept)
461 update();
462 break;
463 }
464 return accept;
465}
466
467QList<QVirtualKeyboardSelectionListModel::Type> TCInputMethod::selectionLists()
468{
469 return QList<QVirtualKeyboardSelectionListModel::Type>() << QVirtualKeyboardSelectionListModel::Type::WordCandidateList;
470}
471
472int TCInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type)
473{
474 Q_UNUSED(type);
475 Q_D(TCInputMethod);
476 return d->candidates.size();
477}
478
479QVariant TCInputMethod::selectionListData(QVirtualKeyboardSelectionListModel::Type type, int index, QVirtualKeyboardSelectionListModel::Role role)
480{
481 QVariant result;
482 Q_D(TCInputMethod);
483 switch (role) {
484 case QVirtualKeyboardSelectionListModel::Role::Display:
485 result = QVariant(d->candidates.at(i: index));
486 break;
487 case QVirtualKeyboardSelectionListModel::Role::WordCompletionLength:
488 result.setValue(0);
489 break;
490 default:
491 result = QVirtualKeyboardAbstractInputMethod::selectionListData(type, index, role);
492 break;
493 }
494 return result;
495}
496
497void TCInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index)
498{
499 Q_UNUSED(type);
500 Q_D(TCInputMethod);
501 QString finalWord = d->candidates.at(i: index);
502 reset();
503 inputContext()->commit(text: finalWord);
504 if (d->setCandidates(values: d->phraseDictionary.getWords(input: finalWord.left(n: 1)), highlightDefault: false)) {
505 emit selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
506 emit selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, index: d->highlightIndex);
507 }
508}
509
510void TCInputMethod::reset()
511{
512 Q_D(TCInputMethod);
513 d->reset();
514}
515
516void TCInputMethod::update()
517{
518 Q_D(TCInputMethod);
519 if (d->highlightIndex >= 0) {
520 QString finalWord = d->pickHighlighted();
521 d->reset();
522 inputContext()->commit(text: finalWord);
523 } else {
524 inputContext()->clear();
525 d->reset();
526 }
527}
528
529} // namespace QtVirtualKeyboard
530QT_END_NAMESPACE
531

source code of qtvirtualkeyboard/src/plugins/tcime/tcinputmethod.cpp