refactor: introduce "view models"

This change prepares Keysmith for moving logic away from QML to C++

 - Added view model classes for each defined Navigation::Page instance
 - Added 'flows' to provide a C++ equivalent for control flow logic
   which currently still resides in QML

The purpose the view model classes is to provide data (properties) and
actions (methods to invoke) to the QML page UI. These are relatively
thin wrappers to expose the C++ state (Store) and logic (flows) as an
easy to use API for the QML UI.
master
Johan Ouwerkerk 2021-02-17 21:58:28 +01:00 committed by Bhushan Shah
parent bdcdb85bb6
commit 7274dc4f25
5 changed files with 584 additions and 0 deletions

View File

@ -6,7 +6,9 @@
set(keysmith_SRCS
keysmith.cpp
cli.cpp
flows_p.cpp
state_p.cpp
vms.cpp
)
add_library(keysmith_lib STATIC ${keysmith_SRCS})

207
src/app/flows_p.cpp Normal file
View File

@ -0,0 +1,207 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "flows_p.h"
#include "cli.h"
#include "state_p.h"
#include "vms.h"
#include "../logging_p.h"
#include <KLocalizedString>
#include <QTimer>
KEYSMITH_LOGGER(logger, ".app.flows_p")
namespace app
{
accounts::AccountStorage * storageOf(Keysmith *app)
{
Q_ASSERT_X(app, Q_FUNC_INFO, "should have a Keysmith application instance");
auto accounts = app->store().accounts();
Q_ASSERT_X(accounts, Q_FUNC_INFO, "should have an AccountStorage instance");
return accounts;
}
Navigation * navigationFor(Keysmith *app)
{
auto nav = app->navigation();
Q_ASSERT_X(nav, Q_FUNC_INFO, "should have a Navigation instance");
return nav;
}
InitialFlow::InitialFlow(Keysmith *app) :
QObject(app), m_started(false), m_uriToAdd(false), m_uriParsed(false), m_passwordPromptResolved(false),
m_app(app), m_input(new model::AccountInput(this)),
m_passwordRequest(new model::PasswordRequest(storageOf(app)->secret(), this))
{
QObject::connect(m_passwordRequest, &model::PasswordRequest::passwordRequestChanged,
this, &InitialFlow::resume);
QObject::connect(m_passwordRequest, &model::PasswordRequest::passwordAccepted, this, &InitialFlow::resume);
QObject::connect(accountListOf(m_app), &model::SimpleAccountListModel::loadedChanged,
this, &InitialFlow::resume);
}
void InitialFlow::run(const QCommandLineParser &parser)
{
flowStateOf(m_app)->setFlowRunning(true);
overviewStateOf(m_app)->setActionsEnabled(false);
m_started = true;
const auto argv = parser.positionalArguments();
if (argv.isEmpty()) {
qCDebug(logger) << "No URIs to handle, moving on:" << this;
QTimer::singleShot(0, this, &InitialFlow::resume);
return;
}
qCDebug(logger) << "Will first parse given URI(s):" << this;
m_uriToAdd = true;
auto job = new CommandLineAccountJob(m_input);
QObject::connect(job, &CommandLineAccountJob::newAccountProcessed, this, &InitialFlow::onNewAccountProcessed);
QObject::connect(job, &CommandLineAccountJob::newAccountInvalid, this, &InitialFlow::onNewAccountInvalid);
job->run(argv[0]);
}
void InitialFlow::onNewAccountProcessed(void)
{
Q_ASSERT_X(m_started, Q_FUNC_INFO, "should have properly started the flow first");
m_uriParsed = true;
auto vm = new AddAccountViewModel(m_input, accountListOf(m_app), true, false);
QObject::connect(vm, &AddAccountViewModel::accepted, this, &InitialFlow::resume);
QObject::connect(vm, &AddAccountViewModel::cancelled, this, &InitialFlow::onNewAccountRejected);
navigationFor(m_app)->navigate(Navigation::Page::AddAccount, vm);
}
void InitialFlow::onNewAccountAccepted(void)
{
Q_ASSERT_X(m_started, Q_FUNC_INFO, "should have properly started the flow first");
Q_ASSERT_X(m_uriToAdd && m_uriParsed, Q_FUNC_INFO, "should have parsed URIs first");
accountListOf(m_app)->addAccount(m_input);
m_uriToAdd = false;
QTimer::singleShot(0, this, &InitialFlow::resume);
}
void InitialFlow::onNewAccountRejected(void)
{
Q_ASSERT_X(m_uriToAdd && m_uriParsed, Q_FUNC_INFO, "should have parsed URIs first");
m_uriToAdd = false;
QTimer::singleShot(0, this, &InitialFlow::resume);
}
void InitialFlow::onNewAccountInvalid(void)
{
Q_ASSERT_X(m_started, Q_FUNC_INFO, "should have properly started the flow first");
auto vm = new ErrorViewModel(
i18nc("@title:window", "Invalid account"),
i18nc("@info:label", "The account you are trying to add is invalid. You can either quit the app, or continue without adding the account."),
true
);
QObject::connect(vm, &ErrorViewModel::dismissed, this, &InitialFlow::onNewAccountRejected);
navigationFor(m_app)->navigate(Navigation::Page::Error, vm);
}
void InitialFlow::resume(void)
{
if (!m_started) {
qCDebug(logger) << "Blocking progress: flow has not been started yet:" << this;
return;
}
if (m_uriToAdd && !m_uriParsed) {
qCDebug(logger) << "Blocking progress: URI parsing has not completed yet:" << this;
return;
}
if (m_passwordRequest->keyAvailable()) {
if (m_uriToAdd) {
const auto accounts = accountListOf(m_app);
if (!accounts->isAccountStillAvailable(m_input->name(), m_input->issuer())) {
auto vm = new RenameAccountViewModel(m_input, accountListOf(m_app));
QObject::connect(vm, &RenameAccountViewModel::cancelled, this, &InitialFlow::onNewAccountRejected);
QObject::connect(vm, &RenameAccountViewModel::accepted, this, &InitialFlow::onNewAccountAccepted);
navigationFor(m_app)->navigate(Navigation::Page::RenameAccount, vm);
return;
}
if (accounts->loaded()) {
QTimer::singleShot(0, this, &InitialFlow::onNewAccountAccepted);
return;
}
qCDebug(logger)
<< "Blocking progress: accounts not fully loaded:"
<< "Waiting to see if new account remains available:" << this;
return;
}
auto vm = new AccountsOverviewViewModel(m_app);
navigationFor(m_app)->navigate(Navigation::Page::AccountsOverview, vm);
overviewStateOf(m_app)->setActionsEnabled(true);
auto flows = flowStateOf(m_app);
flows->setFlowRunning(false);
flows->setInitialFlowDone(true);
QTimer::singleShot(0, this, &QObject::deleteLater);
return;
}
if (!m_passwordPromptResolved) {
if (m_passwordRequest->firstRun()) {
m_passwordPromptResolved = true;
auto vm = new SetupPasswordViewModel(m_passwordRequest);
navigationFor(m_app)->navigate(Navigation::Page::SetupPassword, vm);
return;
}
if (m_passwordRequest->previouslyDefined()) {
m_passwordPromptResolved = true;
auto vm = new UnlockAccountsViewModel(m_passwordRequest);
navigationFor(m_app)->navigate(Navigation::Page::UnlockAccounts, vm);
return;
}
qCDebug(logger) << "Blocking progress: password request has not yet been resolved:" << this;
return;
}
qCDebug(logger) << "Blocking progress: waiting for the next event:" << this;
}
ManualAddAccountFlow::ManualAddAccountFlow(Keysmith *app) :
QObject(app), m_app(app), m_input(new model::AccountInput(this))
{
Q_ASSERT_X(app, Q_FUNC_INFO, "should have a Keysmith instance");
}
void ManualAddAccountFlow::run(void)
{
flowStateOf(m_app)->setFlowRunning(true);
overviewStateOf(m_app)->setActionsEnabled(false);
auto vm = new AddAccountViewModel(m_input, accountListOf(m_app), false, true);
QObject::connect(vm, &AddAccountViewModel::accepted, this, &ManualAddAccountFlow::onAccepted);
QObject::connect(vm, &AddAccountViewModel::cancelled, this, &ManualAddAccountFlow::back);
navigationFor(m_app)->push(Navigation::Page::AddAccount, vm);
}
void ManualAddAccountFlow::onAccepted(void)
{
accountListOf(m_app)->addAccount(m_input);
QTimer::singleShot(0, this, &ManualAddAccountFlow::back);
}
void ManualAddAccountFlow::back(void)
{
auto vm = new AccountsOverviewViewModel(m_app);
navigationFor(m_app)->navigate(Navigation::Page::AccountsOverview, vm);
overviewStateOf(m_app)->setActionsEnabled(true);
flowStateOf(m_app)->setFlowRunning(false);
QTimer::singleShot(0, this, &QObject::deleteLater);
}
}

