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 || !inputTypeString.compare("deleteContent")) {
60 // deleteContent is direction-agnostic. WebKit emits it for generic
61 // editing deletions, and embedded virtual keyboards drive backspace
62 // this way, so treat it as a backward delete.
63 const ResolvedRange resolved = resolveRange(getFirstRange());
64
65 const int deleteFrom = resolved.replaceFrom <= 0 ? resolved.replaceFrom : -1;
66 commitText(QString(), deleteFrom, resolved.replaceLength);
67 } else if (!inputTypeString.compare("deleteContentForward")) {
68 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);
69 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Delete, Qt::NoModifier);
70 } else if (!inputTypeString.compare("deleteWordBackward")) {
71 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Backspace, Qt::ControlModifier);
72 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Backspace, Qt::ControlModifier);
73 } else if (!inputTypeString.compare("deleteWordForward")) {
74 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Delete, Qt::ControlModifier);
75 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Delete, Qt::ControlModifier);
76 } else if (!inputTypeString.compare("insertParagraph")) {
77 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
78 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Return, Qt::NoModifier);
79 } else if (!inputTypeString.compare("insertLineBreak")) {
80 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Return, Qt::ShiftModifier);
81 QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Return, Qt::ShiftModifier);
82 } else if (!inputTypeString.compare("insertCompositionText")) {
83
84 qCDebug(qLcQpaWasmInputContext) << "insertCompositionText : " << inputStr;
85 if (event["isComposing"].as<bool>()) {
86 const CompositionRange range = getFirstRange();
87 setPreeditString(inputStr);
88 insertPreedit(range.end - range.start);
89 }
90 } else if (!inputTypeString.compare("insertReplacementText")) {
91
92 qCDebug(qLcQpaWasmInputContext) << "insertReplacementText >>>>" << "inputString : " << inputStr;
93 const CompositionRange range = getFirstRange();
94
95 if (range.isValid) {
96 const ResolvedRange resolved = resolveRange(range);
97 commitText(inputStr, resolved.replaceFrom, resolved.replaceLength);
98 } else {
99 QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
100 QCoreApplication::sendEvent(m_focusObject, &queryEvent);
101 int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
102 QString textFieldString = queryEvent.value(Qt::ImTextBeforeCursor).toString();
103
104 int spaceIndex = textFieldString.lastIndexOf(' ') + 1;
105 int replaceIndex = (qCursorPosition - spaceIndex);
106 commitText(inputStr, -replaceIndex, replaceIndex);
107 }
108 } else if (!inputTypeString.compare("deleteCompositionText")) {
109 setPreeditString("");
110 insertPreedit();
111 } else if (!inputTypeString.compare("insertFromComposition")) {
112 setPreeditString(inputStr);
113 insertPreedit();
114 } else if (!inputTypeString.compare("insertText")) {
115 const CompositionRange range = getFirstRange();
116
117 if (range.isValid && range.start != range.end) {
118 const ResolvedRange resolved = resolveRange(range);
119 commitText(inputStr, resolved.replaceFrom, resolved.replaceLength);
120 } else {
121 commitText(inputStr);
122 }
123 } else if (!inputTypeString.compare("insertFromPaste")) {
124 commitText(inputStr);
125 // These can be supported here,
126 // But now, keyCallback in QWasmWindow
127 // will take them as exceptions.
128 //} else if (!inputTypeString.compare("deleteByCut")) {
129 } else {
130 qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType \"" <<
131 inputType.as<std::string>() << "\" is not supported in Qt yet";
132 }
133
134 // Prevent the event from propagting to any listeners outside of the Qt window
135 event.call<void>("stopPropagation");
136
137 // The composition target range set by the preceding "beforeinput" event has
138 // been consumed by this input event; clear it so a later input event without
139 // a fresh beforeinput cannot reuse a stale range.
140 m_compositionRange = emscripten::val::null();
141}
142
143void QWasmInputContext::compositionEndCallback(emscripten::val event)
144{
145 const auto inputStr = QString::fromEcmaString(event["data"]);
146
147 if (preeditString().isEmpty()) // we get final results from inputCallback
148 return;
149
150 if (inputStr != preeditString()) {
151 qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO
152 << "Composition string" << inputStr
153 << "is differ from" << preeditString();
154 }
155 commitPreeditAndClear();
156}
157
158void QWasmInputContext::compositionStartCallback(emscripten::val event)
159{
160 Q_UNUSED(event);
161
162 // Do nothing when starting composition
163}
164
165void QWasmInputContext::compositionUpdateCallback(emscripten::val event)
166{
167 const auto compositionStr = QString::fromEcmaString(event["data"]);
168 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << compositionStr;
169
170 setPreeditString(compositionStr);
171}
172
173void QWasmInputContext::beforeInputCallback(emscripten::val event)
174{
175 m_compositionRange = event.call<emscripten::val>("getTargetRanges");
176
177 if (const CompositionRange range = getFirstRange(); range.isValid) {
178 qCDebug(qLcQpaWasmInputContext) << "startOffset" << range.start;
179 qCDebug(qLcQpaWasmInputContext) << "endOffset" << range.end;
180 }
181}
182
183// Returns the first of the target ranges from the most recent "beforeinput"
184// event (InputEvent.getTargetRanges(), a possibly empty list of StaticRanges).
185QWasmInputContext::CompositionRange QWasmInputContext::getFirstRange() const
186{
187 CompositionRange range;
188 if (!m_compositionRange.isNull() && !m_compositionRange.isUndefined()
189 && m_compositionRange["length"].as<int>() > 0) {
190 emscripten::val first = m_compositionRange[0];
191 range.start = first["startOffset"].as<int>();
192 range.end = first["endOffset"].as<int>();
193 range.isValid = true;
194 }
195 return range;
196}
197
198// Resolves a composition range against the current input cursor position for
199// the focus object. Returns (replaceFrom, replaceLength) which can be passed
200// to QInputMethodEvent, where replaceFrom is an offset from the cursor position.
201QWasmInputContext::ResolvedRange QWasmInputContext::resolveRange(const CompositionRange &range) const
202{
203 QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
204 QCoreApplication::sendEvent(m_focusObject, &queryEvent);
205 const int cursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
206 return { range.start - cursorPosition, range.end - range.start };
207}
208
209QWasmInputContext::QWasmInputContext()
210{
211 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
212}
213
214QWasmInputContext::~QWasmInputContext()
215{
216}
217
218void QWasmInputContext::update(Qt::InputMethodQueries queries)
219{
220 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << queries;
221
222 if (m_inputElement.isNull())
223 return;
224
225 if (queries & Qt::ImInputItemClipRectangle) {
226 const QRect inputItemRectangle = QPlatformInputContext::inputItemRectangle().toRect();
227 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "propagating inputItemRectangle:" << inputItemRectangle;
228 m_inputElement["style"].set("left", std::to_string(inputItemRectangle.x()) + "px");
229 m_inputElement["style"].set("top", std::to_string(inputItemRectangle.y()) + "px");
230 m_inputElement["style"].set("width", "1px"); // "1px" to avoid overflowing window layout FIXME: fix layout
231 m_inputElement["style"].set("height", "1px");
232 }
233}
234
235void QWasmInputContext::showInputPanel()
236{
237 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
238
239 // Note: showInputPanel not necessarily called, we also
240 // need to make sure the input panel is shown in response
241 // to setFocusObject()
242 if (!m_inputElement.isNull())
243 focusOnFocusWindow(QWasmWindow::InputFocus);
244}
245
246void QWasmInputContext::hideInputPanel()
247{
248 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
249
250 // hide only if m_focusObject does not exist
251 if (m_focusObject)
252 return;
253
254 focusOnFocusWindow(QWasmWindow::WindowFocus);
255}
256
257void QWasmInputContext::setFocusObject(QObject *object)
258{
259 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << object << inputMethodAccepted();
260
261 // Commit the previous composition before change m_focusObject
262 if (m_focusObject && !m_preeditString.isEmpty())
263 commitPreeditAndClear();
264
265 m_focusObject = object;
266
267 // Handle the case where the current focus object does not accept input
268 // (becuse it is e.g. a button, or is null)
269 if (!inputMethodAccepted()) {
270 // "park" the input element now that it is unused
271 if (!m_inputElement.isNull()) {
272 m_inputElement["style"].set("left", "0px");
273 m_inputElement["style"].set("top", "0px");
274 m_inputElement = emscripten::val::null();
275 }
276
277 // Set focus to the focus helper for the current focus window. This moves focus
278 // away from the input element which will hide virtual keyboards.
279 focusOnFocusWindow(QWasmWindow::WindowFocus);
280 } else {
281 // Make the input element's properties match the current focus obj
282 m_inputElement = QWasmWindow::focusedWindowInputElement();
283 if (!m_inputElement.isNull()) {
284 updateInputElement();
285
286 // Set foucs to the input element. This shows (and re-shows) the virtual
287 // keyboard on mobile devices.
288 focusOnFocusWindow(QWasmWindow::InputFocus);
289 }
290 }
291}
292
293void QWasmInputContext::updateInputElement()
294{
295 if (QWasmAccessibility::isEnabled())
296 return;
297
298 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
299
300 QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
301 QCoreApplication::sendEvent(m_focusObject, &queryEvent);
302
303 qCDebug(qLcQpaWasmInputContext) << "Qt surrounding text: " << queryEvent.value(Qt::ImSurroundingText).toString();
304 qCDebug(qLcQpaWasmInputContext) << "Qt current selection: " << queryEvent.value(Qt::ImCurrentSelection).toString();
305 qCDebug(qLcQpaWasmInputContext) << "Qt text before cursor: " << queryEvent.value(Qt::ImTextBeforeCursor).toString();
306 qCDebug(qLcQpaWasmInputContext) << "Qt text after cursor: " << queryEvent.value(Qt::ImTextAfterCursor).toString();
307 qCDebug(qLcQpaWasmInputContext) << "Qt cursor position: " << queryEvent.value(Qt::ImCursorPosition).toInt();
308 qCDebug(qLcQpaWasmInputContext) << "Qt anchor position: " << queryEvent.value(Qt::ImAnchorPosition).toInt();
309
310 m_inputElement.set("value", queryEvent.value(Qt::ImSurroundingText).toString().toStdString());
311 m_inputElement.set("selectionStart", queryEvent.value(Qt::ImAnchorPosition).toUInt());
312 m_inputElement.set("selectionEnd", queryEvent.value(Qt::ImCursorPosition).toUInt());
313
314 Qt::InputMethodHints imHints = static_cast<Qt::InputMethodHints>(queryEvent.value(Qt::ImHints).toInt());
315
316 // Set input mode, use "password" for ImhHiddenText to prevent unwanted interactions with
317 // spell checking and auto-complete.
318 if (imHints & Qt::ImhHiddenText)
319 m_inputElement.set("type", "password");
320 else
321 m_inputElement.set("type", "text");
322
323 // Change inputmode to suit Qt imhints
324 std::string inMode = "text";
325 if (imHints & Qt::ImhDigitsOnly)
326 inMode = "numeric";
327 if (imHints & Qt::ImhFormattedNumbersOnly)
328 inMode = "decimal";
329 if (imHints & Qt::ImhDialableCharactersOnly)
330 inMode = "tel";
331 if (imHints & Qt::ImhEmailCharactersOnly)
332 inMode = "email";
333 if (imHints & Qt::ImhUrlCharactersOnly)
334 inMode = "url";
335 // FIXME: handle search mode?
336 m_inputElement.set("inputMode", inMode);
337}
338
339// Moves native focus to the focus helper (WindowFocus) or the input element
340// (InputFocus) of the currently focused window. InputFocus shows the virtual
341// keyboard on mobile; WindowFocus moves focus off the input element and hides it.
342void QWasmInputContext::focusOnFocusWindow(QWasmWindow::FocusTarget target)
343{
344 if (QWasmWindow *focusWindow = QWasmWindow::focusWindow())
345 focusWindow->focus(target);
346}
347
348void QWasmInputContext::setPreeditString(QString preeditStr)
349{
350 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << preeditStr;
351 m_preeditString = preeditStr;
352}
353
354void QWasmInputContext::insertPreedit(int replaceLength)
355{
356 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString;
357 if (replaceLength == 0)
358 replaceLength = m_preeditString.length();
359
360 QList<QInputMethodEvent::Attribute> attributes;
361 {
362 QInputMethodEvent::Attribute attr_cursor(QInputMethodEvent::Cursor,
363 0,
364 1);
365 attributes.append(attr_cursor);
366
367 QTextCharFormat format;
368 format.setFontUnderline(true);
369 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
370 QInputMethodEvent::Attribute attr_format(QInputMethodEvent::TextFormat,
371 0,
372 replaceLength, format);
373 attributes.append(attr_format);
374 }
375
376 QInputMethodEvent e(m_preeditString, attributes);
377 QCoreApplication::sendEvent(m_focusObject, &e);
378}
379
380void QWasmInputContext::commitPreeditAndClear()
381{
382 if (m_preeditString.isEmpty())
383 return;
384 QInputMethodEvent e;
385 e.setCommitString(m_preeditString);
386 m_preeditString.clear();
387 QCoreApplication::sendEvent(m_focusObject, &e);
388}
389
390// Commits text to the focus object, optionally replacing replaceLength
391// characters starting replaceFrom positions relative to the cursor.
392void QWasmInputContext::commitText(const QString &text, int replaceFrom, int replaceLength)
393{
394 qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << text << replaceFrom << replaceLength;
395 QInputMethodEvent e;
396 e.setCommitString(text, replaceFrom, replaceLength);
397 QCoreApplication::sendEvent(m_focusObject, &e);
398 m_preeditString.clear();
399}
400
401QT_END_NAMESPACE
Combined button and popup list for selecting options.