1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include <QtVirtualKeyboard/private/shifthandler_p.h> |
5 | #include <QtVirtualKeyboard/private/qvirtualkeyboardinputcontext_p.h> |
6 | #include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h> |
7 | #include <QtVirtualKeyboard/qvirtualkeyboardinputengine.h> |
8 | #include <QtCore/private/qobject_p.h> |
9 | #include <QSet> |
10 | #include <QGuiApplication> |
11 | #include <QElapsedTimer> |
12 | #include <QStyleHints> |
13 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | size_t qHash(QLocale::Language lang, size_t seed) |
17 | { |
18 | return qHash(key: ushort(lang), seed); |
19 | } |
20 | |
21 | namespace QtVirtualKeyboard { |
22 | |
23 | class ShiftHandlerPrivate : public QObjectPrivate |
24 | { |
25 | public: |
26 | ShiftHandlerPrivate() : |
27 | QObjectPrivate(), |
28 | inputContext(nullptr), |
29 | sentenceEndingCharacters(QLatin1String(".!?" ) + QChar(Qt::Key_exclamdown) + QChar(Qt::Key_questiondown)), |
30 | autoCapitalizationEnabled(false), |
31 | toggleShiftEnabled(false), |
32 | shift(false), |
33 | shiftChanged(false), |
34 | capsLock(false), |
35 | resetWhenVisible(false), |
36 | manualShiftLanguageFilter(QSet<QLocale::Language>() << QLocale::Arabic << QLocale::Persian << QLocale::Hindi << QLocale::Korean << QLocale::Thai), |
37 | manualCapsInputModeFilter(QSet<QVirtualKeyboardInputEngine::InputMode>() << QVirtualKeyboardInputEngine::InputMode::Cangjie << QVirtualKeyboardInputEngine::InputMode::Zhuyin << QVirtualKeyboardInputEngine::InputMode::Hebrew), |
38 | noAutoUppercaseInputModeFilter(QSet<QVirtualKeyboardInputEngine::InputMode>() << QVirtualKeyboardInputEngine::InputMode::FullwidthLatin << QVirtualKeyboardInputEngine::InputMode::Pinyin << QVirtualKeyboardInputEngine::InputMode::Cangjie << QVirtualKeyboardInputEngine::InputMode::Zhuyin << QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting << QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting << QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting << QVirtualKeyboardInputEngine::InputMode::Romaji), |
39 | allCapsInputModeFilter(QSet<QVirtualKeyboardInputEngine::InputMode>() << QVirtualKeyboardInputEngine::InputMode::Hiragana << QVirtualKeyboardInputEngine::InputMode::Katakana) |
40 | { |
41 | } |
42 | |
43 | QVirtualKeyboardInputContext *inputContext; |
44 | QString sentenceEndingCharacters; |
45 | bool autoCapitalizationEnabled; |
46 | bool toggleShiftEnabled; |
47 | bool shift; |
48 | bool shiftChanged; |
49 | bool capsLock; |
50 | bool resetWhenVisible; |
51 | QLocale locale; |
52 | QElapsedTimer timer; |
53 | const QSet<QLocale::Language> manualShiftLanguageFilter; |
54 | const QSet<QVirtualKeyboardInputEngine::InputMode> manualCapsInputModeFilter; |
55 | const QSet<QVirtualKeyboardInputEngine::InputMode> noAutoUppercaseInputModeFilter; |
56 | const QSet<QVirtualKeyboardInputEngine::InputMode> allCapsInputModeFilter; |
57 | }; |
58 | |
59 | /*! |
60 | \qmltype ShiftHandler |
61 | \inqmlmodule QtQuick.VirtualKeyboard |
62 | \ingroup qtvirtualkeyboard-internal-qml |
63 | \instantiates QtVirtualKeyboard::ShiftHandler |
64 | \brief Manages the shift state. |
65 | */ |
66 | |
67 | /*! |
68 | \class QtVirtualKeyboard::ShiftHandler |
69 | \internal |
70 | \inmodule QtVirtualKeyboard |
71 | \brief Manages the shift state. |
72 | */ |
73 | |
74 | ShiftHandler::ShiftHandler(QVirtualKeyboardInputContext *parent) : |
75 | QObject(*new ShiftHandlerPrivate(), parent) |
76 | { |
77 | Q_D(ShiftHandler); |
78 | d->inputContext = parent; |
79 | } |
80 | |
81 | void ShiftHandler::init() |
82 | { |
83 | Q_D(ShiftHandler); |
84 | connect(asender: d->inputContext, SIGNAL(inputMethodHintsChanged()), SLOT(restart())); |
85 | connect(asender: d->inputContext->priv(), SIGNAL(inputItemChanged()), SLOT(restart())); |
86 | connect(asender: d->inputContext->inputEngine(), SIGNAL(inputModeChanged()), SLOT(restart())); |
87 | connect(asender: d->inputContext, SIGNAL(preeditTextChanged()), SLOT(autoCapitalize())); |
88 | connect(asender: d->inputContext, SIGNAL(surroundingTextChanged()), SLOT(autoCapitalize())); |
89 | connect(asender: d->inputContext, SIGNAL(cursorPositionChanged()), SLOT(autoCapitalize())); |
90 | connect(asender: d->inputContext, SIGNAL(localeChanged()), SLOT(localeChanged())); |
91 | connect(qGuiApp->inputMethod(), SIGNAL(visibleChanged()), SLOT(inputMethodVisibleChanged())); |
92 | d->locale = QLocale(d->inputContext->locale()); |
93 | } |
94 | |
95 | /*! |
96 | \internal |
97 | */ |
98 | ShiftHandler::~ShiftHandler() |
99 | { |
100 | |
101 | } |
102 | |
103 | QString ShiftHandler::sentenceEndingCharacters() const |
104 | { |
105 | Q_D(const ShiftHandler); |
106 | return d->sentenceEndingCharacters; |
107 | } |
108 | |
109 | void ShiftHandler::setSentenceEndingCharacters(const QString &value) |
110 | { |
111 | Q_D(ShiftHandler); |
112 | if (d->sentenceEndingCharacters != value) { |
113 | d->sentenceEndingCharacters = value; |
114 | autoCapitalize(); |
115 | emit sentenceEndingCharactersChanged(); |
116 | } |
117 | } |
118 | |
119 | bool ShiftHandler::isAutoCapitalizationEnabled() const |
120 | { |
121 | Q_D(const ShiftHandler); |
122 | return d->autoCapitalizationEnabled; |
123 | } |
124 | |
125 | bool ShiftHandler::isToggleShiftEnabled() const |
126 | { |
127 | Q_D(const ShiftHandler); |
128 | return d->toggleShiftEnabled; |
129 | } |
130 | |
131 | bool ShiftHandler::isShiftActive() const |
132 | { |
133 | Q_D(const ShiftHandler); |
134 | return d->shift; |
135 | } |
136 | |
137 | void ShiftHandler::setShiftActive(bool active) |
138 | { |
139 | Q_D(ShiftHandler); |
140 | if (d->shift != active) { |
141 | d->shift = active; |
142 | d->shiftChanged = true; |
143 | emit shiftActiveChanged(); |
144 | if (!d->capsLock) |
145 | emit uppercaseChanged(); |
146 | } |
147 | } |
148 | |
149 | bool ShiftHandler::isCapsLockActive() const |
150 | { |
151 | Q_D(const ShiftHandler); |
152 | return d->capsLock; |
153 | } |
154 | |
155 | void ShiftHandler::setCapsLockActive(bool active) |
156 | { |
157 | Q_D(ShiftHandler); |
158 | if (d->capsLock != active) { |
159 | d->capsLock = active; |
160 | emit capsLockActiveChanged(); |
161 | if (!d->shift) |
162 | emit uppercaseChanged(); |
163 | } |
164 | } |
165 | |
166 | bool ShiftHandler::isUppercase() const |
167 | { |
168 | Q_D(const ShiftHandler); |
169 | return d->shift || d->capsLock; |
170 | } |
171 | |
172 | /*! |
173 | \since 1.2 |
174 | |
175 | \qmlmethod void ShiftHandler::toggleShift() |
176 | |
177 | Toggles the current shift state. |
178 | |
179 | This method provides the functionality of the shift key. |
180 | |
181 | \sa toggleShiftEnabled |
182 | */ |
183 | /*! |
184 | \since 1.2 |
185 | \internal |
186 | |
187 | \fn void QtVirtualKeyboard::ShiftHandler::toggleShift() |
188 | |
189 | Toggles the current shift state. |
190 | |
191 | This method provides the functionality of the shift key. |
192 | |
193 | \sa toggleShiftEnabled |
194 | */ |
195 | void ShiftHandler::toggleShift() |
196 | { |
197 | Q_D(ShiftHandler); |
198 | if (!d->toggleShiftEnabled) |
199 | return; |
200 | if (d->manualShiftLanguageFilter.contains(value: d->locale.language())) { |
201 | setCapsLockActive(false); |
202 | setShiftActive(!d->shift); |
203 | } else if (d->manualCapsInputModeFilter.contains(value: d->inputContext->inputEngine()->inputMode())) { |
204 | bool capsLock = d->capsLock; |
205 | setCapsLockActive(!capsLock); |
206 | setShiftActive(!capsLock); |
207 | } else { |
208 | if (d->capsLock) { |
209 | setCapsLockActive(!d->capsLock && d->shift && !d->shiftChanged); |
210 | } |
211 | |
212 | QStyleHints *style = QGuiApplication::styleHints(); |
213 | |
214 | if (!d->timer.isValid() || d->timer.elapsed() > style->mouseDoubleClickInterval()) { |
215 | d->timer.restart(); |
216 | } else if (d->timer.elapsed() < style->mouseDoubleClickInterval() && !d->capsLock) { |
217 | setCapsLockActive(!d->capsLock && d->shift && !d->shiftChanged); |
218 | } |
219 | |
220 | setShiftActive(d->capsLock || !d->shift); |
221 | d->shiftChanged = false; |
222 | } |
223 | } |
224 | |
225 | /*! Clears the toggle shift timer. |
226 | \internal |
227 | |
228 | */ |
229 | void ShiftHandler::clearToggleShiftTimer() |
230 | { |
231 | Q_D(ShiftHandler); |
232 | d->timer.invalidate(); |
233 | } |
234 | |
235 | void ShiftHandler::reset() |
236 | { |
237 | Q_D(ShiftHandler); |
238 | if (d->inputContext->priv()->inputItem() || QT_VIRTUALKEYBOARD_FORCE_EVENTS_WITHOUT_FOCUS) { |
239 | Qt::InputMethodHints inputMethodHints = d->inputContext->inputMethodHints(); |
240 | QVirtualKeyboardInputEngine::InputMode inputMode = d->inputContext->inputEngine()->inputMode(); |
241 | bool preferUpperCase = (inputMethodHints & (Qt::ImhPreferUppercase | Qt::ImhUppercaseOnly)); |
242 | bool autoCapitalizationEnabled = !(d->inputContext->inputMethodHints() & (Qt::ImhNoAutoUppercase | |
243 | Qt::ImhUppercaseOnly | Qt::ImhLowercaseOnly | Qt::ImhEmailCharactersOnly | |
244 | Qt::ImhUrlCharactersOnly | Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | |
245 | Qt::ImhDigitsOnly)) && !d->noAutoUppercaseInputModeFilter.contains(value: inputMode); |
246 | bool toggleShiftEnabled = !(inputMethodHints & (Qt::ImhUppercaseOnly | Qt::ImhLowercaseOnly)); |
247 | // For filtered languages reset the initial shift status to lower case |
248 | // and allow manual shift change |
249 | if (d->manualShiftLanguageFilter.contains(value: d->locale.language()) || |
250 | d->manualCapsInputModeFilter.contains(value: inputMode)) { |
251 | preferUpperCase = false; |
252 | autoCapitalizationEnabled = false; |
253 | toggleShiftEnabled = true; |
254 | } else if (d->allCapsInputModeFilter.contains(value: inputMode)) { |
255 | preferUpperCase = true; |
256 | autoCapitalizationEnabled = false; |
257 | toggleShiftEnabled = false; |
258 | } |
259 | setToggleShiftEnabled(toggleShiftEnabled); |
260 | setAutoCapitalizationEnabled(autoCapitalizationEnabled); |
261 | setCapsLockActive(preferUpperCase); |
262 | if (preferUpperCase) |
263 | setShiftActive(preferUpperCase); |
264 | else |
265 | autoCapitalize(); |
266 | } |
267 | } |
268 | |
269 | void ShiftHandler::autoCapitalize() |
270 | { |
271 | Q_D(ShiftHandler); |
272 | if (d->capsLock) |
273 | return; |
274 | if (!d->autoCapitalizationEnabled || !d->inputContext->preeditText().isEmpty()) { |
275 | setShiftActive(false); |
276 | } else { |
277 | int cursorPosition = d->inputContext->cursorPosition(); |
278 | bool preferLowerCase = d->inputContext->inputMethodHints() & Qt::ImhPreferLowercase; |
279 | if (cursorPosition == 0) { |
280 | setShiftActive(!preferLowerCase); |
281 | } else { // space after sentence-ending character triggers auto-capitalization |
282 | QString text = d->inputContext->surroundingText(); |
283 | text.truncate(pos: cursorPosition); |
284 | if (text.trimmed().size() == 0) |
285 | setShiftActive(!preferLowerCase); |
286 | else if (text.endsWith(c: QLatin1Char(' '))) |
287 | setShiftActive(d->sentenceEndingCharacters.contains(c: QStringView{text}.right(n: 2)[0]) |
288 | && !preferLowerCase); |
289 | else |
290 | setShiftActive(false); |
291 | } |
292 | } |
293 | } |
294 | |
295 | void ShiftHandler::restart() |
296 | { |
297 | Q_D(ShiftHandler); |
298 | const QGuiApplication *app = qGuiApp; |
299 | if (!app || !app->inputMethod()->isVisible()) { |
300 | d->resetWhenVisible = true; |
301 | return; |
302 | } |
303 | reset(); |
304 | } |
305 | |
306 | void ShiftHandler::localeChanged() |
307 | { |
308 | Q_D(ShiftHandler); |
309 | d->locale = QLocale(d->inputContext->locale()); |
310 | restart(); |
311 | } |
312 | |
313 | void ShiftHandler::inputMethodVisibleChanged() |
314 | { |
315 | Q_D(ShiftHandler); |
316 | if (!d->resetWhenVisible) |
317 | return; |
318 | |
319 | const QGuiApplication *app = qGuiApp; |
320 | if (app && app->inputMethod()->isVisible()) { |
321 | d->resetWhenVisible = false; |
322 | reset(); |
323 | } |
324 | } |
325 | |
326 | void ShiftHandler::setAutoCapitalizationEnabled(bool enabled) |
327 | { |
328 | Q_D(ShiftHandler); |
329 | if (d->autoCapitalizationEnabled != enabled) { |
330 | d->autoCapitalizationEnabled = enabled; |
331 | emit autoCapitalizationEnabledChanged(); |
332 | } |
333 | } |
334 | |
335 | void ShiftHandler::setToggleShiftEnabled(bool enabled) |
336 | { |
337 | Q_D(ShiftHandler); |
338 | if (d->toggleShiftEnabled != enabled) { |
339 | d->toggleShiftEnabled = enabled; |
340 | emit toggleShiftEnabledChanged(); |
341 | } |
342 | } |
343 | |
344 | /*! |
345 | \property QtVirtualKeyboard::ShiftHandler::sentenceEndingCharacters |
346 | |
347 | This property specifies the sentence ending characters which |
348 | will cause shift state change. |
349 | |
350 | By default, the property is initialized to sentence |
351 | ending characters found in the ASCII range (i.e. ".!?"). |
352 | */ |
353 | |
354 | /*! |
355 | \qmlproperty string ShiftHandler::sentenceEndingCharacters |
356 | |
357 | This property specifies the sentence ending characters which |
358 | will cause shift state change. |
359 | |
360 | By default, the property is initialized to sentence |
361 | ending characters found in the ASCII range (i.e. ".!?"). |
362 | */ |
363 | |
364 | /*! |
365 | \since 1.2 |
366 | |
367 | \property QtVirtualKeyboard::ShiftHandler::autoCapitalizationEnabled |
368 | |
369 | This property provides the current state of the automatic |
370 | capitalization feature. |
371 | */ |
372 | |
373 | /*! |
374 | \since 1.2 |
375 | |
376 | \qmlproperty bool ShiftHandler::autoCapitalizationEnabled |
377 | |
378 | This property provides the current state of the automatic |
379 | capitalization feature. |
380 | */ |
381 | |
382 | /*! |
383 | \since 1.2 |
384 | |
385 | \property QtVirtualKeyboard::ShiftHandler::toggleShiftEnabled |
386 | |
387 | This property provides the current state of the toggleShift() |
388 | method. When true, the current shift state can be changed by |
389 | calling the toggleShift() method. |
390 | */ |
391 | |
392 | /*! |
393 | \since 1.2 |
394 | |
395 | \qmlproperty bool ShiftHandler::toggleShiftEnabled |
396 | |
397 | This property provides the current state of the toggleShift() |
398 | method. When true, the current shift state can be changed by |
399 | calling the toggleShift() method. |
400 | */ |
401 | |
402 | } // namespace QtVirtualKeyboard |
403 | QT_END_NAMESPACE |
404 | |