Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwasminputcontext.cpp
Go to the documentation of this file.
1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
5#include "qwasmwindow.h"
7
8#include <QRectF>
9#include <QLoggingCategory>
10#include <qguiapplication.h>
11#include <qwindow.h>
12#include <qpa/qplatforminputcontext.h>
13#include <qpa/qwindowsysteminterface.h>
14#if QT_CONFIG(clipboard)
15#include <QClipboard>
16#endif
17#include <QtGui/qtextobject.h>
18
20
21Q_LOGGING_CATEGORY(qLcQpaWasmInputContext, "qt.qpa.wasm.inputcontext")
22
23using namespace qstdweb;
24
25void QWasmInputContext::inputCallback(emscripten::val event)
26{
27 emscripten::val inputType = event["inputType"];
28 if (inputType.isNull() || inputType.isUndefined())
29 return;
30 const auto inputTypeString = inputType.as<std::string>();
31
32 // also may be dataTransfer
33 // containing rich text
34 emscripten::val inputData = event["data"];
35 QString inputStr = (!inputData.isNull() && !inputData.isUndefined())
36 ? QString::fromEcmaString(inputData) : QString();
37
38 // There are many inputTypes for InputEvent
39 // https://www.w3.org/TR/input-events-1/
40 // Some of them should be implemented here later.
41 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType : " << inputTypeString;
42 if (!inputTypeString.compare("deleteContentBackward")) {
43
44 QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
45 QCoreApplication::sendEvent(m_focusObject, &queryEvent);
46 int cursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
47 int deleteLength = rangesPair.second - rangesPair.first;
48 int deleteFrom = -1;
49
50 if (cursorPosition >= rangesPair.first) {
51 deleteFrom = -(cursorPosition - rangesPair.first);
52 }
53 QInputMethodEvent e;
54 e.setCommitString(QString(), deleteFrom, deleteLength);
55 QCoreApplication::sendEvent(m_focusObject, &e);
56
57 rangesPair.first = 0;
58 rangesPair.second = 0;
59
60 event.call<void>("stopImmediatePropagation");
61 return;
62 } else if (!inputTypeString.compare("deleteContentForward")) {
63 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);
64 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Delete, Qt::NoModifier);
65 event.call<void>("stopImmediatePropagation");
66 return;
67 } else if (!inputTypeString.compare("insertCompositionText")) {
68 qCDebug(qLcQpaWasmInputContext) << "insertCompositionText : " << inputStr;
69 event.call<void>("stopImmediatePropagation");
70
71 QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
72 QCoreApplication::sendEvent(m_focusObject, &queryEvent);
73
74 int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt() ;
75 int replaceIndex = (qCursorPosition - rangesPair.first);
76 int replaceLength = rangesPair.second - rangesPair.first;
77
78 setPreeditString(inputStr, replaceIndex);
79 insertPreedit(replaceLength);
80
81 rangesPair.first = 0;
82 rangesPair.second = 0;
83 event.call<void>("stopImmediatePropagation");
84 return;
85 } else if (!inputTypeString.compare("insertReplacementText")) {
86 // the previous input string up to the space, needs replaced with this
87 // used on iOS when continuing composition after focus change
88 // there's no range given
89
90 qCDebug(qLcQpaWasmInputContext) << "insertReplacementText >>>>" << "inputString : " << inputStr;
91 emscripten::val ranges = event.call<emscripten::val>("getTargetRanges");
92
93 m_preeditString.clear();
94 std::string elementString = m_inputElement["value"].as<std::string>();
95 QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
96 QCoreApplication::sendEvent(m_focusObject, &queryEvent);
97 QString textFieldString = queryEvent.value(Qt::ImTextBeforeCursor).toString();
98 int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
99
100 if (rangesPair.first != 0 || rangesPair.second != 0) {
101
102 int replaceIndex = (qCursorPosition - rangesPair.first);
103 int replaceLength = rangesPair.second - rangesPair.first;
104 replaceText(inputStr, -replaceIndex, replaceLength);
105 rangesPair.first = 0;
106 rangesPair.second = 0;
107
108 } else {
109 int spaceIndex = textFieldString.lastIndexOf(' ') + 1;
110 int replaceIndex = (qCursorPosition - spaceIndex);
111
112 replaceText(inputStr, -replaceIndex, replaceIndex);
113 }
114
115 event.call<void>("stopImmediatePropagation");
116
117 return;
118 } else if (!inputTypeString.compare("deleteCompositionText")) {
119 setPreeditString("", 0);
120 insertPreedit();
121 event.call<void>("stopImmediatePropagation");
122 return;
123 } else if (!inputTypeString.compare("insertFromComposition")) {
124 setPreeditString(inputStr, 0);
125 insertPreedit();
126 event.call<void>("stopImmediatePropagation");
127 return;
128 } else if (!inputTypeString.compare("insertText")) {
129 if ((rangesPair.first != 0 || rangesPair.second != 0)
130 && rangesPair.first != rangesPair.second) {
131
132 QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
133 QCoreApplication::sendEvent(m_focusObject, &queryEvent);
134
135 int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
136 int replaceIndex = (qCursorPosition - rangesPair.first);
137 int replaceLength = rangesPair.second - rangesPair.first;
138
139 replaceText(inputStr, -replaceIndex, replaceLength);
140
141 rangesPair.first = 0;
142 rangesPair.second = 0;
143
144 } else {
145 insertText(inputStr);
146 }
147
148 event.call<void>("stopImmediatePropagation");
149#if QT_CONFIG(clipboard)
150 } else if (!inputTypeString.compare("insertFromPaste")) {
151 insertText(QGuiApplication::clipboard()->text());
152 event.call<void>("stopImmediatePropagation");
153 // These can be supported here,
154 // But now, keyCallback in QWasmWindow
155 // will take them as exceptions.
156 //} else if (!inputTypeString.compare("deleteByCut")) {
157#endif
158 } else {
159 qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType \"" <<
160 inputType.as<std::string>() << "\" is not supported in Qt yet";
161 }
162}
163
164void QWasmInputContext::compositionEndCallback(emscripten::val event)
165{
166 const auto inputStr = QString::fromEcmaString(event["data"]);
167
168 if (preeditString().isEmpty()) // we get final results from inputCallback
169 return;
170
171 if (inputStr != preeditString()) {
172 qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO
173 << "Composition string" << inputStr
174 << "is differ from" << preeditString();
175 }
176 commitPreeditAndClear();
177}
178
179void QWasmInputContext::compositionStartCallback(emscripten::val event)
180{
181 Q_UNUSED(event);
182
183 // Do nothing when starting composition
184}
185
186void QWasmInputContext::compositionUpdateCallback(emscripten::val event)
187{
188 const auto compositionStr = QString::fromEcmaString(event["data"]);
189 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << compositionStr;
190
191 setPreeditString(compositionStr, 0);
192}
193
194void QWasmInputContext::beforeInputCallback(emscripten::val event)
195{
196 emscripten::val ranges = event.call<emscripten::val>("getTargetRanges");
197
198 auto length = ranges["length"].as<int>();
199 for (auto i = 0; i < length; i++) {
200 emscripten::val range = ranges[i];
201 qCDebug(qLcQpaWasmInputContext) << "startOffset" << range["startOffset"].as<int>();
202 qCDebug(qLcQpaWasmInputContext) << "endOffset" << range["endOffset"].as<int>();
203 rangesPair.first = range["startOffset"].as<int>();
204 rangesPair.second = range["endOffset"].as<int>();
205 }
206}
207
208QWasmInputContext::QWasmInputContext()
209{
210 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
211}
212
213QWasmInputContext::~QWasmInputContext()
214{
215}
216
217void QWasmInputContext::update(Qt::InputMethodQueries queries)
218{
219 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << queries;
220
221 if ((queries & Qt::ImEnabled) && (inputMethodAccepted() != m_inputMethodAccepted)) {
222 if (m_focusObject && !m_preeditString.isEmpty())
223 commitPreeditAndClear();
224 updateInputElement();
225 }
226 QPlatformInputContext::update(queries);
227}
228
229void QWasmInputContext::showInputPanel()
230{
231 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
232
233 // Note: showInputPanel not necessarily called, we shall
234 // still accept input if we have a focus object and
235 // inputMethodAccepted().
236 updateInputElement();
237}
238
239void QWasmInputContext::updateGeometry()
240{
241 if (QWasmAccessibility::isEnabled())
242 return;
243
244 if (m_inputElement.isNull())
245 return;
246
247 const QWindow *focusWindow = QGuiApplication::focusWindow();
248 if (!m_focusObject || !focusWindow || !m_inputMethodAccepted) {
249 m_inputElement["style"].set("left", "0px");
250 m_inputElement["style"].set("top", "0px");
251 } else {
252 Q_ASSERT(focusWindow);
253 Q_ASSERT(m_focusObject);
254 Q_ASSERT(m_inputMethodAccepted);
255
256 const QRect inputItemRectangle = QPlatformInputContext::inputItemRectangle().toRect();
257 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "propagating inputItemRectangle:" << inputItemRectangle;
258 m_inputElement["style"].set("left", std::to_string(inputItemRectangle.x()) + "px");
259 m_inputElement["style"].set("top", std::to_string(inputItemRectangle.y()) + "px");
260 m_inputElement["style"].set("width", "1px");
261 m_inputElement["style"].set("height", "1px");
262 }
263}
264
265void QWasmInputContext::updateInputElement()
266{
267 m_inputMethodAccepted = inputMethodAccepted();
268
269 if (QWasmAccessibility::isEnabled())
270 return;
271
272 // Mobile devices can dismiss keyboard/IME and focus is still on input.
273 // Successive clicks on the same input should open the keyboard/IME.
274 updateGeometry();
275
276 // If there is no focus object, or no visible input panel, remove focus
277 QWasmWindow *focusWindow = QWasmWindow::fromWindow(QGuiApplication::focusWindow());
278 if (!m_focusObject || !focusWindow || !m_inputMethodAccepted) {
279 if (!m_inputElement.isNull()) {
280 m_inputElement.set("value", "");
281 m_inputElement.set("inputMode", std::string("none"));
282 }
283
284 if (focusWindow) {
285 focusWindow->focus();
286 } else {
287 if (!m_inputElement.isNull())
288 m_inputElement.call<void>("blur");
289 }
290
291 m_inputElement = emscripten::val::null();
292 return;
293 }
294
295 Q_ASSERT(focusWindow);
296 Q_ASSERT(m_focusObject);
297 Q_ASSERT(m_inputMethodAccepted);
298
299 m_inputElement = focusWindow->inputElement();
300
301 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << QRectF::fromDOMRect(m_inputElement.call<emscripten::val>("getBoundingClientRect"));
302
303 // Set the text input
304 QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
305 QCoreApplication::sendEvent(m_focusObject, &queryEvent);
306 qCDebug(qLcQpaWasmInputContext) << "Qt surrounding text: " << queryEvent.value(Qt::ImSurroundingText).toString();
307 qCDebug(qLcQpaWasmInputContext) << "Qt current selection: " << queryEvent.value(Qt::ImCurrentSelection).toString();
308 qCDebug(qLcQpaWasmInputContext) << "Qt text before cursor: " << queryEvent.value(Qt::ImTextBeforeCursor).toString();
309 qCDebug(qLcQpaWasmInputContext) << "Qt text after cursor: " << queryEvent.value(Qt::ImTextAfterCursor).toString();
310 qCDebug(qLcQpaWasmInputContext) << "Qt cursor position: " << queryEvent.value(Qt::ImCursorPosition).toInt();
311 qCDebug(qLcQpaWasmInputContext) << "Qt anchor position: " << queryEvent.value(Qt::ImAnchorPosition).toInt();
312
313 m_inputElement.set("value", queryEvent.value(Qt::ImSurroundingText).toString().toStdString());
314
315 m_inputElement.set("selectionStart", queryEvent.value(Qt::ImAnchorPosition).toUInt());
316 m_inputElement.set("selectionEnd", queryEvent.value(Qt::ImCursorPosition).toUInt());
317
318 QInputMethodQueryEvent query((Qt::InputMethodQueries(Qt::ImHints)));
319 QCoreApplication::sendEvent(m_focusObject, &query);
320 if (Qt::InputMethodHints(query.value(Qt::ImHints).toInt()).testFlag(Qt::ImhHiddenText))
321 m_inputElement.set("type", "password");
322 else
323 m_inputElement.set("type", "text");
324
325// change inputmode to suit Qt imhints
326
327 Qt::InputMethodHints imHints = static_cast<Qt::InputMethodHints>(query.value(Qt::ImHints).toInt());
328 std::string inMode = "text";
329
330 if (imHints & Qt::ImhDigitsOnly)
331 inMode = "numeric";
332 if (imHints & Qt::ImhFormattedNumbersOnly)
333 inMode = "decimal";
334 if (imHints & Qt::ImhDialableCharactersOnly)
335 inMode = "tel";
336 if (imHints & Qt::ImhEmailCharactersOnly)
337 inMode = "email";
338 if (imHints & Qt::ImhUrlCharactersOnly)
339 inMode = "url";
340 // search ??
341 m_inputElement.set("inputMode",inMode);
342
343 m_inputElement.call<void>("focus");
344}
345
346void QWasmInputContext::setFocusObject(QObject *object)
347{
348 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << object << inputMethodAccepted();
349
350 // Commit the previous composition before change m_focusObject
351 if (m_focusObject && !m_preeditString.isEmpty())
352 commitPreeditAndClear();
353
354 m_focusObject = object;
355
356 updateInputElement();
357 QPlatformInputContext::setFocusObject(object);
358}
359
360void QWasmInputContext::hideInputPanel()
361{
362 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
363
364 // hide only if m_focusObject does not exist
365 if (!m_focusObject)
366 updateInputElement();
367}
368
369void QWasmInputContext::setPreeditString(QString preeditStr, int replaceSize)
370{
371 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << preeditStr << replaceSize;
372 m_preeditString = preeditStr;
373 m_replaceIndex = replaceSize;
374}
375
376void QWasmInputContext::insertPreedit(int replaceLength)
377{
378 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString;
379 if (replaceLength == 0)
380 replaceLength = m_preeditString.length();
381
382 QList<QInputMethodEvent::Attribute> attributes;
383 {
384 QInputMethodEvent::Attribute attr_cursor(QInputMethodEvent::Cursor,
385 0,
386 1);
387 attributes.append(attr_cursor);
388
389 QTextCharFormat format;
390 format.setFontUnderline(true);
391 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
392 QInputMethodEvent::Attribute attr_format(QInputMethodEvent::TextFormat,
393 0,
394 replaceLength, format);
395 attributes.append(attr_format);
396 }
397
398 QInputMethodEvent e(m_preeditString, attributes);
399 if (m_replaceIndex > 0)
400 e.setCommitString("", -m_replaceIndex, replaceLength);
401 QCoreApplication::sendEvent(m_focusObject, &e);
402}
403
404void QWasmInputContext::commitPreeditAndClear()
405{
406 if (m_preeditString.isEmpty())
407 return;
408 QInputMethodEvent e;
409 e.setCommitString(m_preeditString);
410 m_preeditString.clear();
411 QCoreApplication::sendEvent(m_focusObject, &e);
412}
413
414void QWasmInputContext::insertText(QString inputStr, bool replace)
415{ // commitString
416 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr << replace;
417 Q_UNUSED(replace);
418 if (!inputStr.isEmpty()) {
419 const int replaceLen = 0;
420 QInputMethodEvent e;
421 e.setCommitString(inputStr, -replaceLen, replaceLen);
422 QCoreApplication::sendEvent(m_focusObject, &e);
423 }
424}
425
426/* This will replace the text in the focusobject at replaceFrom position, and replaceSize length
427 with the text in inputStr. */
428
429 void QWasmInputContext::replaceText(QString inputStr, int replaceFrom, int replaceSize)
430 {
431 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr << replaceFrom << replaceSize;
432
433 QList<QInputMethodEvent::Attribute> attributes;
434 {
435 QInputMethodEvent::Attribute attr_cursor(QInputMethodEvent::Cursor,
436 0, // start
437 1); // length
438 attributes.append(attr_cursor);
439
440 QTextCharFormat format;
441 format.setFontUnderline(true);
442 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
443 QInputMethodEvent::Attribute attr_format(QInputMethodEvent::TextFormat,
444 0,
445 replaceSize,
446 format);
447 attributes.append(attr_format);
448 }
449
450 QInputMethodEvent e1(QString(), attributes);
451 e1.setCommitString(inputStr, replaceFrom, replaceSize);
452 QCoreApplication::sendEvent(m_focusObject, &e1);
453
454 m_preeditString.clear();
455 }
456
457QT_END_NAMESPACE
static QWasmWindow * fromWindow(const QWindow *window)
friend class QWasmCompositor
Combined button and popup list for selecting options.