Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "qmlaccessible.h"
24 :
25 : #include <QQuickWindow>
26 : #include <QTextBoundaryFinder>
27 :
28 : #include "log.h"
29 :
30 : using namespace muse;
31 : using namespace muse::ui;
32 : using namespace muse::accessibility;
33 :
34 0 : AccessibleItem::AccessibleItem(QObject* parent)
35 0 : : QObject(parent), Contextable(muse::iocCtxForQmlObject(this))
36 : {
37 0 : }
38 :
39 496 : AccessibleItem::AccessibleItem(const muse::modularity::ContextPtr& iocCtx, QObject* parent)
40 1488 : : QObject(parent), Contextable(iocCtx)
41 : {
42 1488 : }
43 :
44 992 : AccessibleItem::~AccessibleItem()
45 : {
46 496 : QList<AccessibleItem*> children = m_children;
47 496 : for (AccessibleItem* ch : children) {
48 0 : ch->setAccessibleParent(nullptr);
49 : }
50 :
51 496 : if (accessibilityController() && accessibilityController()->isReg(this)) {
52 0 : accessibilityController()->unreg(this);
53 : }
54 :
55 496 : if (m_accessibleParent) {
56 1 : m_accessibleParent->removeChild(this);
57 : }
58 992 : }
59 :
60 0 : const IAccessible* AccessibleItem::accessibleParent() const
61 : {
62 0 : if (m_accessibleParent) {
63 0 : return static_cast<const IAccessible*>(m_accessibleParent);
64 : }
65 :
66 0 : return accessibleRoot();
67 : }
68 :
69 0 : IAccessible::Role AccessibleItem::accessibleRole() const
70 : {
71 0 : return static_cast<IAccessible::Role>(m_role);
72 : }
73 :
74 0 : QString AccessibleItem::accessibleName() const
75 : {
76 0 : return m_name;
77 : }
78 :
79 0 : QString AccessibleItem::accessibleDescription() const
80 : {
81 0 : return m_description;
82 : }
83 :
84 0 : bool AccessibleItem::accessibleState(State st) const
85 : {
86 0 : return m_state.value(st, false);
87 : }
88 :
89 0 : size_t AccessibleItem::accessibleChildCount() const
90 : {
91 0 : return static_cast<size_t>(m_children.size());
92 : }
93 :
94 0 : IAccessible* AccessibleItem::accessibleChild(size_t i) const
95 : {
96 0 : return m_children.value(static_cast<int>(i), nullptr);
97 : }
98 :
99 0 : QWindow* AccessibleItem::accessibleWindow() const
100 : {
101 0 : if (m_window) {
102 0 : return m_window;
103 : }
104 :
105 0 : QQuickItem* visualItem = resolveVisualItem();
106 0 : if (!visualItem) {
107 0 : return nullptr;
108 : }
109 :
110 0 : return visualItem->window();
111 : }
112 :
113 0 : muse::modularity::ContextPtr AccessibleItem::iocContext() const
114 : {
115 0 : return Contextable::iocContext();
116 : }
117 :
118 0 : QRect AccessibleItem::accessibleRect() const
119 : {
120 0 : QQuickItem* vitem = resolveVisualItem();
121 0 : if (!vitem || !vitem->window()) {
122 0 : return QRect();
123 : }
124 :
125 0 : QPointF scenePos = vitem->mapToScene(QPointF(0, 0));
126 0 : QPoint globalPos = vitem->window()->mapToGlobal(scenePos.toPoint());
127 0 : return QRect(globalPos.x(), globalPos.y(), vitem->width(), vitem->height());
128 : }
129 :
130 0 : bool AccessibleItem::accessibleIgnored() const
131 : {
132 0 : return m_ignored;
133 : }
134 :
135 0 : QVariant AccessibleItem::accessibleValue() const
136 : {
137 0 : return m_value;
138 : }
139 :
140 0 : QVariant AccessibleItem::accessibleMaximumValue() const
141 : {
142 0 : return m_maximumValue;
143 : }
144 :
145 0 : QVariant AccessibleItem::accessibleMinimumValue() const
146 : {
147 0 : return m_minimumValue;
148 : }
149 :
150 0 : QVariant AccessibleItem::accessibleValueStepSize() const
151 : {
152 0 : return m_stepSize;
153 : }
154 :
155 0 : void AccessibleItem::accessibleSelection(int selectionIndex, int* startOffset, int* endOffset) const
156 : {
157 0 : if (selectionIndex == 0) {
158 0 : *startOffset = m_selectionStart;
159 0 : *endOffset = m_selectionEnd;
160 : } else {
161 0 : *startOffset = -1;
162 0 : *endOffset = -1;
163 : }
164 0 : }
165 :
166 0 : int AccessibleItem::accessibleSelectionCount() const
167 : {
168 0 : return m_selectedText.size();
169 : }
170 :
171 0 : int AccessibleItem::accessibleCursorPosition() const
172 : {
173 0 : return cursorPosition();
174 : }
175 :
176 0 : QString AccessibleItem::accessibleText(int start, int end) const
177 : {
178 : #if defined(Q_OS_LINUX)
179 0 : return text().mid(start, end - start + 1);
180 : #else
181 : return text().mid(start, end - start);
182 : #endif
183 : }
184 :
185 : //! NOTE: qaccessible.cpp - textLineBoundary
186 0 : static QString textLineBoundary(int beforeAtAfter, const QString& text, int offset, int* startOffset, int* endOffset)
187 : {
188 0 : Q_ASSERT(beforeAtAfter >= -1 && beforeAtAfter <= 1);
189 0 : Q_ASSERT(*startOffset == -1 && *endOffset == -1);
190 0 : int length = text.length();
191 0 : Q_ASSERT(offset >= 0 && offset <= length);
192 :
193 : // move offset into the right range (if asking for line before or after
194 0 : if (beforeAtAfter == 1) {
195 0 : offset = text.indexOf(QChar::LineFeed, qMin(offset, length - 1));
196 0 : if (offset < 0) {
197 0 : return QString(); // after the last line comes nothing
198 : }
199 0 : ++offset; // move after the newline
200 0 : } else if (beforeAtAfter == -1) {
201 0 : offset = text.lastIndexOf(QChar::LineFeed, qMax(offset - 1, 0));
202 0 : if (offset < 0) {
203 0 : return QString(); // before first line comes nothing
204 : }
205 : }
206 :
207 0 : if (offset > 0) {
208 0 : *startOffset = text.lastIndexOf(QChar::LineFeed, offset - 1);
209 : }
210 0 : ++*startOffset; // move to the char after the newline (0 if lastIndexOf returned -1)
211 :
212 0 : *endOffset = text.indexOf(QChar::LineFeed, qMin(offset, length - 1)) + 1; // include newline char
213 0 : if (*endOffset <= 0 || *endOffset > length) {
214 0 : *endOffset = length; // if the text doesn't end with a newline it ends at length
215 : }
216 0 : return text.mid(*startOffset, *endOffset - *startOffset);
217 : }
218 :
219 : //! NOTE: qaccessible.cpp - textBeforeOffset
220 0 : QString AccessibleItem::accessibleTextBeforeOffset(int offset, TextBoundaryType boundaryType, int* startOffset, int* endOffset) const
221 : {
222 0 : const QString txt = accessibleText(0, accessibleCharacterCount());
223 :
224 0 : if (offset == -1) {
225 0 : offset = txt.length();
226 : }
227 :
228 0 : *startOffset = *endOffset = -1;
229 0 : if (txt.isEmpty() || offset < 0 || offset >= txt.length()) {
230 0 : return QString();
231 : }
232 :
233 : // type initialized just to silence a compiler warning [-Werror=maybe-uninitialized]
234 0 : QTextBoundaryFinder::BoundaryType type = QTextBoundaryFinder::Grapheme;
235 0 : switch (boundaryType) {
236 0 : case TextBoundaryType::CharBoundary:
237 0 : type = QTextBoundaryFinder::Grapheme;
238 0 : break;
239 0 : case TextBoundaryType::WordBoundary:
240 0 : type = QTextBoundaryFinder::Word;
241 0 : break;
242 0 : case TextBoundaryType::SentenceBoundary:
243 0 : type = QTextBoundaryFinder::Sentence;
244 0 : break;
245 0 : case TextBoundaryType::LineBoundary:
246 : case TextBoundaryType::ParagraphBoundary:
247 : // Lines can not use QTextBoundaryFinder since Line there means any potential line-break.
248 0 : return textLineBoundary(-1, txt, offset, startOffset, endOffset);
249 0 : case TextBoundaryType::NoBoundary:
250 : // return empty, this function currently only supports single lines, so there can be no line after
251 0 : return QString();
252 0 : default:
253 0 : Q_UNREACHABLE();
254 : }
255 :
256 : // keep behavior in sync with QTextCursor::movePosition()!
257 :
258 0 : QTextBoundaryFinder boundary(type, txt);
259 0 : boundary.setPosition(offset);
260 :
261 : do {
262 0 : if ((boundary.boundaryReasons() & (QTextBoundaryFinder::StartOfItem | QTextBoundaryFinder::EndOfItem))) {
263 0 : break;
264 : }
265 0 : } while (boundary.toPreviousBoundary() > 0);
266 0 : Q_ASSERT(boundary.position() >= 0);
267 0 : *endOffset = boundary.position();
268 :
269 0 : while (boundary.toPreviousBoundary() > 0) {
270 0 : if ((boundary.boundaryReasons() & (QTextBoundaryFinder::StartOfItem | QTextBoundaryFinder::EndOfItem))) {
271 0 : break;
272 : }
273 : }
274 0 : Q_ASSERT(boundary.position() >= 0);
275 0 : *startOffset = boundary.position();
276 :
277 0 : return txt.mid(*startOffset, *endOffset - *startOffset);
278 0 : }
279 :
280 : //! NOTE: qaccessible.cpp - textAfterOffset
281 0 : QString AccessibleItem::accessibleTextAfterOffset(int offset, TextBoundaryType boundaryType, int* startOffset, int* endOffset) const
282 : {
283 0 : const QString txt = accessibleText(0, accessibleCharacterCount());
284 :
285 0 : if (offset == -1) {
286 0 : offset = txt.length();
287 : }
288 :
289 0 : *startOffset = *endOffset = -1;
290 0 : if (txt.isEmpty() || offset < 0 || offset >= txt.length()) {
291 0 : return QString();
292 : }
293 :
294 : // type initialized just to silence a compiler warning [-Werror=maybe-uninitialized]
295 0 : QTextBoundaryFinder::BoundaryType type = QTextBoundaryFinder::Grapheme;
296 0 : switch (boundaryType) {
297 0 : case TextBoundaryType::CharBoundary:
298 0 : type = QTextBoundaryFinder::Grapheme;
299 0 : break;
300 0 : case TextBoundaryType::WordBoundary:
301 0 : type = QTextBoundaryFinder::Word;
302 0 : break;
303 0 : case TextBoundaryType::SentenceBoundary:
304 0 : type = QTextBoundaryFinder::Sentence;
305 0 : break;
306 0 : case TextBoundaryType::LineBoundary:
307 : case TextBoundaryType::ParagraphBoundary:
308 : // Lines can not use QTextBoundaryFinder since Line there means any potential line-break.
309 0 : return textLineBoundary(1, txt, offset, startOffset, endOffset);
310 0 : case TextBoundaryType::NoBoundary:
311 : // return empty, this function currently only supports single lines, so there can be no line after
312 0 : return QString();
313 0 : default:
314 0 : Q_UNREACHABLE();
315 : }
316 :
317 : // keep behavior in sync with QTextCursor::movePosition()!
318 :
319 0 : QTextBoundaryFinder boundary(type, txt);
320 0 : boundary.setPosition(offset);
321 :
322 : while (true) {
323 0 : int toNext = boundary.toNextBoundary();
324 0 : if ((boundary.boundaryReasons() & (QTextBoundaryFinder::StartOfItem | QTextBoundaryFinder::EndOfItem))) {
325 0 : break;
326 : }
327 0 : if (toNext < 0 || toNext >= txt.length()) {
328 0 : break; // not found, the boundary might not exist
329 : }
330 0 : }
331 0 : Q_ASSERT(boundary.position() <= txt.length());
332 0 : *startOffset = boundary.position();
333 :
334 : while (true) {
335 0 : int toNext = boundary.toNextBoundary();
336 0 : if ((boundary.boundaryReasons() & (QTextBoundaryFinder::StartOfItem | QTextBoundaryFinder::EndOfItem))) {
337 0 : break;
338 : }
339 0 : if (toNext < 0 || toNext >= txt.length()) {
340 0 : break; // not found, the boundary might not exist
341 : }
342 0 : }
343 0 : Q_ASSERT(boundary.position() <= txt.length());
344 0 : *endOffset = boundary.position();
345 :
346 0 : if ((*startOffset == -1) || (*endOffset == -1) || (*startOffset == *endOffset)) {
347 0 : *endOffset = -1;
348 0 : *startOffset = -1;
349 : }
350 :
351 0 : return txt.mid(*startOffset, *endOffset - *startOffset);
352 0 : }
353 :
354 0 : QString AccessibleItem::accessibleTextAtOffset(int, TextBoundaryType, int* startOffset, int* endOffset) const
355 : {
356 : //! NOTE: very simplified selection mode
357 0 : *startOffset = m_selectionStart;
358 0 : *endOffset = m_selectionEnd;
359 :
360 0 : return accessibleText(*startOffset, *endOffset);
361 : }
362 :
363 0 : int AccessibleItem::accessibleCharacterCount() const
364 : {
365 0 : return text().size();
366 : }
367 :
368 0 : async::Channel<IAccessible::Property, Val> AccessibleItem::accessiblePropertyChanged() const
369 : {
370 0 : return m_accessiblePropertyChanged;
371 : }
372 :
373 0 : async::Channel<IAccessible::State, bool> AccessibleItem::accessibleStateChanged() const
374 : {
375 0 : return m_accessibleStateChanged;
376 : }
377 :
378 0 : int AccessibleItem::accessibleRowIndex() const
379 : {
380 0 : return m_row;
381 : }
382 :
383 0 : void AccessibleItem::classBegin()
384 : {
385 0 : }
386 :
387 0 : void AccessibleItem::componentComplete()
388 : {
389 0 : if (accessibilityController() && !accessibilityController()->isReg(this)) {
390 0 : accessibilityController()->reg(this);
391 : }
392 0 : }
393 :
394 0 : AccessibleItem* AccessibleItem::accessibleParent_property() const
395 : {
396 0 : return m_accessibleParent;
397 : }
398 :
399 0 : MUAccessible::Role AccessibleItem::role() const
400 : {
401 0 : return m_role;
402 : }
403 :
404 0 : QString AccessibleItem::name() const
405 : {
406 0 : return m_name;
407 : }
408 :
409 0 : QString AccessibleItem::description() const
410 : {
411 0 : return m_description;
412 : }
413 :
414 0 : QVariant AccessibleItem::value() const
415 : {
416 0 : return m_value;
417 : }
418 :
419 0 : QVariant AccessibleItem::maximumValue() const
420 : {
421 0 : return m_maximumValue;
422 : }
423 :
424 0 : QVariant AccessibleItem::minimumValue() const
425 : {
426 0 : return m_minimumValue;
427 : }
428 :
429 0 : QVariant AccessibleItem::stepSize() const
430 : {
431 0 : return m_stepSize;
432 : }
433 :
434 0 : QString AccessibleItem::text() const
435 : {
436 0 : return m_text;
437 : }
438 :
439 0 : QString AccessibleItem::selectedText() const
440 : {
441 0 : return m_selectedText;
442 : }
443 :
444 0 : int AccessibleItem::selectionStart() const
445 : {
446 0 : return m_selectionStart;
447 : }
448 :
449 0 : int AccessibleItem::selectionEnd() const
450 : {
451 0 : return m_selectionEnd;
452 : }
453 :
454 0 : int AccessibleItem::cursorPosition() const
455 : {
456 0 : return m_cursorPosition;
457 : }
458 :
459 0 : int AccessibleItem::row() const
460 : {
461 0 : return m_row;
462 : }
463 :
464 0 : bool AccessibleItem::ignored() const
465 : {
466 0 : return m_ignored;
467 : }
468 :
469 0 : QQuickItem* AccessibleItem::visualItem() const
470 : {
471 0 : return m_visualItem;
472 : }
473 :
474 0 : QWindow* AccessibleItem::window() const
475 : {
476 0 : return m_window;
477 : }
478 :
479 311 : void AccessibleItem::setState(IAccessible::State st, bool arg)
480 : {
481 311 : if (m_state.value(st, false) == arg) {
482 128 : return;
483 : }
484 :
485 183 : m_state[st] = arg;
486 183 : emit stateChanged();
487 :
488 183 : if (!m_ignored) {
489 183 : m_accessibleStateChanged.send(st, arg);
490 : }
491 : }
492 :
493 743 : void AccessibleItem::setAccessibleParent(AccessibleItem* p)
494 : {
495 743 : if (m_accessibleParent == p) {
496 0 : return;
497 : }
498 :
499 743 : if (m_accessibleParent) {
500 371 : m_accessibleParent->removeChild(this);
501 : }
502 :
503 743 : m_accessibleParent = p;
504 :
505 743 : if (m_accessibleParent) {
506 372 : m_accessibleParent->addChild(this);
507 372 : m_accessiblePropertyChanged.send(IAccessible::Property::Parent, Val());
508 : }
509 :
510 743 : emit accessiblePrnChanged();
511 : }
512 :
513 0 : void AccessibleItem::setRole(MUAccessible::Role role)
514 : {
515 0 : if (m_role == role) {
516 0 : return;
517 : }
518 :
519 0 : m_role = role;
520 0 : emit roleChanged(m_role);
521 :
522 0 : m_state[State::Enabled] = true;
523 : }
524 :
525 0 : void AccessibleItem::setName(QString name)
526 : {
527 0 : if (m_name == name) {
528 0 : return;
529 : }
530 :
531 0 : m_name = name;
532 0 : emit nameChanged(m_name);
533 0 : m_accessiblePropertyChanged.send(IAccessible::Property::Name, Val(name));
534 : }
535 :
536 0 : void AccessibleItem::setDescription(QString description)
537 : {
538 0 : if (m_description == description) {
539 0 : return;
540 : }
541 :
542 0 : m_description = description;
543 0 : emit descriptionChanged(m_description);
544 0 : m_accessiblePropertyChanged.send(IAccessible::Property::Description, Val(description));
545 : }
546 :
547 0 : void AccessibleItem::setValue(QVariant value)
548 : {
549 0 : if (m_value == value) {
550 0 : return;
551 : }
552 :
553 0 : m_value = value;
554 0 : emit valueChanged(m_value);
555 0 : m_accessiblePropertyChanged.send(IAccessible::Property::Value, Val::fromQVariant(value));
556 : }
557 :
558 0 : void AccessibleItem::setMaximumValue(QVariant maximumValue)
559 : {
560 0 : if (m_maximumValue == maximumValue) {
561 0 : return;
562 : }
563 :
564 0 : m_maximumValue = maximumValue;
565 0 : emit maximumValueChanged(m_maximumValue);
566 : }
567 :
568 0 : void AccessibleItem::setMinimumValue(QVariant minimumValue)
569 : {
570 0 : if (m_minimumValue == minimumValue) {
571 0 : return;
572 : }
573 :
574 0 : m_minimumValue = minimumValue;
575 0 : emit minimumValueChanged(m_minimumValue);
576 : }
577 :
578 0 : void AccessibleItem::setStepSize(QVariant stepSize)
579 : {
580 0 : if (m_stepSize == stepSize) {
581 0 : return;
582 : }
583 :
584 0 : m_stepSize = stepSize;
585 0 : emit stepSizeChanged(m_stepSize);
586 : }
587 :
588 0 : void AccessibleItem::setText(const QString& text)
589 : {
590 0 : if (m_text == text) {
591 0 : return;
592 : }
593 :
594 0 : m_text = text;
595 :
596 : #if defined(Q_OS_MACOS)
597 : //! NOTE: For VoiceOver, text must also be in value
598 : setValue(text);
599 : #endif
600 :
601 0 : emit textChanged();
602 : }
603 :
604 0 : void AccessibleItem::setSelectedText(const QString& selectedText)
605 : {
606 0 : if (m_selectedText == selectedText) {
607 0 : return;
608 : }
609 :
610 0 : m_selectedText = selectedText;
611 0 : emit selectedTextChanged();
612 : }
613 :
614 0 : void AccessibleItem::setSelectionStart(int selectionStart)
615 : {
616 0 : if (m_selectionStart == selectionStart) {
617 0 : return;
618 : }
619 :
620 0 : m_selectionStart = selectionStart;
621 0 : emit selectionStartChanged();
622 : }
623 :
624 0 : void AccessibleItem::setSelectionEnd(int selectionEnd)
625 : {
626 0 : if (m_selectionEnd == selectionEnd) {
627 0 : return;
628 : }
629 :
630 0 : m_selectionEnd = selectionEnd;
631 0 : emit selectionEndChanged();
632 : }
633 :
634 0 : void AccessibleItem::setCursorPosition(int cursorPosition)
635 : {
636 0 : if (m_cursorPosition == cursorPosition) {
637 0 : return;
638 : }
639 :
640 0 : m_cursorPosition = cursorPosition;
641 0 : emit cursorPositionChanged();
642 0 : m_accessiblePropertyChanged.send(IAccessible::Property::TextCursor, Val());
643 : }
644 :
645 0 : void AccessibleItem::setRow(int row)
646 : {
647 0 : if (m_row == row) {
648 0 : return;
649 : }
650 :
651 0 : m_row = row;
652 0 : emit rowChanged();
653 : }
654 :
655 0 : void AccessibleItem::setIgnored(bool ignored)
656 : {
657 0 : if (m_ignored == ignored) {
658 0 : return;
659 : }
660 :
661 0 : m_ignored = ignored;
662 0 : emit ignoredChanged(m_ignored);
663 : }
664 :
665 0 : void AccessibleItem::setVisualItem(QQuickItem* item)
666 : {
667 0 : if (m_visualItem == item) {
668 0 : return;
669 : }
670 :
671 0 : m_visualItem = item;
672 0 : emit visualItemChanged(item);
673 : }
674 :
675 0 : void AccessibleItem::setWindow(QWindow* window)
676 : {
677 0 : if (m_window == window) {
678 0 : return;
679 : }
680 :
681 0 : m_window = window;
682 0 : emit windowChanged();
683 : }
684 :
685 0 : const IAccessible* AccessibleItem::accessibleRoot() const
686 : {
687 0 : return accessibilityController()->accessibleRoot();
688 : }
689 :
690 372 : void AccessibleItem::addChild(AccessibleItem* item)
691 : {
692 372 : m_children.append(item);
693 372 : }
694 :
695 372 : void AccessibleItem::removeChild(AccessibleItem* item)
696 : {
697 372 : m_children.removeOne(item);
698 372 : }
699 :
700 0 : QQuickItem* AccessibleItem::resolveVisualItem() const
701 : {
702 0 : if (m_visualItem) {
703 0 : return m_visualItem;
704 : }
705 :
706 0 : QObject* prn = parent();
707 0 : while (prn) {
708 0 : QQuickItem* vitem = qobject_cast<QQuickItem*>(prn);
709 0 : if (vitem) {
710 0 : return vitem;
711 : }
712 0 : prn = prn->parent();
713 : }
714 :
715 0 : return nullptr;
716 : }
|