56
src/app/flows_p.h Normal file
View File

@ -0,0 +1,56 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#ifndef APP_FLOWS_P_H
#define APP_FLOWS_P_H
#include <QCommandLineParser>
#include "keysmith.h"
#include "vms.h"
#include "../model/accounts.h"
#include "../model/password.h"
namespace app
{
class InitialFlow: public QObject
{
Q_OBJECT
public:
explicit InitialFlow(Keysmith *app);
public:
void run(const QCommandLineParser &parser);
private Q_SLOTS:
void onNewAccountInvalid(void);
void onNewAccountProcessed(void);
void onNewAccountAccepted(void);
void onNewAccountRejected(void);
void resume(void);
private:
bool m_started;
bool m_uriToAdd;
bool m_uriParsed;
bool m_passwordPromptResolved;
private:
Keysmith * const m_app;
model::AccountInput * const m_input;
model::PasswordRequest * const m_passwordRequest;
};
class ManualAddAccountFlow: public QObject
{
Q_OBJECT
public:
explicit ManualAddAccountFlow(Keysmith *app);
void run(void);
private Q_SLOTS:
void back(void);
void onAccepted(void);
private:
Keysmith * const m_app;
model::AccountInput * const m_input;
};
}
#endif

180
src/app/vms.cpp Normal file
View File

