mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
Add: UI tour to complement the Mudlet tutorial (#9385)
<!-- Keep the title short & concise so anyone non-technical can
understand it,
the title appears in PTB changelogs -->
#### Brief overview of PR changes/additions
Added a UI tour of the main Mudlet features to complement the tutorial
we already have that explains how MUDs work.
#### Motivation for adding to Mudlet
#2823, helps first-time players.
#### Other info (issues closed, discussion etc)
https://github.com/user-attachments/assets/a65d10fd-3fd2-4dae-85f2-2a83cf85fb1f
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
This commit is contained in:
parent
b59d5a90b7
commit
69d7d41694
7 changed files with 577 additions and 0 deletions
|
|
@ -206,6 +206,7 @@ set(mudlet_SRCS
|
|||
TToolBar.cpp
|
||||
TTreeWidget.cpp
|
||||
TTrigger.cpp
|
||||
TUiTour.cpp
|
||||
TVar.cpp
|
||||
VarUnit.cpp
|
||||
WideComboBox.cpp
|
||||
|
|
@ -435,6 +436,7 @@ set(mudlet_HDRS
|
|||
TToolBar.h
|
||||
TTreeWidget.h
|
||||
TTrigger.h
|
||||
TUiTour.h
|
||||
TVar.h
|
||||
utils.h
|
||||
VarUnit.h
|
||||
|
|
|
|||
422
src/TUiTour.cpp
Normal file
422
src/TUiTour.cpp
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@hey.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#include "TUiTour.h"
|
||||
|
||||
#include "Host.h"
|
||||
#include "TCommandLine.h"
|
||||
#include "TMainConsole.h"
|
||||
#include "mudlet.h"
|
||||
|
||||
#include <QFrame>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QMenuBar>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QTextDocumentFragment>
|
||||
#include <QToolBar>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
constexpr int spotlightMargin = 6;
|
||||
constexpr int spotlightRadius = 8;
|
||||
constexpr int cardMaxWidth = 420;
|
||||
constexpr int cardScreenMargin = 12;
|
||||
constexpr int cardSpotlightGap = 16;
|
||||
const QLatin1String settingsKeyTourShown("uiTourShown");
|
||||
}
|
||||
|
||||
TUiTour::TUiTour(mudlet* pMainWindow)
|
||||
: QWidget(pMainWindow)
|
||||
, mpMainWindow(pMainWindow)
|
||||
{
|
||||
setObjectName(qsl("uiTour"));
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
//: Name of the interface tour overlay, announced by screen readers
|
||||
setAccessibleName(tr("Mudlet interface tour"));
|
||||
parentWidget()->installEventFilter(this);
|
||||
|
||||
createCard();
|
||||
buildSteps();
|
||||
}
|
||||
|
||||
// The tour is meant to run automatically only once ever, and only for players
|
||||
// new to Mudlet - it can always be revisited via Help menu
|
||||
bool TUiTour::shouldShowOnFirstProfile()
|
||||
{
|
||||
if (mudlet::self()->experiencedMudletPlayer()) {
|
||||
return false;
|
||||
}
|
||||
return !mudlet::getQSettings()->value(settingsKeyTourShown, false).toBool();
|
||||
}
|
||||
|
||||
void TUiTour::rememberShown()
|
||||
{
|
||||
auto* settings = mudlet::getQSettings();
|
||||
settings->setValue(settingsKeyTourShown, true);
|
||||
settings->sync();
|
||||
}
|
||||
|
||||
void TUiTour::start()
|
||||
{
|
||||
resizeToParent();
|
||||
show();
|
||||
raise();
|
||||
setFocus();
|
||||
setStep(0, 1);
|
||||
}
|
||||
|
||||
void TUiTour::createCard()
|
||||
{
|
||||
mpCard = new QFrame(this);
|
||||
mpCard->setObjectName(qsl("uiTourCard"));
|
||||
mpCard->setAutoFillBackground(true);
|
||||
mpCard->setFrameShape(QFrame::StyledPanel);
|
||||
mpCard->setStyleSheet(qsl("QFrame#uiTourCard {"
|
||||
" background-color: palette(window);"
|
||||
" border: 1px solid palette(mid);"
|
||||
" border-radius: 8px;"
|
||||
"}"));
|
||||
|
||||
mpTitleLabel = new QLabel(mpCard);
|
||||
mpTitleLabel->setWordWrap(true);
|
||||
QFont titleFont = mpTitleLabel->font();
|
||||
titleFont.setBold(true);
|
||||
titleFont.setPointSize(titleFont.pointSize() + 2);
|
||||
mpTitleLabel->setFont(titleFont);
|
||||
|
||||
mpBodyLabel = new QLabel(mpCard);
|
||||
mpBodyLabel->setWordWrap(true);
|
||||
mpBodyLabel->setTextFormat(Qt::RichText);
|
||||
|
||||
mpProgressLabel = new QLabel(mpCard);
|
||||
|
||||
//: Button on the interface tour that dismisses the tour
|
||||
mpSkipButton = new QPushButton(tr("Skip tour"), mpCard);
|
||||
mpSkipButton->setFlat(true);
|
||||
//: Button on the interface tour that goes back to the previous step
|
||||
mpBackButton = new QPushButton(tr("Back"), mpCard);
|
||||
//: Button on the interface tour that advances to the next step
|
||||
mpNextButton = new QPushButton(tr("Next"), mpCard);
|
||||
mpNextButton->setDefault(true);
|
||||
|
||||
connect(mpSkipButton, &QPushButton::clicked, this, &TUiTour::slot_finish);
|
||||
connect(mpBackButton, &QPushButton::clicked, this, &TUiTour::slot_back);
|
||||
connect(mpNextButton, &QPushButton::clicked, this, &TUiTour::slot_next);
|
||||
|
||||
auto* buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->addWidget(mpProgressLabel);
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(mpSkipButton);
|
||||
buttonLayout->addWidget(mpBackButton);
|
||||
buttonLayout->addWidget(mpNextButton);
|
||||
|
||||
auto* cardLayout = new QVBoxLayout(mpCard);
|
||||
cardLayout->setContentsMargins(16, 16, 16, 12);
|
||||
cardLayout->setSpacing(8);
|
||||
cardLayout->addWidget(mpTitleLabel);
|
||||
cardLayout->addWidget(mpBodyLabel);
|
||||
cardLayout->addLayout(buttonLayout);
|
||||
}
|
||||
|
||||
void TUiTour::buildSteps()
|
||||
{
|
||||
auto activeConsole = []() -> TMainConsole* {
|
||||
Host* pHost = mudlet::self()->getActiveHost();
|
||||
return pHost ? pHost->mpConsole.data() : nullptr;
|
||||
};
|
||||
auto widgetRect = [this](QWidget* widget) -> QRect {
|
||||
if (!widget || !widget->isVisible()) {
|
||||
return {};
|
||||
}
|
||||
return {widget->mapTo(mpMainWindow, QPoint(0, 0)), widget->size()};
|
||||
};
|
||||
auto toolbarButtonRect = [this, widgetRect](const QString& name) -> QRect {
|
||||
QToolBar* toolbar = mpMainWindow->mpMainToolBar;
|
||||
if (!toolbar || !toolbar->isVisible()) {
|
||||
return {};
|
||||
}
|
||||
return widgetRect(toolbar->findChild<QWidget*>(name));
|
||||
};
|
||||
auto menuTitleRect = [this](QMenu* menu) -> QRect {
|
||||
QMenuBar* menuBar = mpMainWindow->menuBar();
|
||||
if (!menu || !menuBar || !menuBar->isVisible()) {
|
||||
return {};
|
||||
}
|
||||
const QRect titleRect = menuBar->actionGeometry(menu->menuAction());
|
||||
if (titleRect.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return {menuBar->mapTo(mpMainWindow, titleRect.topLeft()), titleRect.size()};
|
||||
};
|
||||
|
||||
mSteps.clear();
|
||||
|
||||
mSteps.push_back({nullptr,
|
||||
//: Title of the first step of the interface tour
|
||||
tr("Welcome to Mudlet!"),
|
||||
//: Body of the first step of the interface tour
|
||||
tr("New here? This quick tour points out the most important parts of Mudlet - it takes less than a minute. "
|
||||
"Click anywhere or use the arrow keys to move through it.")});
|
||||
|
||||
mSteps.push_back({[activeConsole, widgetRect]() -> QRect {
|
||||
TMainConsole* console = activeConsole();
|
||||
return console ? widgetRect(console->mpMainDisplay) : QRect();
|
||||
},
|
||||
//: Title of the interface tour step highlighting the main text display
|
||||
tr("The game window"),
|
||||
//: Body of the interface tour step highlighting the main text display
|
||||
tr("Text from the game appears here. Scroll up to review earlier text - the newest text stays visible in a split view while you do.")});
|
||||
|
||||
mSteps.push_back({[activeConsole, widgetRect]() -> QRect {
|
||||
TMainConsole* console = activeConsole();
|
||||
return console ? widgetRect(console->mpCommandLine.data()) : QRect();
|
||||
},
|
||||
//: Title of the interface tour step highlighting the command input line
|
||||
tr("The input line"),
|
||||
//: Body of the interface tour step highlighting the command input line
|
||||
tr("Type game commands here and press Enter to send them. Use the up and down arrow keys to bring back commands you typed before.")});
|
||||
|
||||
// The toolbar is hidden by default, so these two steps point at the
|
||||
// equivalent menu when its button isn't on screen
|
||||
mSteps.push_back({[this, toolbarButtonRect, menuTitleRect]() -> QRect {
|
||||
const QRect buttonRect = toolbarButtonRect(qsl("triggers_action"));
|
||||
return buttonRect.isEmpty() ? menuTitleRect(mpMainWindow->menuEditor) : buttonRect;
|
||||
},
|
||||
//: Title of the interface tour step highlighting the scripting tools
|
||||
tr("Automate your game"),
|
||||
//: Body of the interface tour step highlighting the scripting tools
|
||||
tr("Triggers, aliases, timers and scripts let Mudlet react to the game for you and shorten what you type. "
|
||||
"You will find them in the script editor, right here - start simple, no programming needed.")});
|
||||
|
||||
mSteps.push_back({[this, toolbarButtonRect, menuTitleRect]() -> QRect {
|
||||
const QRect buttonRect = toolbarButtonRect(qsl("settings_action"));
|
||||
return buttonRect.isEmpty() ? menuTitleRect(mpMainWindow->menuOptions) : buttonRect;
|
||||
},
|
||||
//: Title of the interface tour step highlighting the preferences
|
||||
tr("Make Mudlet yours"),
|
||||
//: Body of the interface tour step highlighting the preferences
|
||||
tr("Fonts, colors, the map, accessibility options and much more can be adjusted in the settings, right here.")});
|
||||
|
||||
mSteps.push_back({nullptr,
|
||||
//: Title of the last step of the interface tour
|
||||
tr("That's it - have fun!"),
|
||||
//: Body of the last step of the interface tour. The tour can be re-run via the named menu entry.
|
||||
tr("For a hands-on lesson, connect to the <b>Mudlet Tutorial</b> game. "
|
||||
"And if you ever want to see this tour again, it lives in Help → Take a UI tour.")});
|
||||
}
|
||||
|
||||
void TUiTour::setStep(int index, int direction)
|
||||
{
|
||||
const int stepCount = static_cast<int>(mSteps.size());
|
||||
// A step's target can be off screen right now, e.g. a toolbar button
|
||||
// tucked away in the overflow area
|
||||
while (index >= 0 && index < stepCount) {
|
||||
const TourStep& step = mSteps.at(index);
|
||||
if (!step.spotlightResolver || !step.spotlightResolver().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
index += direction;
|
||||
}
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
if (index >= stepCount) {
|
||||
slot_finish();
|
||||
return;
|
||||
}
|
||||
mCurrentStep = index;
|
||||
updateCard();
|
||||
update();
|
||||
}
|
||||
|
||||
void TUiTour::updateCard()
|
||||
{
|
||||
const TourStep& step = mSteps.at(mCurrentStep);
|
||||
mpTitleLabel->setText(step.title);
|
||||
mpBodyLabel->setText(step.body);
|
||||
//: Progress through the interface tour, %1 is the current step number, %2 the total number of steps
|
||||
mpProgressLabel->setText(tr("%1 of %2").arg(QString::number(mCurrentStep + 1), QString::number(mSteps.size())));
|
||||
|
||||
const bool lastStep = mCurrentStep == static_cast<int>(mSteps.size()) - 1;
|
||||
mpBackButton->setEnabled(mCurrentStep > 0);
|
||||
mpSkipButton->setVisible(!lastStep);
|
||||
//: Button on the last step of the interface tour that closes it. The other label option is "Next".
|
||||
mpNextButton->setText(lastStep ? tr("Finish") : tr("Next"));
|
||||
|
||||
positionCard();
|
||||
|
||||
mudlet::self()->announce(qsl("%1. %2").arg(step.title, QTextDocumentFragment::fromHtml(step.body).toPlainText()));
|
||||
}
|
||||
|
||||
QRect TUiTour::spotlightRect() const
|
||||
{
|
||||
const TourStep& step = mSteps.at(mCurrentStep);
|
||||
if (!step.spotlightResolver) {
|
||||
return {};
|
||||
}
|
||||
const QRect targetRect = step.spotlightResolver();
|
||||
if (targetRect.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return targetRect.adjusted(-spotlightMargin, -spotlightMargin, spotlightMargin, spotlightMargin).intersected(rect());
|
||||
}
|
||||
|
||||
void TUiTour::positionCard()
|
||||
{
|
||||
mpCard->setFixedWidth(qMin(cardMaxWidth, width() - 2 * cardScreenMargin));
|
||||
mpCard->adjustSize();
|
||||
|
||||
const QRect within = rect().adjusted(cardScreenMargin, cardScreenMargin, -cardScreenMargin, -cardScreenMargin);
|
||||
const QRect spot = spotlightRect();
|
||||
QRect cardRect = mpCard->rect();
|
||||
|
||||
if (spot.isEmpty()) {
|
||||
cardRect.moveCenter(rect().center());
|
||||
} else {
|
||||
// Clamped so a spotlight near a window corner still gets its card
|
||||
// alongside rather than falling back to the window center
|
||||
const int clampedX = qBound(within.left(), spot.center().x() - cardRect.width() / 2, qMax(within.left(), within.right() - cardRect.width()));
|
||||
const int clampedY = qBound(within.top(), spot.center().y() - cardRect.height() / 2, qMax(within.top(), within.bottom() - cardRect.height()));
|
||||
|
||||
QRect below = cardRect;
|
||||
below.moveLeft(clampedX);
|
||||
below.moveTop(spot.bottom() + cardSpotlightGap);
|
||||
|
||||
QRect above = cardRect;
|
||||
above.moveLeft(clampedX);
|
||||
above.moveBottom(spot.top() - cardSpotlightGap);
|
||||
|
||||
QRect right = cardRect;
|
||||
right.moveTop(clampedY);
|
||||
right.moveLeft(spot.right() + cardSpotlightGap);
|
||||
|
||||
QRect left = cardRect;
|
||||
left.moveTop(clampedY);
|
||||
left.moveRight(spot.left() - cardSpotlightGap);
|
||||
|
||||
// When nothing fits (the spotlight covers most of the window) the
|
||||
// card floats over the spotlit area
|
||||
cardRect.moveCenter(rect().center());
|
||||
for (const QRect& candidate : {below, above, right, left}) {
|
||||
if (within.contains(candidate)) {
|
||||
cardRect = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cardRect.moveLeft(qBound(within.left(), cardRect.left(), qMax(within.left(), within.right() - cardRect.width())));
|
||||
cardRect.moveTop(qBound(within.top(), cardRect.top(), qMax(within.top(), within.bottom() - cardRect.height())));
|
||||
mpCard->setGeometry(cardRect);
|
||||
}
|
||||
|
||||
void TUiTour::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const QRect spot = spotlightRect();
|
||||
QPainterPath dimPath;
|
||||
dimPath.addRect(rect());
|
||||
if (!spot.isEmpty()) {
|
||||
QPainterPath cutout;
|
||||
cutout.addRoundedRect(spot, spotlightRadius, spotlightRadius);
|
||||
dimPath -= cutout;
|
||||
}
|
||||
painter.fillPath(dimPath, QColor(0, 0, 0, 180));
|
||||
|
||||
if (!spot.isEmpty()) {
|
||||
painter.setPen(QPen(palette().color(QPalette::Highlight), 2));
|
||||
painter.drawRoundedRect(spot, spotlightRadius, spotlightRadius);
|
||||
}
|
||||
}
|
||||
|
||||
void TUiTour::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
const bool leftToRight = QGuiApplication::isLeftToRight();
|
||||
const Qt::Key nextKey = leftToRight ? Qt::Key_Right : Qt::Key_Left;
|
||||
const Qt::Key backKey = leftToRight ? Qt::Key_Left : Qt::Key_Right;
|
||||
|
||||
if (event->key() == Qt::Key_Escape) {
|
||||
event->accept();
|
||||
slot_finish();
|
||||
} else if (event->key() == backKey || event->key() == Qt::Key_PageUp) {
|
||||
event->accept();
|
||||
slot_back();
|
||||
} else if (event->key() == nextKey || event->key() == Qt::Key_Space || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter || event->key() == Qt::Key_PageDown) {
|
||||
event->accept();
|
||||
slot_next();
|
||||
} else {
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void TUiTour::mouseReleaseEvent(QMouseEvent* event)
|
||||
{
|
||||
event->accept();
|
||||
slot_next();
|
||||
}
|
||||
|
||||
bool TUiTour::eventFilter(QObject* watched, QEvent* event)
|
||||
{
|
||||
if (watched == parent() && (event->type() == QEvent::Resize || event->type() == QEvent::LayoutRequest)) {
|
||||
resizeToParent();
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void TUiTour::resizeToParent()
|
||||
{
|
||||
setGeometry(parentWidget()->rect());
|
||||
positionCard();
|
||||
update();
|
||||
}
|
||||
|
||||
void TUiTour::slot_next()
|
||||
{
|
||||
if (mCurrentStep >= static_cast<int>(mSteps.size()) - 1) {
|
||||
slot_finish();
|
||||
return;
|
||||
}
|
||||
setStep(mCurrentStep + 1, 1);
|
||||
}
|
||||
|
||||
void TUiTour::slot_back()
|
||||
{
|
||||
if (mCurrentStep > 0) {
|
||||
setStep(mCurrentStep - 1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
void TUiTour::slot_finish()
|
||||
{
|
||||
emit signal_tourFinished();
|
||||
close();
|
||||
}
|
||||
99
src/TUiTour.h
Normal file
99
src/TUiTour.h
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#ifndef MUDLET_TUITOUR_H
|
||||
#define MUDLET_TUITOUR_H
|
||||
|
||||
/***************************************************************************
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@hey.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
class QFrame;
|
||||
class QKeyEvent;
|
||||
class QLabel;
|
||||
class QMouseEvent;
|
||||
class QPaintEvent;
|
||||
class QPushButton;
|
||||
|
||||
class mudlet;
|
||||
|
||||
// A one-time spotlight tour over the main window that points out the most
|
||||
// important parts of the interface to first-time players
|
||||
class TUiTour : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TUiTour(mudlet* pMainWindow);
|
||||
|
||||
static bool shouldShowOnFirstProfile();
|
||||
static void rememberShown();
|
||||
|
||||
void start();
|
||||
|
||||
signals:
|
||||
// Emitted on user-driven dismissal only, not when the widget goes down
|
||||
// with the application - handlers touch state that no longer exists then
|
||||
void signal_tourFinished();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||
|
||||
private slots:
|
||||
void slot_next();
|
||||
void slot_back();
|
||||
void slot_finish();
|
||||
|
||||
private:
|
||||
struct TourStep
|
||||
{
|
||||
// Returns the area to spotlight in main window coordinates, empty when
|
||||
// the target is currently unavailable; a step without a resolver shows
|
||||
// a centered card over a fully dimmed window instead
|
||||
std::function<QRect()> spotlightResolver;
|
||||
QString title;
|
||||
QString body;
|
||||
};
|
||||
|
||||
void buildSteps();
|
||||
void createCard();
|
||||
void setStep(int index, int direction);
|
||||
void updateCard();
|
||||
void positionCard();
|
||||
QRect spotlightRect() const;
|
||||
void resizeToParent();
|
||||
|
||||
mudlet* mpMainWindow = nullptr;
|
||||
std::vector<TourStep> mSteps;
|
||||
int mCurrentStep = 0;
|
||||
|
||||
QFrame* mpCard = nullptr;
|
||||
QLabel* mpTitleLabel = nullptr;
|
||||
QLabel* mpBodyLabel = nullptr;
|
||||
QLabel* mpProgressLabel = nullptr;
|
||||
QPushButton* mpSkipButton = nullptr;
|
||||
QPushButton* mpBackButton = nullptr;
|
||||
QPushButton* mpNextButton = nullptr;
|
||||
};
|
||||
|
||||
#endif // MUDLET_TUITOUR_H
|
||||
Binary file not shown.
|
|
@ -42,6 +42,7 @@
|
|||
#include "TGameDetails.h"
|
||||
#include "TRoomDB.h"
|
||||
#include "TTabBar.h"
|
||||
#include "TUiTour.h"
|
||||
#include "XMLimport.h"
|
||||
#include "dlgAboutDialog.h"
|
||||
#include "dlgConnectionProfiles.h"
|
||||
|
|
@ -581,6 +582,7 @@ void mudlet::init()
|
|||
connect(dactionNewMapWindow, &QAction::triggered, this, &mudlet::slot_newMapWindow);
|
||||
|
||||
connect(dactionHelp, &QAction::triggered, this, &mudlet::slot_showHelpDialog);
|
||||
connect(dactionUiTour, &QAction::triggered, this, &mudlet::slot_showUiTour);
|
||||
connect(dactionVideo, &QAction::triggered, this, &mudlet::slot_showHelpDialogVideo);
|
||||
connect(dactionForum, &QAction::triggered, this, &mudlet::slot_showHelpDialogForum);
|
||||
connect(dactionDiscord, &QAction::triggered, this, &mudlet::slot_profileDiscord);
|
||||
|
|
@ -3924,6 +3926,31 @@ void mudlet::slot_showHelpDialog()
|
|||
QDesktopServices::openUrl(QUrl("https://wiki.mudlet.org/w/Manual:Contents"));
|
||||
}
|
||||
|
||||
void mudlet::slot_showUiTour()
|
||||
{
|
||||
if (mpUiTour) {
|
||||
mpUiTour->raise();
|
||||
mpUiTour->setFocus();
|
||||
return;
|
||||
}
|
||||
mpUiTour = new TUiTour(this);
|
||||
connect(mpUiTour, &TUiTour::signal_tourFinished, this, &mudlet::slot_uiTourClosed);
|
||||
TUiTour::rememberShown();
|
||||
mpUiTour->start();
|
||||
}
|
||||
|
||||
// Covers every way the tour can go away: Finish, Skip and Esc all close it
|
||||
void mudlet::slot_uiTourClosed()
|
||||
{
|
||||
for (auto pHost : mHostManager) {
|
||||
pHost->getLuaInterpreter()->compileAndExecuteScript(qsl("if mudlet then mudlet.uiTourPending = false end"));
|
||||
TEvent event{};
|
||||
event.mArgumentList.append(qsl("sysUiTourFinished"));
|
||||
event.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
|
||||
pHost->raiseEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void mudlet::slot_showHelpDialogVideo()
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl("https://www.mudlet.org/media/"));
|
||||
|
|
@ -4768,6 +4795,14 @@ void mudlet::slot_connectionDialogueFinished(const QString& profile, bool connec
|
|||
mudlet::installModulesList(pHost, modules);
|
||||
}
|
||||
|
||||
// Decided before packages install so their scripts can see the flag and
|
||||
// hold off on their own introductions until the tour is done - the
|
||||
// tutorial package does this
|
||||
const bool showUiTour = TUiTour::shouldShowOnFirstProfile();
|
||||
if (showUiTour) {
|
||||
pHost->getLuaInterpreter()->compileAndExecuteScript(qsl("mudlet = mudlet or {} mudlet.uiTourPending = true"));
|
||||
}
|
||||
|
||||
// install default packages
|
||||
for (const auto& package : std::as_const(mPackagesToInstallList)) {
|
||||
pHost->installPackage(package, enums::PackageModuleType::Package);
|
||||
|
|
@ -4828,6 +4863,12 @@ void mudlet::slot_connectionDialogueFinished(const QString& profile, bool connec
|
|||
pHost->raiseEvent(event);
|
||||
pHost->mIsProfileLoadingSequence = false;
|
||||
emit signal_profileLoaded();
|
||||
|
||||
if (showUiTour) {
|
||||
// give the freshly opened profile a moment to finish laying out before
|
||||
// the tour starts highlighting parts of it
|
||||
QTimer::singleShot(1000, this, &mudlet::slot_showUiTour);
|
||||
}
|
||||
}
|
||||
|
||||
void mudlet::installModulesList(Host* pHost, QStringList modules)
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ class TScrollBox;
|
|||
class TTabBar;
|
||||
class TTimer;
|
||||
class TToolBar;
|
||||
class TUiTour;
|
||||
|
||||
class mudlet : public QMainWindow, public Ui::main_window
|
||||
{
|
||||
|
|
@ -486,6 +487,8 @@ public slots:
|
|||
void slot_showKeyDialog();
|
||||
void slot_showPreferencesDialog();
|
||||
void slot_showScriptDialog();
|
||||
void slot_showUiTour();
|
||||
void slot_uiTourClosed();
|
||||
static void restoreProfileFocus(const QString& profileName);
|
||||
static void setupEditorFocusRestoration(dlgTriggerEditor* pEditor, const QString& profileName, QWidget* targetWindow = nullptr);
|
||||
void setupNotepadFocusRestoration(dlgNotepad* pNotepad);
|
||||
|
|
@ -706,6 +709,7 @@ private:
|
|||
qreal mBlinkTimeMs = 0.0;
|
||||
int mBlinkClientCount = 0;
|
||||
QPointer<QToolBar> mpToolBarReplay;
|
||||
QPointer<TUiTour> mpUiTour;
|
||||
QWidget* mpWidget_profileContainer = nullptr;
|
||||
// read-only value to see if the interface is light or dark. To set the value,
|
||||
// use setAppearance instead
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@
|
|||
<string>Help</string>
|
||||
</property>
|
||||
<addaction name="dactionHelp"/>
|
||||
<addaction name="dactionUiTour"/>
|
||||
<addaction name="dactionVideo"/>
|
||||
<addaction name="dactionDiscord"/>
|
||||
<addaction name="dactionMudletDiscord"/>
|
||||
|
|
@ -276,6 +277,14 @@
|
|||
<enum>QAction::MenuRole::AboutRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="dactionUiTour">
|
||||
<property name="text">
|
||||
<string>Take a UI tour</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><p>Shows a short interactive tour of the most important parts of Mudlet's interface.</p></string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="dactionVideo">
|
||||
<property name="text">
|
||||
<string>Video tutorials</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue