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