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
5
7#include "qwaylandinputmethodeventbuilder_p.h"
8
9#include <QtCore/qloggingcategory.h>
10#include <QtGui/qguiapplication.h>
11#include <QtGui/private/qhighdpiscaling_p.h>
12#include <QtGui/qevent.h>
13#include <QtGui/qwindow.h>
14#include <QTextCharFormat>
15
17
18Q_LOGGING_CATEGORY(qLcQpaWaylandTextInput, "qt.qpa.wayland.textinput")
19
20namespace QtWaylandClient {
21
22QWaylandTextInputv3::QWaylandTextInputv3(QWaylandDisplay *display,
23 struct ::zwp_text_input_v3 *text_input)
24 : QtWayland::zwp_text_input_v3(text_input)
25{
26 Q_UNUSED(display)
27}
28
30{
31 destroy();
32}
33
34namespace {
35const Qt::InputMethodQueries supportedQueries3 = Qt::ImEnabled |
36 Qt::ImSurroundingText |
37 Qt::ImCursorPosition |
38 Qt::ImAnchorPosition |
39 Qt::ImHints |
40 Qt::ImCursorRectangle;
41}
42
43void QWaylandTextInputv3::enableSurface(::wl_surface *surface)
44{
45 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << surface;
46
47 if (m_surface == surface)
48 return; // already enabled
49 if (m_surface)
50 qCWarning(qLcQpaWaylandTextInput()) << Q_FUNC_INFO << "Try to enable surface" << surface << "with focusing surface" << m_surface;
51
52 m_surface = surface;
53 m_pendingPreeditString.clear();
54 m_pendingCommitString.clear();
55 m_pendingDeleteBeforeText = 0;
56 m_pendingDeleteAfterText = 0;
57 m_surroundingText.clear();
58 m_cursor = 0;
59 m_cursorPos = 0;
60 m_anchorPos = 0;
61 m_contentHint = 0;
62 m_contentPurpose = 0;
63 m_cursorRect = QRect();
64
65 enable();
66 updateState(supportedQueries3, update_state_enter);
67}
68
69void QWaylandTextInputv3::disableSurface(::wl_surface *surface)
70{
71 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << surface;
72
73 if (!m_surface)
74 return; // already disabled
75 if (m_surface != surface)
76 qCWarning(qLcQpaWaylandTextInput()) << Q_FUNC_INFO << "Try to disable surface" << surface << "with focusing surface" << m_surface;
77
78 m_currentPreeditString.clear();
79 m_surface = nullptr;
80 disable();
82}
83
84void QWaylandTextInputv3::zwp_text_input_v3_enter(struct ::wl_surface *surface)
85{
86 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << m_surface << surface;
87
88 if (m_surface)
89 qCWarning(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "Got enter event without leaving a surface " << m_surface;
90
91 enableSurface(surface);
92}
93
94void QWaylandTextInputv3::zwp_text_input_v3_leave(struct ::wl_surface *surface)
95{
96 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO;
97
98 if (!m_surface)
99 return; // Nothing to leave
100
101 if (m_surface != surface)
102 qCWarning(qLcQpaWaylandTextInput()) << Q_FUNC_INFO << "Got leave event for surface" << surface << "with focusing surface" << m_surface;
103
104 disableSurface(surface);
105}
106
107void QWaylandTextInputv3::zwp_text_input_v3_preedit_string(const QString &text, int32_t cursorBegin, int32_t cursorEnd)
108{
109 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << text << cursorBegin << cursorEnd;
110 if (!m_surface) {
111 qCWarning(qLcQpaWaylandTextInput) << "Got preedit_string event without entering a surface";
112 return;
113 }
114
115 if (!QGuiApplication::focusObject())
116 return;
117
118 m_pendingPreeditString.text = text;
119 m_pendingPreeditString.cursorBegin = QWaylandInputMethodEventBuilder::indexFromWayland(text, cursorBegin);
120 m_pendingPreeditString.cursorEnd = QWaylandInputMethodEventBuilder::indexFromWayland(text, cursorEnd);
121}
122
124{
125 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << text;
126 if (!m_surface) {
127 qCWarning(qLcQpaWaylandTextInput) << "Got commit_string event without entering a surface";
128 return;
129 }
130
131 if (!QGuiApplication::focusObject())
132 return;
133
134 m_pendingCommitString = text;
135}
136
137void QWaylandTextInputv3::zwp_text_input_v3_delete_surrounding_text(uint32_t beforeText, uint32_t afterText)
138{
139 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << beforeText << afterText;
140 if (!m_surface) {
141 qCWarning(qLcQpaWaylandTextInput) << "Got delete_surrounding_text event without entering a surface";
142 return;
143 }
144
145 if (!QGuiApplication::focusObject())
146 return;
147
148 m_pendingDeleteBeforeText = beforeText;
149 m_pendingDeleteAfterText = afterText;
150}
151
153{
154 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "with serial" << serial << m_currentSerial;
155
156 if (!m_surface)
157 return;
158
159 // This is a case of double click.
160 // text_input_v3 will ignore this done signal and just keep the selection of the clicked word.
161 if (m_cursorPos != m_anchorPos && (m_pendingDeleteBeforeText != 0 || m_pendingDeleteAfterText != 0)) {
162 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "Ignore done";
163 m_pendingDeleteBeforeText = 0;
164 m_pendingDeleteAfterText = 0;
165 m_pendingPreeditString.clear();
166 m_pendingCommitString.clear();
167 return;
168 }
169
170 QObject *focusObject = QGuiApplication::focusObject();
171 if (!focusObject)
172 return;
173
174 if (!m_surface) {
175 qCWarning(qLcQpaWaylandTextInput) << Q_FUNC_INFO << serial << "Surface is not enabled yet";
176 return;
177 }
178
179 if ((m_pendingPreeditString == m_currentPreeditString)
180 && (m_pendingCommitString.isEmpty() && m_pendingDeleteBeforeText == 0
181 && m_pendingDeleteAfterText == 0)) {
182 // Current done doesn't need additional updates
183 m_pendingPreeditString.clear();
184 return;
185 }
186
187 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "PREEDIT" << m_pendingPreeditString.text << m_pendingPreeditString.cursorBegin;
188
189 QList<QInputMethodEvent::Attribute> attributes;
190 {
191 if (m_pendingPreeditString.cursorBegin != -1 ||
192 m_pendingPreeditString.cursorEnd != -1) {
193 // Current supported cursor shape is just line.
194 // It means, cursorEnd and cursorBegin are the same.
195 QInputMethodEvent::Attribute attribute1(QInputMethodEvent::Cursor,
196 m_pendingPreeditString.cursorBegin,
197 1);
198 attributes.append(attribute1);
199 }
200
201 // only use single underline style for now
202 QTextCharFormat format;
203 format.setFontUnderline(true);
204 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
205 QInputMethodEvent::Attribute attribute2(QInputMethodEvent::TextFormat,
206 0,
207 m_pendingPreeditString.text.length(), format);
208 attributes.append(attribute2);
209 }
210 QInputMethodEvent event(m_pendingPreeditString.text, attributes);
211
212 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "DELETE" << m_pendingDeleteBeforeText << m_pendingDeleteAfterText;
213 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "COMMIT" << m_pendingCommitString;
214
215 int replaceFrom = 0;
216 int replaceLength = 0;
217 if (m_pendingDeleteBeforeText != 0 || m_pendingDeleteAfterText != 0) {
218 // A workaround for reselection
219 // It will disable redundant commit after reselection
220 m_condReselection = true;
221 const QByteArray &utf8 = QStringView{m_surroundingText}.toUtf8();
222 if (m_cursorPos < int(m_pendingDeleteBeforeText)) {
223 replaceFrom = -QString::fromUtf8(QByteArrayView{utf8}.first(m_pendingDeleteBeforeText)).size();
224 replaceLength = QString::fromUtf8(QByteArrayView{utf8}.first(m_pendingDeleteBeforeText + m_pendingDeleteAfterText)).size();
225 } else {
226 replaceFrom = -QString::fromUtf8(QByteArrayView{utf8}.sliced(m_cursorPos - m_pendingDeleteBeforeText, m_pendingDeleteBeforeText)).size();
227 replaceLength = QString::fromUtf8(QByteArrayView{utf8}.sliced(m_cursorPos - m_pendingDeleteBeforeText, m_pendingDeleteBeforeText + m_pendingDeleteAfterText)).size();
228 }
229 }
230
231 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "DELETE from " << replaceFrom << " length " << replaceLength;
232 event.setCommitString(m_pendingCommitString,
233 replaceFrom,
234 replaceLength);
235 m_currentPreeditString = m_pendingPreeditString;
236 m_pendingPreeditString.clear();
237 m_pendingCommitString.clear();
238 m_pendingDeleteBeforeText = 0;
239 m_pendingDeleteAfterText = 0;
240 QCoreApplication::sendEvent(focusObject, &event);
241
242 if (serial == m_currentSerial)
243 updateState(supportedQueries3, update_state_full);
244}
245
247{
248 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO;
249
250 m_pendingPreeditString.clear();
251}
252
254{
255 m_currentSerial = (m_currentSerial < UINT_MAX) ? m_currentSerial + 1U: 0U;
256
257 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << "with serial" << m_currentSerial;
258 QtWayland::zwp_text_input_v3::commit();
259}
260
261void QWaylandTextInputv3::updateState(Qt::InputMethodQueries queries, uint32_t flags)
262{
263 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO << queries << flags;
264
265 if (!QGuiApplication::focusObject())
266 return;
267
268 if (!QGuiApplication::focusWindow() || !QGuiApplication::focusWindow()->handle())
269 return;
270
271 auto *window = static_cast<QWaylandWindow *>(QGuiApplication::focusWindow()->handle());
272 auto *surface = window->wlSurface();
273 if (!surface || (surface != m_surface))
274 return;
275
276 queries &= supportedQueries3;
277 bool needsCommit = false;
278
279 QInputMethodQueryEvent event(queries);
280 QCoreApplication::sendEvent(QGuiApplication::focusObject(), &event);
281
282 // For some reason, a query for Qt::ImSurroundingText gives an empty string even though it is not.
283 if (!(queries & Qt::ImSurroundingText) && event.value(Qt::ImSurroundingText).toString().isEmpty()) {
284 return;
285 }
286
287 if (queries & Qt::ImCursorRectangle) {
288 const QRect &cRect = event.value(Qt::ImCursorRectangle).toRect();
289 const QRect &windowRect = QGuiApplication::inputMethod()->inputItemTransform().mapRect(cRect);
290 const QRect &nativeRect = QHighDpi::toNativePixels(windowRect, QGuiApplication::focusWindow());
291 const QMargins margins = window->clientSideMargins();
292 const QRect &surfaceRect = nativeRect.translated(margins.left(), margins.top());
293 if (surfaceRect != m_cursorRect) {
294 set_cursor_rectangle(surfaceRect.x(), surfaceRect.y(), surfaceRect.width(), surfaceRect.height());
295 m_cursorRect = surfaceRect;
296 needsCommit = true;
297 }
298 }
299
300 if ((queries & Qt::ImSurroundingText) || (queries & Qt::ImCursorPosition) || (queries & Qt::ImAnchorPosition)) {
301 QString text = event.value(Qt::ImSurroundingText).toString();
302 int cursor = event.value(Qt::ImCursorPosition).toInt();
303 int anchor = event.value(Qt::ImAnchorPosition).toInt();
304
305 qCDebug(qLcQpaWaylandTextInput) << "Original surrounding_text from InputMethodQuery: " << text << cursor << anchor;
306
307 // Make sure text is not too big
308 // surround_text cannot exceed 4000byte in wayland protocol
309 // The worst case will be supposed here.
310 const int MAX_MESSAGE_SIZE = 4000;
311
312 const QByteArray utf8 = text.toUtf8();
313 const int textSize = utf8.size();
314 if (textSize > MAX_MESSAGE_SIZE) {
315 qCDebug(qLcQpaWaylandTextInput) << "SurroundText size is over "
316 << MAX_MESSAGE_SIZE
317 << " byte, some text will be clipped.";
318 const int selectionStart = qMin(cursor, anchor);
319 const int selectionEnd = qMax(cursor, anchor);
320 const int selectionLength = selectionEnd - selectionStart;
321 QByteArray selection = QStringView{text}.sliced(selectionStart, selectionLength).toUtf8();
322 const int selectionSize = selection.size();
323 // If selection is bigger than 4000 byte, it is fixed to 4000 byte.
324 // anchor will be moved in the 4000 byte boundary.
325 if (selectionSize > MAX_MESSAGE_SIZE) {
326 if (anchor > cursor) {
327 cursor = 0;
328 anchor = MAX_MESSAGE_SIZE;
329 text = QString::fromUtf8(QByteArrayView{selection}.sliced(0, MAX_MESSAGE_SIZE));
330 } else {
331 anchor = 0;
332 cursor = MAX_MESSAGE_SIZE;
333 text = QString::fromUtf8(QByteArrayView{selection}.sliced(selectionSize - MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE));
334 }
335 } else {
336 // This is not optimal in some cases.
337 // For examples, if the cursor position and
338 // the selectionEnd are close to the end of the surround text,
339 // the tail of the text might always be clipped.
340 // However all the cases of over 4000 byte are just exceptions.
341 int selEndSize = QStringView{text}.first(selectionEnd).toUtf8().size();
342 cursor = QWaylandInputMethodEventBuilder::indexToWayland(text, cursor);
343 anchor = QWaylandInputMethodEventBuilder::indexToWayland(text, anchor);
344 if (selEndSize < MAX_MESSAGE_SIZE) {
345 text = QString::fromUtf8(QByteArrayView{utf8}.first(MAX_MESSAGE_SIZE));
346 } else {
347 const int startOffset = selEndSize - MAX_MESSAGE_SIZE;
348 text = QString::fromUtf8(QByteArrayView{utf8}.sliced(startOffset, MAX_MESSAGE_SIZE));
349 cursor -= startOffset;
350 anchor -= startOffset;
351 }
352 }
353 } else {
354 cursor = QWaylandInputMethodEventBuilder::indexToWayland(text, cursor);
355 anchor = QWaylandInputMethodEventBuilder::indexToWayland(text, anchor);
356 }
357 qCDebug(qLcQpaWaylandTextInput) << "Modified surrounding_text: " << text << cursor << anchor;
358
359 if (m_surroundingText != text || m_cursorPos != cursor || m_anchorPos != anchor) {
360 qCDebug(qLcQpaWaylandTextInput) << "Current surrounding_text: " << m_surroundingText << m_cursorPos << m_anchorPos;
361 qCDebug(qLcQpaWaylandTextInput) << "New surrounding_text: " << text << cursor << anchor;
362
363 set_surrounding_text(text, cursor, anchor);
364
365 // A workaround in the case of reselection
366 // It will work when re-clicking a preedit text
367 if (m_condReselection) {
368 qCDebug(qLcQpaWaylandTextInput) << "\"commit\" is disabled when Reselection by changing focus";
369 m_condReselection = false;
370 needsCommit = false;
371
372 }
373
374 m_surroundingText = text;
375 m_cursorPos = cursor;
376 m_anchorPos = anchor;
377 m_cursor = cursor;
378 }
379 }
380
381 if (queries & Qt::ImHints) {
382 QWaylandInputMethodContentType contentType = QWaylandInputMethodContentType::convertV3(static_cast<Qt::InputMethodHints>(event.value(Qt::ImHints).toInt()));
383 qCDebug(qLcQpaWaylandTextInput) << m_contentHint << contentType.hint;
384 qCDebug(qLcQpaWaylandTextInput) << m_contentPurpose << contentType.purpose;
385
386 if (m_contentHint != contentType.hint || m_contentPurpose != contentType.purpose) {
387 qCDebug(qLcQpaWaylandTextInput) << "set_content_type: " << contentType.hint << contentType.purpose;
388 set_content_type(contentType.hint, contentType.purpose);
389
390 m_contentHint = contentType.hint;
391 m_contentPurpose = contentType.purpose;
392 needsCommit = true;
393 }
394 }
395
396 if (needsCommit)
397 commit();
398}
399
401{
402 Q_UNUSED(cursor);
403}
404
406{
407 return false;
408}
409
411{
412 qCDebug(qLcQpaWaylandTextInput) << Q_FUNC_INFO;
413 return m_cursorRect;
414}
415
417{
418 return QLocale();
419}
420
422{
423 return Qt::LeftToRight;
424}
425
426}
427
428QT_END_NAMESPACE
void disableSurface(::wl_surface *) override
void zwp_text_input_v3_commit_string(const QString &text) override
Qt::LayoutDirection inputDirection() const 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_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_done(uint32_t serial) override