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
qwaylandtextinputv3.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
8#include "qwaylandinputmethodeventbuilder_p.h"
9
10#include <QtCore/qloggingcategory.h>
11#include <QtGui/qguiapplication.h>
12#include <QtGui/private/qguiapplication_p.h>
13#include <QtGui/private/qhighdpiscaling_p.h>
14#include <QtGui/qpa/qplatformintegration.h>
15#include <QtGui/qpa/qplatforminputcontext.h>
16#include <QtGui/qevent.h>
17#include <QtGui/qwindow.h>
18#include <QtGui/qpalette.h>
19
20#include <QTextCharFormat>
21
23
24Q_LOGGING_CATEGORY(qLcQpaWaylandTextInput, "qt.qpa.wayland.textinput")
25
26namespace QtWaylandClient {
27
28QWaylandTextInputv3::QWaylandTextInputv3(QWaylandDisplay *display,
29 struct ::zwp_text_input_v3 *text_input)
30 : QtWayland::zwp_text_input_v3(text_input)
31{
32 Q_UNUSED(display)
33
34 if (version() >= ZWP_TEXT_INPUT_V3_SET_AVAILABLE_ACTIONS_SINCE_VERSION) {
35 uint32_t availableActions[1] = {action_submit};
36 const QByteArray availableActionsData = QByteArray::fromRawData(reinterpret_cast<char *>(availableActions), sizeof(availableActions));
37 set_available_actions(availableActionsData);
38 }
39}
40
42{
43 destroy();
44}
45
46namespace {
47const Qt::InputMethodQueries supportedQueries3 = Qt::ImEnabled |
48 Qt::ImSurroundingText |
49 Qt::ImCursorPosition |
50 Qt::ImAnchorPosition |
51 Qt::ImHints |
52 Qt::ImCursorRectangle;
53}
54
55void QWaylandTextInputv3::zwp_text_input_v3_enter(struct ::wl_surface *surface)
56{
57 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "Trying to enable surface" << surface << "with focusing surface" << m_surface;
58
59 if (m_surface == surface)
60 return; // already enabled
61
62 m_surface = surface;
63 m_pendingPreeditString.clear();
64 m_pendingCommitString.clear();
65 m_pendingDeleteBeforeText = 0;
66 m_pendingDeleteAfterText = 0;
67 m_surroundingText.clear();
68 m_cursor = 0;
69 m_cursorPos = 0;
70 m_anchorPos = 0;
71 m_contentHint = 0;
72 m_contentPurpose = 0;
73 m_cursorRect = QRect();
74
75 enable();
76 updateState(supportedQueries3, update_state_enter);
77}
78
79void QWaylandTextInputv3::zwp_text_input_v3_leave(struct ::wl_surface *surface)
80{
81 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO;
82
83 if (!m_surface)
84 return; // Nothing to leave
85
86 // surface == nullptr means the wl_surface proxy was already freed (surface destroyed
87 // before the leave event was dispatched); treat it as normal teardown, not a mismatch.
88 if (surface && m_surface != surface)
89 qCWarning(qLcQpaWaylandTextInput()) << Q_FUNC_INFO << "Got leave event for surface" << surface << "with focusing surface" << m_surface;
90
91 m_currentPreeditString.clear();
92 m_surface = nullptr;
93 disable();
95}
96
97void QWaylandTextInputv3::zwp_text_input_v3_preedit_string(const QString &text, int32_t cursorBegin, int32_t cursorEnd)
98{
99 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << text << cursorBegin << cursorEnd;
100 if (!m_surface) {
101 qCWarning(qLcQpaWaylandTextInput) << "Got preedit_string event without entering a surface";
102 return;
103 }
104
105 if (!QGuiApplication::focusObject())
106 return;
107
108 m_pendingPreeditString.text = text;
109 m_pendingPreeditString.cursorBegin = cursorBegin;
110 m_pendingPreeditString.cursorEnd = cursorEnd;
111}
112
114{
115 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << text;
116 if (!m_surface) {
117 qCWarning(qLcQpaWaylandTextInput) << "Got commit_string event without entering a surface";
118 return;
119 }
120
121 if (!QGuiApplication::focusObject())
122 return;
123
124 m_pendingCommitString = text;
125}
126
127void QWaylandTextInputv3::zwp_text_input_v3_delete_surrounding_text(uint32_t beforeText, uint32_t afterText)
128{
129 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << beforeText << afterText;
130 if (!m_surface) {
131 qCWarning(qLcQpaWaylandTextInput) << "Got delete_surrounding_text event without entering a surface";
132 return;
133 }
134
135 if (!QGuiApplication::focusObject())
136 return;
137
138 m_pendingDeleteBeforeText = beforeText;
139 m_pendingDeleteAfterText = afterText;
140}
141
143{
144 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "with serial" << serial << m_currentSerial;
145
146 if (!m_surface)
147 return;
148
149 // This is a case of double click.
150 // text_input_v3 will ignore this done signal and just keep the selection of the clicked word.
151 if (m_cursorPos != m_anchorPos && (m_pendingDeleteBeforeText != 0 || m_pendingDeleteAfterText != 0)) {
152 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "Ignore done";
153 m_pendingDeleteBeforeText = 0;
154 m_pendingDeleteAfterText = 0;
155 m_pendingPreeditString.clear();
156 m_pendingCommitString.clear();
157 return;
158 }
159
160 QObject *focusObject = QGuiApplication::focusObject();
161 if (!focusObject)
162 return;
163
164 if (!m_surface) {
165 qCWarning(qLcQpaWaylandTextInput) << Q_FUNC_INFO << serial << "Surface is not enabled yet";
166 return;
167 }
168
169 if ((m_pendingPreeditString == m_currentPreeditString)
170 && (m_pendingCommitString.isEmpty() && m_pendingDeleteBeforeText == 0
171 && m_pendingDeleteAfterText == 0)) {
172 // Current done doesn't need additional updates
173 m_pendingPreeditString.clear();
174 return;
175 }
176
177 const int newCursorIndex = QWaylandInputMethodEventBuilder::indexFromWayland(m_pendingPreeditString.text, m_pendingPreeditString.cursorBegin);
178 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "PREEDIT" << m_pendingPreeditString.text << newCursorIndex;
179
180 QList<QInputMethodEvent::Attribute> attributes;
181 {
182 if (m_pendingPreeditString.cursorBegin == -1 &&
183 m_pendingPreeditString.cursorEnd == -1) {
184 QInputMethodEvent::Attribute attribute(QInputMethodEvent::Cursor,
185 0,
186 0); // hide cursor
187 attributes.append(attribute);
188 } else if (m_pendingPreeditString.cursorBegin != 0 ||
189 m_pendingPreeditString.cursorEnd != 0) {
190 // Current supported cursor shape is just line.
191 // It means, cursorEnd and cursorBegin are the same.
192 QInputMethodEvent::Attribute attribute1(QInputMethodEvent::Cursor,
193 newCursorIndex,
194 1); // keep visible
195 attributes.append(attribute1);
196 }
197
198 if (version() == 1) {
199 // only use single underline style for now
200 QTextCharFormat format;
201 format.setFontUnderline(true);
202 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
203 QInputMethodEvent::Attribute attribute2(QInputMethodEvent::TextFormat,
204 0,
205 m_pendingPreeditString.text.length(), format);
206 attributes.append(attribute2);
207 } else {
208 for (const StyleHint &prededitStyle : std::as_const(m_pendingPreeditString.styleHints)) {
209 int begin = QWaylandInputMethodEventBuilder::indexFromWayland(m_pendingPreeditString.text, prededitStyle.begin);
210 int end = QWaylandInputMethodEventBuilder::indexFromWayland(m_pendingPreeditString.text, prededitStyle.end);
211 QTextCharFormat format;
212
213 // styles taken from https://github.com/ibus/ibus/wiki/Wayland-Colors
214 switch (prededitStyle.hint) {
215 case preedit_hint_whole:
216 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
217 break;
218 case preedit_hint_selection:
219 format.setForeground(QPalette().highlightedText());
220 format.setBackground(QPalette().highlight());
221 break;
222 case preedit_hint_prediction:
223 // this is meant to be normal text on a light grey
224 format.setBackground(QPalette().placeholderText());
225 break;
226 case preedit_hint_prefix:
227 format.setForeground(QColor("#F90F0F"));
228 break;
229 case preedit_hint_suffix:
230 format.setForeground(QColor("#1EDC1A"));
231 break;
232 case preedit_hint_spelling_error:
233 format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
234 format.setUnderlineColor(QColor("#A40000"));
235 break;
236 case preedit_hint_compose_error:
237 format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
238 format.setUnderlineColor(QColor("#FF00FF"));
239 break;
240 }
241
242 QInputMethodEvent::Attribute attribute2(QInputMethodEvent::TextFormat,
243 begin,
244 end - begin,
245 format);
246 attributes.append(attribute2);
247 }
248 }
249 }
250 QInputMethodEvent event(m_pendingPreeditString.text, attributes);
251
252 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "DELETE" << m_pendingDeleteBeforeText << m_pendingDeleteAfterText;
253 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "COMMIT" << m_pendingCommitString;
254
255 int replaceFrom = 0;
256 int replaceLength = 0;
257 if (m_pendingDeleteBeforeText != 0 || m_pendingDeleteAfterText != 0) {
258 // A workaround for reselection
259 // It will disable redundant commit after reselection
260 m_condReselection = true;
261 const QByteArray &utf8 = QStringView{m_surroundingText}.toUtf8();
262 if (m_cursorPos < int(m_pendingDeleteBeforeText)) {
263 replaceFrom = -QString::fromUtf8(QByteArrayView{utf8}.first(m_pendingDeleteBeforeText)).size();
264 replaceLength = QString::fromUtf8(QByteArrayView{utf8}.first(m_pendingDeleteBeforeText + m_pendingDeleteAfterText)).size();
265 } else {
266 replaceFrom = -QString::fromUtf8(QByteArrayView{utf8}.sliced(m_cursorPos - m_pendingDeleteBeforeText, m_pendingDeleteBeforeText)).size();
267 replaceLength = QString::fromUtf8(QByteArrayView{utf8}.sliced(m_cursorPos - m_pendingDeleteBeforeText, m_pendingDeleteBeforeText + m_pendingDeleteAfterText)).size();
268 }
269 }
270
271 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "DELETE from " << replaceFrom << " length " << replaceLength;
272 event.setCommitString(m_pendingCommitString,
273 replaceFrom,
274 replaceLength);
275 m_currentPreeditString = m_pendingPreeditString;
276 m_pendingPreeditString.clear();
277 m_pendingCommitString.clear();
278 m_pendingDeleteBeforeText = 0;
279 m_pendingDeleteAfterText = 0;
280 QCoreApplication::sendEvent(focusObject, &event);
281
282 if (serial == m_currentSerial)
283 updateState(supportedQueries3, update_state_full);
284}
285
287{
288 const QLocale locale(language);
289 if (m_locale != locale) {
290 m_locale = locale;
291 QGuiApplicationPrivate::platformIntegration()->inputContext()->emitLocaleChanged();
292 }
293}
294
295void QWaylandTextInputv3::zwp_text_input_v3_action(uint32_t action, uint32_t serial)
296{
297 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << action << serial;
298 switch (action) {
299 case action_none:
300 break;
301 case action_submit: {
302 if (!QGuiApplication::focusObject())
303 break;
304 QKeyEvent keyPressEvent(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier);
305 QCoreApplication::sendEvent(QGuiApplication::focusObject(), &keyPressEvent);
306 QKeyEvent keyReleaseEvent(QEvent::KeyRelease, Qt::Key_Enter, Qt::NoModifier);
307 QCoreApplication::sendEvent(QGuiApplication::focusObject(), &keyReleaseEvent);
308 break;
309 }
310 default:
311 // it's a bug as we declare our supported actions on startup
312 qCWarning(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "Unexpected text input action received. This is a compositor bug";
313 break;
314 }
315}
316
317void QWaylandTextInputv3::zwp_text_input_v3_preedit_hint(uint32_t begin, uint32_t end, uint32_t hint)
318{
319 Q_ASSERT(hint <= preedit_hint_compose_error);
320 // they have to be cached as raw values, as we can't work out cursor indexes without the text
321 m_pendingPreeditString.styleHints.append({begin, end, static_cast<preedit_hint>(hint)});
322}
323
324
326{
327 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO;
328
329 m_pendingPreeditString.clear();
330}
331
333{
334 m_currentSerial = (m_currentSerial < UINT_MAX) ? m_currentSerial + 1U: 0U;
335
336 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "with serial" << m_currentSerial;
337 QtWayland::zwp_text_input_v3::commit();
338}
339
340void QWaylandTextInputv3::updateState(Qt::InputMethodQueries queries, uint32_t flags)
341{
342 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << queries << flags;
343
344 if (!QGuiApplication::focusObject())
345 return;
346
347 if (!QGuiApplication::focusWindow() || !QGuiApplication::focusWindow()->handle())
348 return;
349
350 auto *window = static_cast<QWaylandWindow *>(QGuiApplication::focusWindow()->handle());
351 auto *surface = window->wlSurface();
352 if (!surface || (surface != m_surface))
353 return;
354
355 queries &= supportedQueries3;
356 bool needsCommit = false;
357
358 QInputMethodQueryEvent event(queries);
359 QCoreApplication::sendEvent(QGuiApplication::focusObject(), &event);
360
361 // For some reason, a query for Qt::ImSurroundingText gives an empty string even though it is not.
362 if (!(queries & Qt::ImSurroundingText) && event.value(Qt::ImSurroundingText).toString().isEmpty()) {
363 return;
364 }
365
366 if (queries & Qt::ImCursorRectangle) {
367 const QRect &cRect = event.value(Qt::ImCursorRectangle).toRect();
368 const QRect &windowRect = QGuiApplication::inputMethod()->inputItemTransform().mapRect(cRect);
369 const QRect &nativeRect = QHighDpi::toNativePixels(windowRect, QGuiApplication::focusWindow());
370 const QMargins margins = window->clientSideMargins();
371 const QRect &surfaceRect = nativeRect.translated(margins.left(), margins.top());
372 if (surfaceRect != m_cursorRect) {
373 set_cursor_rectangle(surfaceRect.x(), surfaceRect.y(), surfaceRect.width(), surfaceRect.height());
374 m_cursorRect = surfaceRect;
375 needsCommit = true;
376 }
377 }
378
379 if ((queries & Qt::ImSurroundingText) || (queries & Qt::ImCursorPosition) || (queries & Qt::ImAnchorPosition)) {
380 QString text = event.value(Qt::ImSurroundingText).toString();
381 int cursor = event.value(Qt::ImCursorPosition).toInt();
382 int anchor = event.value(Qt::ImAnchorPosition).toInt();
383
384 qCDebug(qLcQpaWaylandTextInput) << "Original surrounding_text from InputMethodQuery: " << text << cursor << anchor;
385
386 // Make sure text is not too big
387 // surround_text cannot exceed 4000byte in wayland protocol
388 // The worst case will be supposed here.
389 const int MAX_MESSAGE_SIZE = 4000;
390
391 const QByteArray utf8 = text.toUtf8();
392 const int textSize = utf8.size();
393 if (textSize > MAX_MESSAGE_SIZE) {
394 qCDebug(qLcQpaWaylandTextInput) << "SurroundText size is over "
395 << MAX_MESSAGE_SIZE
396 << " byte, some text will be clipped.";
397 const int selectionStart = qMin(cursor, anchor);
398 const int selectionEnd = qMax(cursor, anchor);
399 const int selectionLength = selectionEnd - selectionStart;
400 QByteArray selection = QStringView{text}.sliced(selectionStart, selectionLength).toUtf8();
401 const int selectionSize = selection.size();
402 // If selection is bigger than 4000 byte, it is fixed to 4000 byte.
403 // anchor will be moved in the 4000 byte boundary.
404 if (selectionSize > MAX_MESSAGE_SIZE) {
405 if (anchor > cursor) {
406 cursor = 0;
407 anchor = MAX_MESSAGE_SIZE;
408 text = QString::fromUtf8(QByteArrayView{selection}.sliced(0, MAX_MESSAGE_SIZE));
409 } else {
410 anchor = 0;
411 cursor = MAX_MESSAGE_SIZE;
412 text = QString::fromUtf8(QByteArrayView{selection}.sliced(selectionSize - MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE));
413 }
414 } else {
415 // This is not optimal in some cases.
416 // For examples, if the cursor position and
417 // the selectionEnd are close to the end of the surround text,
418 // the tail of the text might always be clipped.
419 // However all the cases of over 4000 byte are just exceptions.
420 int selEndSize = QStringView{text}.first(selectionEnd).toUtf8().size();
421 cursor = QWaylandInputMethodEventBuilder::indexToWayland(text, cursor);
422 anchor = QWaylandInputMethodEventBuilder::indexToWayland(text, anchor);
423 if (selEndSize < MAX_MESSAGE_SIZE) {
424 text = QString::fromUtf8(QByteArrayView{utf8}.first(MAX_MESSAGE_SIZE));
425 } else {
426 const int startOffset = selEndSize - MAX_MESSAGE_SIZE;
427 text = QString::fromUtf8(QByteArrayView{utf8}.sliced(startOffset, MAX_MESSAGE_SIZE));
428 cursor -= startOffset;
429 anchor -= startOffset;
430 }
431 }
432 } else {
433 cursor = QWaylandInputMethodEventBuilder::indexToWayland(text, cursor);
434 anchor = QWaylandInputMethodEventBuilder::indexToWayland(text, anchor);
435 }
436 qCDebug(qLcQpaWaylandTextInput) << "Modified surrounding_text: " << text << cursor << anchor;
437
438 if (m_surroundingText != text || m_cursorPos != cursor || m_anchorPos != anchor) {
439 qCDebug(qLcQpaWaylandTextInput) << "Current surrounding_text: " << m_surroundingText << m_cursorPos << m_anchorPos;
440 qCDebug(qLcQpaWaylandTextInput) << "New surrounding_text: " << text << cursor << anchor;
441
442 set_surrounding_text(text, cursor, anchor);
443
444 // A workaround in the case of reselection
445 // It will work when re-clicking a preedit text
446 if (m_condReselection) {
447 qCDebug(qLcQpaWaylandTextInput) << "\"commit\" is disabled when Reselection by changing focus";
448 m_condReselection = false;
449 needsCommit = false;
450
451 }
452
453 m_surroundingText = text;
454 m_cursorPos = cursor;
455 m_anchorPos = anchor;
456 m_cursor = cursor;
457 }
458 }
459
460 if (queries & Qt::ImHints) {
461 QWaylandInputMethodContentType contentType = QWaylandInputMethodContentType::convertV3(static_cast<Qt::InputMethodHints>(event.value(Qt::ImHints).toInt()));
462
463 if (version() >= ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN_SINCE_VERSION)
464 contentType.hint |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN;
465
466 qCDebug(qLcQpaWaylandTextInput) << m_contentHint << contentType.hint;
467 qCDebug(qLcQpaWaylandTextInput) << m_contentPurpose << contentType.purpose;
468
469 if (m_contentHint != contentType.hint || m_contentPurpose != contentType.purpose) {
470 qCDebug(qLcQpaWaylandTextInput) << "set_content_type: " << contentType.hint << contentType.purpose;
471 set_content_type(contentType.hint, contentType.purpose);
472
473 m_contentHint = contentType.hint;
474 m_contentPurpose = contentType.purpose;
475 needsCommit = true;
476 }
477 }
478
479 if (needsCommit)
480 commit();
481}
482
484{
485 Q_UNUSED(cursor);
486}
487
489{
490 return false;
491}
492
494{
495 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO;
496 return m_cursorRect;
497}
498
500{
501 return m_locale;
502}
503
505{
506 return Qt::LeftToRight;
507}
508
510{
511 if (version() >= ZWP_TEXT_INPUT_V3_SHOW_INPUT_PANEL_SINCE_VERSION)
512 show_input_panel();
513}
514
516{
517 if (version() >= ZWP_TEXT_INPUT_V3_HIDE_INPUT_PANEL_SINCE_VERSION)
518 hide_input_panel();
519}
520
521
522
523}
524
525QT_END_NAMESPACE
void zwp_text_input_v3_commit_string(const QString &text) override
Qt::LayoutDirection inputDirection() const override
void zwp_text_input_v3_preedit_hint(uint32_t begin, uint32_t end, uint32_t hint) override
void updateState(Qt::InputMethodQueries queries, uint32_t flags) override
void zwp_text_input_v3_enter(struct ::wl_surface *surface) override
void zwp_text_input_v3_preedit_string(const QString &text, int32_t cursor_begin, int32_t cursor_end) override
void zwp_text_input_v3_language(const QString &language) override
void zwp_text_input_v3_delete_surrounding_text(uint32_t before_length, uint32_t after_length) override
void zwp_text_input_v3_leave(struct ::wl_surface *surface) override
void setCursorInsidePreedit(int cursor) override
void zwp_text_input_v3_action(uint32_t action, uint32_t serial) override
void zwp_text_input_v3_done(uint32_t serial) override
Combined button and popup list for selecting options.