1// Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
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 "qwaylandinputmethodeventbuilder_p.h"
5
6#include <QBrush>
7#include <QGuiApplication>
8#include <QInputMethod>
9#include <QPalette>
10#include <QTextCharFormat>
11
12#ifdef QT_BUILD_WAYLANDCOMPOSITOR_LIB
13#include <QtWaylandCompositor/private/qwayland-server-text-input-unstable-v2.h>
14#include <QtWaylandCompositor/private/qwayland-server-text-input-unstable-v4-wip.h>
15#else
16#include <QtWaylandClient/private/qwayland-text-input-unstable-v2.h>
17#include <QtWaylandClient/private/qwayland-text-input-unstable-v4-wip.h>
18#endif
19
20QT_BEGIN_NAMESPACE
21
22QWaylandInputMethodEventBuilder::~QWaylandInputMethodEventBuilder()
23{
24}
25
26void QWaylandInputMethodEventBuilder::reset()
27{
28 m_anchor = 0;
29 m_cursor = 0;
30 m_deleteBefore = 0;
31 m_deleteAfter = 0;
32 m_preeditCursor = 0;
33 m_preeditStyles.clear();
34}
35
36void QWaylandInputMethodEventBuilder::setCursorPosition(int32_t index, int32_t anchor)
37{
38 m_cursor = index;
39 m_anchor = anchor;
40}
41
42void QWaylandInputMethodEventBuilder::setDeleteSurroundingText(uint32_t beforeLength, uint32_t afterLength)
43{
44 m_deleteBefore = beforeLength;
45 m_deleteAfter = afterLength;
46}
47
48void QWaylandInputMethodEventBuilder::addPreeditStyling(uint32_t index, uint32_t length, uint32_t style)
49{
50 QTextCharFormat format;
51
52 switch (style) {
53 case ZWP_TEXT_INPUT_V2_PREEDIT_STYLE_NONE:
54 break;
55 case ZWP_TEXT_INPUT_V2_PREEDIT_STYLE_DEFAULT:
56 case ZWP_TEXT_INPUT_V2_PREEDIT_STYLE_UNDERLINE:
57 format.setFontUnderline(true);
58 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
59 m_preeditStyles.append(t: QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, index, length, format));
60 break;
61 case ZWP_TEXT_INPUT_V2_PREEDIT_STYLE_ACTIVE:
62 case ZWP_TEXT_INPUT_V2_PREEDIT_STYLE_INACTIVE:
63 format.setFontWeight(QFont::Bold);
64 format.setFontUnderline(true);
65 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
66 m_preeditStyles.append(t: QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, index, length, format));
67 break;
68 case ZWP_TEXT_INPUT_V2_PREEDIT_STYLE_HIGHLIGHT:
69 case ZWP_TEXT_INPUT_V2_PREEDIT_STYLE_SELECTION:
70 {
71 format.setFontUnderline(true);
72 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
73 QPalette palette = qApp->palette();
74 format.setBackground(QBrush(palette.color(cg: QPalette::Active, cr: QPalette::Highlight)));
75 format.setForeground(QBrush(palette.color(cg: QPalette::Active, cr: QPalette::HighlightedText)));
76 m_preeditStyles.append(t: QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, index, length, format));
77 }
78 break;
79 case ZWP_TEXT_INPUT_V2_PREEDIT_STYLE_INCORRECT:
80 format.setFontUnderline(true);
81 format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
82 format.setUnderlineColor(QColor(Qt::red));
83 m_preeditStyles.append(t: QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, index, length, format));
84 break;
85 default:
86 break;
87 }
88}
89
90void QWaylandInputMethodEventBuilder::setPreeditCursor(int32_t index)
91{
92 m_preeditCursor = index;
93}
94
95QInputMethodEvent *QWaylandInputMethodEventBuilder::buildCommit(const QString &text)
96{
97 QList<QInputMethodEvent::Attribute> attributes;
98
99 const QPair<int, int> replacement = replacementForDeleteSurrounding();
100
101 if (m_cursor != 0 || m_anchor != 0) {
102 QString surrounding = QInputMethod::queryFocusObject(query: Qt::ImSurroundingText, argument: QVariant()).toString();
103 const int cursor = QInputMethod::queryFocusObject(query: Qt::ImCursorPosition, argument: QVariant()).toInt();
104 const int anchor = QInputMethod::queryFocusObject(query: Qt::ImAnchorPosition, argument: QVariant()).toInt();
105 const int absoluteCursor = QInputMethod::queryFocusObject(query: Qt::ImAbsolutePosition, argument: QVariant()).toInt();
106
107 const int absoluteOffset = absoluteCursor - cursor;
108
109 const int cursorAfterCommit = qMin(a: anchor, b: cursor) + replacement.first + text.size();
110 surrounding.replace(i: qMin(a: anchor, b: cursor) + replacement.first,
111 len: qAbs(t: anchor - cursor) + replacement.second, after: text);
112
113 attributes.push_back(t: QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
114 indexFromWayland(text: surrounding, length: m_cursor, base: cursorAfterCommit) + absoluteOffset,
115 indexFromWayland(text: surrounding, length: m_anchor, base: cursorAfterCommit) + absoluteOffset,
116 QVariant()));
117 }
118
119 QInputMethodEvent *event = new QInputMethodEvent(QString(), attributes);
120 event->setCommitString(commitString: text, replaceFrom: replacement.first, replaceLength: replacement.second);
121
122 return event;
123}
124
125QInputMethodEvent *QWaylandInputMethodEventBuilder::buildPreedit(const QString &text)
126{
127 QList<QInputMethodEvent::Attribute> attributes;
128
129 if (m_preeditCursor < 0) {
130 attributes.append(t: QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, 0, 0, QVariant()));
131 } else {
132 attributes.append(t: QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, indexFromWayland(text, length: m_preeditCursor), 1, QVariant()));
133 }
134
135 for (const QInputMethodEvent::Attribute &attr : std::as_const(t&: m_preeditStyles)) {
136 int start = indexFromWayland(text, length: attr.start);
137 int length = indexFromWayland(text, length: attr.start + attr.length) - start;
138 attributes.append(t: QInputMethodEvent::Attribute(attr.type, start, length, attr.value));
139 }
140
141 QInputMethodEvent *event = new QInputMethodEvent(text, attributes);
142
143 const QPair<int, int> replacement = replacementForDeleteSurrounding();
144 event->setCommitString(commitString: QString(), replaceFrom: replacement.first, replaceLength: replacement.second);
145
146 return event;
147}
148
149QPair<int, int> QWaylandInputMethodEventBuilder::replacementForDeleteSurrounding()
150{
151 if (m_deleteBefore == 0 && m_deleteAfter == 0)
152 return QPair<int, int>(0, 0);
153
154 const QString &surrounding = QInputMethod::queryFocusObject(query: Qt::ImSurroundingText, argument: QVariant()).toString();
155 const int cursor = QInputMethod::queryFocusObject(query: Qt::ImCursorPosition, argument: QVariant()).toInt();
156 const int anchor = QInputMethod::queryFocusObject(query: Qt::ImAnchorPosition, argument: QVariant()).toInt();
157
158 const int selectionStart = qMin(a: cursor, b: anchor);
159 const int selectionEnd = qMax(a: cursor, b: anchor);
160
161 const int deleteBefore = selectionStart - indexFromWayland(text: surrounding, length: -m_deleteBefore, base: selectionStart);
162 const int deleteAfter = indexFromWayland(text: surrounding, length: m_deleteAfter, base: selectionEnd) - selectionEnd;
163
164 return QPair<int, int>(-deleteBefore, deleteBefore + deleteAfter);
165}
166
167QWaylandInputMethodContentType QWaylandInputMethodContentType::convert(Qt::InputMethodHints hints)
168{
169 uint32_t hint = ZWP_TEXT_INPUT_V2_CONTENT_HINT_NONE;
170 uint32_t purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_NORMAL;
171
172 if (hints & Qt::ImhHiddenText)
173 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_HIDDEN_TEXT;
174 if (hints & Qt::ImhSensitiveData)
175 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_SENSITIVE_DATA;
176 if ((hints & Qt::ImhNoAutoUppercase) == 0)
177 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_AUTO_CAPITALIZATION;
178 if (hints & Qt::ImhPreferNumbers) {
179 // Nothing yet
180 }
181 if (hints & Qt::ImhPreferUppercase)
182 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_UPPERCASE;
183 if (hints & Qt::ImhPreferLowercase)
184 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_LOWERCASE;
185 if ((hints & Qt::ImhNoPredictiveText) == 0) {
186 hint |= (ZWP_TEXT_INPUT_V2_CONTENT_HINT_AUTO_COMPLETION
187 | ZWP_TEXT_INPUT_V2_CONTENT_HINT_AUTO_CORRECTION);
188 }
189
190 if ((hints & Qt::ImhDate) && (hints & Qt::ImhTime) == 0)
191 purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_DATE;
192 else if ((hints & Qt::ImhDate) && (hints & Qt::ImhTime))
193 purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_DATETIME;
194 else if ((hints & Qt::ImhDate) == 0 && (hints & Qt::ImhTime))
195 purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_TIME;
196
197 if (hints & Qt::ImhPreferLatin)
198 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_LATIN;
199 if (hints & Qt::ImhMultiLine)
200 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_MULTILINE;
201 if (hints & Qt::ImhDigitsOnly)
202 purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_DIGITS;
203 if (hints & Qt::ImhFormattedNumbersOnly)
204 purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_NUMBER;
205 if (hints & Qt::ImhUppercaseOnly)
206 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_UPPERCASE;
207 if (hints & Qt::ImhLowercaseOnly)
208 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_LOWERCASE;
209 if (hints & Qt::ImhDialableCharactersOnly)
210 purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_PHONE;
211 if (hints & Qt::ImhEmailCharactersOnly)
212 purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_EMAIL;
213 if (hints & Qt::ImhUrlCharactersOnly)
214 purpose = ZWP_TEXT_INPUT_V2_CONTENT_PURPOSE_URL;
215 if (hints & Qt::ImhLatinOnly)
216 hint |= ZWP_TEXT_INPUT_V2_CONTENT_HINT_LATIN;
217
218 return QWaylandInputMethodContentType{.hint: hint, .purpose: purpose};
219}
220
221QWaylandInputMethodContentType QWaylandInputMethodContentType::convertV4(Qt::InputMethodHints hints)
222{
223 uint32_t hint = ZWP_TEXT_INPUT_V4_CONTENT_HINT_NONE;
224 uint32_t purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_NORMAL;
225
226 if (hints & Qt::ImhHiddenText)
227 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_HIDDEN_TEXT;
228 if (hints & Qt::ImhSensitiveData)
229 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_SENSITIVE_DATA;
230 if ((hints & Qt::ImhNoAutoUppercase) == 0)
231 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_AUTO_CAPITALIZATION;
232 if (hints & Qt::ImhPreferNumbers) {
233 // Nothing yet
234 }
235 if (hints & Qt::ImhPreferUppercase)
236 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_UPPERCASE;
237 if (hints & Qt::ImhPreferLowercase)
238 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_LOWERCASE;
239 if ((hints & Qt::ImhNoPredictiveText) == 0) {
240 hint |= (ZWP_TEXT_INPUT_V4_CONTENT_HINT_COMPLETION
241 | ZWP_TEXT_INPUT_V4_CONTENT_HINT_SPELLCHECK);
242 }
243
244 if ((hints & Qt::ImhDate) && (hints & Qt::ImhTime) == 0)
245 purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_DATE;
246 else if ((hints & Qt::ImhDate) && (hints & Qt::ImhTime))
247 purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_DATETIME;
248 else if ((hints & Qt::ImhDate) == 0 && (hints & Qt::ImhTime))
249 purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_TIME;
250 if (hints & Qt::ImhPreferLatin)
251 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_LATIN;
252 if (hints & Qt::ImhMultiLine)
253 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_MULTILINE;
254 if (hints & Qt::ImhDigitsOnly)
255 purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_DIGITS;
256 if (hints & Qt::ImhFormattedNumbersOnly)
257 purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_NUMBER;
258 if (hints & Qt::ImhUppercaseOnly)
259 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_UPPERCASE;
260 if (hints & Qt::ImhLowercaseOnly)
261 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_LOWERCASE;
262 if (hints & Qt::ImhDialableCharactersOnly)
263 purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_PHONE;
264 if (hints & Qt::ImhEmailCharactersOnly)
265 purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_EMAIL;
266 if (hints & Qt::ImhUrlCharactersOnly)
267 purpose = ZWP_TEXT_INPUT_V4_CONTENT_PURPOSE_URL;
268 if (hints & Qt::ImhLatinOnly)
269 hint |= ZWP_TEXT_INPUT_V4_CONTENT_HINT_LATIN;
270
271 return QWaylandInputMethodContentType{.hint: hint, .purpose: purpose};
272}
273
274int QWaylandInputMethodEventBuilder::indexFromWayland(const QString &text, int length, int base)
275{
276 if (length == 0)
277 return base;
278
279 if (length < 0) {
280 const QByteArray &utf8 = QStringView{text}.left(n: base).toUtf8();
281 return QString::fromUtf8(ba: utf8.left(len: qMax(a: utf8.size() + length, b: 0))).size();
282 } else {
283 const QByteArray &utf8 = QStringView{text}.mid(pos: base).toUtf8();
284 return QString::fromUtf8(ba: utf8.left(len: length)).size() + base;
285 }
286}
287
288int QWaylandInputMethodEventBuilder::trimmedIndexFromWayland(const QString &text, int length, int base)
289{
290 if (length == 0)
291 return base;
292
293 if (length < 0) {
294 const QByteArray &utf8 = QStringView{text}.left(n: base).toUtf8();
295 const int len = utf8.size();
296 const int start = len + length;
297 if (start <= 0)
298 return 0;
299
300 for (int i = 0; i < 4; i++) {
301 if (start + i >= len)
302 return base;
303
304 const unsigned char ch = utf8.at(i: start + i);
305 // check if current character is a utf8's initial character.
306 if (ch < 0x80 || ch > 0xbf)
307 return QString::fromUtf8(ba: utf8.left(len: start + i)).size();
308 }
309 } else {
310 const QByteArray &utf8 = QStringView{text}.mid(pos: base).toUtf8();
311 const int len = utf8.size();
312 const int start = length;
313 if (start >= len)
314 return base + QString::fromUtf8(ba: utf8).size();
315
316 for (int i = 0; i < 4; i++) {
317 const unsigned char ch = utf8.at(i: start - i);
318 // check if current character is a utf8's initial character.
319 if (ch < 0x80 || ch > 0xbf)
320 return base + QString::fromUtf8(ba: utf8.left(len: start - i)).size();
321 }
322 }
323 return -1;
324}
325
326int QWaylandInputMethodEventBuilder::indexToWayland(const QString &text, int length, int base)
327{
328 return QStringView{text}.mid(pos: base, n: length).toUtf8().size();
329}
330
331QT_END_NAMESPACE
332
333

source code of qtwayland/src/shared/qwaylandinputmethodeventbuilder.cpp