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