1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qwaylandtextinputv3.h"
5#include "qwaylandtextinputv3_p.h"
6
7#include <QtWaylandCompositor/QWaylandCompositor>
8#include <QtWaylandCompositor/private/qwaylandseat_p.h>
9
10#include "qwaylandsurface.h"
11#include "qwaylandview.h"
12#include "qwaylandinputmethodeventbuilder_p.h"
13
14#include <QGuiApplication>
15#include <QInputMethodEvent>
16#include <qpa/qwindowsysteminterface.h>
17
18#if QT_CONFIG(xkbcommon)
19#include <QtGui/private/qxkbcommon_p.h>
20#endif
21
22QT_BEGIN_NAMESPACE
23
24Q_DECLARE_LOGGING_CATEGORY(qLcWaylandCompositorTextInput)
25
26QWaylandTextInputV3ClientState::QWaylandTextInputV3ClientState()
27{
28}
29
30Qt::InputMethodQueries QWaylandTextInputV3ClientState::updatedQueries(const QWaylandTextInputV3ClientState &other) const
31{
32 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
33
34 Qt::InputMethodQueries queries;
35
36 if (hints != other.hints)
37 queries |= Qt::ImHints;
38 if (cursorRectangle != other.cursorRectangle)
39 queries |= Qt::ImCursorRectangle;
40 if (surroundingText != other.surroundingText)
41 queries |= Qt::ImSurroundingText | Qt::ImCurrentSelection;
42 if (cursorPosition != other.cursorPosition)
43 queries |= Qt::ImCursorPosition | Qt::ImCurrentSelection;
44 if (anchorPosition != other.anchorPosition)
45 queries |= Qt::ImAnchorPosition | Qt::ImCurrentSelection;
46
47 return queries;
48}
49
50Qt::InputMethodQueries QWaylandTextInputV3ClientState::mergeChanged(const QWaylandTextInputV3ClientState &other) {
51
52 Qt::InputMethodQueries queries;
53
54 if ((other.changedState & Qt::ImHints) && hints != other.hints) {
55 hints = other.hints;
56 queries |= Qt::ImHints;
57 }
58
59 if ((other.changedState & Qt::ImCursorRectangle) && cursorRectangle != other.cursorRectangle) {
60 cursorRectangle = other.cursorRectangle;
61 queries |= Qt::ImCursorRectangle;
62 }
63
64 if ((other.changedState & Qt::ImSurroundingText) && surroundingText != other.surroundingText) {
65 surroundingText = other.surroundingText;
66 queries |= Qt::ImSurroundingText | Qt::ImCurrentSelection;
67 }
68
69 if ((other.changedState & Qt::ImCursorPosition) && cursorPosition != other.cursorPosition) {
70 cursorPosition = other.cursorPosition;
71 queries |= Qt::ImCursorPosition | Qt::ImCurrentSelection;
72 }
73
74 if ((other.changedState & Qt::ImAnchorPosition) && anchorPosition != other.anchorPosition) {
75 anchorPosition = other.anchorPosition;
76 queries |= Qt::ImAnchorPosition | Qt::ImCurrentSelection;
77 }
78
79 return queries;
80}
81
82QWaylandTextInputV3Private::QWaylandTextInputV3Private(QWaylandCompositor *compositor)
83 : compositor(compositor)
84 , currentState(new QWaylandTextInputV3ClientState)
85 , pendingState(new QWaylandTextInputV3ClientState)
86{
87}
88
89void QWaylandTextInputV3Private::sendInputMethodEvent(QInputMethodEvent *event)
90{
91 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
92
93 if (!focusResource || !focusResource->handle)
94 return;
95
96 bool needsDone = false;
97
98 const QString &newPreeditString = event->preeditString();
99
100 // Current cursor shape is only line. It means both cursorBegin
101 // and cursorEnd will be the same values.
102 int32_t preeditCursorPos = newPreeditString.toUtf8().size();
103
104 if (event->replacementLength() > 0) {
105 int replacementStart = event->replacementStart();
106 int replacementLength = event->replacementLength();
107 const int cursorPos = currentState->cursorPosition;
108 if (currentState->cursorPosition < -event->replacementStart()) {
109 qCWarning(qLcWaylandCompositorTextInput)
110 << Q_FUNC_INFO
111 << "Invalid replacementStart :" << replacementStart
112 << "on the cursorPosition :" << cursorPos;
113 replacementStart = -cursorPos;
114 }
115 auto targetText = QStringView{currentState->surroundingText}.sliced(pos: cursorPos + replacementStart);
116 if (targetText.length() < replacementLength) {
117 qCWarning(qLcWaylandCompositorTextInput)
118 << Q_FUNC_INFO
119 << "Invalid replacementLength :" << replacementLength
120 << "for the surrounding text :" << targetText;
121 replacementLength = targetText.length();
122 }
123 const int before = targetText.first(n: -replacementStart).toUtf8().size();
124 const int after = targetText.first(n: replacementLength).toUtf8().size() - before;
125
126 send_delete_surrounding_text(focusResource->handle, before, after);
127 needsDone = true;
128
129 // The commit will also be applied here
130 currentState->surroundingText.replace(i: cursorPos + replacementStart,
131 len: replacementLength,
132 after: event->commitString());
133 currentState->cursorPosition = cursorPos + replacementStart + event->commitString().length();
134 currentState->anchorPosition = cursorPos + replacementStart + event->commitString().length();
135 qApp->inputMethod()->update(queries: Qt::ImSurroundingText | Qt::ImCursorPosition | Qt::ImAnchorPosition);
136 }
137
138 if (currentPreeditString != newPreeditString) {
139 currentPreeditString = newPreeditString;
140 send_preedit_string(focusResource->handle, currentPreeditString, preeditCursorPos, preeditCursorPos);
141 needsDone = true;
142 }
143 if (!event->commitString().isEmpty()) {
144 send_commit_string(focusResource->handle, event->commitString());
145 needsDone = true;
146 }
147
148 if (needsDone)
149 send_done(focusResource->handle, serials[focusResource]);
150}
151
152
153void QWaylandTextInputV3Private::sendKeyEvent(QKeyEvent *event)
154{
155 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
156
157 if (!focusResource || !focusResource->handle)
158 return;
159
160 send_commit_string(focusResource->handle, event->text());
161
162 send_done(focusResource->handle, serials[focusResource]);
163}
164
165QVariant QWaylandTextInputV3Private::inputMethodQuery(Qt::InputMethodQuery property, QVariant argument) const
166{
167 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO << property;
168
169 switch (property) {
170 case Qt::ImHints:
171 return QVariant(static_cast<int>(currentState->hints));
172 case Qt::ImCursorRectangle:
173 return currentState->cursorRectangle;
174 case Qt::ImFont:
175 // Not supported
176 return QVariant();
177 case Qt::ImCursorPosition:
178 qCDebug(qLcWaylandCompositorTextInput) << currentState->cursorPosition;
179 return currentState->cursorPosition;
180 case Qt::ImSurroundingText:
181 qCDebug(qLcWaylandCompositorTextInput) << currentState->surroundingText;
182 return currentState->surroundingText;
183 case Qt::ImCurrentSelection:
184 return currentState->surroundingText.mid(position: qMin(a: currentState->cursorPosition, b: currentState->anchorPosition),
185 n: qAbs(t: currentState->anchorPosition - currentState->cursorPosition));
186 case Qt::ImMaximumTextLength:
187 // Not supported
188 return QVariant();
189 case Qt::ImAnchorPosition:
190 qCDebug(qLcWaylandCompositorTextInput) << currentState->anchorPosition;
191 return currentState->anchorPosition;
192 case Qt::ImAbsolutePosition:
193 // We assume the surrounding text is our whole document for now
194 return currentState->cursorPosition;
195 case Qt::ImTextAfterCursor:
196 if (argument.isValid())
197 return currentState->surroundingText.mid(position: currentState->cursorPosition, n: argument.toInt());
198 return currentState->surroundingText.mid(position: currentState->cursorPosition);
199 case Qt::ImTextBeforeCursor:
200 if (argument.isValid())
201 return currentState->surroundingText.left(n: currentState->cursorPosition).right(n: argument.toInt());
202 return currentState->surroundingText.left(n: currentState->cursorPosition);
203
204 default:
205 return QVariant();
206 }
207}
208
209void QWaylandTextInputV3Private::setFocus(QWaylandSurface *surface)
210{
211 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO << surface;
212
213 if (focusResource && focus) {
214 qApp->inputMethod()->commit();
215 qApp->inputMethod()->hide();
216 inputPanelVisible = false;
217 send_leave(focusResource->handle, focus->resource());
218 currentPreeditString.clear();
219 }
220
221 if (focus != surface)
222 focusDestroyListener.reset();
223
224 Resource *resource = surface ? resourceMap().value(surface->waylandClient()) : 0;
225 if (resource && surface) {
226 send_enter(resource->handle, surface->resource());
227
228 if (focus != surface)
229 focusDestroyListener.listenForDestruction(resource: surface->resource());
230 }
231
232 focus = surface;
233 focusResource = resource;
234}
235
236void QWaylandTextInputV3Private::zwp_text_input_v3_bind_resource(Resource *resource)
237{
238 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
239
240 serials.insert(resource, 0);
241}
242
243void QWaylandTextInputV3Private::zwp_text_input_v3_destroy_resource(Resource *resource)
244{
245 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
246
247 serials.remove(resource);
248 if (focusResource == resource)
249 focusResource = nullptr;
250}
251
252void QWaylandTextInputV3Private::zwp_text_input_v3_destroy(Resource *resource)
253{
254 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
255
256 wl_resource_destroy(resource->handle);
257}
258
259void QWaylandTextInputV3Private::zwp_text_input_v3_enable(Resource *resource)
260{
261 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
262
263 Q_Q(QWaylandTextInputV3);
264
265 pendingState.reset(other: new QWaylandTextInputV3ClientState);
266
267 enabledSurfaces.insert(resource, focus);
268 emit q->surfaceEnabled(surface: focus);
269
270 inputPanelVisible = true;
271 qApp->inputMethod()->show();
272}
273
274void QWaylandTextInputV3Private::zwp_text_input_v3_disable(QtWaylandServer::zwp_text_input_v3::Resource *resource)
275{
276 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
277
278 Q_Q(QWaylandTextInputV3);
279
280 QWaylandSurface *s = enabledSurfaces.take(resource);
281 emit q->surfaceDisabled(surface: s);
282
283 // When reselecting a word by setFocus
284 qApp->inputMethod()->commit();
285
286 qApp->inputMethod()->reset();
287 pendingState.reset(other: new QWaylandTextInputV3ClientState);
288}
289
290void QWaylandTextInputV3Private::zwp_text_input_v3_set_cursor_rectangle(Resource *resource, int32_t x, int32_t y, int32_t width, int32_t height)
291{
292 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO << x << y << width << height;
293
294 if (resource != focusResource)
295 return;
296
297 pendingState->cursorRectangle = QRect(x, y, width, height);
298
299 pendingState->changedState |= Qt::ImCursorRectangle;
300}
301
302void QWaylandTextInputV3Private::zwp_text_input_v3_commit(Resource *resource)
303{
304 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
305
306 serials[resource] = serials[resource] < UINT_MAX ? serials[resource] + 1U : 0U;
307
308 // Just increase serials and ignore empty commits
309 if (!pendingState->changedState) {
310 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO << "pendingState is not changed";
311 return;
312 }
313
314 // Selection starts.
315 // But since qtvirtualkeyboard with hunspell does not reset its preedit string,
316 // compositor forces to reset inputMethod.
317 if ((currentState->cursorPosition == currentState->anchorPosition)
318 && (pendingState->cursorPosition != pendingState->anchorPosition))
319 qApp->inputMethod()->reset();
320
321 // Enable reselection
322 // This is a workaround to make qtvirtualkeyboad's state empty by clearing State::InputMethodClick.
323 if (currentState->surroundingText == pendingState->surroundingText && currentState->cursorPosition != pendingState->cursorPosition)
324 qApp->inputMethod()->invokeAction(a: QInputMethod::Click, cursorPosition: pendingState->cursorPosition);
325
326 Qt::InputMethodQueries queries = currentState->mergeChanged(other: *pendingState.data());
327 pendingState.reset(other: new QWaylandTextInputV3ClientState);
328
329 if (queries) {
330 qCDebug(qLcWaylandCompositorTextInput) << "QInputMethod::update() after commit with" << queries;
331
332 qApp->inputMethod()->update(queries);
333 }
334}
335
336void QWaylandTextInputV3Private::zwp_text_input_v3_set_content_type(Resource *resource, uint32_t hint, uint32_t purpose)
337{
338 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO << hint << purpose;
339
340 if (resource != focusResource)
341 return;
342
343 pendingState->hints = Qt::ImhNone;
344
345 if ((hint & content_hint_completion) == 0)
346 pendingState->hints |= Qt::ImhNoPredictiveText;
347 if ((hint & content_hint_auto_capitalization) == 0)
348 pendingState->hints |= Qt::ImhNoAutoUppercase;
349 if ((hint & content_hint_lowercase) != 0)
350 pendingState->hints |= Qt::ImhPreferLowercase;
351 if ((hint & content_hint_uppercase) != 0)
352 pendingState->hints |= Qt::ImhPreferUppercase;
353 if ((hint & content_hint_hidden_text) != 0)
354 pendingState->hints |= Qt::ImhHiddenText;
355 if ((hint & content_hint_sensitive_data) != 0)
356 pendingState->hints |= Qt::ImhSensitiveData;
357 if ((hint & content_hint_latin) != 0)
358 pendingState->hints |= Qt::ImhLatinOnly;
359 if ((hint & content_hint_multiline) != 0)
360 pendingState->hints |= Qt::ImhMultiLine;
361
362 switch (purpose) {
363 case content_purpose_normal:
364 break;
365 case content_purpose_alpha:
366 pendingState->hints |= Qt::ImhUppercaseOnly | Qt::ImhLowercaseOnly;
367 break;
368 case content_purpose_digits:
369 pendingState->hints |= Qt::ImhDigitsOnly;
370 break;
371 case content_purpose_number:
372 pendingState->hints |= Qt::ImhFormattedNumbersOnly;
373 break;
374 case content_purpose_phone:
375 pendingState->hints |= Qt::ImhDialableCharactersOnly;
376 break;
377 case content_purpose_url:
378 pendingState->hints |= Qt::ImhUrlCharactersOnly;
379 break;
380 case content_purpose_email:
381 pendingState->hints |= Qt::ImhEmailCharactersOnly;
382 break;
383 case content_purpose_name:
384 case content_purpose_password:
385 break;
386 case content_purpose_date:
387 pendingState->hints |= Qt::ImhDate;
388 break;
389 case content_purpose_time:
390 pendingState->hints |= Qt::ImhTime;
391 break;
392 case content_purpose_datetime:
393 pendingState->hints |= Qt::ImhDate | Qt::ImhTime;
394 break;
395 case content_purpose_terminal:
396 default:
397 break;
398 }
399
400 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO << pendingState->hints;
401
402 pendingState->changedState |= Qt::ImHints;
403}
404
405void QWaylandTextInputV3Private::zwp_text_input_v3_set_surrounding_text(Resource *resource, const QString &text, int32_t cursor, int32_t anchor)
406{
407 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO << text << cursor << anchor;
408
409 if (resource != focusResource)
410 return;
411
412 pendingState->surroundingText = text;
413 pendingState->cursorPosition = QWaylandInputMethodEventBuilder::indexFromWayland(text, length: cursor);
414 pendingState->anchorPosition = QWaylandInputMethodEventBuilder::indexFromWayland(text, length: anchor);
415
416 pendingState->changedState |= Qt::ImSurroundingText | Qt::ImCursorPosition | Qt::ImAnchorPosition;
417}
418
419void QWaylandTextInputV3Private::zwp_text_input_v3_set_text_change_cause(Resource *resource, uint32_t cause)
420{
421 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
422
423 Q_UNUSED(resource);
424 Q_UNUSED(cause);
425}
426
427QWaylandTextInputV3::QWaylandTextInputV3(QWaylandObject *container, QWaylandCompositor *compositor)
428 : QWaylandCompositorExtensionTemplate(container, *new QWaylandTextInputV3Private(compositor))
429{
430 connect(sender: &d_func()->focusDestroyListener, signal: &QWaylandDestroyListener::fired,
431 context: this, slot: &QWaylandTextInputV3::focusSurfaceDestroyed);
432}
433
434QWaylandTextInputV3::~QWaylandTextInputV3()
435{
436}
437
438void QWaylandTextInputV3::sendInputMethodEvent(QInputMethodEvent *event)
439{
440 Q_D(QWaylandTextInputV3);
441
442 d->sendInputMethodEvent(event);
443}
444
445void QWaylandTextInputV3::sendKeyEvent(QKeyEvent *event)
446{
447 Q_D(QWaylandTextInputV3);
448
449 d->sendKeyEvent(event);
450}
451
452QVariant QWaylandTextInputV3::inputMethodQuery(Qt::InputMethodQuery property, QVariant argument) const
453{
454 const Q_D(QWaylandTextInputV3);
455
456 return d->inputMethodQuery(property, argument);
457}
458
459QWaylandSurface *QWaylandTextInputV3::focus() const
460{
461 const Q_D(QWaylandTextInputV3);
462
463 return d->focus;
464}
465
466void QWaylandTextInputV3::setFocus(QWaylandSurface *surface)
467{
468 Q_D(QWaylandTextInputV3);
469
470 d->setFocus(surface);
471}
472
473void QWaylandTextInputV3::focusSurfaceDestroyed(void *)
474{
475 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
476
477 Q_D(QWaylandTextInputV3);
478
479 d->focusDestroyListener.reset();
480
481 d->focus = nullptr;
482 d->focusResource = nullptr;
483}
484
485bool QWaylandTextInputV3::isSurfaceEnabled(QWaylandSurface *surface) const
486{
487 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO;
488
489 const Q_D(QWaylandTextInputV3);
490
491 return d->enabledSurfaces.values().contains(surface);
492}
493
494void QWaylandTextInputV3::add(::wl_client *client, uint32_t id, int version)
495{
496 qCDebug(qLcWaylandCompositorTextInput) << Q_FUNC_INFO << client << id << version;
497
498 Q_D(QWaylandTextInputV3);
499
500 d->add(client, id, version);
501}
502
503const wl_interface *QWaylandTextInputV3::interface()
504{
505 return QWaylandTextInputV3Private::interface();
506}
507
508QByteArray QWaylandTextInputV3::interfaceName()
509{
510 return QWaylandTextInputV3Private::interfaceName();
511}
512
513QT_END_NAMESPACE
514

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtwayland/src/compositor/extensions/qwaylandtextinputv3.cpp