// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "introductionwidget.h"
#include "welcometr.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace Core;
using namespace Utils;
namespace Welcome::Internal {
const char kTakeTourSetting[] = "TakeUITour";
struct Item
{
QString pointerAnchorObjectName;
QString title;
QString brief;
QString description;
};
class IntroductionWidget : public QWidget
{
public:
IntroductionWidget(Core::ModeManager::Style previousModeStyle);
protected:
bool event(QEvent *e) override;
bool eventFilter(QObject *obj, QEvent *ev) override;
void paintEvent(QPaintEvent *ev) override;
void keyPressEvent(QKeyEvent *ke) override;
void mouseReleaseEvent(QMouseEvent *me) override;
private:
void finish();
void step();
void setStep(uint index);
void resizeToParent();
QWidget *m_textWidget;
QLabel *m_stepText;
QLabel *m_continueLabel;
QImage m_borderImage;
QString m_bodyCss;
std::vector- m_items;
QPointer m_stepPointerAnchor;
uint m_step = 0;
Core::ModeManager::Style m_previousModeStyle;
};
IntroductionWidget::IntroductionWidget(Core::ModeManager::Style previousModeStyle)
: QWidget(ICore::dialogParent()),
m_borderImage(":/welcome/images/border.png"),
m_previousModeStyle(previousModeStyle)
{
Core::ModeManager::setModeStyle(Core::ModeManager::Style::IconsAndText);
setFocusPolicy(Qt::StrongFocus);
setFocus();
parentWidget()->installEventFilter(this);
QPalette p = palette();
p.setColor(QPalette::WindowText, QColor(220, 220, 220));
setPalette(p);
m_textWidget = new QWidget(this);
auto layout = new QVBoxLayout;
m_textWidget->setLayout(layout);
m_stepText = new QLabel(this);
m_stepText->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_stepText->setWordWrap(true);
m_stepText->setTextFormat(Qt::RichText);
// why is palette not inherited???
m_stepText->setPalette(palette());
connect(m_stepText, &QLabel::linkActivated, this, [this](const QString &link) {
// clicking the User Interface link should both open the documentation page
// and step forward (=end the tour)
step();
QDesktopServices::openUrl(link);
});
layout->addWidget(m_stepText);
m_continueLabel = new QLabel(this);
m_continueLabel->setAlignment(Qt::AlignCenter);
m_continueLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_continueLabel->setWordWrap(true);
m_continueLabel->setFont(StyleHelper::uiFont(StyleHelper::UiElementH4));
m_continueLabel->setPalette(palette());
layout->addWidget(m_continueLabel);
m_bodyCss = "font-size: 16px;";
m_items = {
{QLatin1String("ModeSelector"),
Tr::tr("Mode Selector"),
Tr::tr("Select different modes depending on the task at hand."),
Tr::tr("
"
"Welcome: | Open examples, tutorials, and "
"recent sessions and projects. |
"
"Edit: | Work with code and navigate your project. |
"
"Design: | Visually edit Widget-based user interfaces, state charts and UML models. |
"
"Debug: | Analyze your application with a debugger or other "
"analyzers. |
"
"Projects: | Manage project settings. |
"
"Help: | Browse the help database. |
"
"
")},
{QLatin1String("KitSelector.Button"),
Tr::tr("Kit Selector"),
Tr::tr("Select the active project or project configuration."),
{}},
{QLatin1String("Run.Button"),
Tr::tr("Run Button"),
Tr::tr("Run the active project. By default this builds the project first."),
{}},
{QLatin1String("Debug.Button"),
Tr::tr("Debug Button"),
Tr::tr("Run the active project in a debugger."),
{}},
{QLatin1String("Build.Button"), Tr::tr("Build Button"), Tr::tr("Build the active project."), {}},
{QLatin1String("LocatorInput"),
Tr::tr("Locator"),
Tr::tr("Type here to open a file from any open project."),
Tr::tr("Or:"
""
"- type
c<space><pattern>
to jump to a class definition "
"- type
f<space><pattern>
to open a file from the file "
"system "
"- click on the magnifier icon for a complete list of possible options
"
"
")},
{QLatin1String("OutputPaneButtons"),
Tr::tr("Output"),
Tr::tr("Find compile and application output here, "
"as well as a list of configuration and build issues, "
"and the panel for global searches."),
{}},
{QLatin1String("ProgressInfo"),
Tr::tr("Progress Indicator"),
Tr::tr("Progress information about running tasks is shown here."),
{}},
{{},
Tr::tr("Escape to Editor"),
Tr::tr("Pressing the Escape key brings you back to the editor. Press it "
"multiple times to also hide context help and output, giving the editor more "
"space."),
{}},
{{},
Tr::tr("The End"),
Tr::tr("You have now completed the UI tour. To learn more about the highlighted "
"controls, see User "
"Interface."),
{}}};
setStep(0);
resizeToParent();
}
bool IntroductionWidget::event(QEvent *e)
{
if (e->type() == QEvent::ShortcutOverride) {
e->accept();
return true;
}
return QWidget::event(e);
}
bool IntroductionWidget::eventFilter(QObject *obj, QEvent *ev)
{
if (obj == parent() && ev->type() == QEvent::Resize)
resizeToParent();
return QWidget::eventFilter(obj, ev);
}
const int SPOTLIGHTMARGIN = 18;
const int POINTER_SIZE = 30;
const int POINTER_WIDTH = 3;
static int margin(const QRect &small, const QRect &big, Qt::Alignment side)
{
switch (side) {
case Qt::AlignRight:
return qMax(0, big.right() - small.right());
case Qt::AlignTop:
return qMax(0, small.top() - big.top());
case Qt::AlignBottom:
return qMax(0, big.bottom() - small.bottom());
case Qt::AlignLeft:
return qMax(0, small.x() - big.x());
default:
QTC_ASSERT(false, return 0);
}
}
static int oppositeMargin(const QRect &small, const QRect &big, Qt::Alignment side)
{
switch (side) {
case Qt::AlignRight:
return margin(small, big, Qt::AlignLeft);
case Qt::AlignTop:
return margin(small, big, Qt::AlignBottom);
case Qt::AlignBottom:
return margin(small, big, Qt::AlignTop);
case Qt::AlignLeft:
return margin(small, big, Qt::AlignRight);
default:
QTC_ASSERT(false, return 100000);
}
}
static const QVector pointerPolygon(const QRect &anchorRect, const QRect &fullRect)
{
// Put the arrow opposite to the smallest margin,
// with priority right, top, bottom, left.
// Not very sophisticated but sufficient for current use cases.
QVector alignments{Qt::AlignRight, Qt::AlignTop, Qt::AlignBottom, Qt::AlignLeft};
Utils::sort(alignments, [anchorRect, fullRect](Qt::Alignment a, Qt::Alignment b) {
return oppositeMargin(anchorRect, fullRect, a) < oppositeMargin(anchorRect, fullRect, b);
});
const auto alignIt = std::find_if(alignments.cbegin(),
alignments.cend(),
[anchorRect, fullRect](Qt::Alignment align) {
return margin(anchorRect, fullRect, align) > POINTER_SIZE;
});
if (alignIt == alignments.cend())
return {{}}; // no side with sufficient space found
const qreal arrowHeadWidth = POINTER_SIZE/3.;
if (*alignIt == Qt::AlignRight) {
const qreal middleY = anchorRect.center().y();
const qreal startX = anchorRect.right() + POINTER_SIZE;
const qreal endX = anchorRect.right() + POINTER_WIDTH;
return {{QVector{{startX, middleY}, {endX, middleY}}},
QVector{{endX + arrowHeadWidth, middleY - arrowHeadWidth},
{endX, middleY},
{endX + arrowHeadWidth, middleY + arrowHeadWidth}}};
}
if (*alignIt == Qt::AlignTop) {
const qreal middleX = anchorRect.center().x();
const qreal startY = anchorRect.y() - POINTER_SIZE;
const qreal endY = anchorRect.y() - POINTER_WIDTH;
return {{QVector{{middleX, startY}, {middleX, endY}}},
QVector{{middleX - arrowHeadWidth, endY - arrowHeadWidth},
{middleX, endY},
{middleX + arrowHeadWidth, endY - arrowHeadWidth}}};
}
if (*alignIt == Qt::AlignBottom) {
const qreal middleX = anchorRect.center().x();
const qreal startY = anchorRect.y() + POINTER_WIDTH;
const qreal endY = anchorRect.y() + POINTER_SIZE;
return {{QVector{{middleX, startY}, {middleX, endY}}},
QVector{{middleX - arrowHeadWidth, endY + arrowHeadWidth},
{middleX, endY},
{middleX + arrowHeadWidth, endY + arrowHeadWidth}}};
}
// Qt::AlignLeft
const qreal middleY = anchorRect.center().y();
const qreal startX = anchorRect.x() - POINTER_WIDTH;
const qreal endX = anchorRect.x() - POINTER_SIZE;
return {{QVector{{startX, middleY}, {endX, middleY}}},
QVector{{endX - arrowHeadWidth, middleY - arrowHeadWidth},
{endX, middleY},
{endX - arrowHeadWidth, middleY + arrowHeadWidth}}};
}
void IntroductionWidget::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setOpacity(.87);
const QColor backgroundColor = Qt::black;
if (m_stepPointerAnchor) {
const QPoint anchorPos = m_stepPointerAnchor->mapTo(parentWidget(), QPoint{0, 0});
const QRect anchorRect(anchorPos, m_stepPointerAnchor->size());
const QRect spotlightRect = anchorRect.adjusted(-SPOTLIGHTMARGIN,
-SPOTLIGHTMARGIN,
SPOTLIGHTMARGIN,
SPOTLIGHTMARGIN);
// darken the background to create a spotlighted area
if (spotlightRect.left() > 0) {
p.fillRect(0, 0, spotlightRect.left(), height(), backgroundColor);
}
if (spotlightRect.top() > 0) {
p.fillRect(spotlightRect.left(),
0,
width() - spotlightRect.left(),
spotlightRect.top(),
backgroundColor);
}
if (spotlightRect.right() < width() - 1) {
p.fillRect(spotlightRect.right() + 1,
spotlightRect.top(),
width() - spotlightRect.right() - 1,
height() - spotlightRect.top(),
backgroundColor);
}
if (spotlightRect.bottom() < height() - 1) {
p.fillRect(spotlightRect.left(),
spotlightRect.bottom() + 1,
spotlightRect.width(),
height() - spotlightRect.bottom() - 1,
backgroundColor);
}
// smooth borders of the spotlighted area by gradients
StyleHelper::drawCornerImage(m_borderImage,
&p,
spotlightRect,
SPOTLIGHTMARGIN,
SPOTLIGHTMARGIN,
SPOTLIGHTMARGIN,
SPOTLIGHTMARGIN);
// draw pointer
const QColor qtGreen(65, 205, 82);
p.setOpacity(1.);
p.setPen(QPen(QBrush(qtGreen),
POINTER_WIDTH,
Qt::SolidLine,
Qt::RoundCap,
Qt::MiterJoin));
p.setRenderHint(QPainter::Antialiasing);
for (const QPolygonF &poly : pointerPolygon(spotlightRect, rect()))
p.drawPolyline(poly);
} else {
p.fillRect(rect(), backgroundColor);
}
}
void IntroductionWidget::keyPressEvent(QKeyEvent *ke)
{
if (ke->key() == Qt::Key_Escape)
finish();
else if ((ke->modifiers()
& (Qt::ControlModifier | Qt::AltModifier | Qt::ShiftModifier | Qt::MetaModifier))
== Qt::NoModifier) {
const Qt::Key backKey = QGuiApplication::isLeftToRight() ? Qt::Key_Left : Qt::Key_Right;
if (ke->key() == backKey) {
if (m_step > 0)
setStep(m_step - 1);
} else {
step();
}
}
}
void IntroductionWidget::mouseReleaseEvent(QMouseEvent *me)
{
me->accept();
step();
}
void IntroductionWidget::finish()
{
Core::ModeManager::setModeStyle(m_previousModeStyle);
hide();
deleteLater();
}
void IntroductionWidget::step()
{
if (m_step >= m_items.size() - 1)
finish();
else
setStep(m_step + 1);
}
void IntroductionWidget::setStep(uint index)
{
QTC_ASSERT(index < m_items.size(), return);
m_step = index;
m_continueLabel->setText(Tr::tr("UI Introduction %1/%2 >").arg(m_step + 1).arg(m_items.size()));
const Item &item = m_items.at(m_step);
m_stepText->setText("" + "" + item.title
+ "
" + item.brief + "
" + item.description + "");
const QString anchorObjectName = m_items.at(m_step).pointerAnchorObjectName;
if (!anchorObjectName.isEmpty()) {
m_stepPointerAnchor = parentWidget()->findChild(anchorObjectName);
QTC_CHECK(m_stepPointerAnchor);
} else {
m_stepPointerAnchor.clear();
}
update();
}
void IntroductionWidget::resizeToParent()
{
QTC_ASSERT(parentWidget(), return);
setGeometry(QRect(QPoint(0, 0), parentWidget()->size()));
m_textWidget->setGeometry(QRect(width()/4, height()/4, width()/2, height()/2));
}
// Public access.
void runUiTour()
{
auto intro = new IntroductionWidget(Core::ModeManager::modeStyle());
intro->show();
}
void askUserAboutIntroduction()
{
InfoBar *infoBar = ICore::infoBar();
// CheckableMessageBox for compatibility with Qt Creator < 4.11
if (!CheckableDecider(Key(kTakeTourSetting)).shouldAskAgain()
|| !infoBar->canInfoBeAdded(kTakeTourSetting))
return;
InfoBarEntry
info(kTakeTourSetting,
Tr::tr("See where the important UI elements are and how they are used. "
"To take the tour later, select Help > UI Tour."),
InfoBarEntry::GlobalSuppression::Enabled);
info.setTitle(Tr::tr("Take a UI Tour?"));
info.setInfoType(InfoLabel::Information);
info.addCustomButton(
Tr::tr("Take UI Tour"),
[] { runUiTour(); },
{},
InfoBarEntry::ButtonAction::SuppressPersistently);
infoBar->addInfo(info);
}
} // Welcome::Internal