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 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | QWaylandInputMethodEventBuilder::~QWaylandInputMethodEventBuilder() |
23 | { |
24 | } |
25 | |
26 | void 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 | |
36 | void QWaylandInputMethodEventBuilder::setCursorPosition(int32_t index, int32_t anchor) |
37 | { |
38 | m_cursor = index; |
39 | m_anchor = anchor; |
40 | } |
41 | |
42 | void QWaylandInputMethodEventBuilder::setDeleteSurroundingText(uint32_t beforeLength, uint32_t afterLength) |
43 | { |
44 | m_deleteBefore = beforeLength; |
45 | m_deleteAfter = afterLength; |
46 | } |
47 | |
48 | void 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 | |
90 | void QWaylandInputMethodEventBuilder::setPreeditCursor(int32_t index) |
91 | { |
92 | m_preeditCursor = index; |
93 | } |
94 | |
95 | QInputMethodEvent *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 | |
125 | QInputMethodEvent *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 | |
149 | QPair<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 | |
167 | QWaylandInputMethodContentType 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 | |
221 | QWaylandInputMethodContentType 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 | |
274 | int 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 | |
288 | int 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 | |
326 | int QWaylandInputMethodEventBuilder::indexToWayland(const QString &text, int length, int base) |
327 | { |
328 | return QStringView{text}.mid(pos: base, n: length).toUtf8().size(); |
329 | } |
330 | |
331 | QT_END_NAMESPACE |
332 | |
333 | |