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