diff options
author | Eike Ziller <eike.ziller@qt.io> | 2018-12-10 17:10:17 +0100 |
---|---|---|
committer | Eike Ziller <eike.ziller@qt.io> | 2018-12-18 09:53:36 +0000 |
commit | 8770d83fc8bd71691e7cbb5114a1c0b4b8cb4ed8 (patch) | |
tree | fee2c821d811b903581761a10dd506d046839bbd /src/plugins/welcome | |
parent | 5330502978094074ad1039b19f7a579ae4852871 (diff) |
Add general UI introduction
Adds a Help > UI Introduction, which highlights basic aspects of the Qt
Creator UI.
The pages to show are defined in a format that can later move into a
text based configuration file like JSON. It specifies an optional object
name of a widget to highlight, a title, a brief description and a longer
description (potentially with tables, lists and other simple HTML).
Fixes: QTCREATORBUG-21585
Change-Id: Idb64c87e1d752bc24437588278093a96be0eeddb
Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
Reviewed-by: Tobias Hunger <tobias.hunger@qt.io>
Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io>
Diffstat (limited to 'src/plugins/welcome')
-rw-r--r-- | src/plugins/welcome/images/border.png | bin | 0 -> 852 bytes | |||
-rw-r--r-- | src/plugins/welcome/introductionwidget.cpp | 399 | ||||
-rw-r--r-- | src/plugins/welcome/introductionwidget.h | 82 | ||||
-rw-r--r-- | src/plugins/welcome/welcome.pro | 6 | ||||
-rw-r--r-- | src/plugins/welcome/welcome.qrc | 1 | ||||
-rw-r--r-- | src/plugins/welcome/welcomeplugin.cpp | 15 |
6 files changed, 502 insertions, 1 deletions
diff --git a/src/plugins/welcome/images/border.png b/src/plugins/welcome/images/border.png Binary files differnew file mode 100644 index 00000000000..f19494d9d21 --- /dev/null +++ b/src/plugins/welcome/images/border.png diff --git a/src/plugins/welcome/introductionwidget.cpp b/src/plugins/welcome/introductionwidget.cpp new file mode 100644 index 00000000000..407f69dfab4 --- /dev/null +++ b/src/plugins/welcome/introductionwidget.cpp @@ -0,0 +1,399 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "introductionwidget.h" + +#include <utils/algorithm.h> +#include <utils/checkablemessagebox.h> +#include <utils/qtcassert.h> +#include <utils/stylehelper.h> + +#include <QEvent> +#include <QKeyEvent> +#include <QLabel> +#include <QPainter> +#include <QPushButton> +#include <QVBoxLayout> + +using namespace Utils; + +const char kTakeTourSetting[] = "TakeUITour"; + +namespace Welcome { +namespace Internal { + +void IntroductionWidget::askUserAboutIntroduction(QWidget *parent, QSettings *settings) +{ + if (!CheckableMessageBox::shouldAskAgain(settings, kTakeTourSetting)) + return; + auto messageBox = new CheckableMessageBox(parent); + messageBox->setWindowTitle(tr("Take a UI Tour")); + messageBox->setIconPixmap(QMessageBox::standardIcon(QMessageBox::Question)); + messageBox->setText( + tr("Do you want to take a quick UI tour? This shows where the most important user " + "interface elements are, and how they are used, and will only take a minute. You can " + "also take the tour later by selecting Help > UI Tour.")); + messageBox->setCheckBoxVisible(true); + messageBox->setCheckBoxText(CheckableMessageBox::msgDoNotAskAgain()); + messageBox->setChecked(true); + messageBox->setStandardButtons(QDialogButtonBox::Cancel); + QPushButton *tourButton = messageBox->addButton(tr("Take UI Tour"), QDialogButtonBox::AcceptRole); + connect(messageBox, &QDialog::finished, parent, [parent, settings, messageBox, tourButton]() { + if (messageBox->isChecked()) + CheckableMessageBox::doNotAskAgain(settings, kTakeTourSetting); + if (messageBox->clickedButton() == tourButton) { + auto intro = new IntroductionWidget(parent); + intro->show(); + } + messageBox->deleteLater(); + }); + messageBox->show(); +} + +IntroductionWidget::IntroductionWidget(QWidget *parent) + : QWidget(parent), + m_borderImage(std::make_unique<QImage>(":/welcome/images/border.png")) +{ + setFocusPolicy(Qt::StrongFocus); + setFocus(); + parent->installEventFilter(this); + + QPalette p = palette(); + p.setColor(QPalette::Foreground, 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()); + m_stepText->setOpenExternalLinks(true); + m_stepText->installEventFilter(this); + 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); + auto fnt = font(); + fnt.setPointSizeF(fnt.pointSizeF() * 1.5); + m_continueLabel->setFont(fnt); + m_continueLabel->setPalette(palette()); + layout->addWidget(m_continueLabel); + m_bodyCss = "font-size: 16px;"; + m_items = { + {"ModeSelector", + tr("Mode Selector"), + tr("Select different modes depending on the task at hand."), + tr("<p style=\"margin-top: 30px\"><table>" + "<tr><td style=\"padding-right: 20px\">Welcome:</td><td>Open examples, tutorials, and " + "recent sessions and projects.</td></tr>" + "<tr><td>Edit:</td><td>Work with code and navigate your project.</td></tr>" + "<tr><td>Design:</td><td>Work with UI designs for Qt Widgets or Qt Quick.</td></tr>" + "<tr><td>Debug:</td><td>Analyze your application with a debugger or other " + "analyzer.</td></tr>" + "<tr><td>Projects:</td><td>Manage project settings.</td></tr>" + "<tr><td>Help:</td><td>Browse the help database.</td></tr>" + "</table></p>")}, + {"KitSelector.Button", + tr("Kit Selector"), + tr("Select the active project or project configuration."), + ""}, + {"Run.Button", + tr("Run Button"), + tr("Run the active project. By default this builds the project first."), + ""}, + {"Debug.Button", tr("Debug Button"), tr("Run the active project in a debugger."), ""}, + {"Build.Button", tr("Build Button"), tr("Build the active project."), ""}, + {"LocatorInput", + tr("Locator"), + tr("Type here to open a file from any open project."), + tr("Or:" + "<ul>" + "<li>type <code>c<space><pattern></code> to jump to a class definition</li>" + "<li>type <code>f<space><pattern></code> to open a file from the file " + "system</li>" + "<li>click on the magnifier icon for a complete list of possible options</li>" + "</ul>")}, + {"OutputPaneButtons", + tr("Output Panes"), + tr("Find compile and application output here, " + "as well as a list of configuration and build issues, " + "and the panel for global searches."), + ""}, + {"ProgressInfo", + tr("Progress Indicator"), + tr("Progress information about running tasks is shown here."), + ""}, + {{}, + tr("Escape to Editor"), + tr("Pressing the Escape key brings you back to the editor. Press it " + "multiple times to also hide output panes and context help, giving the editor more " + "space."), + ""}, + {{}, + tr("The End"), + tr("You have now completed the UI tour. To learn more about the highlighted " + "controls, see <a style=\"color: #41CD52\" " + "href=\"qthelp://org.qt-project.qtcreator/doc/creator-quick-tour.html\">User " + "Interface</a>."), + ""}}; + 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(); + else if (obj == m_stepText && ev->type() == QEvent::MouseButtonRelease) + step(); + 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<QPolygonF> 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<Qt::Alignment> 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<QPointF>{{startX, middleY}, {endX, middleY}}}, + QVector<QPointF>{{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<QPointF>{{middleX, startY}, {middleX, endY}}}, + QVector<QPointF>{{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<QPointF>{{middleX, startY}, {middleX, endY}}}, + QVector<QPointF>{{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<QPointF>{{startX, middleY}, {endX, middleY}}}, + QVector<QPointF>{{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(), {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::NoModifier) + step(); +} + +void IntroductionWidget::mouseReleaseEvent(QMouseEvent *me) +{ + me->accept(); + step(); +} + +void IntroductionWidget::finish() +{ + 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("UI Introduction %1/%2 >").arg(m_step + 1).arg(m_items.size())); + const Item &item = m_items.at(m_step); + m_stepText->setText("<html><body style=\"" + m_bodyCss + "\">" + "<h1>" + item.title + + "</h1><p>" + item.brief + "</p>" + item.description + "</body></html>"); + const QString anchorObjectName = m_items.at(m_step).pointerAnchorObjectName; + if (!anchorObjectName.isEmpty()) { + m_stepPointerAnchor = parentWidget()->findChild<QWidget *>(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)); +} + +} // namespace Internal +} // namespace Welcome diff --git a/src/plugins/welcome/introductionwidget.h b/src/plugins/welcome/introductionwidget.h new file mode 100644 index 00000000000..69212e77623 --- /dev/null +++ b/src/plugins/welcome/introductionwidget.h @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QImage> +#include <QPointer> +#include <QWidget> + +#include <memory> + +QT_BEGIN_NAMESPACE +class QLabel; +class QSettings; +QT_END_NAMESPACE + +namespace Welcome { +namespace Internal { + +struct Item +{ + QString pointerAnchorObjectName; + QString title; + QString brief; + QString description; +}; + +class IntroductionWidget : public QWidget +{ + Q_OBJECT +public: + explicit IntroductionWidget(QWidget *parent = nullptr); + + static void askUserAboutIntroduction(QWidget *parent, QSettings *settings); + +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; + std::unique_ptr<QImage> m_borderImage; + QString m_bodyCss; + std::vector<Item> m_items; + QPointer<QWidget> m_stepPointerAnchor; + uint m_step = 0; +}; + +} // namespace Internal +} // namespace Welcome diff --git a/src/plugins/welcome/welcome.pro b/src/plugins/welcome/welcome.pro index 612739a692f..7123b4abb6c 100644 --- a/src/plugins/welcome/welcome.pro +++ b/src/plugins/welcome/welcome.pro @@ -1,8 +1,12 @@ include(../../qtcreatorplugin.pri) -SOURCES += welcomeplugin.cpp +SOURCES += welcomeplugin.cpp \ + introductionwidget.cpp DEFINES += WELCOME_LIBRARY RESOURCES += welcome.qrc + +HEADERS += \ + introductionwidget.h diff --git a/src/plugins/welcome/welcome.qrc b/src/plugins/welcome/welcome.qrc index 28e8d22d1c8..d5b825d7e16 100644 --- a/src/plugins/welcome/welcome.qrc +++ b/src/plugins/welcome/welcome.qrc @@ -22,5 +22,6 @@ <file>images/new@2x.png</file> <file>images/expandarrow.png</file> <file>images/expandarrow@2x.png</file> + <file>images/border.png</file> </qresource> </RCC> diff --git a/src/plugins/welcome/welcomeplugin.cpp b/src/plugins/welcome/welcomeplugin.cpp index aa804bbe6d8..b70639fb4e2 100644 --- a/src/plugins/welcome/welcomeplugin.cpp +++ b/src/plugins/welcome/welcomeplugin.cpp @@ -23,11 +23,14 @@ ** ****************************************************************************/ +#include "introductionwidget.h" + #include <extensionsystem/iplugin.h> #include <extensionsystem/pluginmanager.h> #include <app/app_version.h> +#include <coreplugin/actionmanager/actioncontainer.h> #include <coreplugin/actionmanager/actionmanager.h> #include <coreplugin/actionmanager/command.h> #include <coreplugin/coreconstants.h> @@ -139,6 +142,18 @@ public: { m_welcomeMode->initPlugins(); ModeManager::activateMode(m_welcomeMode->id()); + auto introAction = new QAction(tr("UI Tour"), this); + connect(introAction, &QAction::triggered, this, []() { + auto intro = new IntroductionWidget(ICore::mainWindow()); + intro->show(); + }); + Command *cmd = ActionManager::registerAction(introAction, "Welcome.UITour"); + ActionContainer *mhelp = ActionManager::actionContainer(Core::Constants::M_HELP); + if (QTC_GUARD(mhelp)) + mhelp->addAction(cmd, Core::Constants::G_HELP_HELP); + connect(ICore::instance(), &ICore::coreOpened, this, []() { + IntroductionWidget::askUserAboutIntroduction(ICore::mainWindow(), ICore::settings()); + }, Qt::QueuedConnection); } WelcomeMode *m_welcomeMode = nullptr; |