1 #include <QMainWindow>
2 #include <QWidget>
3 #include "scoreaccessibility.h"
4 #include "musescore.h"
5 #include "libmscore/segment.h"
6 #include "libmscore/timesig.h"
7 #include "libmscore/score.h"
8 #include "libmscore/measure.h"
9 #include "libmscore/spanner.h"
10 #include "libmscore/sig.h"
11 #include "libmscore/staff.h"
12 #include "libmscore/part.h"
13 #include "libmscore/sym.h"
14 #include "inspector/inspector.h"
15 #include "selectionwindow.h"
16 #include "playpanel.h"
17 #include "synthcontrol.h"
18 #include "mixer/mixer.h"
19 #include "drumroll.h"
20 #include "pianoroll/pianoroll.h"
21 
22 namespace Ms{
23 
24 //---------------------------------------------------------
25 //   AccessibleScoreView
26 //---------------------------------------------------------
27 
AccessibleScoreView(ScoreView * scView)28 AccessibleScoreView::AccessibleScoreView(ScoreView* scView)
29    : QAccessibleWidget(scView)
30       {
31       s = scView;
32       }
33 
childCount() const34 int AccessibleScoreView::childCount() const
35       {
36       return 0;
37       }
38 
child(int) const39 QAccessibleInterface* AccessibleScoreView::child(int /*index*/) const
40       {
41       return 0;
42       }
43 
parent() const44 QAccessibleInterface* AccessibleScoreView::parent() const
45       {
46       return QAccessibleWidget::parent();
47       }
48 
rect() const49 QRect AccessibleScoreView::rect() const
50       {
51       // TODO: calculate this ourselves?
52       //QPoint origin = s->mapToGlobal(QPoint(0, 0));
53       //return s->rect().translated(origin);
54       return QAccessibleWidget::rect();
55       }
56 
isValid() const57 bool AccessibleScoreView::isValid() const
58       {
59       return true;
60       }
61 
62 #if 1
63 // TODO: determine if setting state explicitly would be helpful
state() const64 QAccessible::State AccessibleScoreView::state() const
65       {
66       QAccessible::State st = QAccessibleWidget::state();
67       st.focusable = 1;
68       st.selectable = 1;
69       st.active = 1;
70       //st.animated = 1;
71       return st;
72       }
73 #endif
74 
role() const75 QAccessible::Role AccessibleScoreView::role() const
76       {
77       // TODO: determine optimum role
78       // StaticText has the advantage of being read by Windows Narrator
79       //return QAccessible::Graphic;
80       return QAccessible::StaticText;
81       }
82 
text(QAccessible::Text t) const83 QString AccessibleScoreView::text(QAccessible::Text t) const
84       {
85       switch (t) {
86             case QAccessible::Name:
87                   // TODO:
88                   // leave empty to prevent name from being read on value/description change
89                   // name will need to be in containing widget so it is read on tab change
90                   // and we will need to be sure to read that
91                   //return "";
92                   return s->score()->title();
93             case QAccessible::Value:
94             case QAccessible::Description: {
95                   QString msg = s->score()->accessibleMessage();
96                   QString info = s->score()->accessibleInfo();
97                   if (msg.isEmpty())
98                         return info;
99                   s->score()->setAccessibleMessage(""); // clear the message
100                   if (info.isEmpty())
101                         return msg;
102                   return tr("%1, %2").arg(msg).arg(info);
103                   }
104             default:
105                   return QString();
106            }
107       }
108 
109 #if 1
110 // TODO: determine best option here
111 // without this override, Qt determines window by looking upwards in hierarchy
112 // we can supposedly duplicate that by returning nullptr
113 // qApp->focusWindow() is the "old" return value,
114 // but it could conceivably refer to something other than main window, which seems wrong
window() const115 QWindow* AccessibleScoreView::window() const {
116       //return nullptr;
117       //return QWdiegt::window();
118       return mscore->windowHandle();      // qApp->focusWindow();
119       }
120 #endif
121 
ScoreViewFactory(const QString & classname,QObject * object)122 QAccessibleInterface* AccessibleScoreView::ScoreViewFactory(const QString &classname, QObject *object)
123       {
124           QAccessibleInterface *iface = 0;
125           if (classname == QLatin1String("Ms::ScoreView") && object && object->isWidgetType()){
126 //                qDebug("Creating interface for ScoreView object");
127                 iface = static_cast<QAccessibleInterface*>(new AccessibleScoreView(static_cast<ScoreView*>(object)));
128                 }
129 
130           return iface;
131       }
132 
interface_cast(QAccessible::InterfaceType t)133 void* AccessibleScoreView::interface_cast(QAccessible::InterfaceType t)
134       {
135 #ifdef SCOREVIEW_VALUEINTERFACE
136       if (t == QAccessible::ValueInterface)
137             return static_cast<QAccessibleValueInterface*>(this);
138 #endif
139 #ifdef SCOREVIEW_IMAGEINTERFACE
140       if (t == QAccessible::ImageInterface)
141             return static_cast<QAccessibleImageInterface*>(this);
142 #endif
143       return QAccessibleWidget::interface_cast(t);
144       }
145 
146 #ifdef SCOREVIEW_VALUEINTERFACE
147 
setCurrentValue(const QVariant & val)148 void AccessibleScoreView::setCurrentValue(const QVariant& val)
149       {
150       QString str = val.toString();
151       s->score()->setAccessibleInfo(str);
152       }
153 
currentValue() const154 QVariant AccessibleScoreView::currentValue() const
155       {
156       return s->score()->accessibleInfo();
157       }
158 
maximumValue() const159 QVariant AccessibleScoreView::maximumValue() const
160       {
161       return QString();
162       }
163 
minimumValue() const164 QVariant AccessibleScoreView::minimumValue() const
165       {
166       return QString();
167       }
168 
minimumStepSize() const169 QVariant AccessibleScoreView::minimumStepSize() const
170       {
171       return QString();
172       }
173 
174 #endif
175 
176 #ifdef SCOREVIEW_IMAGEINTERFACE
177 
imageDescription() const178 QString AccessibleScoreView::imageDescription() const
179       {
180       return s->score()->accessibleInfo();
181       }
182 
imageSize() const183 QSize AccessibleScoreView::imageSize() const
184       {
185       return s->size();
186       }
187 
imagePosition() const188 QPoint AccessibleScoreView::imagePosition() const
189       {
190       return QPoint();
191       }
192 
193 #endif
194 
195 
196 ScoreAccessibility* ScoreAccessibility::inst = 0;
197 
ScoreAccessibility(QMainWindow * mainWindow)198 ScoreAccessibility::ScoreAccessibility(QMainWindow* mainWindow) : QObject(mainWindow)
199       {
200       this->mainWindow = mainWindow;
201       statusBarLabel = new QLabel(mainWindow->statusBar());
202       mainWindow->statusBar()->addWidget(statusBarLabel);
203       }
204 
createInstance(QMainWindow * mainWindow)205 void ScoreAccessibility::createInstance(QMainWindow* mainWindow)
206       {
207       if (!inst) {
208             inst = new ScoreAccessibility(mainWindow);
209             }
210       }
211 
~ScoreAccessibility()212 ScoreAccessibility::~ScoreAccessibility()
213       {
214       }
215 
clearAccessibilityInfo()216 void ScoreAccessibility::clearAccessibilityInfo()
217       {
218       statusBarLabel->setText("");
219       MuseScoreView* view = static_cast<MuseScore*>(mainWindow)->currentScoreView();
220       if (view)
221             view->score()->setAccessibleInfo(tr("No selection"));
222       _oldBar = -1;
223       _oldStaff = -1;
224       }
225 
currentInfoChanged()226 void ScoreAccessibility::currentInfoChanged()
227       {
228       ScoreView* scoreView =  static_cast<MuseScore*>(mainWindow)->currentScoreView();
229       Score* score = scoreView->score();
230       int oldStaff = _oldStaff;
231       int oldBar = _oldBar;
232       _oldStaff = -1;
233       _oldBar = -1;
234       QString oldStatus = statusBarLabel->text();
235       QString oldScreenReaderInfo = score->accessibleInfo();
236       clearAccessibilityInfo();
237       if (score->selection().isSingle()) {
238             Element* e = score->selection().element();
239             if (!e) {
240                   return;
241                   }
242             Element* el = e->isSpannerSegment() ? static_cast<SpannerSegment*>(e)->spanner() : e;
243             QString barsAndBeats = "";
244             QString optimizedBarsAndBeats = "";
245             if (el->isSpanner()) {
246                   Spanner* s = static_cast<Spanner*>(el);
247                   std::pair<int, float> bar_beat = barbeat(s->startSegment());
248                   barsAndBeats += tr("Start Measure: %1; Start Beat: %2").arg(QString::number(bar_beat.first)).arg(QString::number(bar_beat.second));
249                   Segment* seg = s->endSegment();
250                   if(!seg)
251                         seg = score->lastSegment()->prev1MM(SegmentType::ChordRest);
252 
253                   if (seg->tick() != score->lastSegment()->prev1MM(SegmentType::ChordRest)->tick() &&
254                       s->type() != ElementType::SLUR                                               &&
255                       s->type() != ElementType::TIE                                                )
256                         seg = seg->prev1MM(SegmentType::ChordRest);
257 
258                   bar_beat = barbeat(seg);
259                   barsAndBeats += "; " + tr("End Measure: %1; End Beat: %2").arg(QString::number(bar_beat.first)).arg(QString::number(bar_beat.second));
260                   optimizedBarsAndBeats = barsAndBeats;
261                   }
262             else {
263                   std::pair<int, float>bar_beat = barbeat(el);
264                   if (bar_beat.first) {
265                         _oldBar = bar_beat.first;
266                         barsAndBeats += " " + tr("Measure: %1").arg(QString::number(bar_beat.first));
267                         if (bar_beat.first != oldBar)
268                               optimizedBarsAndBeats += " " + tr("Measure: %1").arg(QString::number(bar_beat.first));
269                         if (bar_beat.second) {
270                               barsAndBeats += "; " + tr("Beat: %1").arg(QString::number(bar_beat.second));
271                               optimizedBarsAndBeats += "; " + tr("Beat: %1").arg(QString::number(bar_beat.second));
272                               }
273                         }
274                   }
275 
276             QString rez = e->accessibleInfo();
277             if (!barsAndBeats.isEmpty())
278                   rez += "; " + barsAndBeats;
279             else
280                   oldScreenReaderInfo.clear();  // force regeneration for elements with no barbeat info - see below
281 
282             QString staff = "";
283             QString optimizedStaff = "";
284             if (e->staffIdx() + 1) {
285                   _oldStaff = e->staffIdx();
286                   staff = tr("Staff: %1").arg(QString::number(e->staffIdx() + 1));
287                   QString staffName = e->staff()->part()->longName(e->tick());
288                   if (staffName.isEmpty())
289                         staffName = e->staff()->partName();
290                   if (staffName.isEmpty()) {
291                         staffName = tr("Unnamed");    // for screenreader only
292                         rez = QString("%1; %2").arg(rez).arg(staff);
293                         }
294                   else {
295                         rez = QString("%1; %2 (%3)").arg(rez).arg(staff).arg(staffName.replace('\n', ' ')); // no newlines in the status bar
296                         }
297                   if (e->staffIdx() != oldStaff)
298                         optimizedStaff = QString("%1 (%2)").arg(staff).arg(staffName);
299                   }
300 
301             statusBarLabel->setText(rez);
302 
303             if (scoreView->mscoreState() & STATE_ALLTEXTUAL_EDIT) {
304                   // Don't say element name during text editing.
305                   score->setAccessibleInfo("");
306                   return;
307                   }
308 
309             QString screenReaderRez;
310             QString newScreenReaderInfo = e->screenReaderInfo();
311             if (rez != oldStatus || newScreenReaderInfo != oldScreenReaderInfo || oldScreenReaderInfo.isEmpty()) {
312                   // status has changed since last call, or there is no existing screenreader info
313                   //
314                   // build new screenreader info
315                   screenReaderRez = QString("%1%2 %3 %4").arg(newScreenReaderInfo).arg(optimizedBarsAndBeats).arg(optimizedStaff).arg(e->accessibleExtraInfo());
316                   makeReadable(screenReaderRez);
317                   }
318             else {
319                   // status has not changed since last call, and there is existing screenreader info
320                   //
321                   // if this function is called twice within the same command,
322                   // then status does not change between calls,
323                   // but the second call may result in too much information being optimized away for screenreader
324                   // so in these cases, let the previous screenreader info stand
325                   // (this is relevant only for elements with bar and beat info)
326                   screenReaderRez = oldScreenReaderInfo;
327                   }
328             score->setAccessibleInfo(screenReaderRez);
329             }
330       else if (score->selection().isRange()) {
331             QString barsAndBeats = "";
332             std::pair<int, float> bar_beat;
333 
334             bar_beat = barbeat(score->selection().startSegment());
335             barsAndBeats += " " + tr("Start Measure: %1; Start Beat: %2").arg(QString::number(bar_beat.first)).arg(QString::number(bar_beat.second));
336             Segment* endSegment = score->selection().endSegment();
337 
338             if (!endSegment)
339                   endSegment = score->lastSegment();
340             else
341                   endSegment = endSegment->prev1MM();
342 
343             bar_beat = barbeat(endSegment);
344             barsAndBeats += " " + tr("End Measure: %1; End Beat: %2").arg(QString::number(bar_beat.first)).arg(QString::number(bar_beat.second));
345             statusBarLabel->setText(tr("Range Selection") + barsAndBeats);
346             score->setAccessibleInfo(tr("Range Selection") + barsAndBeats);
347             }
348       else if (score->selection().isList()) {
349             statusBarLabel->setText(tr("List Selection"));
350             score->setAccessibleInfo(tr("List Selection"));
351             }
352       }
353 
instance()354 ScoreAccessibility* ScoreAccessibility::instance()
355       {
356       return inst;
357       }
358 
updateAccessibilityInfo()359 void ScoreAccessibility::updateAccessibilityInfo()
360       {
361       ScoreView* w = static_cast<MuseScore*>(mainWindow)->currentScoreView();
362       if (!w)
363             return;
364 
365       currentInfoChanged();
366 
367       //getInspector->isAncestorOf is used so that inspector and search dialog don't loose focus
368       //when this method is called
369       //TODO: create a class to manage focus and replace this massive if
370       QWidget* focusWidget = qApp->focusWidget();
371       if ((focusWidget != w) &&
372            !(mscore->inspector() && mscore->inspector()->isAncestorOf(focusWidget)) &&
373            !(mscore->searchDialog() && mscore->searchDialog()->isAncestorOf(focusWidget)) &&
374            !(mscore->getSelectionWindow() && mscore->getSelectionWindow()->isAncestorOf(focusWidget)) &&
375            !(mscore->getPlayPanel() && mscore->getPlayPanel()->isAncestorOf(focusWidget)) &&
376            !(mscore->getSynthControl() && mscore->getSynthControl()->isAncestorOf(focusWidget)) &&
377            !(mscore->getMixer() && mscore->getMixer()->isAncestorOf(focusWidget)) &&
378            !(mscore->searchDialog() && mscore->searchDialog()->isAncestorOf(focusWidget)) &&
379            !(mscore->getDrumrollEditor() && mscore->getDrumrollEditor()->isAncestorOf(focusWidget)) &&
380            !(mscore->getPianorollEditor() && mscore->getPianorollEditor()->isAncestorOf(focusWidget))) {
381             mscore->activateWindow();
382             w->setFocus();
383             }
384 #if 0
385       else if (focusWidget == w) {
386             w->clearFocus();
387             w->setFocus();
388             }
389 #endif
390 
391       // Try to send message to the screen reader. Note that NVDA will
392       // ignore the message if it is the same as the previous message.
393       updateAccessibility();
394 
395 #if defined(Q_OS_WIN)
396       // HACK: send the message again after a short delay to force NVDA
397       // to read it even if it is the same as before. This is useful when
398       // cursoring through a word with repeated characters, such as "food".
399       // Without this hack NVDA would say "f", "o", *silence*, "d".
400       QTimer::singleShot(0, this, &ScoreAccessibility::updateAccessibility);
401 #endif
402       }
403 
updateAccessibility()404 void ScoreAccessibility::updateAccessibility()
405       {
406       ScoreView* w = static_cast<MuseScore*>(mainWindow)->currentScoreView();
407       if (!w)
408             return;
409 
410       QObject* obj = static_cast<QObject*>(w);
411       QAccessibleValueChangeEvent vcev(obj, w->score()->accessibleInfo());
412       QAccessible::updateAccessibility(&vcev);
413       // TODO:
414       // some screenreaders may respond better to other events
415       // the version of Qt used may also be relevant, and platform too
416       //QAccessibleEvent ev1(obj, QAccessible::NameChanged);
417       //QAccessible::updateAccessibility(&ev1);
418       //QAccessibleEvent ev2(obj, QAccessible::DescriptionChanged);
419       //QAccessible::updateAccessibility(&ev2);
420       }
421 
barbeat(Element * e)422 std::pair<int, float> ScoreAccessibility::barbeat(Element *e)
423       {
424       if (!e) {
425             return std::pair<int, float>(0, 0.0F);
426             }
427 
428       int bar = 0;
429       int beat = 0;
430       int ticks = 0;
431       TimeSigMap* tsm = e->score()->sigmap();
432       Element* p = e;
433       int ticksB = ticks_beat(tsm->timesig(0).timesig().denominator());
434       while(p && p->type() != ElementType::SEGMENT && p->type() != ElementType::MEASURE)
435             p = p->parent();
436 
437       if (!p) {
438             return std::pair<int, float>(0, 0.0F);
439             }
440       else if (p->type() == ElementType::SEGMENT) {
441             Segment* seg = static_cast<Segment*>(p);
442             tsm->tickValues(seg->tick().ticks(), &bar, &beat, &ticks);
443             ticksB = ticks_beat(tsm->timesig(seg->tick().ticks()).timesig().denominator());
444             }
445       else if (p->type() == ElementType::MEASURE) {
446             Measure* m = static_cast<Measure*>(p);
447             bar = m->no();
448             beat = -1;
449             ticks = 0;
450             }
451       return std::pair<int,float>(bar + 1, beat + 1 + ticks / static_cast<float>(ticksB));
452       }
453 
makeReadable(QString & s)454 void ScoreAccessibility::makeReadable(QString& s)
455       {
456       static std::vector<std::pair<QString, QString>> unicodeReplacements {
457             { "♭", " " + tr("flat") },
458             { "♮", " " + tr("natural") },
459             { "♯", " " + tr("sharp") },
460             { "��", " " + tr("double flat") },
461             { "��", " " + tr("double sharp") },
462       };
463 
464       if (!QAccessible::isActive())
465             return;
466       for (auto const &r : unicodeReplacements)
467             s.replace(r.first, r.second);
468       ScoreFont* sf = gscore->scoreFont();
469       for (auto id : Sym::commonScoreSymbols) {
470             if (id == SymId::space)
471                   continue;               // don't read "space"
472             QString src = sf->toString(id);
473             QString replacement = Sym::id2userName(id);
474             s.replace(src, replacement);
475             }
476       }
477 
478 }
479