LCOV - code coverage report
Current view: top level - src/engraving/accessibility - accessibleitem.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 4.5 % 245 11
Test Date: 2026-11-05 04:07:02

            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              : }
        

Generated by: LCOV version 2.0-1