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