1 | /* |
2 | SPDX-FileCopyrightText: 2010 Sebastian Sauer <mail@dipe.org> |
3 | SPDX-FileCopyrightText: 2012 Frederik Gladhorn <gladhorn@kde.org> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #ifndef _KATE_VIEW_ACCESSIBLE_ |
9 | #define _KATE_VIEW_ACCESSIBLE_ |
10 | |
11 | #ifndef QT_NO_ACCESSIBILITY |
12 | |
13 | #include "katedocument.h" |
14 | #include "kateview.h" |
15 | #include "kateviewinternal.h" |
16 | |
17 | #include <KLocalizedString> |
18 | #include <QAccessible> |
19 | #include <QAccessibleWidget> |
20 | #include <QTextBoundaryFinder> |
21 | |
22 | /** |
23 | * This class implements a QAccessible-interface for a KateViewInternal. |
24 | * |
25 | * This is the root class for the kateview. The \a KateCursorAccessible class |
26 | * represents the cursor in the kateview and is a child of this class. |
27 | */ |
28 | class KateViewAccessible : public QAccessibleWidget, public QAccessibleTextInterface, public QAccessibleEditableTextInterface |
29 | { |
30 | public: |
31 | explicit KateViewAccessible(KateViewInternal *view) |
32 | : QAccessibleWidget(view, QAccessible::EditableText) |
33 | , m_lastPosition(-1) |
34 | { |
35 | // to invalidate positionFromCursor cache when the document is changed |
36 | m_conn = QObject::connect(sender: view->view()->document(), signal: &KTextEditor::Document::textChanged, slot: [this]() { |
37 | m_lastPosition = -1; |
38 | }); |
39 | } |
40 | |
41 | void *interface_cast(QAccessible::InterfaceType t) override |
42 | { |
43 | if (t == QAccessible::EditableTextInterface) |
44 | return static_cast<QAccessibleEditableTextInterface *>(this); |
45 | if (t == QAccessible::TextInterface) |
46 | return static_cast<QAccessibleTextInterface *>(this); |
47 | return nullptr; |
48 | } |
49 | |
50 | ~KateViewAccessible() override |
51 | { |
52 | QObject::disconnect(m_conn); |
53 | } |
54 | |
55 | QAccessibleInterface *childAt(int x, int y) const override |
56 | { |
57 | Q_UNUSED(x); |
58 | Q_UNUSED(y); |
59 | return nullptr; |
60 | } |
61 | |
62 | void setText(QAccessible::Text t, const QString &text) override |
63 | { |
64 | if (t == QAccessible::Value && view()->view()->document()) { |
65 | view()->view()->document()->setText(text); |
66 | m_lastPosition = -1; |
67 | } |
68 | } |
69 | |
70 | QAccessible::State state() const override |
71 | { |
72 | QAccessible::State s = QAccessibleWidget::state(); |
73 | s.focusable = view()->focusPolicy() != Qt::NoFocus; |
74 | s.focused = view()->hasFocus(); |
75 | s.editable = true; |
76 | s.multiLine = true; |
77 | s.selectableText = true; |
78 | return s; |
79 | } |
80 | |
81 | QString text(QAccessible::Text t) const override |
82 | { |
83 | QString s; |
84 | if (view()->view()->document()) { |
85 | if (t == QAccessible::Name) { |
86 | s = view()->view()->document()->documentName(); |
87 | } |
88 | if (t == QAccessible::Value) { |
89 | s = view()->view()->document()->text(); |
90 | } |
91 | } |
92 | return s; |
93 | } |
94 | |
95 | int characterCount() const override |
96 | { |
97 | return view()->view()->document()->text().size(); |
98 | } |
99 | |
100 | void addSelection(int startOffset, int endOffset) override |
101 | { |
102 | KTextEditor::Range range; |
103 | range.setRange(start: cursorFromInt(position: startOffset), end: cursorFromInt(position: endOffset)); |
104 | view()->view()->setSelection(range); |
105 | view()->view()->setCursorPosition(cursorFromInt(position: endOffset)); |
106 | } |
107 | |
108 | QString attributes(int offset, int *startOffset, int *endOffset) const override |
109 | { |
110 | Q_UNUSED(offset); |
111 | *startOffset = 0; |
112 | *endOffset = characterCount(); |
113 | return QString(); |
114 | } |
115 | |
116 | QRect characterRect(int offset) const override |
117 | { |
118 | KTextEditor::Cursor c = cursorFromInt(position: offset); |
119 | if (!c.isValid()) { |
120 | return QRect(); |
121 | } |
122 | QPoint p = view()->cursorToCoordinate(cursor: c); |
123 | KTextEditor::Cursor endCursor = KTextEditor::Cursor(c.line(), c.column() + 1); |
124 | QPoint size = view()->cursorToCoordinate(cursor: endCursor) - p; |
125 | return QRect(view()->mapToGlobal(p), QSize(size.x(), size.y())); |
126 | } |
127 | |
128 | int cursorPosition() const override |
129 | { |
130 | KTextEditor::Cursor c = view()->cursorPosition(); |
131 | return positionFromCursor(view: view(), cursor: c); |
132 | } |
133 | |
134 | int offsetAtPoint(const QPoint &point) const override |
135 | { |
136 | if (view()) { |
137 | KTextEditor::Cursor c = view()->coordinatesToCursor(coord: point); |
138 | return positionFromCursor(view: view(), cursor: c); |
139 | } |
140 | return 0; |
141 | } |
142 | |
143 | void removeSelection(int selectionIndex) override |
144 | { |
145 | if (selectionIndex != 0) { |
146 | return; |
147 | } |
148 | view()->view()->clearSelection(); |
149 | } |
150 | |
151 | void scrollToSubstring(int startIndex, int /*endIndex*/) override |
152 | { |
153 | auto c = cursorFromInt(position: startIndex); |
154 | if (!c.isValid()) { |
155 | return; |
156 | } |
157 | view()->view()->setScrollPosition(c); |
158 | } |
159 | |
160 | void selection(int selectionIndex, int *startOffset, int *endOffset) const override |
161 | { |
162 | if (selectionIndex != 0 || !view()->view()->selection()) { |
163 | *startOffset = 0; |
164 | *endOffset = 0; |
165 | return; |
166 | } |
167 | KTextEditor::Range range = view()->view()->selectionRange(); |
168 | *startOffset = positionFromCursor(view: view(), cursor: range.start()); |
169 | *endOffset = positionFromCursor(view: view(), cursor: range.end()); |
170 | } |
171 | |
172 | int selectionCount() const override |
173 | { |
174 | return view()->view()->selection() ? 1 : 0; |
175 | } |
176 | |
177 | void setCursorPosition(int position) override |
178 | { |
179 | view()->view()->setCursorPosition(cursorFromInt(position)); |
180 | } |
181 | |
182 | void setSelection(int selectionIndex, int startOffset, int endOffset) override |
183 | { |
184 | if (selectionIndex != 0) { |
185 | return; |
186 | } |
187 | KTextEditor::Range range = KTextEditor::Range(cursorFromInt(position: startOffset), cursorFromInt(position: endOffset)); |
188 | view()->view()->setSelection(range); |
189 | } |
190 | |
191 | QString text(int startOffset, int endOffset) const override |
192 | { |
193 | if (startOffset > endOffset) { |
194 | return QString(); |
195 | } |
196 | return view()->view()->document()->text().mid(position: startOffset, n: endOffset - startOffset); |
197 | } |
198 | |
199 | QString textAtOffset(int offset, QAccessible::TextBoundaryType boundaryType, int *startOffset, int *endOffset) const override |
200 | { |
201 | *startOffset = -1; |
202 | *endOffset = -1; |
203 | if (!view() || !startOffset || !endOffset) { |
204 | return {}; |
205 | } |
206 | if (offset == -1) { |
207 | offset = positionFromCursor(view: view(), cursor: view()->view()->doc()->documentEnd()); |
208 | } |
209 | KTextEditor::Cursor c = cursorFromInt(position: offset); |
210 | if (!c.isValid()) { |
211 | return {}; |
212 | } |
213 | auto doc = view()->view()->doc(); |
214 | |
215 | switch (boundaryType) { |
216 | case QAccessible::TextBoundaryType::CharBoundary: { |
217 | QString t = doc->characterAt(position: c); |
218 | *startOffset = offset; |
219 | *endOffset = *startOffset + 1; |
220 | return t; |
221 | } break; |
222 | case QAccessible::TextBoundaryType::WordBoundary: { |
223 | QString t = doc->wordAt(cursor: c); |
224 | *startOffset = offset; |
225 | *endOffset = offset + t.size(); |
226 | return t; |
227 | } break; |
228 | case QAccessible::TextBoundaryType::LineBoundary: |
229 | case QAccessible::TextBoundaryType::ParagraphBoundary: { |
230 | const QString line = doc->line(line: c.line()); |
231 | if (line.isEmpty()) { |
232 | *startOffset = offset; |
233 | *endOffset = offset; |
234 | return {}; |
235 | } |
236 | const int start = positionFromCursor(view: view(), cursor: KTextEditor::Cursor(c.line(), 0)); |
237 | *startOffset = start; |
238 | *endOffset = offset + line.size(); |
239 | return line; |
240 | } break; |
241 | case QAccessible::TextBoundaryType::SentenceBoundary: { |
242 | const QString line = doc->line(line: c.line()); |
243 | if (line.isEmpty()) { |
244 | *startOffset = offset; |
245 | *endOffset = offset; |
246 | return {}; |
247 | } |
248 | QTextBoundaryFinder bf(QTextBoundaryFinder::BoundaryType::Sentence, line); |
249 | int e = bf.toNextBoundary(); |
250 | if (e != -1) { |
251 | int start = positionFromCursor(view: view(), cursor: KTextEditor::Cursor(c.line(), 0)); |
252 | *startOffset = start; |
253 | *endOffset = start + bf.position(); |
254 | return line.mid(position: 0, n: e); |
255 | } |
256 | } break; |
257 | case QAccessible::TextBoundaryType::NoBoundary: { |
258 | const QString text = doc->text(); |
259 | *startOffset = 0; |
260 | *endOffset = text.size(); |
261 | return text; |
262 | } break; |
263 | } |
264 | |
265 | return {}; |
266 | } |
267 | |
268 | QString textBeforeOffset(int /*offset*/, QAccessible::TextBoundaryType /*boundaryType*/, int *startOffset, int *endOffset) const override |
269 | { |
270 | // FIXME |
271 | *startOffset = -1; |
272 | *endOffset = -1; |
273 | return {}; |
274 | } |
275 | QString textAfterOffset(int /*offset*/, QAccessible::TextBoundaryType /*boundaryType*/, int *startOffset, int *endOffset) const override |
276 | { |
277 | // FIXME |
278 | *startOffset = -1; |
279 | *endOffset = -1; |
280 | return {}; |
281 | } |
282 | |
283 | void deleteText(int startOffset, int endOffset) override |
284 | { |
285 | KTextEditor::Document *document = view()->view()->document(); |
286 | KTextEditor::Range range(document->offsetToCursor(offset: startOffset), document->offsetToCursor(offset: endOffset)); |
287 | document->removeText(range); |
288 | } |
289 | |
290 | void insertText(int offset, const QString &text) override |
291 | { |
292 | KTextEditor::Document *document = view()->view()->document(); |
293 | KTextEditor::Cursor cursor = document->offsetToCursor(offset); |
294 | document->insertText(position: cursor, text); |
295 | } |
296 | |
297 | void replaceText(int startOffset, int endOffset, const QString &text) override |
298 | { |
299 | KTextEditor::Document *document = view()->view()->document(); |
300 | KTextEditor::Range range(document->offsetToCursor(offset: startOffset), document->offsetToCursor(offset: endOffset)); |
301 | document->replaceText(range, text); |
302 | } |
303 | |
304 | /** |
305 | * When possible, using the last returned value m_lastPosition do the count |
306 | * from the last cursor position m_lastCursor. |
307 | * @return the number of chars (including one character for new lines) |
308 | * from the beginning of the file. |
309 | */ |
310 | int positionFromCursor(KateViewInternal *view, KTextEditor::Cursor cursor) const |
311 | { |
312 | int pos = m_lastPosition; |
313 | const KTextEditor::DocumentPrivate *doc = view->view()->doc(); |
314 | |
315 | // m_lastPosition < 0 is invalid, calculate from the beginning of the document |
316 | if (m_lastPosition < 0 || view != m_lastView) { |
317 | pos = doc->cursorToOffset(c: cursor) - cursor.column(); |
318 | } else { |
319 | // if the lines are the same, just add the cursor.column(), otherwise |
320 | if (cursor.line() != m_lastCursor.line()) { |
321 | // If the cursor is after the previous cursor |
322 | if (m_lastCursor.line() < cursor.line()) { |
323 | for (int line = m_lastCursor.line(); line < cursor.line(); ++line) { |
324 | pos += doc->lineLength(line); |
325 | } |
326 | // add new line character for each line |
327 | pos += cursor.line() - m_lastCursor.line(); |
328 | } else { |
329 | for (int line = cursor.line(); line < m_lastCursor.line(); ++line) { |
330 | pos -= doc->lineLength(line); |
331 | } |
332 | // remove new line character for each line |
333 | pos -= m_lastCursor.line() - cursor.line(); |
334 | } |
335 | } |
336 | } |
337 | m_lastCursor = cursor; |
338 | m_lastPosition = pos; |
339 | |
340 | return pos + cursor.column(); |
341 | } |
342 | |
343 | private: |
344 | inline KateViewInternal *view() const |
345 | { |
346 | return static_cast<KateViewInternal *>(object()); |
347 | } |
348 | |
349 | KTextEditor::Cursor cursorFromInt(int position) const |
350 | { |
351 | return view()->view()->doc()->offsetToCursor(offset: position); |
352 | } |
353 | |
354 | QString textLine(int shiftLines, int offset, int *startOffset, int *endOffset) const |
355 | { |
356 | KTextEditor::Cursor pos = cursorFromInt(position: offset); |
357 | pos.setColumn(0); |
358 | if (shiftLines) { |
359 | pos.setLine(pos.line() + shiftLines); |
360 | } |
361 | *startOffset = positionFromCursor(view: view(), cursor: pos); |
362 | QString line = view()->view()->document()->line(line: pos.line()) + QLatin1Char('\n'); |
363 | *endOffset = *startOffset + line.length(); |
364 | return line; |
365 | } |
366 | |
367 | private: |
368 | // Cache data for positionFromCursor |
369 | mutable KateViewInternal *m_lastView; |
370 | mutable KTextEditor::Cursor m_lastCursor; |
371 | // m_lastPosition stores the positionFromCursor, with the cursor always in column 0 |
372 | mutable int m_lastPosition; |
373 | // to disconnect the signal |
374 | QMetaObject::Connection m_conn; |
375 | }; |
376 | |
377 | /** |
378 | * Factory-function used to create \a KateViewAccessible instances for KateViewInternal |
379 | * to make the KateViewInternal accessible. |
380 | */ |
381 | QAccessibleInterface *accessibleInterfaceFactory(const QString &key, QObject *object) |
382 | { |
383 | Q_UNUSED(key) |
384 | // if (key == QLatin1String("KateViewInternal")) |
385 | if (KateViewInternal *view = qobject_cast<KateViewInternal *>(object)) { |
386 | return new KateViewAccessible(view); |
387 | } |
388 | return nullptr; |
389 | } |
390 | |
391 | #endif |
392 | #endif |
393 | |