diff options
Diffstat (limited to 'src/gui/newwizard')
26 files changed, 1784 insertions, 0 deletions
diff --git a/src/gui/newwizard/CMakeLists.txt b/src/gui/newwizard/CMakeLists.txt new file mode 100644 index 000000000..28e188870 --- /dev/null +++ b/src/gui/newwizard/CMakeLists.txt @@ -0,0 +1,49 @@ +add_library(newwizard OBJECT + pages/abstractsetupwizardpage.h + pages/abstractsetupwizardpage.cpp + + pages/serverurlsetupwizardpage.ui + pages/serverurlsetupwizardpage.h + pages/serverurlsetupwizardpage.cpp + + pages/basiccredentialssetupwizardpage.ui + pages/basiccredentialssetupwizardpage.h + pages/basiccredentialssetupwizardpage.cpp + + pages/accountconfiguredwizardpage.ui + pages/accountconfiguredwizardpage.h + pages/accountconfiguredwizardpage.cpp + + pages/oauthcredentialssetupwizardpage.ui + pages/oauthcredentialssetupwizardpage.h + pages/oauthcredentialssetupwizardpage.cpp + + setupwizardwindow.ui + setupwizardwindow.h + setupwizardwindow.cpp + + setupwizardcontroller.h + setupwizardcontroller.cpp + + setupwizardaccountbuilder.cpp + setupwizardaccountbuilder.h + + pagination.cpp + pagination.h + + jobs/resolveurljobfactory.cpp + jobs/resolveurljobfactory.h +) +target_link_libraries(newwizard PUBLIC Qt5::Widgets libsync) +set_target_properties(newwizard PROPERTIES AUTOUIC ON AUTORCC ON) +target_include_directories(newwizard PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") +target_compile_definitions(newwizard + PRIVATE QT_NO_CAST_TO_ASCII + QT_NO_CAST_FROM_ASCII + QT_NO_URL_CAST_FROM_STRING + QT_NO_CAST_FROM_BYTEARRAY + QT_USE_QSTRINGBUILDER + QT_MESSAGELOGCONTEXT # enable function name and line number in debug output + QT_DEPRECATED_WARNINGS + QT_NO_FOREACH +) diff --git a/src/gui/newwizard/jobs/resolveurljobfactory.cpp b/src/gui/newwizard/jobs/resolveurljobfactory.cpp new file mode 100644 index 000000000..f77a3a29b --- /dev/null +++ b/src/gui/newwizard/jobs/resolveurljobfactory.cpp @@ -0,0 +1,71 @@ +#include "resolveurljobfactory.h" +#include "common/utility.h" +#include "gui/updateurldialog.h" +#include <QNetworkReply> + +namespace { +Q_LOGGING_CATEGORY(lcResolveUrl, "wizard.resolveurl") +} + +namespace OCC::Wizard::Jobs { + +ResolveUrlJobFactory::ResolveUrlJobFactory(QNetworkAccessManager *nam, QObject *parent) + : AbstractCoreJobFactory(nam, parent) +{ +} + +CoreJob *ResolveUrlJobFactory::startJob(const QUrl &url) +{ + auto *job = new CoreJob; + + QNetworkRequest req(Utility::concatUrlPath(url, QStringLiteral("status.php"))); + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true); + + auto *reply = nam()->get(req); + + connect(reply, &QNetworkReply::finished, job, [oldUrl = url, reply, job] { + reply->deleteLater(); + + const auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (reply->error() != QNetworkReply::NoError) { + setJobError(job, tr("Failed to resolve the url %1, error: %2").arg(oldUrl.toDisplayString(), reply->errorString()), reply->error()); + qCWarning(lcResolveUrl) << job->errorMessage(); + } else { + const auto newUrl = reply->url().adjusted(QUrl::RemoveFilename); + + if (newUrl != oldUrl) { + qCInfo(lcResolveUrl) << oldUrl << "was redirected to" << newUrl; + + if (newUrl.scheme() == QLatin1String("https") && oldUrl.host() == newUrl.host()) { + qCInfo(lcResolveUrl()) << "redirect accepted automatically"; + setJobResult(job, newUrl); + } else { + auto *dialog = new UpdateUrlDialog( + QStringLiteral("Confirm new URL"), + QStringLiteral( + "While accessing the server, we were redirected from %1 to another URL: %2\n\n" + "Do you wish to permanently use the new URL?") + .arg(oldUrl.toString(), newUrl.toString()), + oldUrl, + newUrl, + nullptr); + + connect(dialog, &UpdateUrlDialog::accepted, job, [=]() { + setJobResult(job, newUrl); + }); + + connect(dialog, &UpdateUrlDialog::rejected, job, [=]() { + setJobError(job, tr("User rejected redirect from %1 to %2").arg(oldUrl.toDisplayString(), newUrl.toDisplayString()), QNetworkReply::InsecureRedirectError); + }); + + dialog->show(); + } + } + } + }); + + return job; +} + +} diff --git a/src/gui/newwizard/jobs/resolveurljobfactory.h b/src/gui/newwizard/jobs/resolveurljobfactory.h new file mode 100644 index 000000000..1b25c60ec --- /dev/null +++ b/src/gui/newwizard/jobs/resolveurljobfactory.h @@ -0,0 +1,15 @@ +#pragma once + +#include "abstractcorejob.h" + +namespace OCC::Wizard::Jobs { + +class ResolveUrlJobFactory : public AbstractCoreJobFactory +{ +public: + explicit ResolveUrlJobFactory(QNetworkAccessManager *nam, QObject *parent = nullptr); + + CoreJob *startJob(const QUrl &url) override; +}; + +} diff --git a/src/gui/newwizard/pages/abstractsetupwizardpage.cpp b/src/gui/newwizard/pages/abstractsetupwizardpage.cpp new file mode 100644 index 000000000..295d400d7 --- /dev/null +++ b/src/gui/newwizard/pages/abstractsetupwizardpage.cpp @@ -0,0 +1,7 @@ +#include "abstractsetupwizardpage.h" + +namespace OCC::Wizard { + +AbstractSetupWizardPage::~AbstractSetupWizardPage() = default; + +} diff --git a/src/gui/newwizard/pages/abstractsetupwizardpage.h b/src/gui/newwizard/pages/abstractsetupwizardpage.h new file mode 100644 index 000000000..58c65cf0b --- /dev/null +++ b/src/gui/newwizard/pages/abstractsetupwizardpage.h @@ -0,0 +1,22 @@ +#pragma once + +#include <QWidget> + +namespace OCC::Wizard { + +class AbstractSetupWizardPage : public QWidget +{ + Q_OBJECT + +public: + ~AbstractSetupWizardPage() override; + +Q_SIGNALS: + /** + * Emitted after a page has been displayed within the wizard. + * Can be used to, e.g., set the focus on widgets in order to make navigation with the keyboard easier. + */ + void pageDisplayed(); +}; + +} diff --git a/src/gui/newwizard/pages/accountconfiguredwizardpage.cpp b/src/gui/newwizard/pages/accountconfiguredwizardpage.cpp new file mode 100644 index 000000000..f0bf2077c --- /dev/null +++ b/src/gui/newwizard/pages/accountconfiguredwizardpage.cpp @@ -0,0 +1,20 @@ +#include "accountconfiguredwizardpage.h" +#include <QMessageBox> + +#include "gui/selectivesyncdialog.h" +#include "theme.h" +#include "ui_accountconfiguredwizardpage.h" + +namespace OCC::Wizard { + +AccountConfiguredWizardPage::AccountConfiguredWizardPage() + : _ui(new ::Ui::AccountConfiguredWizardPage) +{ + _ui->setupUi(this); +} + +AccountConfiguredWizardPage::~AccountConfiguredWizardPage() noexcept +{ + delete _ui; +} +} diff --git a/src/gui/newwizard/pages/accountconfiguredwizardpage.h b/src/gui/newwizard/pages/accountconfiguredwizardpage.h new file mode 100644 index 000000000..b75c3e22c --- /dev/null +++ b/src/gui/newwizard/pages/accountconfiguredwizardpage.h @@ -0,0 +1,23 @@ +#pragma once + +#include "abstractsetupwizardpage.h" + +namespace Ui { +class AccountConfiguredWizardPage; +} + +namespace OCC::Wizard { + +class AccountConfiguredWizardPage : public AbstractSetupWizardPage +{ + Q_OBJECT + +public: + AccountConfiguredWizardPage(); + ~AccountConfiguredWizardPage() noexcept override; + +private: + ::Ui::AccountConfiguredWizardPage *_ui; +}; + +} diff --git a/src/gui/newwizard/pages/accountconfiguredwizardpage.ui b/src/gui/newwizard/pages/accountconfiguredwizardpage.ui new file mode 100644 index 000000000..f56a74d7c --- /dev/null +++ b/src/gui/newwizard/pages/accountconfiguredwizardpage.ui @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AccountConfiguredWizardPage</class> + <widget class="QWidget" name="AccountConfiguredWizardPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>732</width> + <height>434</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>✓ You're all set!</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/gui/newwizard/pages/basiccredentialssetupwizardpage.cpp b/src/gui/newwizard/pages/basiccredentialssetupwizardpage.cpp new file mode 100644 index 000000000..9434a92da --- /dev/null +++ b/src/gui/newwizard/pages/basiccredentialssetupwizardpage.cpp @@ -0,0 +1,34 @@ +#include "basiccredentialssetupwizardpage.h" + +#include "theme.h" +#include "ui_basiccredentialssetupwizardpage.h" + +namespace OCC::Wizard { + +BasicCredentialsSetupWizardPage::BasicCredentialsSetupWizardPage(const QUrl &serverUrl) + : _ui(new ::Ui::BasicCredentialsSetupWizardPage) +{ + _ui->setupUi(this); + + _ui->urlLabel->setText(serverUrl.toString()); + + connect(this, &AbstractSetupWizardPage::pageDisplayed, this, [this]() { + _ui->usernameLineEdit->setFocus(); + }); +} + +QString BasicCredentialsSetupWizardPage::username() const +{ + return _ui->usernameLineEdit->text(); +} + +QString BasicCredentialsSetupWizardPage::password() const +{ + return _ui->passwordLineEdit->text(); +} + +BasicCredentialsSetupWizardPage::~BasicCredentialsSetupWizardPage() +{ + delete _ui; +} +} diff --git a/src/gui/newwizard/pages/basiccredentialssetupwizardpage.h b/src/gui/newwizard/pages/basiccredentialssetupwizardpage.h new file mode 100644 index 000000000..44cb7ff07 --- /dev/null +++ b/src/gui/newwizard/pages/basiccredentialssetupwizardpage.h @@ -0,0 +1,26 @@ +#pragma once + +#include "abstractsetupwizardpage.h" + +namespace Ui { +class BasicCredentialsSetupWizardPage; +} + +namespace OCC::Wizard { + +class BasicCredentialsSetupWizardPage : public AbstractSetupWizardPage +{ + Q_OBJECT + +public: + BasicCredentialsSetupWizardPage(const QUrl &serverUrl); + ~BasicCredentialsSetupWizardPage() noexcept override; + + QString username() const; + QString password() const; + +private: + ::Ui::BasicCredentialsSetupWizardPage *_ui; +}; + +} diff --git a/src/gui/newwizard/pages/basiccredentialssetupwizardpage.ui b/src/gui/newwizard/pages/basiccredentialssetupwizardpage.ui new file mode 100644 index 000000000..0cd46c6cf --- /dev/null +++ b/src/gui/newwizard/pages/basiccredentialssetupwizardpage.ui @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BasicCredentialsSetupWizardPage</class> + <widget class="QWidget" name="BasicCredentialsSetupWizardPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>293</width> + <height>258</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Enter your credentials</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="urlLabel"> + <property name="text"> + <string notr="true"><server URL> (placeholder)</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QLineEdit" name="usernameLineEdit"> + <property name="placeholderText"> + <string>Username</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="passwordLineEdit"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Password</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.cpp b/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.cpp new file mode 100644 index 000000000..47ed09945 --- /dev/null +++ b/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.cpp @@ -0,0 +1,34 @@ +#include "oauthcredentialssetupwizardpage.h" + +#include "theme.h" +#include "ui_oauthcredentialssetupwizardpage.h" + +namespace OCC::Wizard { + +OAuthCredentialsSetupWizardPage::OAuthCredentialsSetupWizardPage(const QUrl &serverUrl) + : _ui(new ::Ui::OAuthCredentialsSetupWizardPage) +{ + _ui->setupUi(this); + + _ui->urlLabel->setText(serverUrl.toString()); + + connect(_ui->reopenBrowserButton, &QPushButton::pressed, this, [this]() { + Q_EMIT reopenBrowserButtonPushed(); + }); + + connect(this, &AbstractSetupWizardPage::pageDisplayed, this, [this]() { + _ui->reopenBrowserButton->setFocus(); + }); +} + +void OAuthCredentialsSetupWizardPage::disableReopenBrowserButton() +{ + _ui->reopenBrowserButton->setEnabled(false); +} + +OAuthCredentialsSetupWizardPage::~OAuthCredentialsSetupWizardPage() +{ + delete _ui; +} + +} // namespace OCC::Wizard diff --git a/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.h b/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.h new file mode 100644 index 000000000..d3ecdda7f --- /dev/null +++ b/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.h @@ -0,0 +1,26 @@ +#pragma once +#include "abstractsetupwizardpage.h" + +namespace Ui { +class OAuthCredentialsSetupWizardPage; +} + +namespace OCC::Wizard { + +class OAuthCredentialsSetupWizardPage : public AbstractSetupWizardPage +{ + Q_OBJECT + +public: + explicit OAuthCredentialsSetupWizardPage(const QUrl &serverUrl); + void disableReopenBrowserButton(); + ~OAuthCredentialsSetupWizardPage() noexcept override; + +Q_SIGNALS: + void reopenBrowserButtonPushed(); + +private: + ::Ui::OAuthCredentialsSetupWizardPage *_ui; +}; + +} // namespace OCC::Wizard diff --git a/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.ui b/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.ui new file mode 100644 index 000000000..6093c7a66 --- /dev/null +++ b/src/gui/newwizard/pages/oauthcredentialssetupwizardpage.ui @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OAuthCredentialsSetupWizardPage</class> + <widget class="QWidget" name="OAuthCredentialsSetupWizardPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>375</width> + <height>252</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Please log in in the opened browser tab</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="urlLabel"> + <property name="text"> + <string notr="true"><server URL> (placeholder)</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="reopenBrowserButton"> + <property name="text"> + <string>Reopen Browser Tab</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/gui/newwizard/pages/serverurlsetupwizardpage.cpp b/src/gui/newwizard/pages/serverurlsetupwizardpage.cpp new file mode 100644 index 000000000..cb766ce41 --- /dev/null +++ b/src/gui/newwizard/pages/serverurlsetupwizardpage.cpp @@ -0,0 +1,35 @@ +#include "serverurlsetupwizardpage.h" + +#include "theme.h" +#include "ui_serverurlsetupwizardpage.h" + +namespace OCC::Wizard { + +ServerUrlSetupWizardPage::ServerUrlSetupWizardPage(const QUrl &serverUrl) + : _ui(new ::Ui::ServerUrlSetupWizardPage) +{ + _ui->setupUi(this); + + _ui->welcomeTextLabel->setText(tr("Welcome to %1").arg(Theme::instance()->appNameGUI())); + + _ui->urlLineEdit->setText(serverUrl.toString()); + + // first, we declare this object as an event filter + // this then allows us to make the controller the event filter of the current page + _ui->urlLineEdit->installEventFilter(this); + + connect(this, &AbstractSetupWizardPage::pageDisplayed, this, [this]() { + _ui->urlLineEdit->setFocus(); + }); +} + +QUrl ServerUrlSetupWizardPage::serverUrl() const +{ + return QUrl::fromUserInput(_ui->urlLineEdit->text()); +} + +ServerUrlSetupWizardPage::~ServerUrlSetupWizardPage() +{ + delete _ui; +} +} diff --git a/src/gui/newwizard/pages/serverurlsetupwizardpage.h b/src/gui/newwizard/pages/serverurlsetupwizardpage.h new file mode 100644 index 000000000..b94de8154 --- /dev/null +++ b/src/gui/newwizard/pages/serverurlsetupwizardpage.h @@ -0,0 +1,25 @@ +#pragma once + +#include "abstractsetupwizardpage.h" + +namespace Ui { +class ServerUrlSetupWizardPage; +} + +namespace OCC::Wizard { +class ServerUrlSetupWizardPage : public AbstractSetupWizardPage +{ + Q_OBJECT + +public: + ServerUrlSetupWizardPage(const QUrl &serverUrl); + + QUrl serverUrl() const; + +private: + ::Ui::ServerUrlSetupWizardPage *_ui; + +public: + ~ServerUrlSetupWizardPage() noexcept override; +}; +} diff --git a/src/gui/newwizard/pages/serverurlsetupwizardpage.ui b/src/gui/newwizard/pages/serverurlsetupwizardpage.ui new file mode 100644 index 000000000..3d7bb7f1b --- /dev/null +++ b/src/gui/newwizard/pages/serverurlsetupwizardpage.ui @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ServerUrlSetupWizardPage</class> + <widget class="QWidget" name="ServerUrlSetupWizardPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>448</width> + <height>285</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="welcomeTextLabel"> + <property name="text"> + <string notr="true">Welcome to <app> (placeholder)</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>What is your server's address?</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLineEdit" name="urlLineEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>400</width> + <height>0</height> + </size> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="urlIconLabel"> + <property name="minimumSize"> + <size> + <width>22</width> + <height>22</height> + </size> + </property> + <property name="text"> + <string notr="true"/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/gui/newwizard/pagination.cpp b/src/gui/newwizard/pagination.cpp new file mode 100644 index 000000000..71a1160eb --- /dev/null +++ b/src/gui/newwizard/pagination.cpp @@ -0,0 +1,92 @@ + +#include <QDebug> +#include <QRadioButton> + +#include "pagination.h" + +namespace OCC::Wizard { + +Pagination::Pagination(QWidget *parent) + : QWidget(parent) + , _activePageIndex(0) + , _enabled(true) +{ + // this class manages its own layout + setLayout(new QHBoxLayout(this)); +} + +void Pagination::setEntries(const QStringList &newEntries) +{ + // TODO: more advanced implementation (reuse existing buttons within layout) + // current active page is also lost that way + removeAllItems(); + + for (PageIndex i = 0; i < newEntries.count(); ++i) { + auto entry = newEntries[i]; + + auto newButton = new QRadioButton(entry, this); + + _entries.append(newButton); + layout()->addWidget(newButton); + + connect(newButton, &QRadioButton::clicked, this, [this, i]() { + emit paginationEntryClicked(i); + }); + } + + enableOrDisableButtons(); +} + +// needed to clean up widgets we added to the layout +Pagination::~Pagination() noexcept +{ + removeAllItems(); +} + +void Pagination::removeAllItems() +{ + qDeleteAll(_entries); +} + +PageIndex Pagination::entriesCount() const +{ + return _entries.size(); +} + +void Pagination::enableOrDisableButtons() +{ + for (PageIndex i = 0; i < _entries.count(); ++i) { + const auto enabled = [=]() { + if (_enabled) { + return i < _activePageIndex; + } + + return false; + }(); + + // TODO: use custom QRadioButton which doesn't need to be disabled to not be clickable + // can only jump to pages we have visited before + // to avoid resetting the current page, we don't want to enable the active page either + _entries.at(i)->setEnabled(enabled); + } +} + +void Pagination::setActivePageIndex(PageIndex activePageIndex) +{ + _activePageIndex = activePageIndex; + + for (PageIndex i = 0; i < _entries.count(); ++i) { + // we don't want to store those buttons in this object's state + auto button = qobject_cast<QRadioButton *>(layout()->itemAt(i)->widget()); + button->setChecked(i == activePageIndex); + } + + enableOrDisableButtons(); +} + +PageIndex Pagination::activePageIndex() +{ + return _activePageIndex; +} + +} diff --git a/src/gui/newwizard/pagination.h b/src/gui/newwizard/pagination.h new file mode 100644 index 000000000..5c30e9adc --- /dev/null +++ b/src/gui/newwizard/pagination.h @@ -0,0 +1,45 @@ +#pragma once + +#include <QHBoxLayout> +#include <QRadioButton> +#include <QString> +#include <QWidget> + +namespace OCC::Wizard { + +using PageIndex = QStringList::size_type; + +/** + * Renders pagination entries as radio buttons in a horizontal layout. + */ +class Pagination : public QWidget +{ + Q_OBJECT + +public: + explicit Pagination(QWidget *parent = nullptr); + + ~Pagination() noexcept override; + + void setEntries(const QStringList &newEntries); + + [[nodiscard]] PageIndex entriesCount() const; + + PageIndex activePageIndex(); + +Q_SIGNALS: + void paginationEntryClicked(PageIndex clickedPageIndex); + +public Q_SLOTS: + void setActivePageIndex(PageIndex activePageIndex); + +private: + void removeAllItems(); + void enableOrDisableButtons(); + + QList<QRadioButton *> _entries; + PageIndex _activePageIndex; + bool _enabled; +}; + +} diff --git a/src/gui/newwizard/setupwizardaccountbuilder.cpp b/src/gui/newwizard/setupwizardaccountbuilder.cpp new file mode 100644 index 000000000..6ec6a66f1 --- /dev/null +++ b/src/gui/newwizard/setupwizardaccountbuilder.cpp @@ -0,0 +1,104 @@ + +#include "setupwizardaccountbuilder.h" +#include "gui/accountmanager.h" + +namespace OCC::Wizard { + +AbstractAuthenticationStrategy::~AbstractAuthenticationStrategy() {}; + + +HttpBasicAuthenticationStrategy::HttpBasicAuthenticationStrategy(const QString &username, const QString &password) + : _username(username) + , _password(password) +{ +} + +HttpCredentialsGui *HttpBasicAuthenticationStrategy::makeCreds() +{ + return new HttpCredentialsGui(_username, _password); +} + +bool HttpBasicAuthenticationStrategy::isValid() +{ + return !_username.isEmpty() && !_password.isEmpty(); +} + +QString HttpBasicAuthenticationStrategy::davUser() +{ + return _username; +} + +OAuth2AuthenticationStrategy::OAuth2AuthenticationStrategy(const QString &davUser, const QString &token, const QString &refreshToken) + : _davUser(davUser) + , _token(token) + , _refreshToken(refreshToken) +{ +} + +HttpCredentialsGui *OAuth2AuthenticationStrategy::makeCreds() +{ + return new HttpCredentialsGui(_davUser, _token, _refreshToken); +} + +bool OAuth2AuthenticationStrategy::isValid() +{ + return !_davUser.isEmpty() && !_token.isEmpty() && !_refreshToken.isEmpty(); +} + +QString OAuth2AuthenticationStrategy::davUser() +{ + return _davUser; +} + +SetupWizardAccountBuilder::SetupWizardAccountBuilder() = default; + +void SetupWizardAccountBuilder::setServerUrl(const QUrl &serverUrl, DetermineAuthTypeJob::AuthType authType) +{ + _serverUrl = serverUrl; + _authType = authType; + + // to not keep credentials longer than necessary, we purge them whenever the URL is set + // for this reason, we also don't insert already-known credentials on the credentials pages when switching to them + _authenticationStrategy.reset(); +} + +QUrl SetupWizardAccountBuilder::serverUrl() const +{ + return _serverUrl; +} + +DetermineAuthTypeJob::AuthType SetupWizardAccountBuilder::authType() +{ + return _authType; +} + +AccountPtr SetupWizardAccountBuilder::build() +{ + auto newAccountPtr = Account::create(); + + Q_ASSERT(!_serverUrl.isEmpty() && _serverUrl.isValid()); + newAccountPtr->setUrl(_serverUrl); + + Q_ASSERT(hasValidCredentials()); + + // TODO: perhaps _authenticationStrategy->setUpAccountPtr(...) would be more elegant? no need for getters then + newAccountPtr->setDavUser(_authenticationStrategy->davUser()); + newAccountPtr->setCredentials(_authenticationStrategy->makeCreds()); + + return newAccountPtr; +} + +bool SetupWizardAccountBuilder::hasValidCredentials() const +{ + if (_authenticationStrategy == nullptr) { + return false; + } + + return _authenticationStrategy->isValid(); +} + +void SetupWizardAccountBuilder::setAuthenticationStrategy(AbstractAuthenticationStrategy *strategy) +{ + _authenticationStrategy.reset(strategy); +} +} diff --git a/src/gui/newwizard/setupwizardaccountbuilder.h b/src/gui/newwizard/setupwizardaccountbuilder.h new file mode 100644 index 000000000..d9198cf3f --- /dev/null +++ b/src/gui/newwizard/setupwizardaccountbuilder.h @@ -0,0 +1,113 @@ +#pragma once + +#include "gui/creds/httpcredentialsgui.h" +#include "networkjobs.h" +#include <account.h> + +namespace OCC::Wizard { + +/** + * The server can use varying authentication methods, for instance HTTP Basic or OAuth2. + * Depending on the concrete authentication method the server uses, the account's credentials must be initialized differently. + * We use the strategy pattern to be able to model multiple methods and allow adding new ones by just adding another strategy implementation. + */ +class AbstractAuthenticationStrategy +{ +public: + virtual ~AbstractAuthenticationStrategy(); + + /** + * Create credentials object for use in the account. + * @return credentials + */ + virtual HttpCredentialsGui *makeCreds() = 0; + + /** + * Checks whether the passed credentials are valid. + * @return true if valid, false otherwise + */ + virtual bool isValid() = 0; + + /** + * The username to use for WebDAV authentication along with the secret credentials. + * For some reason, the credentials object must be seeded with this username separately. + * @return username for use with WebDAV + */ + virtual QString davUser() = 0; +}; + +class HttpBasicAuthenticationStrategy : public AbstractAuthenticationStrategy +{ +public: + explicit HttpBasicAuthenticationStrategy(const QString &username, const QString &password); + + HttpCredentialsGui *makeCreds() override; + + bool isValid() override; + + QString davUser() override; + +private: + QString _username; + QString _password; +}; + +class OAuth2AuthenticationStrategy : public AbstractAuthenticationStrategy +{ +public: + explicit OAuth2AuthenticationStrategy(const QString &davUser, const QString &token, const QString &refreshToken); + + HttpCredentialsGui *makeCreds() override; + + bool isValid() override; + + QString davUser() override; + +private: + QString _davUser; + QString _token; + QString _refreshToken; +}; + +/** + * This class constructs an Account object from data entered by the user to the wizard resp. collected while checking the user's information. + * The class does not perform any kind of validation. It is the caller's job to make sure the data is correct. + */ +class SetupWizardAccountBuilder +{ +public: + SetupWizardAccountBuilder(); + + /** + * Set server URL. + * @param serverUrl URL to server + */ + void setServerUrl(const QUrl &serverUrl, DetermineAuthTypeJob::AuthType workflowType); + QUrl serverUrl() const; + + // TODO: move this out of the class's state + DetermineAuthTypeJob::AuthType authType(); + + void setAuthenticationStrategy(AbstractAuthenticationStrategy *strategy); + + /** + * Check whether credentials passed to the builder so far can be used to create a new account object. + * Note that this does not mean they are correct, the method only checks whether there is "enough" data. + * @return true if credentials are valid, false otherwise + */ + bool hasValidCredentials() const; + + /** + * Attempt to build an account from the previously entered information. + * @return built account or null if information is still missing + */ + AccountPtr build(); + +private: + QUrl _serverUrl; + + DetermineAuthTypeJob::AuthType _authType = DetermineAuthTypeJob::AuthType::Unknown; + + std::unique_ptr<AbstractAuthenticationStrategy> _authenticationStrategy; +}; +} diff --git a/src/gui/newwizard/setupwizardcontroller.cpp b/src/gui/newwizard/setupwizardcontroller.cpp new file mode 100644 index 000000000..db13bc2a7 --- /dev/null +++ b/src/gui/newwizard/setupwizardcontroller.cpp @@ -0,0 +1,252 @@ +#include "setupwizardcontroller.h" +#include "creds/oauth.h" +#include "determineauthtypejobfactory.h" +#include "jobs/resolveurljobfactory.h" +#include "pages/accountconfiguredwizardpage.h" +#include "pages/basiccredentialssetupwizardpage.h" +#include "pages/oauthcredentialssetupwizardpage.h" +#include "pages/serverurlsetupwizardpage.h" + +#include <QTimer> + +#include <chrono> + +using namespace std::chrono_literals; + +namespace OCC::Wizard { + +SetupWizardController::SetupWizardController(QWidget *parent) + : QObject(parent) + , _wizardWindow(new SetupWizardWindow(parent)) + , _networkAccessManager(new QNetworkAccessManager(this)) +{ + // initialize pagination + const QStringList paginationEntries = { tr("Server URL"), tr("Credentials"), tr("Sync Options") }; + _wizardWindow->setPaginationEntries(paginationEntries); + + nextStep(std::nullopt, std::nullopt); + + // allow settings dialog to clean up the wizard controller and all the objects it created + connect(_wizardWindow, &SetupWizardWindow::rejected, this, [this]() { + Q_EMIT finished(nullptr); + }); + + connect(_wizardWindow, &SetupWizardWindow::paginationEntryClicked, this, [this, paginationEntries](PageIndex currentPage, PageIndex clickedPageIndex) { + Q_ASSERT(currentPage < paginationEntries.size()); + + nextStep(currentPage, clickedPageIndex); + }); + connect(_wizardWindow, &SetupWizardWindow::nextButtonClicked, this, [this, paginationEntries](PageIndex currentPage) { + Q_ASSERT(currentPage < paginationEntries.size()); + + nextStep(currentPage, std::nullopt); + }); + + // in case the back button is clicked, the current page's data is dismissed, and the previous page should be shown + connect(_wizardWindow, &SetupWizardWindow::backButtonClicked, this, [this](PageIndex currentPage) { + // back button should be disabled on the first page + Q_ASSERT(currentPage > 0); + + nextStep(currentPage, currentPage - 1); + }); +} + +SetupWizardWindow *SetupWizardController::window() +{ + return _wizardWindow; +} + +void SetupWizardController::nextStep(std::optional<PageIndex> currentPage, std::optional<PageIndex> desiredPage) +{ + // should take care of cleaning up the page once the function has finished + QScopedPointer<AbstractSetupWizardPage> page(_currentPage); + + // initial state + if (!currentPage.has_value()) { + desiredPage = 0; + } + + // "next button" workflow + if (!desiredPage.has_value()) { + // try to fill in data appropriately + // if it works, go to next page + // otherwise, show current page again + if (currentPage == 0) { + const auto *pagePtr = qobject_cast<ServerUrlSetupWizardPage *>(_currentPage); + + auto serverUrl = pagePtr->serverUrl(); + + // fix scheme if necessary + // the second half is needed for URLs which contain a port (e.g., host:1234), QUrl then parses host: as scheme + if (serverUrl.scheme().isEmpty()) { + serverUrl = QUrl(QStringLiteral("https://") + serverUrl.toString()); + } + + // TODO: perform some better validation + if (serverUrl.isValid()) { + // (ab)using account builder as a temporary storage for the server URL + // below we will set both the resolved URL as well as the actual auth type + _accountBuilder.setServerUrl(serverUrl, DetermineAuthTypeJob::AuthType::Unknown); + desiredPage = currentPage.value() + 1; + } else { + _wizardWindow->showErrorMessage(QStringLiteral("Invalid server URL")); + desiredPage = currentPage.value(); + } + } + + if (currentPage == 1) { + if (_accountBuilder.authType() == DetermineAuthTypeJob::AuthType::Basic) { + const auto *pagePtr = qobject_cast<BasicCredentialsSetupWizardPage *>(_currentPage); + + const auto username = pagePtr->username(); + const auto password = pagePtr->password(); + + _accountBuilder.setAuthenticationStrategy(new HttpBasicAuthenticationStrategy(username, password)); + + // TODO: actually check whether creds are correct + if (_accountBuilder.hasValidCredentials()) { + desiredPage = currentPage.value() + 1; + } else { + _wizardWindow->showErrorMessage(QStringLiteral("Invalid credentials")); + desiredPage = currentPage.value(); + } + } + + if (_accountBuilder.authType() == DetermineAuthTypeJob::AuthType::OAuth) { + // authentication data is filled in asynchronously, hence all we have to do here is determine the next page + if (_accountBuilder.hasValidCredentials()) { + desiredPage = currentPage.value() + 1; + } else { + _wizardWindow->showErrorMessage(QStringLiteral("Invalid credentials")); + desiredPage = currentPage.value(); + } + } + } + + // final step + if (currentPage == 2) { + auto account = _accountBuilder.build(); + Q_ASSERT(account != nullptr); + emit finished(account); + return; + } + } + + auto showFirstPage = [this](const QString &error = QString()) { + if (!error.isEmpty()) { + _wizardWindow->showErrorMessage(error); + } + + _currentPage = new ServerUrlSetupWizardPage(_accountBuilder.serverUrl()); + _wizardWindow->displayPage(_currentPage, 0); + }; + + if (desiredPage == 0) { + showFirstPage(); + return; + } + + if (desiredPage == 1) { + // first, we must resolve the actual server URL + auto resolveJob = Jobs::ResolveUrlJobFactory(_networkAccessManager).startJob(_accountBuilder.serverUrl()); + + connect(resolveJob, &CoreJob::finished, this, [this, resolveJob, showFirstPage]() { + resolveJob->deleteLater(); + + if (!resolveJob->success()) { + // resolving failed, we need to show an error message + showFirstPage(resolveJob->errorMessage()); + return; + } + + const auto resolvedUrl = qvariant_cast<QUrl>(resolveJob->result()); + + // next, we need to find out which kind of authentication page we have to present to the user + auto authTypeJob = DetermineAuthTypeJobFactory(_networkAccessManager).startJob(resolvedUrl); + + connect(authTypeJob, &CoreJob::finished, authTypeJob, [this, authTypeJob, resolvedUrl]() { + authTypeJob->deleteLater(); + + _accountBuilder.setServerUrl(resolvedUrl, qvariant_cast<DetermineAuthTypeJob::AuthType>(authTypeJob->result())); + + switch (_accountBuilder.authType()) { + case DetermineAuthTypeJob::AuthType::Basic: { + _currentPage = new BasicCredentialsSetupWizardPage(_accountBuilder.serverUrl()); + _wizardWindow->displayPage(_currentPage, 1); + return; + } + + case DetermineAuthTypeJob::AuthType::OAuth: { + auto newPage = new OAuthCredentialsSetupWizardPage(_accountBuilder.serverUrl()); + + // username might not be set yet, shouldn't matter, though + auto oAuth = new OAuth(_accountBuilder.serverUrl(), QStringLiteral(), _networkAccessManager, {}, this); + + connect(oAuth, &OAuth::result, this, [this, newPage](OAuth::Result result, const QString &user, const QString &token, const QString &refreshToken) { + // the button may not be clicked any more, since the server has been shut down right before this signal was emitted by the OAuth instance + newPage->disableReopenBrowserButton(); + + _wizardWindow->slotStartTransition(); + + // bring window up top again, as the browser may have been raised in front of it + _wizardWindow->raise(); + + switch (result) { + case OAuth::Result::LoggedIn: { + _accountBuilder.setAuthenticationStrategy(new OAuth2AuthenticationStrategy(user, token, refreshToken)); + nextStep(1, std::nullopt); + break; + } + case OAuth::Result::Error: { + _wizardWindow->showErrorMessage(tr("Error while trying to log into OAuth2-enabled server.")); + nextStep(1, 0); + break; + } + case OAuth::Result::NotSupported: { + // should never happen + _wizardWindow->showErrorMessage(tr("Server reports that OAuth2 is not supported.")); + nextStep(1, 0); + break; + } + } + }); + + connect(newPage, &OAuthCredentialsSetupWizardPage::reopenBrowserButtonPushed, this, [oAuth]() { + oAuth->openBrowser(); + }); + + _currentPage = newPage; + _wizardWindow->displayPage(_currentPage, 1); + + // moving to next page is only possible once we see a request to our embedded web server + _wizardWindow->disableNextButton(); + + oAuth->startAuthentication(); + + return; + }; + + default: + Q_UNREACHABLE(); + } + }); + }); + + return; + } + + if (desiredPage == 2) { + _currentPage = new AccountConfiguredWizardPage; + _wizardWindow->displayPage(_currentPage, 2); + return; + } + + Q_UNREACHABLE(); +} + +SetupWizardController::~SetupWizardController() noexcept +{ + delete _wizardWindow; + delete _networkAccessManager; +} +} diff --git a/src/gui/newwizard/setupwizardcontroller.h b/src/gui/newwizard/setupwizardcontroller.h new file mode 100644 index 000000000..097150b78 --- /dev/null +++ b/src/gui/newwizard/setupwizardcontroller.h @@ -0,0 +1,49 @@ +#pragma once + +#include "pages/abstractsetupwizardpage.h" +#include <QDialog> +#include <account.h> +#include <optional> +#include <setupwizardaccountbuilder.h> +#include <setupwizardwindow.h> + +namespace OCC::Wizard { +/** + * This class is the backbone of the new setup wizard. It instantiates the required UI elements and fills them with the correct data. It also provides the public API for the settings UI. + * + * The new setup wizard uses dependency injection where applicable. The account object is created using the builder pattern. + */ +class SetupWizardController : public QObject +{ + Q_OBJECT + +public: + explicit SetupWizardController(QWidget *parent); + ~SetupWizardController() noexcept override; + + /** + * Provides access to the controller's setup wizard window. + * @return pointer to window + */ + SetupWizardWindow *window(); + +Q_SIGNALS: + /** + * Emitted when the wizard has finished. It passes the built account object. + */ + void finished(AccountPtr newAccount); + +private: + void nextStep(std::optional<PageIndex> currentPage, std::optional<PageIndex> desiredPage); + + SetupWizardWindow *_wizardWindow; + + // keeping a pointer on the current page allows us to check whether the controller has been initialized yet + // the pointer is also used to clean up the page + QPointer<AbstractSetupWizardPage> _currentPage; + + SetupWizardAccountBuilder _accountBuilder; + + QNetworkAccessManager *_networkAccessManager; +}; +} diff --git a/src/gui/newwizard/setupwizardwindow.cpp b/src/gui/newwizard/setupwizardwindow.cpp new file mode 100644 index 000000000..bf1e2f552 --- /dev/null +++ b/src/gui/newwizard/setupwizardwindow.cpp @@ -0,0 +1,148 @@ +#include "setupwizardwindow.h" + +#include <QLabel> + +#include "ui_setupwizardwindow.h" + +namespace OCC::Wizard { + +SetupWizardWindow::SetupWizardWindow(QWidget *parent) + : QDialog(parent) + , _ui(new ::Ui::SetupWizardWindow) +{ + _ui->setupUi(this); + + slotHideErrorMessageWidget(); + + // cannot do this in Qt Designer + _ui->contentWidget->layout()->setAlignment(Qt::AlignCenter); + + connect(_ui->cancelButton, &QPushButton::clicked, this, &SetupWizardWindow::reject); + + connect(_ui->nextButton, &QPushButton::clicked, this, &SetupWizardWindow::slotMoveToNextPage); + + connect(_ui->backButton, &QPushButton::clicked, this, [this]() { + slotStartTransition(); + emit backButtonClicked(_ui->pagination->activePageIndex()); + }); + + connect(_ui->pagination, &Pagination::paginationEntryClicked, this, [this](PageIndex clickedPageIndex) { + slotStartTransition(); + emit paginationEntryClicked(_ui->pagination->activePageIndex(), clickedPageIndex); + }); + + _ui->transitionProgressIndicator->setFixedSize(32, 32); + + // handle user pressing enter/return key + installEventFilter(this); +} + +void SetupWizardWindow::displayPage(AbstractSetupWizardPage *page, PageIndex index) +{ + _ui->backButton->setEnabled(true); + _ui->nextButton->setEnabled(true); + + if (index == 0) { + _ui->backButton->setEnabled(false); + } else if (index == _ui->pagination->entriesCount()) { + _ui->nextButton->setEnabled(false); + } + + if (index >= (_ui->pagination->entriesCount() - 1)) { + _ui->nextButton->setText(tr("Finish")); + } else { + _ui->nextButton->setText(tr("Next >")); + } + + _currentPage = page; + slotReplaceContent(_currentPage); + + _ui->pagination->setActivePageIndex(index); + _ui->pagination->setEnabled(true); + + connect(_ui->errorMessageDismissButton, &QPushButton::clicked, this, &SetupWizardWindow::slotHideErrorMessageWidget); + + // by default, set focus on the next button + _ui->nextButton->setFocus(); + + // this can optionally be overwritten by the page + Q_EMIT _currentPage->pageDisplayed(); +} + +void SetupWizardWindow::slotStartTransition() +{ + _ui->transitionProgressIndicator->startAnimation(); + _ui->contentWidget->setCurrentWidget(_ui->transitionProgressIndicator); + + // until a new page is displayed by the controller, we want to prevent the user from initiating another page change + _ui->backButton->setEnabled(false); + _ui->nextButton->setEnabled(false); + _ui->pagination->setEnabled(false); + // also, we should assume the user has seen the error message in case one is shown + slotHideErrorMessageWidget(); +} + +void SetupWizardWindow::slotReplaceContent(QWidget *newWidget) +{ + _ui->contentWidget->removeWidget(_currentContentWidget); + + _ui->contentWidget->addWidget(newWidget); + _ui->contentWidget->setCurrentWidget(newWidget); + + _currentContentWidget = newWidget; +} + +void SetupWizardWindow::slotHideErrorMessageWidget() +{ + _ui->errorMessageWidget->hide(); +} + +void SetupWizardWindow::showErrorMessage(const QString &errorMessage) +{ + _ui->errorMessageLabel->setText(errorMessage); + _ui->errorMessageWidget->show(); +} + +void SetupWizardWindow::setPaginationEntries(const QStringList &paginationEntries) +{ + _ui->pagination->setEntries(paginationEntries); +} + +bool SetupWizardWindow::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == _currentPage.data() || obj == this) { + if (event->type() == QEvent::KeyPress) { + auto keyEvent = dynamic_cast<QKeyEvent *>(event); + + switch (keyEvent->key()) { + case Qt::Key_Enter: + Q_FALLTHROUGH(); + case Qt::Key_Return: + slotMoveToNextPage(); + return true; + default: + // no action required, give other handlers a chance + break; + } + } + } + + return QDialog::eventFilter(obj, event); +} + +void SetupWizardWindow::disableNextButton() +{ + _ui->nextButton->setEnabled(false); +} + +SetupWizardWindow::~SetupWizardWindow() noexcept +{ + delete _ui; +} + +void SetupWizardWindow::slotMoveToNextPage() +{ + slotStartTransition(); + emit nextButtonClicked(_ui->pagination->activePageIndex()); +} +} diff --git a/src/gui/newwizard/setupwizardwindow.h b/src/gui/newwizard/setupwizardwindow.h new file mode 100644 index 000000000..068f4138c --- /dev/null +++ b/src/gui/newwizard/setupwizardwindow.h @@ -0,0 +1,78 @@ +#pragma once + +#include <QDialog> +#include <QEvent> +#include <QKeyEvent> +#include <QList> +#include <QPair> +#include <QStackedLayout> +#include <QStackedWidget> + +#include "3rdparty/QProgressIndicator/QProgressIndicator.h" +#include "pages/abstractsetupwizardpage.h" +#include "pagination.h" +#include "setupwizardaccountbuilder.h" + +namespace Ui { +class SetupWizardWindow; +} + +namespace OCC::Wizard { +/** + * This class contains the UI-specific code. It hides the complexity from the controller, and provides a high-level API. + */ +class SetupWizardWindow : public QDialog +{ + Q_OBJECT + +public: + explicit SetupWizardWindow(QWidget *parent); + ~SetupWizardWindow() noexcept override; + + /** + * Set entries in the pagination at the bottom of the wizard UI. + * The entries are identified by their position in the list (read: index). + */ + void setPaginationEntries(const QStringList &paginationEntries); + + /** + * Render this page within the wizard window. + * @param page page to render + * @param index index to highlight in pagination (also used to decide which buttons to enable) + */ + void displayPage(AbstractSetupWizardPage *page, PageIndex index); + + void showErrorMessage(const QString &errorMessage); + + void disableNextButton(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +Q_SIGNALS: + void paginationEntryClicked(PageIndex currentPage, PageIndex clickedPageIndex); + void nextButtonClicked(PageIndex currentPage); + void backButtonClicked(PageIndex currentPage); + +public Q_SLOTS: + /** + * Show "transition to next page" animation. Use displayPage(...) to end it. + */ + void slotStartTransition(); + +private Q_SLOTS: + void slotReplaceContent(QWidget *newWidget); + void slotHideErrorMessageWidget(); + void slotMoveToNextPage(); + +private: + ::Ui::SetupWizardWindow *_ui; + + // the wizard window keeps at most one widget inside the content widget's layout + // we keep a pointer in order to be able to delete it (and thus remove it from the window) when replacing the content + QPointer<QWidget> _currentContentWidget; + + // need to keep track of the current page for event filtering + QPointer<AbstractSetupWizardPage> _currentPage; +}; +} diff --git a/src/gui/newwizard/setupwizardwindow.ui b/src/gui/newwizard/setupwizardwindow.ui new file mode 100644 index 000000000..3d3418b80 --- /dev/null +++ b/src/gui/newwizard/setupwizardwindow.ui @@ -0,0 +1,139 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SetupWizardWindow</class> + <widget class="QDialog" name="SetupWizardWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>750</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add New Account</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0,0"> + <item> + <widget class="QStackedWidget" name="contentWidget"> + <widget class="QProgressIndicator" name="transitionProgressIndicator"/> + </widget> + </item> + <item> + <widget class="QWidget" name="errorMessageWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true">background-color: #ff7070;</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0"> + <item> + <widget class="QLabel" name="errorMessageLabel"> + <property name="text"> + <string notr="true">error message placeholder</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="errorMessageDismissButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Dismiss</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="bottomButtonsLayout"> + <item> + <widget class="QPushButton" name="cancelButton"> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="OCC::Wizard::Pagination" name="pagination" native="true"/> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="backButton"> + <property name="text"> + <string>< Back</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="nextButton"> + <property name="text"> + <string notr="true">placeholder</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>OCC::Wizard::Pagination</class> + <extends>QWidget</extends> + <header>pagination.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>QProgressIndicator</class> + <extends>QWidget</extends> + <header>3rdparty/QProgressIndicator/QProgressIndicator.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> |