2
* This file is part of Maliit Plugins
4
* Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). All rights reserved.
6
* Contact: Mohammad Anwari <Mohammad.Anwari@nokia.com>
8
* Redistribution and use in source and binary forms, with or without modification,
9
* are permitted provided that the following conditions are met:
11
* Redistributions of source code must retain the above copyright notice, this list
12
* of conditions and the following disclaimer.
13
* Redistributions in binary form must reproduce the above copyright notice, this list
14
* of conditions and the following disclaimer in the documentation and/or other materials
15
* provided with the distribution.
16
* Neither the name of Nokia Corporation nor the names of its contributors may be
17
* used to endorse or promote products derived from this software without specific
18
* prior written permission.
20
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
21
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
22
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
23
* THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
#include "abstracttexteditor.h"
33
#include "models/wordribbon.h"
35
#include "logic/chineselanguagefeatures.h"
36
#include "logic/languagefeatures.h"
38
namespace MaliitKeyboard {
40
//! \class EditorOptions
41
//! \brief Plain struct implementing editor options.
43
//! \fn EditorOptions::EditorOptions()
44
//! \brief Constructor.
46
//! Sets backspace_auto_repeat_delay to 500 miliseconds and backspace_auto_repeat_interval to 300 miliseconds.
48
//! \var EditorOptions::backspace_auto_repeat_delay
49
//! \brief Delay before first automatically repeated key in miliseconds.
51
//! \var EditorOptions::backspace_auto_repeat_interval
52
//! \brief Interval between automatically repeated key in miliseconds.
54
//! \class AbstractTextEditor
55
//! \brief Class implementing preedit edition.
57
//! It owns a text model (which can be gotten by text() method) and a
58
//! word engine (word_engine()). The class has to be subclassed and
59
//! subclass has to provide sendPreeditString(), sendCommitString(),
60
//! sendKeyEvent(), invokeAction() and destructor implementations.
62
// The function declaration has to be in one line, because \fn is a
63
// single line parameter.
64
//! \fn void AbstractTextEditor::sendPreeditString(const QString &preedit, Model::Text::PreeditFace face, const Replacement &replacement)
65
//! \brief Sends preedit to application.
66
//! \param preedit Preedit to send.
67
//! \param face Face of the preedit.
68
//! \param replacement Struct describing replacement of the text.
70
//! Implementations of this pure virtual method have to convert \a
71
//! face into specific attributes used by backend they
72
//! support. Preedit text has to be sent as-is. Replacement members
73
//! should be understood as follows: if either start or length are
74
//! lesser than zero or both of them are zeros then those parameters
75
//! should be ignored. Otherwise they mark the beginning and length of
76
//! a surrounding text's substring that is going to be replaced by
77
//! given \a preedit. Cursor position member of replacement should be
78
//! ignored if its value is lesser than zero. Otherwise it describes a
79
//! position of cursor relatively to the beginning of preedit.
81
//! \fn void AbstractTextEditor::sendCommitString(const QString &commit)
82
//! \brief Commits a string to application.
83
//! \param commit String to be commited in place of preedit.
85
//! Implementations of this method should discard current preedit and
86
//! commit given \a commit in its place.
88
//! \fn void AbstractTextEditor::sendKeyEvent(const QKeyEvent &ev)
89
//! \brief Sends a key event to application.
90
//! \param ev Key event to send.
92
//! The implementation should translate passed \a ev to values their
93
//! backend understands and pass this to application.
95
//! \fn void AbstractTextEditor::invokeAction(const QString &action, const QKeySequence &sequence)
96
//! \brief Invokes an action in the application
97
//! \param action Action to invoke
98
//! \param sequence Key sequence to emit when action cannot be called directly
100
//! Application first tries to invoke a signal/slot \a action and when not available
101
//! it will emit the key sequence \a sequence. One would call this method for
102
//! example with "copy", "CTRL+C" arguments.
104
//! \property AbstractTextEditor::preeditEnabled
105
//! \brief Describes whether preedit is enabled.
107
//! When it is \c false then everything typed by virtual keyboard is
108
//! immediately commited.
110
//! \property AbstractTextEditor::autoCorrectEnabled
111
//! \brief Describes whether auto correction on space is enabled.
113
//! When it is \c true then pressing space will commit corrected word
114
//! if it was misspelled. Otherwise it will just commit what was in
117
//! \fn void AbstractTextEditor::autoCapsActivated()
118
//! \brief Emitted when auto capitalization mode is enabled for
121
//! \fn void AbstractTextEditor::autoCorrectEnabledChanged(bool enabled)
122
//! \brief Emitted when auto correction setting changes.
123
//! \param enabled New setting.
125
//! \fn void AbstractTextEditor::preeditEnabledChanged(bool enabled)
126
//! \brief Emitted when preedit setting changes.
127
//! \param enabled New setting.
129
//! \fn void AbstractTextEditor::wordCandidatesChanged(const WordCandidateList &word_candidates)
130
//! \brief Emitted when new word candidates are generated.
131
//! \param word_candidates New word candidates.
133
//! Note that the list might be empty as well to indicate that there
134
//! should be no word candidates.
136
//! \fn void AbstractTextEditor::keyboardClosed()
137
//! \brief Emitted when keyboard close is requested.
139
//! \class AbstractTextEditor::Replacement
140
//! \brief Plain struct containing beginning and length of replacement and
141
//! desired cursor position.
143
//! Mostly used by sendPreeditString() implementations.
145
//! \fn AbstractTextEditor::Replacement::Replacement()
146
//! \brief Constructor.
148
//! Constructs an instance with no replacement and no cursor position
151
//! \fn AbstractTextEditor::Replacement::Replacement(int position)
152
//! \brief Constructor.
153
//! \param position New cursor position.
155
//! Constructs an instance with no replacement and new cursor position.
157
//! \fn AbstractTextEditor::Replacement::Replacement(int r_start, int r_length, int position)
158
//! \brief Constructor.
159
//! \param r_start Replacement start.
160
//! \param r_length Replacement length.
161
//! \param position New cursor position.
163
//! Constructs an instance with a replacement and new cursor position.
165
//! \var AbstractTextEditor::Replacement::start
166
//! \brief Beginning of replacement.
168
//! \var AbstractTextEditor::Replacement::length
169
//! \brief Length of replacement.
171
//! \var AbstractTextEditor::Replacement::cursor_position
172
//! \brief New cursor position relative to the beginning of preedit.
176
//! \brief Checks whether given \a c is a word separator.
177
//! \param c Char to test.
179
//! Other way to do checks would be using isLetterOrNumber() + some
180
//! other methods. But UTF is so crazy that I am not sure whether
181
//! other strange categories are parts of the word or not. It is
182
//! easier to specify punctuations and whitespaces.
183
inline bool isSeparator(const QChar &c)
185
return (c.isPunct() or c.isSpace());
188
//! \brief Extracts a word boundaries at cursor position.
189
//! \param surrounding_text Text from which extraction will happen.
190
//! \param cursor_position Position of cursor within \a surrounding_text.
191
//! \param replacement Place where replacement data will be stored.
193
//! \return whether surrounding text was valid (not empty).
195
//! If cursor is placed right after the word, boundaries of this word
196
//! are extracted. Otherwise if cursor is placed right before the
197
//! word, then no word boundaries are stored - instead invalid
198
//! replacement is stored. It might happen that cursor position is
199
//! outside the string, so \a replacement will have fixed position.
200
bool extractWordBoundariesAtCursor(const QString& surrounding_text,
202
AbstractTextEditor::Replacement *replacement)
204
const int text_length(surrounding_text.length());
206
if (text_length == 0) {
210
// just in case - if cursor is far after last char in surrounding
211
// text we place it right after last char.
212
cursor_position = qBound(0, cursor_position, text_length);
214
// cursor might be placed in after last char (that is to say - its
215
// index might be the one of string terminator) - for simplifying
216
// the algorithm below we fake it as cursor is put on delimiter:
217
// "abc" - surrounding text
218
// | - cursor placement
219
// "abc " - fake surrounding text
220
const QString fake_surrounding_text(surrounding_text + " ");
221
const QChar *const fake_data(fake_surrounding_text.constData());
222
// begin is index of first char in a word
224
// end is index of a char after last char in a word.
225
// -2, because -2 - (-1) = -1 and we would like to
226
// have -1 as invalid length.
229
for (int iter(cursor_position); iter >= 0; --iter) {
230
const QChar &c(fake_data[iter]);
232
if (isSeparator(c)) {
233
if (iter != cursor_position) {
242
// take note that fake_data's last QChar is always a space.
243
for (int iter(cursor_position); iter <= text_length; ++iter) {
244
const QChar &c(fake_data[iter]);
247
if (isSeparator(c)) {
254
replacement->start = begin;
255
replacement->length = end - begin;
256
replacement->cursor_position = cursor_position;
262
} // unnamed namespace
264
EditorOptions::EditorOptions()
265
: backspace_auto_repeat_delay(500)
266
, backspace_auto_repeat_interval(300)
269
class AbstractTextEditorPrivate
272
QTimer auto_repeat_backspace_timer;
274
EditorOptions options;
275
QScopedPointer<Model::Text> text;
276
QScopedPointer<Logic::AbstractWordEngine> word_engine;
277
QScopedPointer<Logic::AbstractLanguageFeatures> language_features;
278
bool preedit_enabled;
279
bool auto_correct_enabled;
280
bool auto_caps_enabled;
281
int ignore_next_cursor_position;
282
QString ignore_next_surrounding_text;
284
explicit AbstractTextEditorPrivate(const EditorOptions &new_options,
285
Model::Text *new_text,
286
Logic::AbstractWordEngine *new_word_engine,
287
Logic::AbstractLanguageFeatures *new_language_features);
291
AbstractTextEditorPrivate::AbstractTextEditorPrivate(const EditorOptions &new_options,
292
Model::Text *new_text,
293
Logic::AbstractWordEngine *new_word_engine,
294
Logic::AbstractLanguageFeatures *new_language_features)
295
: auto_repeat_backspace_timer()
296
, backspace_sent(false)
297
, options(new_options)
299
, word_engine(new_word_engine)
300
, language_features(new_language_features)
301
, preedit_enabled(false)
302
, auto_correct_enabled(false)
303
, auto_caps_enabled(false)
304
, ignore_next_cursor_position(-1)
305
, ignore_next_surrounding_text()
307
auto_repeat_backspace_timer.setSingleShot(true);
311
bool AbstractTextEditorPrivate::valid() const
313
const bool is_invalid(text.isNull() || word_engine.isNull() || language_features.isNull());
316
qCritical() << __PRETTY_FUNCTION__
317
<< "Invalid text model, or no word engine given! The text editor will not function properly.";
320
return (not is_invalid);
323
//! \brief Constructor.
324
//! \param options Editor options.
325
//! \param text Text model.
326
//! \param word_engine Word engine.
327
//! \param language_features Language features.
328
//! \param parent Parent of this instance or \c NULL if none is needed.
330
//! Takes ownership of \a text, \a word_engine and \a language_features.
331
AbstractTextEditor::AbstractTextEditor(const EditorOptions &options,
333
Logic::AbstractWordEngine *word_engine,
334
Logic::AbstractLanguageFeatures *language_features,
337
, d_ptr(new AbstractTextEditorPrivate(options, text, word_engine, language_features))
339
connect(&d_ptr->auto_repeat_backspace_timer, SIGNAL(timeout()),
340
this, SLOT(autoRepeatBackspace()));
342
connect(word_engine, SIGNAL(enabledChanged(bool)),
343
this, SLOT(setPreeditEnabled(bool)));
345
connect(word_engine, SIGNAL(candidatesChanged(WordCandidateList)),
346
this, SIGNAL(wordCandidatesChanged(WordCandidateList)));
348
setPreeditEnabled(word_engine->isEnabled());
351
//! \brief Destructor.
352
AbstractTextEditor::~AbstractTextEditor()
355
//! \brief Gets editor's text model.
356
Model::Text * AbstractTextEditor::text() const
358
Q_D(const AbstractTextEditor);
359
return d->text.data();
362
//! \brief Gets editor's word engine.
363
Logic::AbstractWordEngine * AbstractTextEditor::wordEngine() const
365
Q_D(const AbstractTextEditor);
366
return d->word_engine.data();
369
//! \brief Reacts to key press.
370
//! \param key Pressed key.
372
//! For now it only checks whether backspace was pressed. In such case
373
//! preedit is commited and primary candidate is cleared.
374
void AbstractTextEditor::onKeyPressed(const Key &key)
376
Q_D(AbstractTextEditor);
378
if (not d->valid()) {
382
if (key.action() == Key::ActionBackspace) {
383
if (d->auto_correct_enabled && not d->text->primaryCandidate().isEmpty()) {
384
d->text->setPrimaryCandidate(QString());
385
d->backspace_sent = true;
387
d->backspace_sent = false;
391
d->auto_repeat_backspace_timer.start(d->options.backspace_auto_repeat_delay);
395
//! \brief Reacts to key release.
396
//! \param key Released key.
398
//! If common key is pressed then it is appended to preedit. If
399
//! backspace was pressed then preedit is commited and a character
400
//! before cursor is removed. If space is pressed then primary
401
//! candidate is applied if enabled. In other cases standard behaviour
403
void AbstractTextEditor::onKeyReleased(const Key &key)
405
Q_D(AbstractTextEditor);
407
if (not d->valid()) {
411
const QString &text(key.label().text());
412
Qt::Key event_key = Qt::Key_unknown;
414
switch(key.action()) {
415
case Key::ActionInsert:
416
d->text->appendToPreedit(text);
418
// computeCandidates can change preedit face, so needs to happen
419
// before sending preedit:
420
if (d->preedit_enabled) {
421
d->word_engine->computeCandidates(d->text.data());
424
sendPreeditString(d->text->preedit(), d->text->preeditFace(),
425
Replacement(d->text->cursorPosition()));
427
if (not d->preedit_enabled) {
433
case Key::ActionBackspace: {
436
if (not d->backspace_sent) {
437
event_key = Qt::Key_Backspace;
440
d->auto_repeat_backspace_timer.stop();
443
case Key::ActionSpace: {
444
const bool auto_caps_activated = d->language_features->activateAutoCaps(d->text->preedit());
445
const bool replace_preedit = d->auto_correct_enabled && not d->text->primaryCandidate().isEmpty();
447
if (replace_preedit) {
448
const QString &appendix = d->language_features->appendixForReplacedPreedit(d->text->preedit());
449
d->text->setPreedit(d->text->primaryCandidate());
450
d->text->appendToPreedit(appendix);
452
d->text->appendToPreedit(" ");
456
if (auto_caps_activated && d->auto_caps_enabled) {
457
Q_EMIT autoCapsActivated();
461
case Key::ActionReturn:
462
event_key = Qt::Key_Return;
465
case Key::ActionClose:
466
Q_EMIT keyboardClosed();
469
case Key::ActionLeft:
470
event_key = Qt::Key_Left;
474
event_key = Qt::Key_Up;
477
case Key::ActionRight:
478
event_key = Qt::Key_Right;
481
case Key::ActionDown:
482
event_key = Qt::Key_Down;
485
case Key::ActionCommand:
486
invokeAction(text, QKeySequence::fromString(key.commandSequence()));
488
case Key::ActionLeftLayout:
489
Q_EMIT leftLayoutSelected();
492
case Key::ActionRightLayout:
493
Q_EMIT rightLayoutSelected();
500
if (event_key != Qt::Key_unknown) {
502
QKeyEvent ev(QEvent::KeyPress, event_key, Qt::NoModifier);
507
//! \brief Reacts to sliding into a key.
508
//! \param key Slid in key.
510
//! For now it only set backspace repeat timer if we slide into
512
void AbstractTextEditor::onKeyEntered(const Key &key)
514
Q_D(AbstractTextEditor);
516
if (key.action() == Key::ActionBackspace) {
517
d->backspace_sent = false;
518
d->auto_repeat_backspace_timer.start(d->options.backspace_auto_repeat_delay);
522
//! \brief Reacts to sliding out of a key.
523
//! \param key Slid out key.
525
//! For now it only stops backspace repeat timer if we slide out
527
void AbstractTextEditor::onKeyExited(const Key &key)
529
Q_D(AbstractTextEditor);
531
if (key.action() == Key::ActionBackspace) {
532
d->auto_repeat_backspace_timer.stop();
536
//! \brief Replaces current preedit with given replacement
537
//! \param replacement New preedit.
538
void AbstractTextEditor::replacePreedit(const QString &replacement)
540
Q_D(AbstractTextEditor);
542
if (not d->valid()) {
546
d->text->setPreedit(replacement);
547
// computeCandidates can change preedit face, so needs to happen
548
// before sending preedit:
549
d->word_engine->computeCandidates(d->text.data());
550
sendPreeditString(d->text->preedit(), d->text->preeditFace());
553
//! \brief Replaces current preedit with given replacement and then
555
//! \param replacement New preedit string to commit.
556
void AbstractTextEditor::replaceAndCommitPreedit(const QString &replacement)
558
Q_D(AbstractTextEditor);
560
if (not d->valid()) {
564
const bool auto_caps_activated = d->language_features->activateAutoCaps(d->text->preedit());
565
const QString &appendix(d->language_features->appendixForReplacedPreedit(d->text->preedit()));
566
d->text->setPreedit(replacement);
567
d->text->appendToPreedit(appendix);
570
if (auto_caps_activated && d->auto_caps_enabled) {
571
Q_EMIT autoCapsActivated();
575
//! \brief Clears preedit.
576
void AbstractTextEditor::clearPreedit()
581
//! \brief Returns whether preedit functionality is enabled.
582
//! \sa preeditEnabled
583
bool AbstractTextEditor::isPreeditEnabled() const
585
Q_D(const AbstractTextEditor);
586
return d->preedit_enabled;
589
//! \brief Sets whether enable preedit functionality.
590
//! \param enabled \c true to enable preedit functionality.
591
//! \sa preeditEnabled
592
void AbstractTextEditor::setPreeditEnabled(bool enabled)
594
Q_D(AbstractTextEditor);
596
if (d->preedit_enabled != enabled) {
597
d->preedit_enabled = enabled;
598
Q_EMIT preeditEnabledChanged(d->preedit_enabled);
602
//! \brief Returns whether auto-correct functionality is enabled.
603
//! \sa autoCorrectEnabled
604
bool AbstractTextEditor::isAutoCorrectEnabled() const
606
Q_D(const AbstractTextEditor);
607
return d->auto_correct_enabled;
610
//! \brief Sets whether enable the auto-correct functionality.
611
//! \param enabled \c true to enable auto-correct functionality.
612
//! \sa autoCorrectEnabled
613
void AbstractTextEditor::setAutoCorrectEnabled(bool enabled)
615
Q_D(AbstractTextEditor);
617
if (d->auto_correct_enabled != enabled) {
618
d->auto_correct_enabled = enabled;
619
Q_EMIT autoCorrectEnabledChanged(d->auto_correct_enabled);
623
bool AbstractTextEditor::isAutoCapsEnabled() const
625
Q_D(const AbstractTextEditor);
626
return d->auto_caps_enabled;
629
void AbstractTextEditor::setAutoCapsEnabled(bool enabled)
631
Q_D(AbstractTextEditor);
633
if (d->auto_caps_enabled != enabled) {
634
d->auto_caps_enabled = enabled;
635
Q_EMIT autoCapsEnabledChanged(d->auto_caps_enabled);
639
//! \brief Commits current preedit.
640
void AbstractTextEditor::commitPreedit()
642
Q_D(AbstractTextEditor);
644
if (not d->valid() || d->text->preedit().isEmpty()) {
648
sendCommitString(d->text->preedit());
649
d->text->commitPreedit();
650
d->word_engine->clearCandidates();
653
// TODO: this implementation does not take into account following features:
655
// if there is preedit then first call to autoRepeatBackspace should clean it completely
656
// and following calls should remove remaining text character by character
658
// it is not completely clean how to handle multitouch for backspace,
659
// but we can follow the strategy from meego-keyboard - release pressed
660
// key when user press another one at the same time. Then we do not need to
661
// change anything in this method
662
//! \brief Sends backspace and sets backspace repeat timer.
663
void AbstractTextEditor::autoRepeatBackspace()
665
Q_D(AbstractTextEditor);
667
QKeyEvent ev(QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier);
669
d->backspace_sent = true;
670
d->auto_repeat_backspace_timer.start(d->options.backspace_auto_repeat_interval);
673
//! \brief Emits wordCandidatesChanged() signal with current preedit
675
void AbstractTextEditor::showUserCandidate()
677
Q_D(AbstractTextEditor);
679
if (d->text->preedit().isEmpty()) {
683
WordCandidateList candidates;
684
WordCandidate candidate(WordCandidate::SourceUser, d->text->preedit());
686
candidates << candidate;
688
Q_EMIT wordCandidatesChanged(candidates);
691
//! \brief Adds \a word to user dictionary.
692
//! \param word Word to be added.
693
void AbstractTextEditor::addToUserDictionary(const QString &word)
695
Q_D(AbstractTextEditor);
697
d->word_engine->addToUserDictionary(word);
698
d->text->setPrimaryCandidate(word);
700
Q_EMIT wordCandidatesChanged(WordCandidateList());
703
//! \brief Sends preedit string to application with no replacement.
704
//! \param preedit Preedit to send.
705
//! \param face Face of the preedit.
706
void AbstractTextEditor::sendPreeditString(const QString &preedit,
707
Model::Text::PreeditFace face)
709
sendPreeditString(preedit, face, Replacement());
712
//! \brief Reacts to cursor position change in application's text
714
//! \param cursor_position new cursor position
715
//! \param surrounding_text surrounding text of a preedit
717
//! Extract words with the cursor inside and replaces it with a preedit.
718
//! This is called preedit activation.
719
void AbstractTextEditor::onCursorPositionChanged(int cursor_position,
720
const QString &surrounding_text)
722
Q_D(AbstractTextEditor);
725
if (not extractWordBoundariesAtCursor(surrounding_text, cursor_position, &r)) {
729
if (r.start < 0 or r.length < 0) {
730
if (d->ignore_next_surrounding_text == surrounding_text and
731
d->ignore_next_cursor_position == cursor_position) {
732
d->ignore_next_surrounding_text.clear();
733
d->ignore_next_cursor_position = -1;
735
d->text->setPreedit("");
736
d->text->setCursorPosition(0);
739
const int cursor_pos_relative_word_begin(r.start - r.cursor_position);
740
const int word_begin_relative_cursor_pos(r.cursor_position - r.start);
741
const QString word(surrounding_text.mid(r.start, r.length));
742
Replacement word_r(cursor_pos_relative_word_begin, r.length,
743
word_begin_relative_cursor_pos);
745
d->text->setPreedit(word, word_begin_relative_cursor_pos);
746
// computeCandidates can change preedit face, so needs to happen
747
// before sending preedit:
748
d->word_engine->computeCandidates(d->text.data());
749
sendPreeditString(d->text->preedit(), d->text->preeditFace(), word_r);
750
// Qt is going to send us an event with cursor position places
751
// at the beginning of replaced word and surrounding text
752
// without the replaced word. We want to ignore it.
753
d->ignore_next_cursor_position = r.start;
754
d->ignore_next_surrounding_text = QString(surrounding_text).remove(r.start, r.length);
758
//! \brief sets language features
759
//! \param language id as string (as found in settings file)
761
void AbstractTextEditor::onLanguageChanged(const QString& languageId)
763
Q_D(AbstractTextEditor);
765
if (languageId == "zh_cn_pinyin")
766
d->language_features.reset(new Logic::ChineseLanguageFeatures);
768
d->language_features.reset(new Logic::LanguageFeatures);
771
} // namespace MaliitKeyboard