Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-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 : #include "accessibleitem.h"
23 :
24 : #include "accessibleroot.h"
25 : #include "../dom/score.h"
26 : #include "../dom/measure.h"
27 :
28 : #include "translation.h"
29 : #include "log.h"
30 :
31 : using namespace mu;
32 : using namespace mu::engraving;
33 : using namespace muse::accessibility;
34 : using namespace mu::engraving;
35 :
36 : bool AccessibleItem::enabled = true;
37 :
38 0 : static QString readable(QString s)
39 : {
40 : // Remove undesired pauses in screen reader speech output
41 0 : s.remove(u':');
42 :
43 : // Handle desired pauses
44 0 : s.replace(u';', u','); // NVDA doesn't pause for semicolon
45 :
46 0 : return s;
47 : }
48 :
49 4668 : AccessibleItem::AccessibleItem(EngravingItem* e, Role role)
50 14004 : : muse::Contextable(e->iocContext()), m_element(e), m_role(role)
51 : {
52 14004 : }
53 :
54 4190 : AccessibleItem::~AccessibleItem()
55 : {
56 4190 : m_element = nullptr;
57 :
58 4190 : if (m_registred && accessibilityController()) {
59 0 : accessibilityController()->unreg(this);
60 0 : m_registred = false;
61 : }
62 4190 : }
63 :
64 0 : AccessibleItem* AccessibleItem::clone(EngravingItem* e) const
65 : {
66 0 : return new AccessibleItem(e, m_role);
67 : }
68 :
69 4668 : void AccessibleItem::setup()
70 : {
71 4668 : if (!AccessibleItem::enabled) {
72 0 : return;
73 : }
74 :
75 4668 : if (!accessibilityController()) {
76 4668 : return;
77 : }
78 :
79 0 : accessibilityController()->reg(this);
80 0 : m_registred = true;
81 : }
82 :
83 0 : AccessibleRoot* AccessibleItem::accessibleRoot() const
84 : {
85 0 : if (!m_element) {
86 0 : return nullptr;
87 : }
88 :
89 0 : if (m_element->isType(ElementType::ROOT_ITEM)) {
90 0 : return dynamic_cast<AccessibleRoot*>(m_element->accessible().get());
91 : }
92 :
93 0 : Score* score = m_element->score();
94 0 : if (!score) {
95 0 : return nullptr;
96 : }
97 :
98 0 : RootItem* rootItem = m_element->ownershipParent() ? score->rootItem() : score->dummy()->rootItem();
99 0 : return dynamic_cast<AccessibleRoot*>(rootItem->accessible().get());
100 : }
101 :
102 0 : const EngravingItem* AccessibleItem::element() const
103 : {
104 0 : return m_element;
105 : }
106 :
107 0 : bool AccessibleItem::registered() const
108 : {
109 0 : return m_registred;
110 : }
111 :
112 0 : void AccessibleItem::notifyAboutFocus(bool focused)
113 : {
114 0 : m_accessibleStateChanged.send(IAccessible::State::Focused, focused);
115 0 : }
116 :
117 0 : const IAccessible* AccessibleItem::accessibleParent() const
118 : {
119 0 : if (!m_element) {
120 0 : return nullptr;
121 : }
122 :
123 0 : EngravingItem* p = m_element->accessibleParentItem();
124 0 : if (!p) {
125 0 : return nullptr;
126 : }
127 :
128 0 : return p->accessible().get();
129 : }
130 :
131 0 : size_t AccessibleItem::accessibleChildCount() const
132 : {
133 0 : TRACEFUNC;
134 :
135 0 : if (!m_element) {
136 0 : return 0;
137 : }
138 :
139 0 : size_t count = 0;
140 0 : for (const EngravingItem* item : m_element->accessibleChildren()) {
141 0 : AccessibleItemPtr access = item->accessible();
142 0 : if (access && access->registered()) {
143 0 : ++count;
144 : }
145 0 : }
146 0 : return count;
147 0 : }
148 :
149 0 : IAccessible* AccessibleItem::accessibleChild(size_t i) const
150 : {
151 0 : TRACEFUNC;
152 :
153 0 : if (!m_element) {
154 0 : return nullptr;
155 : }
156 :
157 0 : size_t count = 0;
158 0 : for (const EngravingItem* item : m_element->accessibleChildren()) {
159 0 : AccessibleItemPtr access = item->accessible();
160 0 : if (access && access->registered()) {
161 0 : if (count == i) {
162 0 : return access.get();
163 : }
164 0 : ++count;
165 : }
166 0 : }
167 0 : return nullptr;
168 0 : }
169 :
170 0 : QWindow* AccessibleItem::accessibleWindow() const
171 : {
172 0 : return nullptr;
173 : }
174 :
175 0 : muse::modularity::ContextPtr AccessibleItem::iocContext() const
176 : {
177 0 : return muse::Contextable::iocContext();
178 : }
179 :
180 0 : IAccessible::Role AccessibleItem::accessibleRole() const
181 : {
182 0 : return m_role;
183 : }
184 :
185 0 : QString AccessibleItem::accessibleName() const
186 : {
187 0 : if (!m_element) {
188 0 : return QString();
189 : }
190 :
191 0 : AccessibleRoot* root = accessibleRoot();
192 0 : QString staffInfo = root ? root->staffInfo() : "";
193 0 : QString barsAndBeats = m_element->formatBarsAndBeats();
194 :
195 0 : barsAndBeats.remove(u';'); // Too many pauses in speech
196 :
197 0 : QString name = QString("%1%2%3%4%5")
198 0 : .arg(!staffInfo.isEmpty() ? (staffInfo + "; ") : "")
199 0 : .arg(m_element->screenReaderInfo().toQString())
200 0 : .arg(m_element->visible() ? "" : " " + muse::qtrc("engraving", "invisible"))
201 0 : .arg(!barsAndBeats.isEmpty() ? ("; " + barsAndBeats) : "")
202 0 : .arg((root && root->isRangeSelection()) ? ("; " + muse::qtrc("engraving", "selected")) : "");
203 0 : return readable(name);
204 0 : }
205 :
206 0 : QString AccessibleItem::accessibleDescription() const
207 : {
208 0 : if (!m_element) {
209 0 : return QString();
210 : }
211 :
212 0 : AccessibleRoot* root = accessibleRoot();
213 0 : if (root && root->isRangeSelection()) {
214 0 : return readable(root->rangeSelectionInfo());
215 : }
216 :
217 0 : return readable(m_element->accessibleExtraInfo());
218 : }
219 :
220 0 : QVariant AccessibleItem::accessibleValue() const
221 : {
222 0 : return QVariant();
223 : }
224 :
225 0 : QVariant AccessibleItem::accessibleMaximumValue() const
226 : {
227 0 : return QVariant();
228 : }
229 :
230 0 : QVariant AccessibleItem::accessibleMinimumValue() const
231 : {
232 0 : return QVariant();
233 : }
234 :
235 0 : QVariant AccessibleItem::accessibleValueStepSize() const
236 : {
237 0 : return QVariant();
238 : }
239 :
240 0 : void AccessibleItem::accessibleSelection(int selectionIndex, int* startOffset, int* endOffset) const
241 : {
242 0 : TextCursor* textCursor = this->textCursor();
243 0 : if (selectionIndex == 0 && textCursor && textCursor->hasSelection()) {
244 0 : TextCursor::Range selectionRange = textCursor->selectionRange();
245 0 : *startOffset = selectionRange.startPosition;
246 0 : *endOffset = selectionRange.endPosition;
247 0 : } else {
248 0 : *startOffset = -1;
249 0 : *endOffset = -1;
250 : }
251 0 : }
252 :
253 0 : int AccessibleItem::accessibleSelectionCount() const
254 : {
255 0 : TextCursor* textCursor = this->textCursor();
256 0 : if (!textCursor) {
257 0 : return 0;
258 : }
259 :
260 0 : return static_cast<int>(textCursor->selectedText().size());
261 : }
262 :
263 0 : int AccessibleItem::accessibleCursorPosition() const
264 : {
265 0 : TextCursor* textCursor = this->textCursor();
266 0 : if (!textCursor) {
267 0 : return 0;
268 : }
269 :
270 0 : return textCursor->currentPosition();
271 : }
272 :
273 0 : QString AccessibleItem::accessibleText(int startOffset, int endOffset) const
274 : {
275 0 : if (!m_element || !m_element->isTextBase()) {
276 0 : return QString();
277 : }
278 :
279 0 : TextCursor* textCursor = new TextCursor(toTextBase(m_element));
280 0 : auto startCoord = textCursor->positionToLocalCoord(startOffset);
281 0 : if (startCoord.first == muse::nidx || startCoord.second == muse::nidx) {
282 0 : return QString();
283 : }
284 :
285 0 : textCursor->setRow(startCoord.first);
286 0 : textCursor->setColumn(startCoord.second);
287 0 : textCursor->movePosition(TextCursor::MoveOperation::Right, TextCursor::MoveMode::KeepAnchor,
288 : endOffset - startOffset);
289 :
290 0 : textCursor->setSelectLine(startCoord.first);
291 0 : textCursor->setSelectColumn(startCoord.second);
292 :
293 0 : QString text = textCursor->selectedText();
294 0 : delete textCursor;
295 :
296 0 : return text;
297 0 : }
298 :
299 0 : QString AccessibleItem::accessibleTextBeforeOffset(int, TextBoundaryType, int* startOffset, int* endOffset) const
300 : {
301 0 : NOT_IMPLEMENTED;
302 :
303 0 : *startOffset = -1;
304 0 : *endOffset = -1;
305 0 : return QString();
306 : }
307 :
308 0 : QString AccessibleItem::accessibleTextAfterOffset(int, TextBoundaryType, int* startOffset, int* endOffset) const
309 : {
310 0 : NOT_IMPLEMENTED;
311 :
312 0 : *startOffset = -1;
313 0 : *endOffset = -1;
314 0 : return QString();
315 : }
316 :
317 0 : QString AccessibleItem::accessibleTextAtOffset(int offset, TextBoundaryType boundaryType, int* startOffset, int* endOffset) const
318 : {
319 0 : if (!m_element || !m_element->isTextBase()) {
320 0 : return QString();
321 : }
322 :
323 0 : QString result;
324 :
325 0 : TextCursor* textCursor = new TextCursor(toTextBase(m_element));
326 0 : auto startCoord = textCursor->positionToLocalCoord(offset);
327 0 : if (startCoord.first == muse::nidx || startCoord.second == muse::nidx) {
328 0 : return QString();
329 : }
330 :
331 0 : textCursor->setRow(startCoord.first);
332 0 : textCursor->setColumn(startCoord.second);
333 :
334 0 : switch (boundaryType) {
335 0 : case CharBoundary: {
336 0 : *startOffset = textCursor->currentPosition();
337 0 : textCursor->movePosition(TextCursor::MoveOperation::Right, TextCursor::MoveMode::KeepAnchor);
338 0 : *endOffset = textCursor->currentPosition();
339 0 : break;
340 : }
341 0 : case WordBoundary: {
342 0 : textCursor->movePosition(TextCursor::MoveOperation::WordLeft, TextCursor::MoveMode::MoveAnchor);
343 0 : *startOffset = textCursor->currentPosition();
344 0 : textCursor->movePosition(TextCursor::MoveOperation::NextWord, TextCursor::MoveMode::KeepAnchor);
345 0 : *endOffset = textCursor->currentPosition();
346 0 : break;
347 : }
348 0 : case LineBoundary: {
349 0 : textCursor->movePosition(TextCursor::MoveOperation::StartOfLine, TextCursor::MoveMode::MoveAnchor);
350 0 : *startOffset = textCursor->currentPosition();
351 0 : textCursor->movePosition(TextCursor::MoveOperation::EndOfLine, TextCursor::MoveMode::KeepAnchor);
352 0 : *endOffset = textCursor->currentPosition();
353 0 : break;
354 : }
355 0 : default:
356 0 : NOT_IMPLEMENTED;
357 0 : break;
358 : }
359 :
360 0 : textCursor->setSelectLine(startCoord.first);
361 0 : textCursor->setSelectColumn(startCoord.second);
362 :
363 0 : result = textCursor->selectedText();
364 :
365 0 : delete textCursor;
366 0 : return result;
367 0 : }
368 :
369 0 : int AccessibleItem::accessibleCharacterCount() const
370 : {
371 0 : if (!m_element || !m_element->isTextBase()) {
372 0 : return 0;
373 : }
374 :
375 0 : TextBase* text = toTextBase(m_element);
376 0 : return static_cast<int>(text->plainText().size());
377 : }
378 :
379 0 : int AccessibleItem::accessibleRowIndex() const
380 : {
381 0 : NOT_IMPLEMENTED;
382 0 : return 0;
383 : }
384 :
385 0 : bool AccessibleItem::accessibleState(State st) const
386 : {
387 0 : if (!registered()) {
388 0 : return false;
389 : }
390 :
391 0 : AccessibleRoot* root = accessibleRoot();
392 0 : if (!root || !root->enabled()) {
393 0 : return false;
394 : }
395 :
396 0 : switch (st) {
397 0 : case IAccessible::State::Enabled: return true;
398 0 : case IAccessible::State::Active: return true;
399 0 : case IAccessible::State::Focused: {
400 0 : if (AccessibleItemPtr focusedElement = root->focusedElement().lock()) {
401 0 : return focusedElement->element() == element();
402 0 : }
403 0 : return false;
404 : }
405 0 : case IAccessible::State::Selected: return m_element->selected();
406 0 : default:
407 0 : break;
408 : }
409 0 : return false;
410 : }
411 :
412 0 : QRect AccessibleItem::accessibleRect() const
413 : {
414 0 : if (!registered()) {
415 0 : return QRect();
416 : }
417 :
418 0 : EngravingItem* element = m_element;
419 0 : Measure* measure = element->findMeasure();
420 0 : if (measure) {
421 0 : element = measure;
422 : }
423 :
424 0 : RectF bbox = element->canvasBoundingRect();
425 0 : RectF canvasRect(element->canvasPos(), SizeF(bbox.width(), bbox.height()));
426 :
427 0 : auto rect = accessibleRoot()->toScreenRect(canvasRect).toQRect();
428 0 : return rect;
429 : }
430 :
431 0 : bool AccessibleItem::accessibleIgnored() const
432 : {
433 0 : return false;
434 : }
435 :
436 0 : muse::async::Channel<IAccessible::Property, muse::Val> AccessibleItem::accessiblePropertyChanged() const
437 : {
438 0 : return m_accessiblePropertyChanged;
439 : }
440 :
441 0 : muse::async::Channel<IAccessible::State, bool> AccessibleItem::accessibleStateChanged() const
442 : {
443 0 : return m_accessibleStateChanged;
444 : }
445 :
446 0 : void AccessibleItem::setState(State state, bool arg)
447 : {
448 0 : if (state != State::Focused) {
449 0 : return;
450 : }
451 :
452 0 : AccessibleRoot* root = accessibleRoot();
453 0 : if (arg) {
454 0 : root->setFocusedElement(shared_from_this(), false /*voiceStaffInfoChange*/);
455 : }
456 : }
457 :
458 0 : TextCursor* AccessibleItem::textCursor() const
459 : {
460 0 : if (!m_element || !m_element->isTextBase()) {
461 0 : return nullptr;
462 : }
463 :
464 0 : TextBase* text = toTextBase(m_element);
465 0 : return text ? text->cursor() : nullptr;
466 : }
|