@ -0,0 +1,180 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "vms.h"
#include "state_p.h"
#include "flows_p.h"
namespace app
{
AddAccountViewModel::AddAccountViewModel(model::AccountInput *input, model::SimpleAccountListModel *accounts,
bool quitEnabled, bool validateAvailability, QObject *parent) :
QObject(parent), m_input(input), m_accounts(accounts), m_quitEnabled(quitEnabled),
m_validateAvailability(validateAvailability)
{
}
model::AccountInput * AddAccountViewModel::input(void) const
{
return m_input;
}
model::SimpleAccountListModel * AddAccountViewModel::accounts(void) const
{
return m_accounts;
}
bool AddAccountViewModel::validateAvailability(void) const
{
return m_validateAvailability;
}
bool AddAccountViewModel::quitEnabled(void) const
{
return m_quitEnabled;
}
RenameAccountViewModel::RenameAccountViewModel(model::AccountInput *input, model::SimpleAccountListModel *accounts,
QObject *parent) :
QObject(parent), m_input(input), m_accounts(accounts)
{
}
model::AccountInput * RenameAccountViewModel::input(void) const
{
return m_input;
}
model::SimpleAccountListModel * RenameAccountViewModel::accounts(void) const
{
return m_accounts;
}
ErrorViewModel::ErrorViewModel(const QString &errorTitle, const QString &errorText, bool quitEnabled,
QObject *parent) :
QObject(parent), m_title(errorTitle), m_error(errorText), m_quitEnabled(quitEnabled)
{
}
QString ErrorViewModel::error(void) const
{
return m_error;
}
QString ErrorViewModel::title(void) const
{
return m_title;
}
bool ErrorViewModel::quitEnabled(void) const
{
return m_quitEnabled;
}
OverviewState * overviewStateOf(Keysmith *app)
{
auto overview = app->store().overview();
Q_ASSERT_X(overview, Q_FUNC_INFO, "should have a valid overview state object");
return overview;
}
FlowState * flowStateOf(Keysmith *app)
{
auto flows = app->store().flows();
Q_ASSERT_X(flows, Q_FUNC_INFO, "should have a valid flow state object");
return flows;
}
model::SimpleAccountListModel * accountListOf(Keysmith *app)
{
Q_ASSERT_X(app, Q_FUNC_INFO, "should have a Keysmith application instance");
auto list = app->store().accountList();
Q_ASSERT_X(list, Q_FUNC_INFO, "should have an SimpleAccountListModel instance");
return list;
}
AccountsOverviewViewModel::AccountsOverviewViewModel(Keysmith *app) :
QObject(), m_app(app), m_accounts(accountListOf(app))
{
QObject::connect(overviewStateOf(m_app), &OverviewState::actionsEnabledChanged,
this, &AccountsOverviewViewModel::actionsEnabledChanged);
}
model::SimpleAccountListModel * AccountsOverviewViewModel::accounts(void) const
{
return m_accounts;
}
bool AccountsOverviewViewModel::actionsEnabled(void) const
{
return overviewStateOf(m_app)->actionsEnabled();
}
void AccountsOverviewViewModel::addNewAccount(void)
{
auto flow = new ManualAddAccountFlow(m_app);
flow->run();
}
PasswordViewModel::PasswordViewModel(model::PasswordRequest *request, QObject *parent) :
QObject(parent), m_failed(false), m_request(request)
{
Q_ASSERT_X(request, Q_FUNC_INFO, "should have a valid password request object");
QObject::connect(m_request, &model::PasswordRequest::passwordRejected, this, &PasswordViewModel::rejected);
QObject::connect(m_request, &model::PasswordRequest::passwordStateChanged,
this, &PasswordViewModel::busyChanged);
}
PasswordViewModel::~PasswordViewModel()
{
}
bool PasswordViewModel::busy(void) const
{
return m_request->passwordProvided();
}
bool PasswordViewModel::failed(void) const
{
return m_failed;
}
void PasswordViewModel::rejected(void)
{
if (!m_failed) {
m_failed = true;
Q_EMIT failedChanged();
}
}
SetupPasswordViewModel::SetupPasswordViewModel(model::PasswordRequest *request, QObject *parent) :
PasswordViewModel(request, parent)
{
}
void SetupPasswordViewModel::setup(const QString &password, const QString &confirmedPassword)
{
bool result = !m_request->provideBothPasswords(password, confirmedPassword);
if (result != m_failed) {
m_failed = result;
Q_EMIT failedChanged();
}
}
UnlockAccountsViewModel::UnlockAccountsViewModel(model::PasswordRequest *request, QObject *parent) :
PasswordViewModel(request, parent)
{
}
void UnlockAccountsViewModel::unlock(const QString &password)
{
bool result = !m_request->providePassword(password);
if (result != m_failed) {
m_failed = result;
Q_EMIT failedChanged();
}
}
}

