MuseScore/mscore/tourhandler.cpp

492 lines
17 KiB
C++

#include "tourhandler.h"
#include "musescore.h"
#include "preferences.h"
namespace Ms {
QHash<QString, Tour*> TourHandler::allTours;
QHash<QString, Tour*> TourHandler::shortcutToTour;
QMap<QString, QMap<QString, QString>*> TourHandler::eventNameLookup;
//---------------------------------------------------------
// OverlayWidget
//---------------------------------------------------------
OverlayWidget::OverlayWidget(QList<QWidget*> widgetList, QWidget* parent)
: QWidget{parent}
{
widgets = widgetList;
newParent();
}
//---------------------------------------------------------
// newParent
//---------------------------------------------------------
void OverlayWidget::newParent()
{
if (!parent())
return;
parent()->installEventFilter(this);
resize(qobject_cast<QWidget*>(parent())->size());
raise();
}
//---------------------------------------------------------
// eventFilter
//---------------------------------------------------------
bool OverlayWidget::eventFilter(QObject* obj, QEvent* ev)
{
if (obj == parent()) {
if (ev->type() == QEvent::Resize)
resize(static_cast<QResizeEvent*>(ev)->size());
else if (ev->type() == QEvent::ChildAdded)
raise();
}
return QWidget::eventFilter(obj, ev);
}
//---------------------------------------------------------
// event
//---------------------------------------------------------
bool OverlayWidget::event(QEvent* ev)
{
if (ev->type() == QEvent::ParentAboutToChange) {
if (parent()) parent()->removeEventFilter(this);
}
else if (ev->type() == QEvent::ParentChange)
newParent();
return QWidget::event(ev);
}
//---------------------------------------------------------
// paintEvent
//---------------------------------------------------------
void OverlayWidget::paintEvent(QPaintEvent *)
{
QPainterPath painterPath = QPainterPath();
QPainter p(this);
QWidget* parentWindow = qobject_cast<QWidget*>(parent());
if (parentWindow)
painterPath.addRect(parentWindow->rect());
QPainterPath subPath = QPainterPath();
for (QWidget* w : widgets) {
if (w->isVisible()) {
// Add widget and children visible region mapped to the parentWindow
QRegion region = w->visibleRegion() + w->childrenRegion();
region.translate(w->mapTo(parentWindow, QPoint(0, 0)));
subPath.addRegion(region);
}
}
painterPath -= subPath;
QColor overlayColor = QApplication::palette().color(QPalette::Shadow);
overlayColor.setAlpha(128);
p.fillPath(painterPath, overlayColor);
}
//---------------------------------------------------------
// showWelcomeTour
//---------------------------------------------------------
void TourHandler::showWelcomeTour()
{
if (!delayedWelcomeTour)
startTour("welcome");
}
//---------------------------------------------------------
// showDelayedWelcomeTour
// delays showing the welcome tour when the user
// attempts to open a score or create a new score
//---------------------------------------------------------
void TourHandler::showDelayedWelcomeTour()
{
if (delayedWelcomeTour)
startTour("welcome");
delayedWelcomeTour = false;
}
//---------------------------------------------------------
// loadTours
//---------------------------------------------------------
void TourHandler::loadTours()
{
QStringList nameFilters;
nameFilters << "*.tour";
QString path = mscoreGlobalShare + "tours";
QDirIterator it(path, nameFilters, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
QFile* tourFile = new QFile(it.next());
tourFile->open(QIODevice::ReadOnly);
XmlReader tourXml(tourFile);
while (tourXml.readNextStartElement()) {
if (tourXml.name() == "Tour")
loadTour(tourXml);
else
tourXml.unknown();
}
}
readCompletedTours();
}
//---------------------------------------------------------
// loadTours
//---------------------------------------------------------
void TourHandler::loadTour(XmlReader& tourXml)
{
QString tourName = tourXml.attribute("name");
QList<QString> shortcuts;
Tour* tour = new Tour(tourName);
while (tourXml.readNextStartElement()) {
if (tourXml.name() == "Message") {
QString text;
QList<QString> objectNames;
while (tourXml.readNextStartElement()) {
if (tourXml.name() == "Text") {
QTextDocument doc;
QString rawText = tourXml.readElementText();
QString ttext = qApp->translate("TourXML", rawText.toUtf8().constData());
doc.setHtml(ttext);
text = doc.toPlainText().replace("\\n", "\n");
}
else if (tourXml.name() == "Widget")
objectNames.append(tourXml.readXml());
else
tourXml.unknown();
}
tour->addMessage(text, objectNames);
}
else if (tourXml.name() == "Event") {
QString name = tourXml.attribute("objectName");
QString event = tourXml.readXml();
if (!eventNameLookup.contains(name))
eventNameLookup.insert(name, new QMap<QString, QString>);
eventNameLookup.value(name)->insert(event, tourName);
}
else if (tourXml.name() == "Shortcut") {
shortcuts.append(tourXml.readXml());
}
else
tourXml.unknown();
}
allTours[tourName] = tour;
for (QString s : shortcuts)
shortcutToTour[s] = tour;
}
//---------------------------------------------------------
// resetCompletedTours
//---------------------------------------------------------
void TourHandler::resetCompletedTours()
{
for (auto tour : allTours)
tour->setCompleted(false);
}
//---------------------------------------------------------
// readCompletedTours
//---------------------------------------------------------
void TourHandler::readCompletedTours()
{
QFile completedToursFile(dataPath + "/tours/completedTours.list");
if (!completedToursFile.open(QIODevice::ReadOnly))
return;
QDataStream in(&completedToursFile);
QList<QString> completedTours;
in >> completedTours;
for (QString tourName : completedTours)
if (allTours.contains(tourName))
allTours.value(tourName)->setCompleted(true);
}
//---------------------------------------------------------
// writeCompletedTours
//---------------------------------------------------------
void TourHandler::writeCompletedTours()
{
QDir dir;
dir.mkpath(dataPath);
QString path = dataPath + "/tours";
dir.mkpath(path);
QFile completedToursFile(path + "/completedTours.list");
completedToursFile.open(QIODevice::WriteOnly);
QList<QString> completedTours;
for (Tour* t : allTours.values())
if (t->completed())
completedTours.append(t->tourName());
QDataStream out(&completedToursFile);
out << completedTours;
}
//---------------------------------------------------------
// eventFilter
//---------------------------------------------------------
bool TourHandler::eventFilter(QObject* obj, QEvent* event)
{
QString eventString = QVariant::fromValue(event->type()).value<QString>();
if (eventNameLookup.contains(obj->objectName()) &&
eventNameLookup.value(obj->objectName())->contains(eventString))
startTour(eventNameLookup.value(obj->objectName())->value(eventString));
else if (eventHandler.contains(obj) && eventHandler.value(obj)->contains(event->type()))
startTour(eventHandler.value(obj)->value(event->type()));
return QObject::eventFilter(obj, event);
}
//---------------------------------------------------------
// attachTour
//---------------------------------------------------------
void TourHandler::attachTour(QObject* obj, QEvent::Type eventType, QString tourName)
{
obj->installEventFilter(this);
if (!eventHandler.contains(obj))
eventHandler.insert(obj, new QMap<QEvent::Type, QString>);
eventHandler.value(obj)->insert(eventType, tourName);
}
//---------------------------------------------------------
// addWidgetToTour
//---------------------------------------------------------
void TourHandler::addWidgetToTour(QString tourName, QWidget* widget, QString widgetName)
{
if (allTours.contains(tourName)) {
Tour* tour = allTours.value(tourName);
tour->addNameAndWidget(widgetName, widget);
}
else
qDebug() << tourName << "does not have a tour.";
}
//---------------------------------------------------------
// clearWidgetsFromTour
//---------------------------------------------------------
void TourHandler::clearWidgetsFromTour(QString tourName)
{
if (allTours.contains(tourName))
allTours.value(tourName)->clearWidgets();
else
qDebug() << tourName << "does not have a tour.";
}
//---------------------------------------------------------
// startTour
// lookup string can be a tour name or a shortcut name
//---------------------------------------------------------
void TourHandler::startTour(QString lookupString)
{
if (!preferences.getBool(PREF_UI_APP_STARTUP_SHOWTOURS) || MScore::noGui)
return;
Tour* tour = nullptr;
if (allTours.contains(lookupString))
tour = allTours.value(lookupString);
else if (shortcutToTour.contains(lookupString))
tour = shortcutToTour.value(lookupString);
if (tour) {
if (tour->completed())
return;
displayTour(tour);
tour->setCompleted(true);
}
}
//---------------------------------------------------------
// getDisplayPoints
//---------------------------------------------------------
void TourHandler::positionMessage(QList<QWidget*> widgets, QMessageBox* mbox)
{
// Loads some information into the size of the mbox, a bit of a hack
mbox->show();
// Create a "box" to see where the msgbox should go
bool set = false;
QRect widgetsBox;
for (QWidget* w : widgets) {
if (w->visibleRegion().isEmpty())
continue;
QRect boundingRect = w->visibleRegion().boundingRect();
QPoint topLeft = w->mapToGlobal(QPoint(0, 0));
QPoint bottomRight = w->mapToGlobal(boundingRect.bottomRight());
if (!set) {
widgetsBox.setTopLeft(topLeft);
widgetsBox.setBottomRight(bottomRight);
set = true;
}
else {
widgetsBox.setTop(qMin(widgetsBox.top(), topLeft.y()));
widgetsBox.setLeft(qMin(widgetsBox.left(), topLeft.x()));
widgetsBox.setBottom(qMax(widgetsBox.bottom(), bottomRight.y()));
widgetsBox.setRight(qMax(widgetsBox.right(), bottomRight.x()));
}
}
if (!set)
return; // Should display in center
// Next find where the mbox goes around the widgetsBox
QWidget* mainWindow = widgets.at(0)->window();
int midX = mainWindow->mapToGlobal(QPoint(mainWindow->frameGeometry().width() / 2, 0)).x();
int midY = mainWindow->mapToGlobal(QPoint(0, mainWindow->frameGeometry().height() / 2)).y();
// The longer side decides which side the mbox goes on.
bool topBottom = (widgetsBox.height() < widgetsBox.width());
// Calculate the topLeft point for the mbox
QPoint displayPoint(0, 0);
if (topBottom) {
bool displayAbove = (widgetsBox.center().y() > midY);
if (displayAbove) {
int mBoxHeight = mbox->size().height() + 15; // hack
int y = widgetsBox.top();
displayPoint.setY(y - mBoxHeight);
}
else
displayPoint.setY(widgetsBox.bottom());
int x = (int) (widgetsBox.width() - mbox->size().width()) / 2 + widgetsBox.left();
displayPoint.setX(x);
}
else {
bool displayLeft = (widgetsBox.center().x() > midX);
if (displayLeft) {
int mBoxWidth = mbox->size().width();
int x = widgetsBox.left();
displayPoint.setX(x - mBoxWidth);
}
else
displayPoint.setX(widgetsBox.right());
int y = (widgetsBox.height() - mbox->size().height()) / 2 + widgetsBox.top();
displayPoint.setY(y);
}
// Make sure the box is within the screen
QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
displayPoint.setX(qMax(displayPoint.x(), 0));
displayPoint.setY(qMax(displayPoint.y(), 0));
displayPoint.setX(qMin(displayPoint.x(), screenGeometry.width() - mbox->size().width()));
displayPoint.setY(qMin(displayPoint.y(), screenGeometry.height() - mbox->size().height() - 15));
mbox->move(displayPoint);
}
//---------------------------------------------------------
// displayTour
//---------------------------------------------------------
QList<QWidget*> TourHandler::getWidgetsByNames(Tour* tour, QList<QString> names)
{
QList<QWidget*> widgets;
for (QString name : names) {
// First check internal storage for widget
if (tour->hasNameForWidget(name))
widgets.append(tour->getWidgetsByName(name));
else {
// If not found, check all widgets by object name
auto foundWidgets = mscore->findChildren<QWidget*>(name);
widgets.append(foundWidgets);
}
}
return widgets;
}
//---------------------------------------------------------
// displayTour
//---------------------------------------------------------
void TourHandler::displayTour(Tour* tour)
{
int i = 0;
bool next = true;
bool showTours = true;
QList<TourMessage> tourMessages = tour->messages();
while (i != tourMessages.size()) {
// Set up the message box buttons
QMessageBox* mbox = new QMessageBox(mscore);
mbox->setWindowTitle(tr("Tour"));
QPushButton* backButton = nullptr;
QPushButton* nextButton = nullptr;
QPushButton* closeButton = nullptr;
//QMessageBox doesn't support next/back semantic for various OS styles. QWizard does.
closeButton = mbox->addButton(tr("Close"), QMessageBox::RejectRole);
if (i != 0)
backButton = mbox->addButton(tr("Back"), QMessageBox::NoRole); //Explicit text is bad since it varies depending on the OS. MacOS uses "Go back"
if (i != tourMessages.size() - 1)
nextButton = mbox->addButton(tr("Next"), QMessageBox::YesRole); //MacOS uses "Continue"
else
nextButton = mbox->addButton(tr("End"), QMessageBox::YesRole); // MacOS uses "Done"
// Sets default to last pressed button
if (next)
mbox->setDefaultButton(nextButton);
else
mbox->setDefaultButton(backButton);
mbox->setEscapeButton(closeButton);
// Add text (translation?)
mbox->setText(tourMessages[i].message);
// Add checkbox to show tours
QCheckBox* showToursBox = new QCheckBox(tr("Continue showing tours"), mbox);
showToursBox->setChecked(showTours);
mbox->setCheckBox(showToursBox);
// Display the message box, position it if needed
QList<QWidget*> tourWidgets = getWidgetsByNames(tour, tourMessages[i].widgetNames);
OverlayWidget* overlay = new OverlayWidget(tourWidgets);
if (tourWidgets.isEmpty())
overlay->setParent(mscore);
else {
overlay->setParent(tourWidgets.at(0)->window());
positionMessage(tourWidgets, mbox);
}
overlay->show();
mbox->exec();
overlay->hide();
showTours = showToursBox->isChecked();
// Handle the button presses
if (mbox->clickedButton() == nextButton) {
i++;
next = true;
}
else if (mbox->clickedButton() == backButton) {
i--;
next = false;
}
else
break;
}
preferences.setPreference(PREF_UI_APP_STARTUP_SHOWTOURS, showTours);
}
}