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

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

source code of qtwayland/src/client/qwaylandtextinputv3.cpp