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

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