139
src/app/vms.h Normal file
View File

@ -0,0 +1,139 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#ifndef APP_VMS_H
#define APP_VMS_H
#include "../model/accounts.h"
#include "../model/password.h"
#include "../account/account.h"
#include "keysmith.h"
namespace app
{
OverviewState * overviewStateOf(Keysmith *app);
FlowState * flowStateOf(Keysmith *app);
model::SimpleAccountListModel * accountListOf(Keysmith *app);
class AddAccountViewModel: public QObject
{
Q_OBJECT
Q_PROPERTY(model::AccountInput * input READ input CONSTANT)
Q_PROPERTY(model::SimpleAccountListModel * accounts READ accounts CONSTANT)
Q_PROPERTY(bool validateAvailability READ validateAvailability CONSTANT)
Q_PROPERTY(bool quitEnabled READ quitEnabled CONSTANT)
public:
explicit AddAccountViewModel(model::AccountInput *input, model::SimpleAccountListModel *accounts,
bool quitEnabled, bool validateAvailability,
QObject *parent = nullptr);
model::AccountInput * input(void) const;
model::SimpleAccountListModel * accounts(void) const;
bool validateAvailability(void) const;
bool quitEnabled(void) const;
Q_SIGNALS:
void cancelled(void);
void accepted(void);
private:
model::AccountInput * const m_input;
model::SimpleAccountListModel * const m_accounts;
const bool m_quitEnabled;
const bool m_validateAvailability;
};
class RenameAccountViewModel: public QObject
{
Q_OBJECT
Q_PROPERTY(model::AccountInput * input READ input CONSTANT)
Q_PROPERTY(model::SimpleAccountListModel * accounts READ accounts CONSTANT)
public:
explicit RenameAccountViewModel(model::AccountInput *input, model::SimpleAccountListModel *accounts,
QObject *parent = nullptr);
model::AccountInput * input(void) const;
model::SimpleAccountListModel * accounts(void) const;
Q_SIGNALS:
void cancelled(void);
void accepted(void);
private:
model::AccountInput * const m_input;
model::SimpleAccountListModel * const m_accounts;
};
class ErrorViewModel: public QObject
{
Q_OBJECT
Q_PROPERTY(QString errorText READ error CONSTANT)
Q_PROPERTY(QString errorTitle READ title CONSTANT)
Q_PROPERTY(bool quitEnabled READ quitEnabled CONSTANT)
public:
explicit ErrorViewModel(const QString &errorTitle, const QString &errorText, bool quitEnabled,
QObject *parent = nullptr);
bool quitEnabled(void) const;
QString error(void) const;
QString title(void) const;
Q_SIGNALS:
void dismissed(void);
private:
const QString m_title;
const QString m_error;
const bool m_quitEnabled;
};
class PasswordViewModel: public QObject
{
Q_OBJECT
Q_PROPERTY(bool failed READ failed NOTIFY failedChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public:
explicit PasswordViewModel(model::PasswordRequest *request, QObject *parent = nullptr);
virtual ~PasswordViewModel();
bool failed(void) const;
bool busy(void) const;
Q_SIGNALS:
void failedChanged(void);
void busyChanged(void);
private Q_SLOTS:
void rejected(void);
protected:
bool m_failed;
model::PasswordRequest * const m_request;
};
class SetupPasswordViewModel: public PasswordViewModel
{
Q_OBJECT
public:
explicit SetupPasswordViewModel(model::PasswordRequest *request, QObject *parent = nullptr);
public Q_SLOTS:
void setup(const QString &password, const QString &confirmedPassword);
};
class UnlockAccountsViewModel: public PasswordViewModel
{
Q_OBJECT
public:
explicit UnlockAccountsViewModel(model::PasswordRequest *request, QObject *parent = nullptr);
public Q_SLOTS:
void unlock(const QString &password);
};
class AccountsOverviewViewModel: public QObject
{
Q_OBJECT
Q_PROPERTY(model::SimpleAccountListModel * accounts READ accounts CONSTANT)
Q_PROPERTY(bool actionsEnabled READ actionsEnabled NOTIFY actionsEnabledChanged)
public:
explicit AccountsOverviewViewModel(Keysmith *app);
model::SimpleAccountListModel * accounts(void) const;
bool actionsEnabled(void) const;
public Q_SLOTS:
void addNewAccount(void);
Q_SIGNALS:
void actionsEnabledChanged(void);
private:
Keysmith * const m_app;
model::SimpleAccountListModel * const m_accounts;
};
}
#endif