1 #include "tourhandler.h"
2 #include "musescore.h"
3 #include "preferences.h"
4 #include "qmldockwidget.h"
5 
6 namespace Ms {
7 
8 QHash<QString, Tour*> TourHandler::allTours;
9 QHash<QString, Tour*> TourHandler::shortcutToTour;
10 QMap<QString, QMap<QString, QString>*> TourHandler::eventNameLookup;
11 
12 static int mboxFrameTopThickness = 0;
13 static int mboxFrameBottomThickness = 0;
14 static int mboxFrameLeftThickness = 0;
15 static int mboxFrameRightThickness = 0;
16 
17 //---------------------------------------------------------
18 //   OverlayWidget
19 //---------------------------------------------------------
20 
OverlayWidget(QList<QWidget * > widgetList,QWidget * parent)21 OverlayWidget::OverlayWidget(QList<QWidget*> widgetList, QWidget* parent)
22       : QWidget{parent}
23       {
24       widgets = widgetList;
25       newParent();
26       }
27 
28 //---------------------------------------------------------
29 //   newParent
30 //---------------------------------------------------------
31 
newParent()32 void OverlayWidget::newParent()
33       {
34       if (!parent())
35             return;
36       parent()->installEventFilter(this);
37       resize(qobject_cast<QWidget*>(parent())->size());
38       raise();
39       }
40 
41 //---------------------------------------------------------
42 //   eventFilter
43 //---------------------------------------------------------
44 
eventFilter(QObject * obj,QEvent * ev)45 bool OverlayWidget::eventFilter(QObject* obj, QEvent* ev)
46       {
47       if (obj == parent()) {
48             if (ev->type() == QEvent::Resize)
49                   resize(static_cast<QResizeEvent*>(ev)->size());
50             else if (ev->type() == QEvent::ChildAdded)
51                   raise();
52             }
53       return QWidget::eventFilter(obj, ev);
54       }
55 
56 //---------------------------------------------------------
57 //   event
58 //---------------------------------------------------------
59 
event(QEvent * ev)60 bool OverlayWidget::event(QEvent* ev)
61       {
62       if (ev->type() == QEvent::ParentAboutToChange) {
63             if (parent()) parent()->removeEventFilter(this);
64             }
65       else if (ev->type() == QEvent::ParentChange)
66             newParent();
67       return QWidget::event(ev);
68       }
69 
70 //---------------------------------------------------------
71 //   paintEvent
72 //---------------------------------------------------------
73 
paintEvent(QPaintEvent *)74 void OverlayWidget::paintEvent(QPaintEvent *)
75       {
76       QPainterPath painterPath = QPainterPath();
77       QPainter p(this);
78       QWidget* parentWindow = qobject_cast<QWidget*>(parent());
79       if (parentWindow)
80             painterPath.addRect(parentWindow->rect());
81 
82       QPainterPath subPath = QPainterPath();
83       for (QWidget* w : qAsConst(widgets)) {
84             if (w->isVisible()) {
85                   // Add widget and children visible region mapped to the parentWindow
86                   QRegion region = w->visibleRegion() + w->childrenRegion();
87                   region.translate(w->mapTo(parentWindow, QPoint(0, 0)));
88                   subPath.addRegion(region);
89                   }
90             }
91       painterPath -= subPath;
92 
93       QColor overlayColor = QApplication::palette().color(QPalette::Shadow);
94       overlayColor.setAlpha(128);
95       p.fillPath(painterPath, overlayColor);
96       }
97 
98 //---------------------------------------------------------
99 //   showWelcomeTour
100 //---------------------------------------------------------
101 
showWelcomeTour()102 void TourHandler::showWelcomeTour()
103       {
104       if (!delayedWelcomeTour)
105             startTour("welcome");
106       }
107 
108 //---------------------------------------------------------
109 //   showDelayedWelcomeTour
110 //   delays showing the welcome tour when the user
111 //   attempts to open a score or create a new score
112 //---------------------------------------------------------
113 
showDelayedWelcomeTour()114 void TourHandler::showDelayedWelcomeTour()
115       {
116       if (delayedWelcomeTour)
117             startTour("welcome");
118       delayedWelcomeTour = false;
119       }
120 
121 //---------------------------------------------------------
122 //   loadTours
123 //---------------------------------------------------------
124 
loadTours()125 void TourHandler::loadTours()
126       {
127       QStringList nameFilters;
128       nameFilters << "*.tour";
129       QString path = mscoreGlobalShare + "tours";
130       QDirIterator it(path, nameFilters, QDir::Files, QDirIterator::Subdirectories);
131 
132       while (it.hasNext()) {
133             QFile* tourFile = new QFile(it.next());
134             tourFile->open(QIODevice::ReadOnly);
135             XmlReader tourXml(tourFile);
136             while (tourXml.readNextStartElement()) {
137                   if (tourXml.name() == "Tour")
138                         loadTour(tourXml);
139                   else
140                         tourXml.unknown();
141                   }
142             }
143 
144       readCompletedTours();
145       }
146 
147 //---------------------------------------------------------
148 //   loadTours
149 //---------------------------------------------------------
150 
loadTour(XmlReader & tourXml)151 void TourHandler::loadTour(XmlReader& tourXml)
152       {
153       QString tourName = tourXml.attribute("name");
154       QList<QString> shortcuts;
155       Tour* tour = new Tour(tourName);
156       while (tourXml.readNextStartElement()) {
157             if (tourXml.name() == "Message") {
158                   QString text;
159                   QList<QString> objectNames;
160                   while (tourXml.readNextStartElement()) {
161                         if (tourXml.name() == "Text") {
162                               QTextDocument doc;
163                               QString rawText = tourXml.readElementText();
164                               QString ttext = qApp->translate("TourXML", rawText.toUtf8().constData());
165                               doc.setHtml(ttext);
166                               text = doc.toPlainText().replace("\\n", "\n");
167                               }
168                         else if (tourXml.name() == "Widget")
169                               objectNames.append(tourXml.readXml());
170                         else
171                               tourXml.unknown();
172                         }
173                   tour->addMessage(text, objectNames);
174                   }
175             else if (tourXml.name() == "Event") {
176                   QString name = tourXml.attribute("objectName");
177                   QString event = tourXml.readXml();
178                   if (!eventNameLookup.contains(name))
179                         eventNameLookup.insert(name, new QMap<QString, QString>);
180                   eventNameLookup.value(name)->insert(event, tourName);
181                   }
182             else if (tourXml.name() == "Shortcut") {
183                   shortcuts.append(tourXml.readXml());
184                   }
185             else
186                   tourXml.unknown();
187             }
188 
189       allTours[tourName] = tour;
190       for (const QString &s : shortcuts)
191             shortcutToTour[s] = tour;
192       }
193 
194 //---------------------------------------------------------
195 //   resetCompletedTours
196 //---------------------------------------------------------
197 
resetCompletedTours()198 void TourHandler::resetCompletedTours()
199       {
200       for (auto tour : qAsConst(allTours))
201             tour->setCompleted(false);
202       }
203 
204 //---------------------------------------------------------
205 //   readCompletedTours
206 //---------------------------------------------------------
207 
readCompletedTours()208 void TourHandler::readCompletedTours()
209       {
210       QFile completedToursFile(dataPath + "/tours/completedTours.list");
211       if (!completedToursFile.open(QIODevice::ReadOnly))
212             return;
213 
214       QDataStream in(&completedToursFile);
215       QList<QString> completedTours;
216       in >> completedTours;
217 
218       for (const QString &tourName : qAsConst(completedTours))
219             if (allTours.contains(tourName))
220                   allTours.value(tourName)->setCompleted(true);
221       }
222 
223 //---------------------------------------------------------
224 //   writeCompletedTours
225 //---------------------------------------------------------
226 
writeCompletedTours()227 void TourHandler::writeCompletedTours()
228       {
229       QDir dir;
230       dir.mkpath(dataPath);
231       QString path = dataPath + "/tours";
232       dir.mkpath(path);
233       QFile completedToursFile(path + "/completedTours.list");
234       completedToursFile.open(QIODevice::WriteOnly);
235 
236       QList<QString> completedTours;
237 
238       for (Tour* t : allTours.values())
239             if (t->completed())
240                   completedTours.append(t->tourName());
241 
242       QDataStream out(&completedToursFile);
243       out << completedTours;
244       }
245 
246 //---------------------------------------------------------
247 //   eventFilter
248 //---------------------------------------------------------
249 
eventFilter(QObject * obj,QEvent * event)250 bool TourHandler::eventFilter(QObject* obj, QEvent* event)
251       {
252       QString eventString = QVariant::fromValue(event->type()).value<QString>();
253 
254       if (eventNameLookup.contains(obj->objectName()) &&
255           eventNameLookup.value(obj->objectName())->contains(eventString))
256             startTour(eventNameLookup.value(obj->objectName())->value(eventString));
257       else if (eventHandler.contains(obj) && eventHandler.value(obj)->contains(event->type()))
258             startTour(eventHandler.value(obj)->value(event->type()));
259 
260       return QObject::eventFilter(obj, event);
261       }
262 
263 //---------------------------------------------------------
264 //   attachTour
265 //---------------------------------------------------------
266 
attachTour(QObject * obj,QEvent::Type eventType,QString tourName)267 void TourHandler::attachTour(QObject* obj, QEvent::Type eventType, QString tourName)
268       {
269       obj->installEventFilter(this);
270       if (!eventHandler.contains(obj))
271             eventHandler.insert(obj, new QMap<QEvent::Type, QString>);
272       eventHandler.value(obj)->insert(eventType, tourName);
273       }
274 
275 //---------------------------------------------------------
276 //   addWidgetToTour
277 //---------------------------------------------------------
278 
addWidgetToTour(QString tourName,QWidget * widget,QString widgetName)279 void TourHandler::addWidgetToTour(QString tourName, QWidget* widget, QString widgetName)
280       {
281       if (allTours.contains(tourName)) {
282             Tour* tour = allTours.value(tourName);
283             tour->addNameAndWidget(widgetName, widget);
284             }
285       else
286             qDebug() << tourName << "does not have a tour.";
287       }
288 
289 //---------------------------------------------------------
290 //   clearWidgetsFromTour
291 //---------------------------------------------------------
292 
clearWidgetsFromTour(QString tourName)293 void TourHandler::clearWidgetsFromTour(QString tourName)
294       {
295       if (allTours.contains(tourName))
296             allTours.value(tourName)->clearWidgets();
297       else
298             qDebug() << tourName << "does not have a tour.";
299       }
300 
301 //---------------------------------------------------------
302 //   startTour
303 //   lookup string can be a tour name or a shortcut name
304 //---------------------------------------------------------
305 
startTour(QString lookupString)306 void TourHandler::startTour(QString lookupString)
307       {
308       if (!preferences.getBool(PREF_UI_APP_STARTUP_SHOWTOURS) || MScore::noGui)
309             return;
310 
311       Tour* tour = nullptr;
312       if (allTours.contains(lookupString))
313             tour = allTours.value(lookupString);
314       else if (shortcutToTour.contains(lookupString))
315             tour = shortcutToTour.value(lookupString);
316 
317       if (tour) {
318             if (tour->completed())
319                   return;
320             displayTour(tour);
321             tour->setCompleted(true);
322             }
323       }
324 
325 //---------------------------------------------------------
326 //   getDisplayPoints
327 //---------------------------------------------------------
328 
positionMessage(QList<QWidget * > widgets,QMessageBox * mbox)329 void TourHandler::positionMessage(QList<QWidget*> widgets, QMessageBox* mbox)
330       {
331       // Loads some information into the size of the mbox, a bit of a hack
332       mbox->show();
333 
334       // Create a "box" to see where the msgbox should go
335       bool set = false;
336       QRect widgetsBox;
337 
338       for (QWidget* w : widgets) {
339             if (w->visibleRegion().isEmpty())
340                   continue;
341             QRect boundingRect = w->visibleRegion().boundingRect();
342             QPoint topLeft = w->mapToGlobal(QPoint(0, 0));
343             QPoint bottomRight = w->mapToGlobal(boundingRect.bottomRight());
344 
345             if (!set) {
346                   widgetsBox.setTopLeft(topLeft);
347                   widgetsBox.setBottomRight(bottomRight);
348                   set = true;
349                   }
350             else {
351                   widgetsBox.setTop(qMin(widgetsBox.top(), topLeft.y()));
352                   widgetsBox.setLeft(qMin(widgetsBox.left(), topLeft.x()));
353                   widgetsBox.setBottom(qMax(widgetsBox.bottom(), bottomRight.y()));
354                   widgetsBox.setRight(qMax(widgetsBox.right(), bottomRight.x()));
355                   }
356             }
357       if (!set)
358             return; // Should display in center
359 
360       // Next find where the mbox goes around the widgetsBox
361       QWidget* mainWindow = widgets.at(0)->window();
362       int midX = mainWindow->mapToGlobal(QPoint(mainWindow->frameGeometry().width() / 2, 0)).x();
363       int midY = mainWindow->mapToGlobal(QPoint(0, mainWindow->frameGeometry().height() / 2)).y();
364 
365       // The longer side decides which side the mbox goes on.
366       bool topBottom = (widgetsBox.height() < widgetsBox.width());
367 
368       // Calculate the topLeft point for the mbox
369       QPoint displayPoint(0, 0);
370       if (topBottom) {
371             bool displayAbove = (widgetsBox.center().y() > midY);
372             if (displayAbove)
373                   displayPoint.setY(widgetsBox.top() - mbox->height() - mboxFrameBottomThickness);
374             else
375                   displayPoint.setY(widgetsBox.bottom() + mboxFrameTopThickness);
376 
377             int x = static_cast<int>(widgetsBox.width() - mbox->size().width()) / 2 + widgetsBox.left();
378             displayPoint.setX(x);
379             }
380       else {
381             bool displayLeft = (widgetsBox.center().x() > midX);
382             if (displayLeft)
383                   displayPoint.setX(widgetsBox.left() - mbox->width() - mboxFrameRightThickness);
384             else
385                   displayPoint.setX(widgetsBox.right() + mboxFrameLeftThickness);
386 
387             int y = (widgetsBox.height() - mbox->size().height()) / 2 + widgetsBox.top();
388             displayPoint.setY(y);
389             }
390 
391       // Make sure the box is within the screen
392       QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
393       displayPoint.setX(qMax(displayPoint.x(), 0));
394       displayPoint.setY(qMax(displayPoint.y(), 0));
395       displayPoint.setX(qMin(displayPoint.x(), screenGeometry.width() - mbox->size().width()));
396       displayPoint.setY(qMin(displayPoint.y(), screenGeometry.height() - mbox->size().height() - 15));
397 
398       mbox->move(displayPoint);
399       }
400 
401 //---------------------------------------------------------
402 //   displayTour
403 //---------------------------------------------------------
404 
getWidgetsByNames(Tour * tour,QList<QString> names)405 QList<QWidget*> TourHandler::getWidgetsByNames(Tour* tour, QList<QString> names)
406       {
407       QList<QWidget*> widgets;
408       for (const QString &name : names) {
409             // First check internal storage for widget
410             if (tour->hasNameForWidget(name))
411                   widgets.append(tour->getWidgetsByName(name));
412             else {
413                   // If not found, check all widgets by object name
414                   auto foundWidgets = mscore->findChildren<QWidget*>(name);
415                   widgets.append(foundWidgets);
416                   }
417             }
418       return widgets;
419       }
420 
421 //---------------------------------------------------------
422 //   displayTour
423 //---------------------------------------------------------
424 
displayTour(Tour * tour)425 void TourHandler::displayTour(Tour* tour)
426       {
427       int i = 0;
428       bool next = true;
429       bool showTours = true;
430       QList<TourMessage> tourMessages = tour->messages();
431       while (i != tourMessages.size()) {
432             // Set up the message box buttons
433             QMessageBox* mbox = new QMessageBox(mscore);
434             mbox->setWindowTitle(tr("Tour"));
435             QPushButton* backButton = nullptr;
436             QPushButton* nextButton = nullptr;
437             QPushButton* closeButton = nullptr;
438 
439             //QMessageBox doesn't support next/back semantic for various OS styles. QWizard does.
440             closeButton = mbox->addButton(tr("Close"), QMessageBox::RejectRole);
441             if (i != 0)
442                   backButton = mbox->addButton(tr("Back"), QMessageBox::NoRole); //Explicit text is bad since it varies depending on the OS. MacOS uses "Go back"
443             if (i != tourMessages.size() - 1)
444                   nextButton = mbox->addButton(tr("Next"), QMessageBox::YesRole); //MacOS uses "Continue"
445             else
446                   nextButton = mbox->addButton(tr("End"), QMessageBox::YesRole); // MacOS uses "Done"
447 
448             // Sets default to last pressed button
449             if (next)
450                   mbox->setDefaultButton(nextButton);
451             else
452                   mbox->setDefaultButton(backButton);
453             mbox->setEscapeButton(closeButton);
454 
455             // Add text (translation?)
456             mbox->setText(tourMessages[i].message);
457 
458             // Add checkbox to show tours
459             QCheckBox* showToursBox = new QCheckBox(tr("Continue showing tours"), mbox);
460             showToursBox->setChecked(showTours);
461             mbox->setCheckBox(showToursBox);
462 
463             // Display the message box, position it if needed
464             QList<QWidget*> tourWidgets = getWidgetsByNames(tour, tourMessages[i].widgetNames);
465             OverlayWidget* overlay = new OverlayWidget(tourWidgets);
466             if (tourWidgets.isEmpty())
467                   overlay->setParent(mscore);
468             else {
469                   overlay->setParent(tourWidgets.at(0)->window());
470                   positionMessage(tourWidgets, mbox);
471                   }
472 
473             const std::vector<QmlDockWidget*> qmlDockWidgets = mscore->qmlDockWidgets();
474             if (!tourWidgets.contains(mscore)) {
475                   for (QmlDockWidget* qw : qmlDockWidgets) {
476                         if (!tourWidgets.contains(qw))
477                               qw->setShadowOverlay(true);
478                         }
479                   }
480 
481             overlay->show();
482             mbox->exec();
483             mboxFrameTopThickness = mbox->geometry().top() - mbox->frameGeometry().top();
484             mboxFrameBottomThickness = mbox->frameGeometry().bottom() - mbox->geometry().bottom();
485             mboxFrameLeftThickness = mbox->geometry().left() - mbox->frameGeometry().left();
486             mboxFrameRightThickness = mbox->frameGeometry().right() - mbox->geometry().right();
487             overlay->hide();
488             showTours = showToursBox->isChecked();
489 
490             for (QmlDockWidget* qw : qmlDockWidgets)
491                   qw->setShadowOverlay(false);
492 
493             // Handle the button presses
494             if (mbox->clickedButton() == nextButton) {
495                   i++;
496                   next = true;
497                   }
498             else if (mbox->clickedButton() == backButton) {
499                   i--;
500                   next = false;
501                   }
502             else
503                   break;
504             }
505       preferences.setPreference(PREF_UI_APP_STARTUP_SHOWTOURS, showTours);
506       }
507 
508 }
509