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