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 */
28class KateViewAccessible : public QAccessibleWidget, public QAccessibleTextInterface, public QAccessibleEditableTextInterface
29{
30public:
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
343private:
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
367private:
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 */
381QAccessibleInterface *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

source code of ktexteditor/src/view/kateviewaccessible.h