From ca6a72aa130a35bdd7d2d188a92be6ed4d4ee84c Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:34 -0500 Subject: [PATCH 1/9] Expand poll registry API to filter by finished polls only The PollRegistry class provides a sequence iterator that could perform rudimentary filtering to exclude finished polls, but it could not walk the sequence for finished polls only. This updates the polls filtering mechanism to support that use case with a structure that enables other types of filters for the future. --- src/Makefile.am | 1 + src/gridcoin/voting/filter.h | 17 +++++++++ src/gridcoin/voting/registry.cpp | 60 +++++++++++++++++++++++--------- src/gridcoin/voting/registry.h | 30 ++++++++++++---- 4 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 src/gridcoin/voting/filter.h diff --git a/src/Makefile.am b/src/Makefile.am index 1a2ce76acc..e6d2635082 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -123,6 +123,7 @@ GRIDCOIN_CORE_H = \ gridcoin/upgrade.h \ gridcoin/voting/builders.h \ gridcoin/voting/claims.h \ + gridcoin/voting/filter.h \ gridcoin/voting/fwd.h \ gridcoin/voting/payloads.h \ gridcoin/voting/poll.h \ diff --git a/src/gridcoin/voting/filter.h b/src/gridcoin/voting/filter.h new file mode 100644 index 0000000000..e9a4b2a085 --- /dev/null +++ b/src/gridcoin/voting/filter.h @@ -0,0 +1,17 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#pragma once + +namespace GRC { +//! +//! \brief Bit flags that represents attributes to filter polls by. +//! +enum PollFilterFlag +{ + NO_FILTER = 0, //!< No active filter. Include all results. + ACTIVE = 1, //!< Include unfinished polls. + FINISHED = 2, //!< Include finished polls. +}; +} // namespace GRC diff --git a/src/gridcoin/voting/registry.cpp b/src/gridcoin/voting/registry.cpp index 4eb5ff24b6..0e6dc42d06 100644 --- a/src/gridcoin/voting/registry.cpp +++ b/src/gridcoin/voting/registry.cpp @@ -539,31 +539,36 @@ void PollRegistry::DeleteVote(const ContractContext& ctx) using Sequence = PollRegistry::Sequence; -Sequence::Sequence(const PollMapByTitle& polls, const bool active_only) - : m_polls(polls), m_active_only(active_only) +Sequence::Sequence(const PollMapByTitle& polls, const FilterFlag flags) + : m_polls(polls), m_flags(flags) { } +Sequence Sequence::Where(const FilterFlag flags) const +{ + return Sequence(m_polls, flags); +} + Sequence Sequence::OnlyActive(const bool active_only) const { - return Sequence(m_polls, active_only); + int flags = m_flags; + + if (active_only) { + flags = (flags & ~FINISHED) | ACTIVE; + } + + return Sequence(m_polls, static_cast(flags)); } Sequence::Iterator Sequence::begin() const { - auto iter = m_polls.begin(); - auto end = m_polls.end(); int64_t now = 0; - if (m_active_only) { + if (!((m_flags & ACTIVE) && (m_flags & FINISHED))) { now = GetAdjustedTime(); - - while (iter != end && iter->second.Expired(now)) { - ++iter; - } } - return Iterator(iter, end, m_active_only, now); + return Iterator(m_polls.begin(), m_polls.end(), m_flags, now); } Sequence::Iterator Sequence::end() const @@ -580,13 +585,14 @@ using Iterator = PollRegistry::Sequence::Iterator; Iterator::Iterator( BaseIterator iter, BaseIterator end, - const bool active_only, + const FilterFlag flags, const int64_t now) : m_iter(iter) , m_end(end) - , m_active_only(active_only) + , m_flags(flags) , m_now(now) { + SeekNextMatch(); } Iterator::Iterator(BaseIterator end) : m_iter(end), m_end(end) @@ -615,9 +621,8 @@ Iterator::pointer Iterator::operator->() const Iterator& Iterator::operator++() { - do { - ++m_iter; - } while (m_active_only && m_iter != m_end && m_iter->second.Expired(m_now)); + ++m_iter; + SeekNextMatch(); return *this; } @@ -639,3 +644,26 @@ bool Iterator::operator!=(const Iterator& other) const { return m_iter != other.m_iter; } + +void Iterator::SeekNextMatch() +{ + if (m_flags == FilterFlag::NO_FILTER) { + return; + } + + while (m_iter != m_end) { + if (m_now > 0) { + if (m_flags & ACTIVE) { + if (!m_iter->second.Expired(m_now)) { + break; + } + } else { + if (m_iter->second.Expired(m_now)) { + break; + } + } + } + + ++m_iter; + } +} diff --git a/src/gridcoin/voting/registry.h b/src/gridcoin/voting/registry.h index 36f2889b0f..3a2d712f8a 100644 --- a/src/gridcoin/voting/registry.h +++ b/src/gridcoin/voting/registry.h @@ -5,6 +5,7 @@ #pragma once #include "gridcoin/contract/handler.h" +#include "gridcoin/voting/filter.h" #include "gridcoin/voting/fwd.h" class CTxDB; @@ -132,6 +133,8 @@ class PollRegistry : public IContractHandler class Sequence { public: + using FilterFlag = PollFilterFlag; + //! //! \brief Behaves like a forward \c const iterator. //! @@ -152,7 +155,7 @@ class PollRegistry : public IContractHandler Iterator( BaseIterator iter, BaseIterator end, - const bool active_only, + const FilterFlag flags, const int64_t now); //! @@ -209,23 +212,38 @@ class PollRegistry : public IContractHandler private: BaseIterator m_iter; //!< The current position. BaseIterator m_end; //!< Element after the end of the sequence. - bool m_active_only; //!< Whether to skip finished polls. + FilterFlag m_flags; //!< Attributes to filter polls by. int64_t m_now; //!< Current time in seconds. + + //! + //! \brief Advance the iterator to the next item that matches the + //! configured filters. + //! + void SeekNextMatch(); }; // Iterator //! //! \brief Initialize a poll sequence. //! - //! \param polls The set of poll references in the registry. - //! \param active_only Whether the sequence skips finished polls. + //! \param polls The set of poll references in the registry. + //! \param flags Attributes to filter polls by. //! - Sequence(const PollMapByTitle& polls, const bool active_only = false); + Sequence(const PollMapByTitle& polls, const FilterFlag flags = NO_FILTER); + + //! + //! \brief Set the attributes to filter polls by. + //! + //! \return A new sequence for the specified poll filters. + //! + Sequence Where(const FilterFlag flags) const; //! //! \brief Set whether the sequence skips finished polls. //! //! \param active_only Whether the sequence skips finished polls. //! + //! \return A new sequence for the specified poll status filters. + //! Sequence OnlyActive(const bool active_only = true) const; //! @@ -240,7 +258,7 @@ class PollRegistry : public IContractHandler private: const PollMapByTitle& m_polls; //!< Poll references in the registry. - bool m_active_only; //!< Whether to skip finished polls. + FilterFlag m_flags; //!< Attributes to filter polls by. }; // Sequence //! From 47932f29439771dc23f81cb033f36d9da45e3729 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:36 -0500 Subject: [PATCH 2/9] Create voting GUI view model core abstraction layer This adds view models that provide access to core voting functionality. --- gridcoinresearch.pro | 4 + src/Makefile.qt.include | 8 +- src/gridcoin/voting/result.cpp | 1 + src/gridcoin/voting/result.h | 1 + src/qt/voting/polltablemodel.cpp | 237 ++++++++++++++++++++++++ src/qt/voting/polltablemodel.h | 61 +++++++ src/qt/voting/votingmodel.cpp | 297 +++++++++++++++++++++++++++++++ src/qt/voting/votingmodel.h | 123 +++++++++++++ 8 files changed, 731 insertions(+), 1 deletion(-) create mode 100644 src/qt/voting/polltablemodel.cpp create mode 100644 src/qt/voting/polltablemodel.h create mode 100644 src/qt/voting/votingmodel.cpp create mode 100644 src/qt/voting/votingmodel.h diff --git a/gridcoinresearch.pro b/gridcoinresearch.pro index 49aec3b393..3e3a43da63 100755 --- a/gridcoinresearch.pro +++ b/gridcoinresearch.pro @@ -185,6 +185,8 @@ HEADERS += src/qt/bitcoingui.h \ src/qt/researcher/researcherwizardpoolsummarypage.h \ src/qt/researcher/researcherwizardprojectspage.h \ src/qt/researcher/researcherwizardsummarypage.h \ + src/qt/voting/polltablemodel.h \ + src/qt/voting/votingmodel.h \ src/qt/transactiontablemodel.h \ src/qt/addresstablemodel.h \ src/qt/optionsdialog.h \ @@ -292,6 +294,8 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/researcher/researcherwizardpoolsummarypage.cpp \ src/qt/researcher/researcherwizardprojectspage.cpp \ src/qt/researcher/researcherwizardsummarypage.cpp \ + src/qt/voting/polltablemodel.cpp \ + src/qt/voting/votingmodel.cpp \ src/qt/transactiontablemodel.cpp \ src/qt/addresstablemodel.cpp \ src/qt/optionsdialog.cpp \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 93ae56198d..5319ba3a83 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -166,7 +166,9 @@ QT_MOC_CPP = \ qt/researcher/moc_researcherwizardpoolpage.cpp \ qt/researcher/moc_researcherwizardpoolsummarypage.cpp \ qt/researcher/moc_researcherwizardprojectspage.cpp \ - qt/researcher/moc_researcherwizardsummarypage.cpp + qt/researcher/moc_researcherwizardsummarypage.cpp \ + qt/voting/moc_polltablemodel.cpp \ + qt/voting/moc_votingmodel.cpp GRIDCOIN_MM = \ qt/macdockiconhandler.mm \ @@ -250,6 +252,8 @@ GRIDCOINRESEARCH_QT_H = \ qt/transactionview.h \ qt/upgradeqt.h \ qt/votingdialog.h \ + qt/voting/polltablemodel.h \ + qt/voting/votingmodel.h \ qt/walletmodel.h \ qt/winshutdownmonitor.h @@ -316,6 +320,8 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/transactionview.cpp \ qt/upgradeqt.cpp \ qt/votingdialog.cpp \ + qt/voting/polltablemodel.cpp \ + qt/voting/votingmodel.cpp \ qt/walletmodel.cpp \ qt/winshutdownmonitor.cpp diff --git a/src/gridcoin/voting/result.cpp b/src/gridcoin/voting/result.cpp index 4582d1c128..e984fe4703 100644 --- a/src/gridcoin/voting/result.cpp +++ b/src/gridcoin/voting/result.cpp @@ -1069,6 +1069,7 @@ PollResult::PollResult(Poll poll) : m_poll(std::move(poll)) , m_total_weight(0) , m_invalid_votes(0) + , m_finished(poll.Expired(GetAdjustedTime())) { m_responses.resize(m_poll.Choices().size()); } diff --git a/src/gridcoin/voting/result.h b/src/gridcoin/voting/result.h index b99bf89aed..7a4c455298 100644 --- a/src/gridcoin/voting/result.h +++ b/src/gridcoin/voting/result.h @@ -79,6 +79,7 @@ class PollResult const Poll m_poll; //!< The poll associated with the result. Weight m_total_weight; //!< Aggregate weight of all the votes submitted. size_t m_invalid_votes; //!< Number of votes that failed validation. + bool m_finished; //!< Whether the poll finished as of this result. //! //! \brief The aggregated voting weight tallied for each poll choice. diff --git a/src/qt/voting/polltablemodel.cpp b/src/qt/voting/polltablemodel.cpp new file mode 100644 index 0000000000..b92cab7209 --- /dev/null +++ b/src/qt/voting/polltablemodel.cpp @@ -0,0 +1,237 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/guiutil.h" +#include "qt/voting/polltablemodel.h" +#include "qt/voting/votingmodel.h" + +#include +#include +#include + +using namespace GRC; + +namespace { +class PollTableDataModel : public QAbstractTableModel +{ +public: + PollTableDataModel() + { + qRegisterMetaType>(); + qRegisterMetaType(); + + m_columns + << tr("Title") + << tr("Expiration") + << tr("Weight Type") + << tr("Votes") + << tr("Total Weight") + << tr("Top Answer"); + } + + int rowCount(const QModelIndex &parent) const override + { + Q_UNUSED(parent); + return m_rows.size(); + } + + int columnCount(const QModelIndex &parent) const override + { + Q_UNUSED(parent); + return m_columns.size(); + } + + QVariant data(const QModelIndex &index, int role) const override + { + if (!index.isValid()) { + return QVariant(); + } + + const PollItem* row = static_cast(index.internalPointer()); + + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case PollTableModel::Title: + return row->m_title; + case PollTableModel::Expiration: + return GUIUtil::dateTimeStr(row->m_expiration); + case PollTableModel::WeightType: + return row->m_weight_type; + case PollTableModel::TotalVotes: + return row->m_total_votes; + case PollTableModel::TotalWeight: + return QString::number(row->m_total_weight); + case PollTableModel::TopAnswer: + return row->m_top_answer; + } + break; + + case Qt::TextAlignmentRole: + switch (index.column()) { + case PollTableModel::TotalVotes: + // Pass-through case + case PollTableModel::TotalWeight: + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + } + break; + + case PollTableModel::SortRole: + switch (index.column()) { + case PollTableModel::Title: + return row->m_title; + case PollTableModel::Expiration: + return row->m_expiration; + case PollTableModel::WeightType: + return row->m_weight_type; + case PollTableModel::TotalVotes: + return row->m_total_votes; + case PollTableModel::TotalWeight: + return QVariant::fromValue(row->m_total_weight); + case PollTableModel::TopAnswer: + return row->m_top_answer; + } + break; + } + + return QVariant(); + } + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override + { + if (orientation == Qt::Horizontal) { + if (role == Qt::DisplayRole && section < m_columns.size()) { + return m_columns[section]; + } + } + + return QVariant(); + } + + QModelIndex index(int row, int column, const QModelIndex &parent) const override + { + Q_UNUSED(parent); + + if (row > static_cast(m_rows.size())) { + return QModelIndex(); + } + + void* data = static_cast(const_cast(&m_rows[row])); + + return createIndex(row, column, data); + } + + Qt::ItemFlags flags(const QModelIndex &index) const override + { + if (!index.isValid()) { + return Qt::NoItemFlags; + } + + return (Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } + + void reload(std::vector rows) + { + emit layoutAboutToBeChanged(); + m_rows = std::move(rows); + emit layoutChanged(); + } + +private: + QStringList m_columns; + std::vector m_rows; +}; // PollTableDataModel +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: PollTableModel +// ----------------------------------------------------------------------------- + +PollTableModel::PollTableModel(QObject* parent) + : QSortFilterProxyModel(parent) + , m_data_model(new PollTableDataModel()) + , m_filter_flags(GRC::PollFilterFlag::NO_FILTER) +{ + setSourceModel(m_data_model.get()); + setDynamicSortFilter(true); + setFilterCaseSensitivity(Qt::CaseInsensitive); + setFilterKeyColumn(ColumnIndex::Title); + setSortCaseSensitivity(Qt::CaseInsensitive); + setSortRole(SortRole); +} + +PollTableModel::~PollTableModel() +{ + // Nothing to do yet... +} + +void PollTableModel::setModel(VotingModel* model) +{ + m_model = model; +} + +void PollTableModel::setPollFilterFlags(PollFilterFlag flags) +{ + m_filter_flags = flags; +} + +bool PollTableModel::includesActivePolls() const +{ + return (m_filter_flags & PollFilterFlag::ACTIVE) != 0; +} + +int PollTableModel::size() const +{ + return m_data_model->rowCount(QModelIndex()); +} + +bool PollTableModel::empty() const +{ + return size() == 0; +} + +QString PollTableModel::columnName(int offset) const +{ + return m_data_model->headerData(offset, Qt::Horizontal, Qt::DisplayRole).toString(); +} + +const PollItem* PollTableModel::rowItem(int row) const +{ + QModelIndex index = this->index(row, 0, QModelIndex()); + index = mapToSource(index); + + return static_cast(index.internalPointer()); +} + +void PollTableModel::refresh() +{ + if (!m_model || !m_refresh_mutex.tryLock()) { + return; + } + + QtConcurrent::run([this]() { + static_cast(m_data_model.get()) + ->reload(m_model->buildPollTable(m_filter_flags)); + + m_refresh_mutex.unlock(); + }); +} + +void PollTableModel::changeTitleFilter(const QString& pattern) +{ + emit layoutAboutToBeChanged(); + setFilterFixedString(pattern); + emit layoutChanged(); +} + +Qt::SortOrder PollTableModel::sort(int column) +{ + if (sortColumn() == column) { + QSortFilterProxyModel::sort(column, static_cast(!sortOrder())); + } else { + QSortFilterProxyModel::sort(column, Qt::AscendingOrder); + } + + return sortOrder(); +} diff --git a/src/qt/voting/polltablemodel.h b/src/qt/voting/polltablemodel.h new file mode 100644 index 0000000000..94260b413c --- /dev/null +++ b/src/qt/voting/polltablemodel.h @@ -0,0 +1,61 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLTABLEMODEL_H +#define VOTING_POLLTABLEMODEL_H + +#include "gridcoin/voting/filter.h" + +#include +#include +#include + +class PollItem; +class VotingModel; + +class PollTableModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + enum ColumnIndex + { + Title, + Expiration, + WeightType, + TotalVotes, + TotalWeight, + TopAnswer, + }; + + enum Roles + { + SortRole = Qt::UserRole, + }; + + explicit PollTableModel(QObject* parent = nullptr); + ~PollTableModel(); + + void setModel(VotingModel* model = nullptr); + void setPollFilterFlags(GRC::PollFilterFlag flags); + bool includesActivePolls() const; + + int size() const; + bool empty() const; + QString columnName(int offset) const; + const PollItem* rowItem(int row) const; + +public slots: + void refresh(); + void changeTitleFilter(const QString& pattern); + Qt::SortOrder sort(int column); + +private: + VotingModel* m_model; + std::unique_ptr m_data_model; + GRC::PollFilterFlag m_filter_flags; + QMutex m_refresh_mutex; +}; + +#endif // VOTING_POLLTABLEMODEL_H diff --git a/src/qt/voting/votingmodel.cpp b/src/qt/voting/votingmodel.cpp new file mode 100644 index 0000000000..dbc83d2764 --- /dev/null +++ b/src/qt/voting/votingmodel.cpp @@ -0,0 +1,297 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include "hash.h" +#include "gridcoin/contract/contract.h" +#include "gridcoin/project.h" +#include "gridcoin/voting/builders.h" +#include "gridcoin/voting/poll.h" +#include "gridcoin/voting/registry.h" +#include "gridcoin/voting/result.h" +#include "qt/voting/votingmodel.h" +#include "qt/walletmodel.h" +#include "sync.h" + +using namespace GRC; + +extern CCriticalSection cs_main; + +namespace { +std::optional BuildPollItem(const PollRegistry::Sequence::Iterator& iter) +{ + const PollResultOption result = PollResult::BuildFor(iter->Ref()); + + if (!result) { + return std::nullopt; + } + + const Poll& poll = result->m_poll; + + PollItem item; + item.m_id = QString::fromStdString(iter->Ref().Txid().ToString()); + item.m_title = QString::fromStdString(poll.m_title).replace("_", " "); + item.m_question = QString::fromStdString(poll.m_question).replace("_", " "); + item.m_url = QString::fromStdString(poll.m_url).trimmed(); + item.m_start_time = QDateTime::fromMSecsSinceEpoch(poll.m_timestamp * 1000); + item.m_expiration = QDateTime::fromMSecsSinceEpoch(poll.Expiration() * 1000); + item.m_weight_type = QString::fromStdString(poll.WeightTypeToString()); + item.m_response_type = QString::fromStdString(poll.ResponseTypeToString()); + item.m_total_votes = result->m_votes.size(); + item.m_total_weight = result->m_total_weight / COIN; + item.m_finished = result->m_finished; + item.m_multiple_choice = poll.AllowsMultipleChoices(); + + if (!item.m_url.startsWith("http://") && !item.m_url.startsWith("https://")) { + item.m_url.prepend("http://"); + } + + for (size_t i = 0; i < result->m_responses.size(); ++i) { + item.m_choices.emplace_back( + QString::fromStdString(poll.Choices().At(i)->m_label), + result->m_responses[i].m_votes, + result->m_responses[i].m_weight / COIN); + } + + if (!result->m_votes.empty()) { + item.m_top_answer = QString::fromStdString(result->WinnerLabel()).replace("_", " "); + } + + return item; +} +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: VotingModel +// ----------------------------------------------------------------------------- + +VotingModel::VotingModel(WalletModel& wallet_model) + : m_registry(GetPollRegistry()) + , m_wallet_model(wallet_model) +{ +} + +VotingModel::~VotingModel() +{ +} + +int VotingModel::minPollDurationDays() +{ + return Poll::MIN_DURATION_DAYS; +} + +int VotingModel::maxPollDurationDays() +{ + // The protocol allows poll durations up to 180 days. To limit unhelpful + // or unintentional poll durations, user-facing pieces discourage a poll + // longer than: + // + return 90; // days +} + +int VotingModel::maxPollTitleLength() +{ + // Not strictly accurate: the protocol limits the max length in bytes, but + // Qt limits field lengths in UTF-8 characters which may be represented by + // more than one byte. + // + return Poll::MAX_TITLE_SIZE; +} + +int VotingModel::maxPollUrlLength() +{ + // Not strictly accurate: the protocol limits the max length in bytes, but + // Qt limits field lengths in UTF-8 characters which may be represented by + // more than one byte. + // + return Poll::MAX_URL_SIZE; +} + +int VotingModel::maxPollQuestionLength() +{ + // Not strictly accurate: the protocol limits the max length in bytes, but + // Qt limits field lengths in UTF-8 characters which may be represented by + // more than one byte. + // + return Poll::MAX_QUESTION_SIZE; +} + +int VotingModel::maxPollChoiceLabelLength() +{ + // Not strictly accurate: the protocol limits the max length in bytes, but + // Qt limits field lengths in UTF-8 characters which may be represented by + // more than one byte. + // + return Poll::Choice::MAX_LABEL_SIZE; +} + +QStringList VotingModel::getActiveProjectNames() const +{ + QStringList names; + + for (const auto& project : GetWhitelist().Snapshot().Sorted()) { + names << QString::fromStdString(project.m_name); + } + + return names; +} + +std::vector VotingModel::buildPollTable(const PollFilterFlag flags) const +{ + std::vector items; + + LOCK(cs_main); + + for (const auto iter : m_registry.Polls().Where(flags)) { + if (std::optional item = BuildPollItem(iter)) { + items.push_back(std::move(*item)); + } + } + + return items; +} + +CAmount VotingModel::estimatePollFee() const +{ + // TODO: add core API for more precise fee estimation. + return 50 * COIN; +} + +VotingResult VotingModel::sendPoll( + const QString& title, + const int duration_days, + const QString& question, + const QString& url, + const int weight_type, + const int response_type, + const QStringList& choices) const +{ + PollBuilder builder = PollBuilder(); + + try { + builder = builder + .SetType(PollType::SURVEY) + .SetTitle(title.toStdString()) + .SetDuration(duration_days) + .SetQuestion(question.toStdString()) + .SetWeightType(weight_type) + .SetResponseType(response_type) + .SetUrl(url.toStdString()); + + for (const auto& choice : choices) { + builder = builder.AddChoice(choice.toStdString()); + } + } catch (const VotingError& e) { + return VotingResult(QString::fromStdString(e.what())); + } + + const WalletModel::UnlockContext unlock_context(m_wallet_model.requestUnlock()); + + if (!unlock_context.isValid()) { + return VotingResult(tr("Please unlock the wallet.")); + } + + uint256 txid; + + try { + txid = SendPollContract(std::move(builder)); + } catch (const VotingError& e) { + return VotingResult(QString::fromStdString(e.what())); + } + + return VotingResult(txid); +} + +VotingResult VotingModel::sendVote( + const QString& poll_id, + const std::vector& choice_offsets) const +{ + LOCK(cs_main); + + const uint256 poll_txid = uint256S(poll_id.toStdString()); + const PollReference* ref = m_registry.TryByTxid(poll_txid); + + if (!ref) { + return VotingResult(tr("Poll not found.")); + } + + const PollOption poll = ref->TryReadFromDisk(); + + if (!poll) { + return VotingResult(tr("Failed to load poll from disk")); + } + + try { + VoteBuilder builder = VoteBuilder::ForPoll(*poll, ref->Txid()); + builder = builder.AddResponses(choice_offsets); + + const WalletModel::UnlockContext unlock_context(m_wallet_model.requestUnlock()); + + if (!unlock_context.isValid()) { + return VotingResult(tr("Please unlock the wallet.")); + } + + const uint256 txid = SendVoteContract(std::move(builder)); + + return VotingResult(txid); + } catch (const VotingError& e){ + return VotingResult(e.what()); + } +} + +// ----------------------------------------------------------------------------- +// Class: VoteResultItem +// ----------------------------------------------------------------------------- + +VoteResultItem::VoteResultItem(QString label, double votes, uint64_t weight) + : m_label(label) + , m_votes(votes) + , m_weight(weight) +{ +} + +bool VoteResultItem::operator<(const VoteResultItem& other) const +{ + return m_weight < other.m_weight; +} + +// ----------------------------------------------------------------------------- +// Class: VotingResult +// ----------------------------------------------------------------------------- + +VotingResult::VotingResult(const uint256& txid) + : m_value(QString::fromStdString(txid.ToString())) + , m_ok(true) +{ +} + +VotingResult::VotingResult(const QString& error) + : m_value(error) + , m_ok(false) +{ +} + +bool VotingResult::ok() const +{ + return m_ok; +} + +QString VotingResult::txid() const +{ + if (!m_ok) { + return QString::fromStdString(uint256().ToString()); + } + + return m_value; +} + +QString VotingResult::error() const +{ + if (m_ok) { + return QString(); + } + + return m_value; +} diff --git a/src/qt/voting/votingmodel.h b/src/qt/voting/votingmodel.h new file mode 100644 index 0000000000..dfa6263bf0 --- /dev/null +++ b/src/qt/voting/votingmodel.h @@ -0,0 +1,123 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_VOTINGMODEL_H +#define VOTING_VOTINGMODEL_H + +#include "amount.h" +#include "gridcoin/voting/filter.h" +#include "qt/voting/poll_types.h" + +#include +#include +#include + +namespace GRC { +class Poll; +class PollRegistry; +} + +QT_BEGIN_NAMESPACE +class QStringList; +QT_END_NAMESPACE + +class uint256; +class WalletModel; + +//! +//! \brief An aggregate result for one choice of a poll. +//! +class VoteResultItem +{ +public: + QString m_label; + double m_votes; + uint64_t m_weight; + + explicit VoteResultItem(QString label, double votes, uint64_t weight); + bool operator<(const VoteResultItem& other) const; +}; + +//! +//! \brief Represents a poll contract and associated responses. +//! +class PollItem +{ +public: + QString m_id; + QString m_title; + QString m_question; + QString m_url; + QDateTime m_start_time; + QDateTime m_expiration; + QString m_weight_type; + QString m_response_type; + QString m_top_answer; + uint32_t m_total_votes; + uint64_t m_total_weight; + bool m_finished; + bool m_multiple_choice; + std::vector m_choices; +}; + +//! +//! \brief A variant-like object that stores the result of an attempt to create +//! a poll or vote contract transaction. +//! +class VotingResult +{ +public: + explicit VotingResult(const uint256& txid); + explicit VotingResult(const QString& error); + + bool ok() const; + QString error() const; + QString txid() const; + +private: + QString m_value; + bool m_ok; +}; + +//! +//! \brief Presents voting information for UI components. +//! +class VotingModel : public QObject +{ + Q_OBJECT + +public: + VotingModel(WalletModel& wallet_model); + ~VotingModel(); + + static int minPollDurationDays(); + static int maxPollDurationDays(); + static int maxPollTitleLength(); + static int maxPollUrlLength(); + static int maxPollQuestionLength(); + static int maxPollChoiceLabelLength(); + + QStringList getActiveProjectNames() const; + std::vector buildPollTable(const GRC::PollFilterFlag flags) const; + + CAmount estimatePollFee() const; + + VotingResult sendPoll( + const QString& title, + const int duration_days, + const QString& question, + const QString& url, + const int weight_type, + const int response_type, + const QStringList& choices) const; + VotingResult sendVote( + const QString& poll_id, + const std::vector& choice_offsets) const; + +private: + GRC::PollRegistry& m_registry; + WalletModel& m_wallet_model; +}; // VotingModel + +#endif // VOTING_VOTINGMODEL_H From 00941d879c9f0ff2899d8bdbd031807a6966f731 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:39 -0500 Subject: [PATCH 3/9] Install GUIUtil::formatNiceTimeOffset() from Bitcoin This adds a function that formats a human-friendly time span from newer Bitcoin code modified to round the result time span half-up instead of flooring it. --- src/qt/guiutil.cpp | 46 +++++++++++++++++++++++++++++++ src/qt/guiutil.h | 2 ++ src/qt/locale/bitcoin_en.ts | 54 +++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 6e8d5e779b..4f9f9c176e 100755 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -60,6 +60,52 @@ QString formatTimeOffset(int64_t nTimeOffset) return QString(QObject::tr("%1 s")).arg(QString::number((int)nTimeOffset, 10)); } +QString formatNiceTimeOffset(qint64 secs) +{ + // Represent time from last generated block in human readable text + QString timeBehindText; + const int HOUR_IN_SECONDS = 60*60; + const int DAY_IN_SECONDS = 24*60*60; + const int WEEK_IN_SECONDS = 7*24*60*60; + const int YEAR_IN_SECONDS = 31556952; // Average length of year in Gregorian calendar + + constexpr auto round_half_up = [](int secs, int timeframe_secs) + { + return (secs + (timeframe_secs / 2)) / timeframe_secs; + }; + + if(secs < 60) + { + timeBehindText = QObject::tr("%n second(s)", "", secs); + } + else if(secs < 2*HOUR_IN_SECONDS) + { + timeBehindText = QObject::tr("%n minute(s)", "", round_half_up(secs, 60)); + } + else if(secs < 2*DAY_IN_SECONDS) + { + timeBehindText = QObject::tr("%n hour(s)", "", round_half_up(secs, HOUR_IN_SECONDS)); + } + else if(secs < 2*WEEK_IN_SECONDS) + { + timeBehindText = QObject::tr("%n day(s)", "", round_half_up(secs, DAY_IN_SECONDS)); + } + else if(secs < YEAR_IN_SECONDS) + { + timeBehindText = QObject::tr("%n week(s)", "", round_half_up(secs, WEEK_IN_SECONDS)); + } + else + { + qint64 years = secs / YEAR_IN_SECONDS; + qint64 remainder = secs % YEAR_IN_SECONDS; + timeBehindText = QObject::tr("%1 and %2") + .arg(QObject::tr("%n year(s)", "", years)) + .arg(QObject::tr("%n week(s)","", round_half_up(remainder, WEEK_IN_SECONDS))); + } + + return timeBehindText; +} + QString formatBytes(uint64_t bytes) { if(bytes < 1024) diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index c6f2cc8265..25b2520c53 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -30,6 +30,8 @@ namespace GUIUtil // Format Node Time Offset QString formatTimeOffset(int64_t nTimeOffset); + QString formatNiceTimeOffset(qint64 secs); + // Format Bytes QString formatBytes(uint64_t bytes); diff --git a/src/qt/locale/bitcoin_en.ts b/src/qt/locale/bitcoin_en.ts index 36dcc0e738..890b9e1d37 100644 --- a/src/qt/locale/bitcoin_en.ts +++ b/src/qt/locale/bitcoin_en.ts @@ -1981,6 +1981,60 @@ This label turns red, if the priority is smaller than "medium". %1 s + + + %n second(s) + + %n second + %n seconds + + + + + %n minute(s) + + %n minute + %n minutes + + + + + %n hour(s) + + %n hour + %n hours + + + + + %n day(s) + + %n day + %n days + + + + + + %n week(s) + + %n week + %n weeks + + + + + %1 and %2 + + + + + %n year(s) + + %n year + %n years + + %1 B From cc85f4c9393c124e9b761d04bac4bee8f9eaf863 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:42 -0500 Subject: [PATCH 4/9] Add core signal for new poll received events This adds a core signal that the poll registry broadcasts when a node receives a poll contract in a transaction so that the GUI can respond to the event. --- src/gridcoin/voting/registry.cpp | 11 +++++++ src/gridcoin/voting/registry.h | 7 +++++ src/qt/bitcoin.cpp | 10 ++++++ src/qt/voting/votingmodel.cpp | 52 +++++++++++++++++++++++++++++++- src/qt/voting/votingmodel.h | 14 ++++++++- src/ui_interface.h | 3 ++ 6 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/gridcoin/voting/registry.cpp b/src/gridcoin/voting/registry.cpp index 0e6dc42d06..345fd9b189 100644 --- a/src/gridcoin/voting/registry.cpp +++ b/src/gridcoin/voting/registry.cpp @@ -15,6 +15,8 @@ using namespace GRC; using LogFlags = BCLog::LogFlags; +extern bool fQtActive; + namespace { //! //! \brief Extract a poll title from a legacy vote contract. @@ -289,6 +291,11 @@ const std::vector& PollReference::Votes() const return m_votes; } +int64_t PollReference::Time() const +{ + return m_timestamp; +} + int64_t PollReference::Age(const int64_t now) const { return now - m_timestamp; @@ -460,6 +467,10 @@ void PollRegistry::AddPoll(const ContractContext& ctx) auto result_pair = m_polls_by_txid.emplace(ctx.m_tx.GetHash(), &poll_ref); poll_ref.m_ptxid = &result_pair.first->first; + + if (fQtActive && !poll_ref.Expired(GetAdjustedTime())) { + uiInterface.NewPollReceived(poll_ref.Time()); + } } } diff --git a/src/gridcoin/voting/registry.h b/src/gridcoin/voting/registry.h index 3a2d712f8a..c8c182c2cc 100644 --- a/src/gridcoin/voting/registry.h +++ b/src/gridcoin/voting/registry.h @@ -66,6 +66,13 @@ class PollReference //! const std::vector& Votes() const; + //! + //! \brief Get the timestamp of the poll transaction. + //! + //! \return Poll transaction timestamp in seconds. + //! + int64_t Time() const; + //! //! \brief Get the elapsed time since poll creation. //! diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index bb4a853328..10f40c93b7 100755 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -71,6 +71,15 @@ extern bool bGridcoinCoreInitComplete; static BitcoinGUI *guiref; static QSplashScreen *splashref; +static void RegisterMetaTypes() +{ + // Register meta types used for QMetaObject::invokeMethod and Qt::QueuedConnection + // (...Gridcoin has none yet...) + + // Register typedefs (see https://doc.qt.io/qt-5/qmetatype.html#qRegisterMetaType) + qRegisterMetaType("int64_t"); +} + int StartGridcoinQt(int argc, char *argv[], QApplication& app, OptionsModel& optionsModel); static void SetupUIArgs(ArgsManager& argsman) @@ -288,6 +297,7 @@ int main(int argc, char *argv[]) Q_INIT_RESOURCE(bitcoin); Q_INIT_RESOURCE(bitcoin_locale); + RegisterMetaTypes(); QApplication app(argc, argv); #if defined(WIN32) && defined(QT_GUI) diff --git a/src/qt/voting/votingmodel.cpp b/src/qt/voting/votingmodel.cpp index dbc83d2764..8c51db0031 100644 --- a/src/qt/voting/votingmodel.cpp +++ b/src/qt/voting/votingmodel.cpp @@ -11,15 +11,29 @@ #include "gridcoin/voting/poll.h" #include "gridcoin/voting/registry.h" #include "gridcoin/voting/result.h" +#include "qt/clientmodel.h" #include "qt/voting/votingmodel.h" #include "qt/walletmodel.h" #include "sync.h" +#include "ui_interface.h" using namespace GRC; +using LogFlags = BCLog::LogFlags; extern CCriticalSection cs_main; namespace { +//! +//! \brief Model callback bound to the \c NewPollReceived core signal. +//! +void NewPollReceived(VotingModel* model, int64_t poll_time) +{ + LogPrint(LogFlags::QT, "GUI: received NewPollReceived() core signal"); + + QMetaObject::invokeMethod(model, "handleNewPoll", Qt::QueuedConnection, + Q_ARG(int64_t, poll_time)); +} + std::optional BuildPollItem(const PollRegistry::Sequence::Iterator& iter) { const PollResultOption result = PollResult::BuildFor(iter->Ref()); @@ -67,14 +81,29 @@ std::optional BuildPollItem(const PollRegistry::Sequence::Iterator& it // Class: VotingModel // ----------------------------------------------------------------------------- -VotingModel::VotingModel(WalletModel& wallet_model) +VotingModel::VotingModel(ClientModel& client_model, WalletModel& wallet_model) : m_registry(GetPollRegistry()) + , m_client_model(client_model) , m_wallet_model(wallet_model) + , m_last_poll_time(0) { + subscribeToCoreSignals(); + + // The voting model is constructed after core init finishes. Remember the + // time of the most recent active poll found on start-up to avoid showing + // notifications for these if the node reorganizes the chain: + { + LOCK(cs_main); + + for (const auto iter : m_registry.Polls().OnlyActive()) { + m_last_poll_time = std::max(m_last_poll_time, iter->Ref().Time()); + } + } } VotingModel::~VotingModel() { + unsubscribeFromCoreSignals(); } int VotingModel::minPollDurationDays() @@ -241,6 +270,27 @@ VotingResult VotingModel::sendVote( } } +void VotingModel::subscribeToCoreSignals() +{ + uiInterface.NewPollReceived.connect(boost::bind(NewPollReceived, this, boost::placeholders::_1)); +} + +void VotingModel::unsubscribeFromCoreSignals() +{ + uiInterface.NewPollReceived.disconnect(boost::bind(NewPollReceived, this, boost::placeholders::_1)); +} + +void VotingModel::handleNewPoll(int64_t poll_time) +{ + if (poll_time <= m_last_poll_time || m_client_model.inInitialBlockDownload()) { + return; + } + + m_last_poll_time = poll_time; + + emit newPollReceived(); +} + // ----------------------------------------------------------------------------- // Class: VoteResultItem // ----------------------------------------------------------------------------- diff --git a/src/qt/voting/votingmodel.h b/src/qt/voting/votingmodel.h index dfa6263bf0..6a6ffb4e4c 100644 --- a/src/qt/voting/votingmodel.h +++ b/src/qt/voting/votingmodel.h @@ -22,6 +22,7 @@ QT_BEGIN_NAMESPACE class QStringList; QT_END_NAMESPACE +class ClientModel; class uint256; class WalletModel; @@ -88,7 +89,7 @@ class VotingModel : public QObject Q_OBJECT public: - VotingModel(WalletModel& wallet_model); + VotingModel(ClientModel& client_model, WalletModel& wallet_model); ~VotingModel(); static int minPollDurationDays(); @@ -115,9 +116,20 @@ class VotingModel : public QObject const QString& poll_id, const std::vector& choice_offsets) const; +signals: + void newPollReceived() const; + private: GRC::PollRegistry& m_registry; + ClientModel& m_client_model; WalletModel& m_wallet_model; + int64_t m_last_poll_time; + + void subscribeToCoreSignals(); + void unsubscribeFromCoreSignals(); + +private slots: + void handleNewPoll(int64_t poll_time); }; // VotingModel #endif // VOTING_VOTINGMODEL_H diff --git a/src/ui_interface.h b/src/ui_interface.h index 66dc144f5e..241979bc41 100644 --- a/src/ui_interface.h +++ b/src/ui_interface.h @@ -105,6 +105,9 @@ class CClientUIInterface /** Beacon changed */ boost::signals2::signal BeaconChanged; + /** New poll received **/ + boost::signals2::signal NewPollReceived; + /** * New, updated or cancelled alert. * @note called with lock cs_mapAlerts held. From 265b2c8e218b651f8150a60a74c4959dbf7a9af4 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:44 -0500 Subject: [PATCH 5/9] Create a "new poll" wizard This adds a wizard that will replace the old "create poll" dialog. It provides a more structured process for poll creation. The wizard will present the set of poll types to the user based on community-approved guidelines and request information or disable fields for the selected type. --- gridcoinresearch.pro | 15 + src/Makefile.qt.include | 29 +- src/gridcoin/voting/builders.cpp | 8 +- src/gridcoin/voting/builders.h | 8 +- src/qt/forms/voting/pollwizard.ui | 67 ++++ src/qt/forms/voting/pollwizarddetailspage.ui | 337 +++++++++++++++++++ src/qt/forms/voting/pollwizardprojectpage.ui | 235 +++++++++++++ src/qt/forms/voting/pollwizardsummarypage.ui | 162 +++++++++ src/qt/forms/voting/pollwizardtypepage.ui | 115 +++++++ src/qt/res/stylesheets/dark_stylesheet.qss | 114 ++++++- src/qt/res/stylesheets/light_stylesheet.qss | 103 +++++- src/qt/voting/poll_types.cpp | 69 ++++ src/qt/voting/poll_types.h | 34 ++ src/qt/voting/pollwizard.cpp | 33 ++ src/qt/voting/pollwizard.h | 39 +++ src/qt/voting/pollwizarddetailspage.cpp | 312 +++++++++++++++++ src/qt/voting/pollwizarddetailspage.h | 44 +++ src/qt/voting/pollwizardprojectpage.cpp | 113 +++++++ src/qt/voting/pollwizardprojectpage.h | 35 ++ src/qt/voting/pollwizardsummarypage.cpp | 42 +++ src/qt/voting/pollwizardsummarypage.h | 31 ++ src/qt/voting/pollwizardtypepage.cpp | 79 +++++ src/qt/voting/pollwizardtypepage.h | 38 +++ src/qt/voting/votingmodel.cpp | 11 +- src/qt/voting/votingmodel.h | 8 +- 25 files changed, 2047 insertions(+), 34 deletions(-) create mode 100644 src/qt/forms/voting/pollwizard.ui create mode 100644 src/qt/forms/voting/pollwizarddetailspage.ui create mode 100644 src/qt/forms/voting/pollwizardprojectpage.ui create mode 100644 src/qt/forms/voting/pollwizardsummarypage.ui create mode 100644 src/qt/forms/voting/pollwizardtypepage.ui create mode 100644 src/qt/voting/poll_types.cpp create mode 100644 src/qt/voting/poll_types.h create mode 100644 src/qt/voting/pollwizard.cpp create mode 100644 src/qt/voting/pollwizard.h create mode 100644 src/qt/voting/pollwizarddetailspage.cpp create mode 100644 src/qt/voting/pollwizarddetailspage.h create mode 100644 src/qt/voting/pollwizardprojectpage.cpp create mode 100644 src/qt/voting/pollwizardprojectpage.h create mode 100644 src/qt/voting/pollwizardsummarypage.cpp create mode 100644 src/qt/voting/pollwizardsummarypage.h create mode 100644 src/qt/voting/pollwizardtypepage.cpp create mode 100644 src/qt/voting/pollwizardtypepage.h diff --git a/gridcoinresearch.pro b/gridcoinresearch.pro index 3e3a43da63..67843dc658 100755 --- a/gridcoinresearch.pro +++ b/gridcoinresearch.pro @@ -186,6 +186,11 @@ HEADERS += src/qt/bitcoingui.h \ src/qt/researcher/researcherwizardprojectspage.h \ src/qt/researcher/researcherwizardsummarypage.h \ src/qt/voting/polltablemodel.h \ + src/qt/voting/pollwizard.h \ + src/qt/voting/pollwizarddetailspage.h \ + src/qt/voting/pollwizardprojectpage.h \ + src/qt/voting/pollwizardsummarypage.h \ + src/qt/voting/pollwizardtypepage.h \ src/qt/voting/votingmodel.h \ src/qt/transactiontablemodel.h \ src/qt/addresstablemodel.h \ @@ -295,6 +300,11 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/researcher/researcherwizardprojectspage.cpp \ src/qt/researcher/researcherwizardsummarypage.cpp \ src/qt/voting/polltablemodel.cpp \ + src/qt/voting/pollwizard.cpp \ + src/qt/voting/pollwizarddetailspage.cpp \ + src/qt/voting/pollwizardprojectpage.cpp \ + src/qt/voting/pollwizardsummarypage.cpp \ + src/qt/voting/pollwizardtypepage.cpp \ src/qt/voting/votingmodel.cpp \ src/qt/transactiontablemodel.cpp \ src/qt/addresstablemodel.cpp \ @@ -399,6 +409,11 @@ FORMS += \ src/qt/forms/researcherwizardpoolsummarypage.ui \ src/qt/forms/researcherwizardprojectspage.ui \ src/qt/forms/researcherwizardsummarypage.ui \ + src/qt/forms/voting/pollwizard.ui \ + src/qt/forms/voting/pollwizarddetailspage.ui \ + src/qt/forms/voting/pollwizardprojectpage.ui \ + src/qt/forms/voting/pollwizardsummarypage.ui \ + src/qt/forms/voting/pollwizardtypepage.ui \ src/qt/forms/receivecoinspage.ui \ src/qt/forms/sendcoinsdialog.ui \ src/qt/forms/favoritespage.ui \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 5319ba3a83..99c12e956e 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -102,6 +102,11 @@ QT_FORMS_UI = \ qt/forms/researcherwizardsummarypage.ui \ qt/forms/sendcoinsdialog.ui \ qt/forms/transactiondescdialog.ui \ + qt/forms/voting/pollwizard.ui \ + qt/forms/voting/pollwizarddetailspage.ui \ + qt/forms/voting/pollwizardprojectpage.ui \ + qt/forms/voting/pollwizardsummarypage.ui \ + qt/forms/voting/pollwizardtypepage.ui \ qt/forms/askpassphrasedialog.ui \ qt/forms/sendcoinsentry.ui \ qt/forms/intro.ui @@ -168,6 +173,11 @@ QT_MOC_CPP = \ qt/researcher/moc_researcherwizardprojectspage.cpp \ qt/researcher/moc_researcherwizardsummarypage.cpp \ qt/voting/moc_polltablemodel.cpp \ + qt/voting/moc_pollwizard.cpp \ + qt/voting/moc_pollwizarddetailspage.cpp \ + qt/voting/moc_pollwizardprojectpage.cpp \ + qt/voting/moc_pollwizardsummarypage.cpp \ + qt/voting/moc_pollwizardtypepage.cpp \ qt/voting/moc_votingmodel.cpp GRIDCOIN_MM = \ @@ -178,7 +188,8 @@ GRIDCOIN_MM = \ QT_MOC = \ qt/intro.moc \ qt/overviewpage.moc \ - qt/rpcconsole.moc + qt/rpcconsole.moc \ + qt/voting/pollwizarddetailspage.moc QT_QRC_CPP = qt/qrc_bitcoin.cpp QT_QRC = qt/bitcoin.qrc @@ -251,9 +262,15 @@ GRIDCOINRESEARCH_QT_H = \ qt/transactiontablemodel.h \ qt/transactionview.h \ qt/upgradeqt.h \ - qt/votingdialog.h \ + qt/voting/poll_types.h \ qt/voting/polltablemodel.h \ + qt/voting/pollwizard.h \ + qt/voting/pollwizarddetailspage.h \ + qt/voting/pollwizardprojectpage.h \ + qt/voting/pollwizardsummarypage.h \ + qt/voting/pollwizardtypepage.h \ qt/voting/votingmodel.h \ + qt/votingdialog.h \ qt/walletmodel.h \ qt/winshutdownmonitor.h @@ -319,9 +336,15 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/transactiontablemodel.cpp \ qt/transactionview.cpp \ qt/upgradeqt.cpp \ - qt/votingdialog.cpp \ + qt/voting/poll_types.cpp \ qt/voting/polltablemodel.cpp \ + qt/voting/pollwizard.cpp \ + qt/voting/pollwizarddetailspage.cpp \ + qt/voting/pollwizardprojectpage.cpp \ + qt/voting/pollwizardsummarypage.cpp \ + qt/voting/pollwizardtypepage.cpp \ qt/voting/votingmodel.cpp \ + qt/votingdialog.cpp \ qt/walletmodel.cpp \ qt/winshutdownmonitor.cpp diff --git a/src/gridcoin/voting/builders.cpp b/src/gridcoin/voting/builders.cpp index 30acae1cfb..b0315ada20 100644 --- a/src/gridcoin/voting/builders.cpp +++ b/src/gridcoin/voting/builders.cpp @@ -840,7 +840,7 @@ void SelectFinalInputs(CWallet& wallet, CWalletTx& tx) // Global Functions // ----------------------------------------------------------------------------- -void GRC::SendPollContract(PollBuilder builder) +uint256 GRC::SendPollContract(PollBuilder builder) { std::pair result_pair; @@ -852,9 +852,11 @@ void GRC::SendPollContract(PollBuilder builder) if (!result_pair.second.empty()) { throw VotingError(result_pair.second); } + + return result_pair.first.GetHash(); } -void GRC::SendVoteContract(VoteBuilder builder) +uint256 GRC::SendVoteContract(VoteBuilder builder) { std::pair result_pair; @@ -866,6 +868,8 @@ void GRC::SendVoteContract(VoteBuilder builder) if (!result_pair.second.empty()) { throw VotingError(result_pair.second); } + + return result_pair.first.GetHash(); } // ----------------------------------------------------------------------------- diff --git a/src/gridcoin/voting/builders.h b/src/gridcoin/voting/builders.h index 3ba015c0e1..5d288cb092 100644 --- a/src/gridcoin/voting/builders.h +++ b/src/gridcoin/voting/builders.h @@ -307,10 +307,12 @@ class VoteBuilder //! \param builder An initialized poll builder instance to create the poll //! contract from. //! +//! \return The hash of the transaction that contains the new poll. +//! //! \throws VotingError If the constructed vote is malformed or the transaction //! fails to send. //! -void SendPollContract(PollBuilder builder); +uint256 SendPollContract(PollBuilder builder); //! //! \brief Send a transaction that contains a vote contract. @@ -322,8 +324,10 @@ void SendPollContract(PollBuilder builder); //! \param builder An initialized vote builder instance to create the vote //! contract from. //! +//! \return The hash of the transaction that contains the vote. +//! //! \throws VotingError If the constructed vote is malformed or the transaction //! fails to send. //! -void SendVoteContract(VoteBuilder builder); +uint256 SendVoteContract(VoteBuilder builder); } diff --git a/src/qt/forms/voting/pollwizard.ui b/src/qt/forms/voting/pollwizard.ui new file mode 100644 index 0000000000..bb321fad22 --- /dev/null +++ b/src/qt/forms/voting/pollwizard.ui @@ -0,0 +1,67 @@ + + + PollWizard + + + + 0 + 0 + 500 + 360 + + + + + 0 + 0 + + + + Create a Poll + + + true + + + true + + + QWizard::ClassicStyle + + + QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage|QWizard::NoCancelButtonOnLastPage + + + + + + + + + PollWizardTypePage + QWizardPage +
voting/pollwizardtypepage.h
+ 1 +
+ + PollWizardProjectPage + QWizardPage +
voting/pollwizardprojectpage.h
+ 1 +
+ + PollWizardDetailsPage + QWizardPage +
voting/pollwizarddetailspage.h
+ 1 +
+ + PollWizardSummaryPage + QWizardPage +
voting/pollwizardsummarypage.h
+ 1 +
+
+ + +
diff --git a/src/qt/forms/voting/pollwizarddetailspage.ui b/src/qt/forms/voting/pollwizarddetailspage.ui new file mode 100644 index 0000000000..4073797158 --- /dev/null +++ b/src/qt/forms/voting/pollwizarddetailspage.ui @@ -0,0 +1,337 @@ + + + PollWizardDetailsPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + + 9 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + Poll Details + + + + + + + Qt::Horizontal + + + + + + + Some fields are locked for the selected poll type. + + + + + + + true + + + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + 12 + + + 12 + + + + + Poll Type: + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Duration: + + + durationField + + + + + + + days + + + + + + + + + Title: + + + titleField + + + + + + + + + + Question: + + + questionField + + + + + + + + + + Discussion URL: + + + urlField + + + + + + + 3 + + + + + + + + A link to the main discussion thread on GitHub or Reddit. + + + true + + + + + + + + + Weight Type: + + + weightTypeList + + + + + + + + + + Response Type: + + + responseTypeList + + + + + + + + + + Choices: + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + A poll with a yes/no/abstain response type cannot include any additional custom choices. + + + true + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + true + + + QAbstractItemView::InternalMove + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + durationField + titleField + questionField + urlField + weightTypeList + responseTypeList + + + + diff --git a/src/qt/forms/voting/pollwizardprojectpage.ui b/src/qt/forms/voting/pollwizardprojectpage.ui new file mode 100644 index 0000000000..344cadc2e4 --- /dev/null +++ b/src/qt/forms/voting/pollwizardprojectpage.ui @@ -0,0 +1,235 @@ + + + PollWizardProjectPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + + 9 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + Project Listing Proposal + + + + + + + Qt::Horizontal + + + + + + + Add an unlisted project + + + + + + + Remove a listed project + + + + + + + + 0 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + + + Proposals must follow community guidelines for validation. Please review the wiki and verify that the prequisites have been fulfilled: + + + true + + + + + + + 0 + + + + + + + + + 1 + 0 + + + + <a href="https://gridcoin.us/wiki/whitelist-process">https://gridcoin.us/wiki/whitelist-process</a> + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse + + + + + + + + + Qt::Horizontal + + + + + + + 20 + + + + + Project Name: + + + projectNameField + + + + + + + + + + This project satisfies the Gridcoin listing criteria. + + + + + + + + + + + + + 0 + 1 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + + + Choose a project to delist: + + + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + diff --git a/src/qt/forms/voting/pollwizardsummarypage.ui b/src/qt/forms/voting/pollwizardsummarypage.ui new file mode 100644 index 0000000000..08706cba11 --- /dev/null +++ b/src/qt/forms/voting/pollwizardsummarypage.ui @@ -0,0 +1,162 @@ + + + PollWizardSummaryPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + + 9 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 60 + + + + + + + + :/icons/round_green_check + + + + + + + Poll Created + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 24 + + + + + + + + The poll will activate with the next block. + + + Qt::AlignCenter + + + true + + + + + + + Qt::AlignCenter + + + true + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Copy ID + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/qt/forms/voting/pollwizardtypepage.ui b/src/qt/forms/voting/pollwizardtypepage.ui new file mode 100644 index 0000000000..4a56fb3273 --- /dev/null +++ b/src/qt/forms/voting/pollwizardtypepage.ui @@ -0,0 +1,115 @@ + + + PollWizardTypePage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + + 9 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + Create a Poll + + + + + + + The Gridcoin community established guidelines for polls with requirements for each type. Please read the wiki for more information: + + + true + + + + + + + 0 + + + + + + + + + 1 + 0 + + + + <a href="https://gridcoin.us/wiki/voting.html">https://gridcoin.us/wiki/voting.html</a> + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse + + + + + + + + + Qt::Horizontal + + + + + + + Choose a poll type: + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + diff --git a/src/qt/res/stylesheets/dark_stylesheet.qss b/src/qt/res/stylesheets/dark_stylesheet.qss index 91e7f4ea32..a49411c7db 100644 --- a/src/qt/res/stylesheets/dark_stylesheet.qss +++ b/src/qt/res/stylesheets/dark_stylesheet.qss @@ -209,26 +209,26 @@ QGroupBox::title { font-weight: bold; } -QListWidget, +QListView, QTableView, QTreeWidget { border: 0.065em solid rgb(58, 70, 94); border-radius: 0.26em; } -QListWidget::item, +QListView::item, QTableView::item { background-color: transparent; border: none; } -QListWidget::item { +QListView::item { height: 1.5em; padding-left: 0.5em; padding-right: 0.5em; } -QListWidget QScrollBar:horizontal, +QListView QScrollBar:horizontal, QTableView QScrollBar:horizontal, QTreeWidget QScrollBar:horizontal { border-top: 0.065em solid rgb(33, 44, 58); @@ -236,7 +236,7 @@ QTreeWidget QScrollBar:horizontal { border-top-right-radius: 0; } -QListWidget QScrollBar:vertical, +QListView QScrollBar:vertical, QTableView QScrollBar:vertical, QTreeWidget QScrollBar:vertical { border-left: 0.065em solid rgb(33, 44, 58); @@ -295,13 +295,10 @@ QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover, QLineEdit:focus, QPlainTextEdit:focus, -QSpinBox, +QSpinBox:focus, QSpinBox::up-button:hover, QSpinBox::down-button:hover, QTextEdit:focus, -QToolButton:hover, -QToolButton:focus, -QToolButton:selected, #headerFrame QLineEdit:focus { border-color: rgb(21, 126, 205); color: rgb(195, 199, 201); @@ -335,6 +332,8 @@ QDateTimeEdit::down-arrow { } QComboBox QAbstractItemView { + background: transparent; + border: none; selection-background-color: rgb(26, 145, 235); selection-color: white; } @@ -370,6 +369,33 @@ QToolButton { padding: 0.25em; } +QCommandLinkButton { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(18, 41, 58), stop: 1 rgb(14, 29, 42)); + border: 0.065em solid rgb(58, 70, 94); + color: rgb(195, 199, 201); +} + +QCommandLinkButton:hover, +QToolButton:hover, +QToolButton:focus, +QToolButton:selected { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(3, 43, 72), stop: 1 rgb(1, 20, 35)); + border-color: rgb(21, 126, 205); + color: rgb(225, 241, 253); +} + +QCommandLinkButton:pressed, +QToolButton:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(1, 20, 35), stop: 1 rgb(3, 43, 72)); +} + +QCommandLinkButton:checked, +QToolButton:checked { + background: rgb(3, 43, 72); + border-color: rgb(21, 126, 205); + color: rgb(225, 241, 253); +} + ClickLabel { color: palette(link); text-decoration: underline; @@ -708,6 +734,7 @@ NoResult #titleLabel { #listTransactions { background: none; + border: none; } #listTransactions::item { @@ -794,6 +821,75 @@ TransactionView #filterFrame { padding: 0.5em 1em; } +/* Voting page */ + +PollWizard #pageTitleLabel, +PollWizardTypePage #typeTextLabel { + font-weight: bold; + color: white; +} + +PollResultChoiceItem QProgressBar { + background: rgba(0, 0, 0, 0.2); + border: none; + border-radius: 0.25em; + min-height: 0.5em; + max-height: 0.5em; + padding: 0; +} + +PollWizardDetailsPage #pollTypeAlert { + background: rgba(255, 240, 0, 0.4); + border-radius: 0.25em; + padding: 0.1em 0.2em; + color: white; +} + +PollWizardDetailsPage #errorLabel { + background: rgba(255, 0, 0, 0.4); + border-radius: 0.25em; + padding: 0.1em 0.2em; + color: white; +} + +PollWizardDetailsPage #pollTypeLabel { + background: rgb(75, 70, 80); + border-radius: 0.25em; + padding: 0.1em 0.2em; + color: white; + font-weight: bold; +} + +PollWizardDetailsPage #choicesFrame { + border: 0.065em solid rgb(58, 70, 94); + border-radius: 0.26em; + padding: 0; +} + +PollWizardDetailsPage #choicesList { + border: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +PollWizardDetailsPage #choicesButtonFrame { + border-top: 0.065em solid rgb(58, 70, 94); + border-top-left-radius: 0; + border-top-right-radius: 0; + padding: 0; +} + +PollWizardDetailsPage #choicesButtonFrame QToolButton { + border: none; + border-right: 0.065em solid rgb(58, 70, 94); + border-radius: 0; + padding: 0.1em 0.25em; +} + +PollWizardDetailsPage #choicesButtonFrame QToolButton[firstChild=true] { + border-bottom-left-radius: 0.26em; +} + /* options dialog */ #OptionsDialog #statusLabel{ diff --git a/src/qt/res/stylesheets/light_stylesheet.qss b/src/qt/res/stylesheets/light_stylesheet.qss index aa269e8709..1b7774cbbd 100644 --- a/src/qt/res/stylesheets/light_stylesheet.qss +++ b/src/qt/res/stylesheets/light_stylesheet.qss @@ -207,7 +207,7 @@ QGroupBox::title { font-weight: bold; } -QListWidget, +QListView, QTableView, QTreeWidget { background-color: white; @@ -215,19 +215,19 @@ QTreeWidget { border-radius: 0.26em; } -QListWidget::item, +QListView::item, QTableView::item { background-color: transparent; border: none; } -QListWidget::item { +QListView::item { height: 1.5em; padding-left: 0.5em; padding-right: 0.5em; } -QListWidget QScrollBar:horizontal, +QListView QScrollBar:horizontal, QTableView QScrollBar:horizontal, QTreeWidget QScrollBar:horizontal { border-top: 0.065em solid rgb(194, 199, 205); @@ -235,7 +235,7 @@ QTreeWidget QScrollBar:horizontal { border-top-right-radius: 0; } -QListWidget QScrollBar:vertical, +QListView QScrollBar:vertical, QTableView QScrollBar:vertical, QTreeWidget QScrollBar:vertical { border-left: 0.065em solid rgb(194, 199, 205); @@ -295,10 +295,11 @@ QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover, QLineEdit:focus, QPlainTextEdit:focus, +QPushButton:checked, QPushButton:hover, QPushButton:focus, QPushButton:selected, -QSpinBox, +QSpinBox:focus, QSpinBox::up-button:hover, QSpinBox::down-button:hover, QTextEdit:focus, @@ -310,27 +311,29 @@ QToolButton:selected, color: rgb(88, 92, 107); } +QComboBox, +QPushButton, +QDoubleSpinBox::down-button, +QSpinBox::down-button, +QToolButton { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(255, 255, 255), stop: 1 rgb(244, 245, 249)); +} + QComboBox:disabled, QDateTimeEdit:disabled, QDoubleSpinBox:disabled, +QDoubleSpinBox::down-button:disabled, QLineEdit:disabled, QPlainTextEdit:disabled, QPushButton:disabled, QSpinBox:disabled, +QSpinBox::down-button:disabled, QTextEdit:disabled, QToolButton:disabled { background-color: rgb(240, 240, 240); color: rgb(130, 130, 130); } -QComboBox, -QPushButton, -QDoubleSpinBox::down-button, -QSpinBox::down-button, -QToolButton { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(255, 255, 255), stop: 1 rgb(244, 245, 249)); -} - QComboBox:hover, QComboBox:focus, QPushButton:hover, @@ -359,12 +362,14 @@ QDateTimeEdit::down-arrow { image: url(:/icons/light_chevron_down); } -QComboBox:selected { +QComboBox:selected, +QPushButton:checked { background-color: rgb(250, 240, 255); } QComboBox QAbstractItemView { - background-color: white; + background: transparent; + border: none; color: rgb(70, 73, 85); } @@ -378,6 +383,10 @@ QToolButton { color: rgb(97, 101, 118); } +QToolButton:hover { + color: rgb(120, 20, 255); +} + ClickLabel { color: palette(link); text-decoration: underline; @@ -705,6 +714,7 @@ NoResult #titleLabel { #listTransactions { background: none; + border: none; } #listTransactions::item { @@ -791,6 +801,67 @@ TransactionView #filterFrame { padding: 0.5em 1em; } +/* Voting page */ + +PollWizard #pageTitleLabel, +PollWizardTypePage #typeTextLabel { + color: rgb(43, 52, 69); + font-weight: bold; +} + +PollWizardDetailsPage #pollTypeAlert { + background: rgba(255, 240, 0, 0.6); + border-radius: 0.25em; + padding: 0.1em 0.2em; + color: black; +} + +PollWizardDetailsPage #errorLabel { + background: rgba(255, 0, 0, 0.4); + border-radius: 0.25em; + padding: 0.1em 0.2em; + color: black; +} + +PollWizardDetailsPage #pollTypeLabel { + background-color: rgb(75, 70, 80); + border-radius: 0.25em; + padding: 0.1em 0.2em; + color: white; + font-weight: bold; +} + +PollWizardDetailsPage #choicesFrame { + border: 0.065em solid rgb(194, 199, 205); + border-radius: 0.26em; + padding: 0; +} + +PollWizardDetailsPage #choicesList { + border: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +PollWizardDetailsPage #choicesButtonFrame { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(255, 255, 255), stop: 1 rgb(244, 245, 249)); + border-top: 0.065em solid rgb(194, 199, 205); + border-top-left-radius: 0; + border-top-right-radius: 0; + padding: 0; +} + +PollWizardDetailsPage #choicesButtonFrame QToolButton { + border: none; + border-right: 0.065em solid rgb(194, 199, 205); + border-radius: 0; + padding: 0.1em 0.25em; +} + +PollWizardDetailsPage #choicesButtonFrame QToolButton[firstChild=true] { + border-bottom-left-radius: 0.26em; +} + /* options dialog */ #OptionsDialog #statusLabel{ diff --git a/src/qt/voting/poll_types.cpp b/src/qt/voting/poll_types.cpp new file mode 100644 index 0000000000..8144ce6a45 --- /dev/null +++ b/src/qt/voting/poll_types.cpp @@ -0,0 +1,69 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/voting/poll_types.h" + +#include + +namespace { +struct PollTypeDefinition +{ + const char* m_name; + const char* m_description; + int m_min_duration_days; +}; + +const PollTypeDefinition g_poll_types[] = { + { "Unknown", "Unknown", 0 }, + { + QT_TRANSLATE_NOOP("PollTypes", "Project Listing"), + QT_TRANSLATE_NOOP("PollTypes", "Propose additions or removals of computing projects for research reward eligibility."), + 21, // min duration days + }, + { + QT_TRANSLATE_NOOP("PollTypes", "Protocol Development"), + QT_TRANSLATE_NOOP("PollTypes", "Propose a change to Gridcoin at the protocol level."), + 42, // min duration days + }, + { + QT_TRANSLATE_NOOP("PollTypes", "Governance"), + QT_TRANSLATE_NOOP("PollTypes", "Proposals related to Gridcoin management like poll requirements and funding."), + 21, // min duration days + }, + { + QT_TRANSLATE_NOOP("PollTypes", "Marketing"), + QT_TRANSLATE_NOOP("PollTypes", "Propose marketing initiatives like ad campaigns."), + 21, // min duration days + }, + { + QT_TRANSLATE_NOOP("PollTypes", "Outreach"), + QT_TRANSLATE_NOOP("PollTypes", "For polls about community representation, public relations, and communications."), + 21, // min duration days + }, + { + QT_TRANSLATE_NOOP("PollTypes", "Community"), + QT_TRANSLATE_NOOP("PollTypes", "For other initiatives related to the Gridcoin community."), + 21, // min duration days + }, + { + QT_TRANSLATE_NOOP("PollTypes", "Survey"), + QT_TRANSLATE_NOOP("PollTypes", "For opinion or casual polls without any particular requirements."), + 7, // min duration days + }, +}; +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: PollTypes +// ----------------------------------------------------------------------------- + +PollTypes::PollTypes() +{ + for (const auto& type : g_poll_types) { + emplace_back(); + back().m_name = QCoreApplication::translate("PollTypes", type.m_name); + back().m_description = QCoreApplication::translate("PollTypes", type.m_description); + back().m_min_duration_days = type.m_min_duration_days; + } +} diff --git a/src/qt/voting/poll_types.h b/src/qt/voting/poll_types.h new file mode 100644 index 0000000000..faf692a84f --- /dev/null +++ b/src/qt/voting/poll_types.h @@ -0,0 +1,34 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#pragma once + +#include +#include + +class PollTypeItem +{ +public: + QString m_name; + QString m_description; + int m_min_duration_days; +}; + +class PollTypes : public std::vector +{ +public: + enum PollType + { + PollTypeUnknown, + PollTypeProject, + PollTypeDevelopment, + PollTypeGovernance, + PollTypeMarketing, + PollTypeOutreach, + PollTypeCommunity, + PollTypeSurvey, + }; + + PollTypes(); +}; diff --git a/src/qt/voting/pollwizard.cpp b/src/qt/voting/pollwizard.cpp new file mode 100644 index 0000000000..0b0d5e6b84 --- /dev/null +++ b/src/qt/voting/pollwizard.cpp @@ -0,0 +1,33 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_pollwizard.h" +#include "qt/voting/pollwizard.h" +#include "qt/voting/votingmodel.h" + +// ----------------------------------------------------------------------------- +// Class: PollWizard +// ----------------------------------------------------------------------------- + +PollWizard::PollWizard(VotingModel& voting_model, QWidget* parent) + : QWizard(parent) + , ui(new Ui::PollWizard) + , m_poll_types(new PollTypes()) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + resize(GRC::ScaleSize(this, 670, 580)); + + ui->typePage->setPollTypes(m_poll_types.get()); + ui->projectPage->setModel(&voting_model); + ui->detailsPage->setModel(&voting_model); + ui->detailsPage->setPollTypes(m_poll_types.get()); +} + +PollWizard::~PollWizard() +{ + delete ui; +} diff --git a/src/qt/voting/pollwizard.h b/src/qt/voting/pollwizard.h new file mode 100644 index 0000000000..4c054644e1 --- /dev/null +++ b/src/qt/voting/pollwizard.h @@ -0,0 +1,39 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLWIZARD_H +#define VOTING_POLLWIZARD_H + +#include +#include + +namespace Ui { +class PollWizard; +} + +class PollTypes; +class VotingModel; + +class PollWizard : public QWizard +{ + Q_OBJECT + +public: + enum Pages + { + PageType, + PageProject, + PageDetails, + PageSummary, + }; + + explicit PollWizard(VotingModel& voting_model, QWidget* parent = nullptr); + ~PollWizard(); + +private: + Ui::PollWizard* ui; + std::unique_ptr m_poll_types; +}; + +#endif // VOTING_POLLWIZARD_H diff --git a/src/qt/voting/pollwizarddetailspage.cpp b/src/qt/voting/pollwizarddetailspage.cpp new file mode 100644 index 0000000000..c3b29c9f1a --- /dev/null +++ b/src/qt/voting/pollwizarddetailspage.cpp @@ -0,0 +1,312 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/bitcoinunits.h" +#include "qt/decoration.h" +#include "qt/forms/voting/ui_pollwizarddetailspage.h" +#include "qt/optionsmodel.h" +#include "qt/voting/pollwizard.h" +#include "qt/voting/pollwizarddetailspage.h" +#include "qt/voting/votingmodel.h" + +#include +#include +#include + +namespace { +//! +//! \brief Applies custom appearance and behavior to items in the poll choices +//! editor. +//! +class ChoicesListDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + ChoicesListDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) + { + } + + QWidget* createEditor( + QWidget* parent, + const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + QWidget* editor = QStyledItemDelegate::createEditor(parent, option, index); + + if (QLineEdit* line_edit = qobject_cast(editor)) { + line_edit->setMaxLength(VotingModel::maxPollChoiceLabelLength()); + } + + return editor; + } + +private: + void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override + { + QStyledItemDelegate::initStyleOption(option, index); + + // Display a row number before each choice label: + option->text = QStringLiteral("%1. %2").arg(index.row() + 1).arg(option->text); + } +}; // ChoicesListDelegate + +//! +//! \brief Provides for QWizardPage::registerField() without a real widget. +//! +struct DummyField : public QWidget +{ + DummyField(QWidget* parent = nullptr) : QWidget(parent) { hide(); } +}; +} // Anonymous namespace + +//! +//! \brief Manages the set of choices for the poll choices editor. +//! +class ChoicesListModel : public QStringListModel +{ + Q_OBJECT + +public: + ChoicesListModel(QObject* parent = nullptr) : QStringListModel(parent) + { + } + + bool isComplete(const int response_type) const + { + if (response_type == 0) { + return true; + } + + return rowCount(QModelIndex()) >= 2; + } + + QModelIndex addItem() + { + const int row = rowCount(QModelIndex()); + + if (!insertRows(row, 1)) { + return QModelIndex(); + } + + return index(row); + } + + void removeItem(const QModelIndex& index) + { + if (index.isValid()) { + removeRows(index.row(), 1); + } + + emit completeChanged(); + } + + bool setData(const QModelIndex& index, const QVariant& value, int role) override + { + emit completeChanged(); + + return QStringListModel::setData(index, value.toString().trimmed(), role); + } + + Qt::ItemFlags flags(const QModelIndex& index) const override + { + if (!index.isValid()) { + return QStringListModel::flags(index) | Qt::ItemIsDropEnabled; + } + + return QStringListModel::flags(index) & ~Qt::ItemIsDropEnabled; + } + +signals: + void completeChanged(); +}; // ChoicesListModel + +// ----------------------------------------------------------------------------- +// Class: PollWizardDetailsPage +// ----------------------------------------------------------------------------- + +PollWizardDetailsPage::PollWizardDetailsPage(QWidget* parent) + : QWizardPage(parent) + , ui(new Ui::PollWizardDetailsPage) + , m_poll_types(nullptr) + , m_choices_model(new ChoicesListModel(this)) +{ + ui->setupUi(this); + + GRC::ScaleFontPointSize(ui->pageTitleLabel, 14); + + setCommitPage(true); + setButtonText(QWizard::CommitButton, tr("Create Poll")); + + registerField("title*", ui->titleField); + registerField("durationDays", ui->durationField); + registerField("question", ui->questionField); + registerField("url*", ui->urlField); + registerField("weightType", ui->weightTypeList); + registerField("responseType", ui->responseTypeList); + registerField("txid", new DummyField(this), "", ""); + + ui->durationField->setMinimum(VotingModel::minPollDurationDays()); + ui->durationField->setMaximum(VotingModel::maxPollDurationDays()); + ui->titleField->setMaxLength(VotingModel::maxPollTitleLength()); + ui->questionField->setMaxLength(VotingModel::maxPollQuestionLength()); + ui->urlField->setMaxLength(VotingModel::maxPollUrlLength()); + + ui->weightTypeList->addItem(tr("Balance")); + ui->weightTypeList->addItem(tr("Magnitude+Balance")); + + ui->responseTypeList->addItem(tr("Yes/No/Abstain")); + ui->responseTypeList->addItem(tr("Single Choice")); + ui->responseTypeList->addItem(tr("Multiple Choice")); + + ChoicesListDelegate* choices_delegate = new ChoicesListDelegate(this); + + ui->choicesList->setModel(m_choices_model.get()); + ui->choicesList->setItemDelegate(choices_delegate); + ui->choicesFrame->hide(); + ui->editChoiceButton->hide(); + ui->removeChoiceButton->hide(); + + connect( + ui->responseTypeList, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { + ui->choicesFrame->setVisible(index > 0); + ui->standardChoicesLabel->setVisible(index == 0); + emit completeChanged(); + }); + connect( + ui->choicesList->selectionModel(), &QItemSelectionModel::selectionChanged, + [this](const QItemSelection& selected, const QItemSelection& deselected) { + Q_UNUSED(deselected); + ui->editChoiceButton->setVisible(!selected.isEmpty()); + ui->removeChoiceButton->setVisible(!selected.isEmpty()); + }); + connect(ui->addChoiceButton, &QAbstractButton::clicked, [this]() { + ui->choicesList->edit(m_choices_model->addItem()); + ui->choicesList->scrollToBottom(); + }); + connect(ui->editChoiceButton, &QAbstractButton::clicked, [this]() { + ui->choicesList->edit(ui->choicesList->selectionModel()->selectedIndexes().first()); + }); + connect(ui->removeChoiceButton, &QAbstractButton::clicked, [this]() { + m_choices_model->removeItem(ui->choicesList->selectionModel()->selectedIndexes().first()); + }); + connect(m_choices_model.get(), &ChoicesListModel::completeChanged, [this]() { + emit completeChanged(); + }); +} + +PollWizardDetailsPage::~PollWizardDetailsPage() +{ + delete ui; +} + +void PollWizardDetailsPage::setModel(VotingModel* voting_model) +{ + m_voting_model = voting_model; + + updateIcons(); +} + +void PollWizardDetailsPage::setPollTypes(const PollTypes* const poll_types) +{ + m_poll_types = poll_types; +} + +void PollWizardDetailsPage::initializePage() +{ + if (!m_poll_types) { + return; + } + + ui->errorLabel->hide(); + + const int type_id = field("pollType").toInt(); + const PollTypeItem& poll_type = (*m_poll_types)[type_id]; + + ui->pollTypeLabel->setText(poll_type.m_name); + ui->durationField->setMinimum(poll_type.m_min_duration_days); + ui->durationField->setValue(poll_type.m_min_duration_days); + + if (type_id != PollTypes::PollTypeSurvey) { + ui->pollTypeAlert->show(); + ui->weightTypeList->setCurrentIndex(1); // Magnitude+Balance + ui->weightTypeList->setDisabled(true); + } else { + ui->pollTypeAlert->hide(); + ui->weightTypeList->setEnabled(true); + } + + if (type_id == PollTypes::PollTypeProject) { + ui->titleField->setText(QStringLiteral("[%1] %2") + .arg(poll_type.m_name) + .arg(field("projectPollTitle").toString())); + ui->responseTypeList->setCurrentIndex(0); // Yes/No/Abstain + ui->responseTypeList->setDisabled(true); + } else { + ui->responseTypeList->setEnabled(true); + } +} + +bool PollWizardDetailsPage::validatePage() +{ + if (!m_voting_model) { + return false; + } + + const CAmount burn_fee = m_voting_model->estimatePollFee(); + const QMessageBox::StandardButton pressed = QMessageBox::question( + this, + tr("Create Poll"), + tr("This poll will cost %1 plus a transaction fee. Continue?") + .arg(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, burn_fee)) + ); + + if (pressed != QMessageBox::Yes) { + return false; + } + + const VotingResult result = m_voting_model->sendPoll( + field("title").toString(), + field("durationDays").toInt(), + field("question").toString(), + field("url").toString(), + // The dropdown list only contains non-deprecated weight type options + // which start from offset 2: + field("weightType").toInt() + 2, + field("responseType").toInt() + 1, + m_choices_model->stringList()); + + if (!result.ok()) { + ui->errorLabel->setText(result.error()); + ui->errorLabel->show(); + + return false; + } + + setField("txid", result.txid()); + + return true; +} + +bool PollWizardDetailsPage::isComplete() const +{ + return QWizardPage::isComplete() + && m_choices_model->isComplete(field("responseType").toInt()); +} + +void PollWizardDetailsPage::updateIcons() +{ + if (!m_voting_model) { + return; + } + + const QString theme = m_voting_model->getOptionsModel().getCurrentStyle(); + + ui->addChoiceButton->setIcon(QIcon(":/icons/" + theme + "_add")); + ui->editChoiceButton->setIcon(QIcon(":/icons/" + theme + "_edit")); + ui->removeChoiceButton->setIcon(QIcon(":/icons/" + theme + "_remove")); +} + +#include "qt/voting/pollwizarddetailspage.moc" diff --git a/src/qt/voting/pollwizarddetailspage.h b/src/qt/voting/pollwizarddetailspage.h new file mode 100644 index 0000000000..2992f9bd7a --- /dev/null +++ b/src/qt/voting/pollwizarddetailspage.h @@ -0,0 +1,44 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLWIZARDDETAILSPAGE_H +#define VOTING_POLLWIZARDDETAILSPAGE_H + +#include +#include + +namespace Ui { +class PollWizardDetailsPage; +} + +class ChoicesListModel; +class PollTypes; +class VotingModel; + +class PollWizardDetailsPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit PollWizardDetailsPage(QWidget* parent = nullptr); + ~PollWizardDetailsPage(); + + void setModel(VotingModel* voting_model); + void setPollTypes(const PollTypes* const poll_types); + + void initializePage() override; + bool validatePage() override; + bool isComplete() const override; + +private: + Ui::PollWizardDetailsPage* ui; + VotingModel* m_voting_model; + const PollTypes* m_poll_types; + std::unique_ptr m_choices_model; + +private slots: + void updateIcons(); +}; + +#endif // VOTING_POLLWIZARDDETAILSPAGE_H diff --git a/src/qt/voting/pollwizardprojectpage.cpp b/src/qt/voting/pollwizardprojectpage.cpp new file mode 100644 index 0000000000..87876718a2 --- /dev/null +++ b/src/qt/voting/pollwizardprojectpage.cpp @@ -0,0 +1,113 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_pollwizardprojectpage.h" +#include "qt/voting/pollwizard.h" +#include "qt/voting/pollwizardprojectpage.h" +#include "qt/voting/votingmodel.h" + +#include + +// ----------------------------------------------------------------------------- +// Class: PollWizardProjectPage +// ----------------------------------------------------------------------------- + +PollWizardProjectPage::PollWizardProjectPage(QWidget* parent) + : QWizardPage(parent) + , ui(new Ui::PollWizardProjectPage) +{ + ui->setupUi(this); + + GRC::ScaleFontPointSize(ui->pageTitleLabel, 14); + + // The asterisk denotes a mandatory field: + registerField("projectPollTitle*", ui->projectNameField); + + ui->addWidget->hide(); + ui->removeWidget->hide(); + + QStringListModel* project_names_model = new QStringListModel(this); + ui->projectsList->setModel(project_names_model); + + connect(ui->addRadioButton, &QAbstractButton::toggled, [this](bool checked) { + if (!checked) { + ui->criteriaCheckbox->setChecked(false); + } + + ui->addWidget->setVisible(checked); + setField("projectPollTitle", QVariant()); + emit completeChanged(); + }); + connect(ui->removeRadioButton, &QAbstractButton::toggled, [=](bool checked) { + if (checked && m_voting_model) { + project_names_model->setStringList(m_voting_model->getActiveProjectNames()); + } + + ui->removeWidget->setVisible(checked); + setField("projectPollTitle", QVariant()); + emit completeChanged(); + }); + connect(ui->criteriaCheckbox, &QAbstractButton::toggled, [this](bool checked) { + Q_UNUSED(checked); + emit completeChanged(); + }); + connect(ui->projectsList->selectionModel(), &QItemSelectionModel::selectionChanged, + [=](const QItemSelection& selected, const QItemSelection& deselected) { + Q_UNUSED(deselected); + + if (!selected.isEmpty()) { + const QModelIndex index = selected.indexes().first(); + setField("projectPollTitle", project_names_model->data(index).toString()); + } + + emit completeChanged(); + }); +} + +PollWizardProjectPage::~PollWizardProjectPage() +{ + delete ui; +} + +void PollWizardProjectPage::setModel(VotingModel* voting_model) +{ + m_voting_model = voting_model; +} + +void PollWizardProjectPage::initializePage() +{ + ui->criteriaCheckbox->setChecked(false); + ui->projectsList->selectionModel()->clearSelection(); +} + +bool PollWizardProjectPage::validatePage() +{ + const QString project_name = field("projectPollTitle").toString(); + + if (ui->addRadioButton->isChecked()) { + setField("projectPollTitle", tr("Add %1").arg(project_name)); + } else { + setField("projectPollTitle", tr("Remove %1").arg(project_name)); + } + + return true; +} + +bool PollWizardProjectPage::isComplete() const +{ + if (!QWizardPage::isComplete()) { + return false; + } + + if (ui->addRadioButton->isChecked()) { + return ui->criteriaCheckbox->isChecked(); + } + + if (ui->removeRadioButton->isChecked()) { + return true; + } + + return false; +} diff --git a/src/qt/voting/pollwizardprojectpage.h b/src/qt/voting/pollwizardprojectpage.h new file mode 100644 index 0000000000..e9ad67362b --- /dev/null +++ b/src/qt/voting/pollwizardprojectpage.h @@ -0,0 +1,35 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLWIZARDPROJECTPAGE_H +#define VOTING_POLLWIZARDPROJECTPAGE_H + +#include + +namespace Ui { +class PollWizardProjectPage; +} + +class VotingModel; + +class PollWizardProjectPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit PollWizardProjectPage(QWidget* parent = nullptr); + ~PollWizardProjectPage(); + + void setModel(VotingModel* voting_model); + + void initializePage() override; + bool validatePage() override; + bool isComplete() const override; + +private: + Ui::PollWizardProjectPage* ui; + VotingModel* m_voting_model; +}; + +#endif // VOTING_POLLWIZARDPROJECTPAGE_H diff --git a/src/qt/voting/pollwizardsummarypage.cpp b/src/qt/voting/pollwizardsummarypage.cpp new file mode 100644 index 0000000000..a20b028983 --- /dev/null +++ b/src/qt/voting/pollwizardsummarypage.cpp @@ -0,0 +1,42 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_pollwizardsummarypage.h" +#include "qt/voting/pollwizard.h" +#include "qt/voting/pollwizardsummarypage.h" +#include "qt/voting/votingmodel.h" + +#include + +// ----------------------------------------------------------------------------- +// Class: PollWizardSummaryPage +// ----------------------------------------------------------------------------- + +PollWizardSummaryPage::PollWizardSummaryPage(QWidget* parent) + : QWizardPage(parent) + , ui(new Ui::PollWizardSummaryPage) +{ + ui->setupUi(this); + + GRC::ScaleFontPointSize(ui->pageTitleLabel, 14); + GRC::ScaleFontPointSize(ui->pollTitleLabel, 12); + GRC::ScaleFontPointSize(ui->pollIdLabel, 8); +} + +PollWizardSummaryPage::~PollWizardSummaryPage() +{ + delete ui; +} + +void PollWizardSummaryPage::initializePage() +{ + ui->pollTitleLabel->setText(field("title").toString()); + ui->pollIdLabel->setText(field("txid").toString()); +} + +void PollWizardSummaryPage::on_copyToClipboardButton_clicked() const +{ + QApplication::clipboard()->setText(field("txid").toString()); +} diff --git a/src/qt/voting/pollwizardsummarypage.h b/src/qt/voting/pollwizardsummarypage.h new file mode 100644 index 0000000000..1917ca588b --- /dev/null +++ b/src/qt/voting/pollwizardsummarypage.h @@ -0,0 +1,31 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLWIZARDSUMMARYPAGE_H +#define VOTING_POLLWIZARDSUMMARYPAGE_H + +#include + +namespace Ui { +class PollWizardSummaryPage; +} + +class PollWizardSummaryPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit PollWizardSummaryPage(QWidget* parent = nullptr); + ~PollWizardSummaryPage(); + + void initializePage() override; + +private: + Ui::PollWizardSummaryPage* ui; + +private slots: + void on_copyToClipboardButton_clicked() const; +}; + +#endif // VOTING_POLLWIZARDSUMMARYPAGE_H diff --git a/src/qt/voting/pollwizardtypepage.cpp b/src/qt/voting/pollwizardtypepage.cpp new file mode 100644 index 0000000000..7c3b4f32f6 --- /dev/null +++ b/src/qt/voting/pollwizardtypepage.cpp @@ -0,0 +1,79 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_pollwizardtypepage.h" +#include "qt/voting/pollwizard.h" +#include "qt/voting/pollwizardtypepage.h" +#include "qt/voting/votingmodel.h" + +#include +#include +#include + +// ----------------------------------------------------------------------------- +// Class: PollWizardTypePage +// ----------------------------------------------------------------------------- + +PollWizardTypePage::PollWizardTypePage(QWidget* parent) + : QWizardPage(parent) + , ui(new Ui::PollWizardTypePage) + , m_type_buttons(new QButtonGroup(this)) +{ + ui->setupUi(this); + + GRC::ScaleFontPointSize(ui->pageTitleLabel, 14); + GRC::ScaleFontPointSize(ui->typeTextLabel, 11); + + // QWizardPage::registerField() cannot bind to QButtonGroup because it + // expects a QWidget instance so we provide a proxy widget: + QSpinBox* type_proxy = new QSpinBox(this); + type_proxy->setVisible(false); + + registerField("pollType*", type_proxy); + setField("pollType", PollTypes::PollTypeUnknown); + + connect( + m_type_buttons, QOverload::of(&QButtonGroup::buttonClicked), + [=](QAbstractButton*) { type_proxy->setValue(m_type_buttons->checkedId()); }); +} + +PollWizardTypePage::~PollWizardTypePage() +{ + delete ui; + delete m_type_buttons; +} + +void PollWizardTypePage::setPollTypes(const PollTypes* const poll_types) +{ + const QIcon button_icon(":/icons/tx_contract_voting"); + + // Start with "i = 1" to skip PollTypes::PollTypeUnknown: + for (size_t i = 1, row = 0, column = 0; i < poll_types->size(); ++i) { + const PollTypeItem& poll_type = (*poll_types)[i]; + + QCommandLinkButton* button = new QCommandLinkButton(this); + button->setText(poll_type.m_name); + button->setDescription(poll_type.m_description); + button->setIcon(button_icon); + button->setCheckable(true); + GRC::ScaleFontPointSize(button, 9); + + ui->typesButtonLayout->addWidget(button, row, column); + m_type_buttons->addButton(button, i); + + row += column; + column = !column; + } +} + +int PollWizardTypePage::nextId() const +{ + switch (field("pollType").toInt()) { + case PollTypes::PollTypeProject: + return PollWizard::PageProject; + } + + return PollWizard::PageDetails; +} diff --git a/src/qt/voting/pollwizardtypepage.h b/src/qt/voting/pollwizardtypepage.h new file mode 100644 index 0000000000..cd12101d04 --- /dev/null +++ b/src/qt/voting/pollwizardtypepage.h @@ -0,0 +1,38 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLWIZARDTYPEPAGE_H +#define VOTING_POLLWIZARDTYPEPAGE_H + +#include + +namespace Ui { +class PollWizardTypePage; +} + +class PollTypes; + +QT_BEGIN_NAMESPACE +class QButtonGroup; +QT_END_NAMESPACE + +class PollWizardTypePage : public QWizardPage +{ + Q_OBJECT + +public: + explicit PollWizardTypePage(QWidget* parent = nullptr); + ~PollWizardTypePage(); + + void setPollTypes(const PollTypes* const poll_types); + + int nextId() const override; + +private: + Ui::PollWizardTypePage* ui; + const PollTypes* m_poll_types; + QButtonGroup* m_type_buttons; +}; + +#endif // VOTING_POLLWIZARDTYPEPAGE_H diff --git a/src/qt/voting/votingmodel.cpp b/src/qt/voting/votingmodel.cpp index 8c51db0031..f1b5e1a5d4 100644 --- a/src/qt/voting/votingmodel.cpp +++ b/src/qt/voting/votingmodel.cpp @@ -81,9 +81,13 @@ std::optional BuildPollItem(const PollRegistry::Sequence::Iterator& it // Class: VotingModel // ----------------------------------------------------------------------------- -VotingModel::VotingModel(ClientModel& client_model, WalletModel& wallet_model) +VotingModel::VotingModel( + ClientModel& client_model, + OptionsModel& options_model, + WalletModel& wallet_model) : m_registry(GetPollRegistry()) , m_client_model(client_model) + , m_options_model(options_model) , m_wallet_model(wallet_model) , m_last_poll_time(0) { @@ -156,6 +160,11 @@ int VotingModel::maxPollChoiceLabelLength() return Poll::Choice::MAX_LABEL_SIZE; } +OptionsModel& VotingModel::getOptionsModel() +{ + return m_options_model; +} + QStringList VotingModel::getActiveProjectNames() const { QStringList names; diff --git a/src/qt/voting/votingmodel.h b/src/qt/voting/votingmodel.h index 6a6ffb4e4c..1d6dabe4af 100644 --- a/src/qt/voting/votingmodel.h +++ b/src/qt/voting/votingmodel.h @@ -23,6 +23,7 @@ class QStringList; QT_END_NAMESPACE class ClientModel; +class OptionsModel; class uint256; class WalletModel; @@ -89,7 +90,10 @@ class VotingModel : public QObject Q_OBJECT public: - VotingModel(ClientModel& client_model, WalletModel& wallet_model); + VotingModel( + ClientModel& client_model, + OptionsModel& options_model, + WalletModel& wallet_model); ~VotingModel(); static int minPollDurationDays(); @@ -99,6 +103,7 @@ class VotingModel : public QObject static int maxPollQuestionLength(); static int maxPollChoiceLabelLength(); + OptionsModel& getOptionsModel(); QStringList getActiveProjectNames() const; std::vector buildPollTable(const GRC::PollFilterFlag flags) const; @@ -122,6 +127,7 @@ class VotingModel : public QObject private: GRC::PollRegistry& m_registry; ClientModel& m_client_model; + OptionsModel& m_options_model; WalletModel& m_wallet_model; int64_t m_last_poll_time; From f02a5f931f82d3796459485ef8576a4e279bb4e4 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:47 -0500 Subject: [PATCH 6/9] Overhaul GUI voting page design This updates the GUI voting page according to the proposed design mock ups: - Add a header bar with a poll title filter input - Add tabs for "active" and "completed" polls - Add a list view for polls and update the table view - Add a loading animation displayed when refreshing polls - Limit the max content width for large window sizes - Update colors, icons, layout, sizing, and spacing --- gridcoinresearch.pro | 23 +- src/Makefile.qt.include | 82 +- src/qt/bitcoin.cpp | 3 + src/qt/bitcoin.qrc | 21 + src/qt/bitcoingui.cpp | 13 +- src/qt/bitcoingui.h | 10 +- src/qt/forms/overviewpage.ui | 6 +- src/qt/forms/voting/pollcard.ui | 235 ++++ src/qt/forms/voting/pollcardview.ui | 167 +++ src/qt/forms/voting/polldetails.ui | 137 +++ src/qt/forms/voting/pollresultchoiceitem.ui | 106 ++ src/qt/forms/voting/pollresultdialog.ui | 157 +++ src/qt/forms/voting/polltab.ui | 135 ++ src/qt/forms/voting/votingpage.ui | 235 ++++ src/qt/noresult.cpp | 15 + src/qt/noresult.h | 10 + src/qt/res/icons/icons_dark/add.svg | 1 + src/qt/res/icons/icons_dark/create.svg | 1 + src/qt/res/icons/icons_dark/edit.svg | 1 + src/qt/res/icons/icons_dark/hamburger.svg | 1 + src/qt/res/icons/icons_dark/list_view.svg | 4 + src/qt/res/icons/icons_dark/refresh.svg | 1 + src/qt/res/icons/icons_dark/remove.svg | 1 + src/qt/res/icons/icons_dark/sort_asc.svg | 5 + src/qt/res/icons/icons_dark/table_view.svg | 3 + src/qt/res/icons/icons_dark/vote.svg | 1 + src/qt/res/icons/icons_light/add.svg | 1 + src/qt/res/icons/icons_light/create.svg | 1 + src/qt/res/icons/icons_light/edit.svg | 1 + src/qt/res/icons/icons_light/hamburger.svg | 1 + src/qt/res/icons/icons_light/list_view.svg | 4 + src/qt/res/icons/icons_light/refresh.svg | 1 + src/qt/res/icons/icons_light/remove.svg | 1 + src/qt/res/icons/icons_light/sort_asc.svg | 5 + src/qt/res/icons/icons_light/table_view.svg | 3 + src/qt/res/icons/icons_light/vote.svg | 1 + src/qt/res/icons/open_link.svg | 1 + src/qt/res/stylesheets/dark_stylesheet.qss | 114 +- src/qt/res/stylesheets/light_stylesheet.qss | 116 +- src/qt/voting/pollcard.cpp | 74 ++ src/qt/voting/pollcard.h | 38 + src/qt/voting/pollcardview.cpp | 144 +++ src/qt/voting/pollcardview.h | 55 + src/qt/voting/polldetails.cpp | 42 + src/qt/voting/polldetails.h | 30 + src/qt/voting/pollresultchoiceitem.cpp | 77 ++ src/qt/voting/pollresultchoiceitem.h | 32 + src/qt/voting/pollresultdialog.cpp | 77 ++ src/qt/voting/pollresultdialog.h | 28 + src/qt/voting/polltab.cpp | 292 +++++ src/qt/voting/polltab.h | 81 ++ src/qt/voting/votingpage.cpp | 166 +++ src/qt/voting/votingpage.h | 51 + src/qt/votingdialog.cpp | 1228 ------------------- src/qt/votingdialog.h | 295 ----- 55 files changed, 2781 insertions(+), 1553 deletions(-) create mode 100644 src/qt/forms/voting/pollcard.ui create mode 100644 src/qt/forms/voting/pollcardview.ui create mode 100644 src/qt/forms/voting/polldetails.ui create mode 100644 src/qt/forms/voting/pollresultchoiceitem.ui create mode 100644 src/qt/forms/voting/pollresultdialog.ui create mode 100644 src/qt/forms/voting/polltab.ui create mode 100644 src/qt/forms/voting/votingpage.ui create mode 100644 src/qt/res/icons/icons_dark/add.svg create mode 100644 src/qt/res/icons/icons_dark/create.svg create mode 100644 src/qt/res/icons/icons_dark/edit.svg create mode 100644 src/qt/res/icons/icons_dark/hamburger.svg create mode 100644 src/qt/res/icons/icons_dark/list_view.svg create mode 100644 src/qt/res/icons/icons_dark/refresh.svg create mode 100644 src/qt/res/icons/icons_dark/remove.svg create mode 100644 src/qt/res/icons/icons_dark/sort_asc.svg create mode 100644 src/qt/res/icons/icons_dark/table_view.svg create mode 100644 src/qt/res/icons/icons_dark/vote.svg create mode 100644 src/qt/res/icons/icons_light/add.svg create mode 100644 src/qt/res/icons/icons_light/create.svg create mode 100644 src/qt/res/icons/icons_light/edit.svg create mode 100644 src/qt/res/icons/icons_light/hamburger.svg create mode 100644 src/qt/res/icons/icons_light/list_view.svg create mode 100644 src/qt/res/icons/icons_light/refresh.svg create mode 100644 src/qt/res/icons/icons_light/remove.svg create mode 100644 src/qt/res/icons/icons_light/sort_asc.svg create mode 100644 src/qt/res/icons/icons_light/table_view.svg create mode 100644 src/qt/res/icons/icons_light/vote.svg create mode 100644 src/qt/res/icons/open_link.svg create mode 100644 src/qt/voting/pollcard.cpp create mode 100644 src/qt/voting/pollcard.h create mode 100644 src/qt/voting/pollcardview.cpp create mode 100644 src/qt/voting/pollcardview.h create mode 100644 src/qt/voting/polldetails.cpp create mode 100644 src/qt/voting/polldetails.h create mode 100644 src/qt/voting/pollresultchoiceitem.cpp create mode 100644 src/qt/voting/pollresultchoiceitem.h create mode 100644 src/qt/voting/pollresultdialog.cpp create mode 100644 src/qt/voting/pollresultdialog.h create mode 100644 src/qt/voting/polltab.cpp create mode 100644 src/qt/voting/polltab.h create mode 100644 src/qt/voting/votingpage.cpp create mode 100644 src/qt/voting/votingpage.h delete mode 100644 src/qt/votingdialog.cpp delete mode 100644 src/qt/votingdialog.h diff --git a/gridcoinresearch.pro b/gridcoinresearch.pro index 67843dc658..4e94242a82 100755 --- a/gridcoinresearch.pro +++ b/gridcoinresearch.pro @@ -185,6 +185,12 @@ HEADERS += src/qt/bitcoingui.h \ src/qt/researcher/researcherwizardpoolsummarypage.h \ src/qt/researcher/researcherwizardprojectspage.h \ src/qt/researcher/researcherwizardsummarypage.h \ + src/qt/voting/pollcard.h \ + src/qt/voting/pollcardview.h \ + src/qt/voting/polldetails.h \ + src/qt/voting/pollresultchoiceitem.h \ + src/qt/voting/pollresultdialog.h \ + src/qt/voting/polltab.h \ src/qt/voting/polltablemodel.h \ src/qt/voting/pollwizard.h \ src/qt/voting/pollwizarddetailspage.h \ @@ -192,6 +198,7 @@ HEADERS += src/qt/bitcoingui.h \ src/qt/voting/pollwizardsummarypage.h \ src/qt/voting/pollwizardtypepage.h \ src/qt/voting/votingmodel.h \ + src/qt/voting/votingpage.h \ src/qt/transactiontablemodel.h \ src/qt/addresstablemodel.h \ src/qt/optionsdialog.h \ @@ -267,7 +274,6 @@ HEADERS += src/qt/bitcoingui.h \ src/protocol.h \ src/qt/notificator.h \ src/qt/qtipcserver.h \ - src/qt/votingdialog.h \ src/allocators.h \ src/ui_interface.h \ src/qt/rpcconsole.h \ @@ -299,6 +305,12 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/researcher/researcherwizardpoolsummarypage.cpp \ src/qt/researcher/researcherwizardprojectspage.cpp \ src/qt/researcher/researcherwizardsummarypage.cpp \ + src/qt/voting/pollcard.cpp \ + src/qt/voting/pollcardview.cpp \ + src/qt/voting/polldetails.cpp \ + src/qt/voting/pollresultchoiceitem.cpp \ + src/qt/voting/pollresultdialog.cpp \ + src/qt/voting/polltab.cpp \ src/qt/voting/polltablemodel.cpp \ src/qt/voting/pollwizard.cpp \ src/qt/voting/pollwizarddetailspage.cpp \ @@ -306,6 +318,7 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/voting/pollwizardsummarypage.cpp \ src/qt/voting/pollwizardtypepage.cpp \ src/qt/voting/votingmodel.cpp \ + src/qt/voting/votingpage.cpp \ src/qt/transactiontablemodel.cpp \ src/qt/addresstablemodel.cpp \ src/qt/optionsdialog.cpp \ @@ -319,7 +332,6 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/aboutdialog.cpp \ src/qt/editaddressdialog.cpp \ src/qt/bitcoinaddressvalidator.cpp \ - src/qt/votingdialog.cpp \ src/qt/diagnosticsdialog.cpp \ src/alert.cpp \ src/block.cpp \ @@ -409,11 +421,18 @@ FORMS += \ src/qt/forms/researcherwizardpoolsummarypage.ui \ src/qt/forms/researcherwizardprojectspage.ui \ src/qt/forms/researcherwizardsummarypage.ui \ + src/qt/forms/voting/pollcard.ui \ + src/qt/forms/voting/pollcardview.ui \ + src/qt/forms/voting/polldetails.ui \ + src/qt/forms/voting/pollresultchoiceitem.ui \ + src/qt/forms/voting/pollresultdialog.ui \ + src/qt/forms/voting/polltab.ui \ src/qt/forms/voting/pollwizard.ui \ src/qt/forms/voting/pollwizarddetailspage.ui \ src/qt/forms/voting/pollwizardprojectpage.ui \ src/qt/forms/voting/pollwizardsummarypage.ui \ src/qt/forms/voting/pollwizardtypepage.ui \ + src/qt/forms/voting/votingpage.ui \ src/qt/forms/receivecoinspage.ui \ src/qt/forms/sendcoinsdialog.ui \ src/qt/forms/favoritespage.ui \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 99c12e956e..8a1645f2e6 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -73,6 +73,8 @@ QT_TS = \ QT_FORMS_UI = \ qt/forms/aboutdialog.ui \ + qt/forms/addressbookpage.ui \ + qt/forms/askpassphrasedialog.ui \ qt/forms/coincontroldialog.ui \ qt/forms/consolidateunspentdialog.ui \ qt/forms/consolidateunspentwizard.ui \ @@ -80,13 +82,11 @@ QT_FORMS_UI = \ qt/forms/consolidateunspentwizardselectinputspage.ui \ qt/forms/consolidateunspentwizardsendpage.ui \ qt/forms/diagnosticsdialog.ui \ - qt/forms/favoritespage.ui \ - qt/forms/optionsdialog.ui \ - qt/forms/rpcconsole.ui \ - qt/forms/signverifymessagedialog.ui \ - qt/forms/addressbookpage.ui \ qt/forms/editaddressdialog.ui \ + qt/forms/favoritespage.ui \ + qt/forms/intro.ui \ qt/forms/noresult.ui \ + qt/forms/optionsdialog.ui \ qt/forms/overviewpage.ui \ qt/forms/receivecoinspage.ui \ qt/forms/researcherwizard.ui \ @@ -94,22 +94,29 @@ QT_FORMS_UI = \ qt/forms/researcherwizardbeaconpage.ui \ qt/forms/researcherwizardemailpage.ui \ qt/forms/researcherwizardinvestorpage.ui \ - qt/forms/researcherwizardmodepage.ui \ qt/forms/researcherwizardmodedetailpage.ui \ + qt/forms/researcherwizardmodepage.ui \ qt/forms/researcherwizardpoolpage.ui \ qt/forms/researcherwizardpoolsummarypage.ui \ qt/forms/researcherwizardprojectspage.ui \ qt/forms/researcherwizardsummarypage.ui \ + qt/forms/rpcconsole.ui \ qt/forms/sendcoinsdialog.ui \ + qt/forms/sendcoinsentry.ui \ + qt/forms/signverifymessagedialog.ui \ qt/forms/transactiondescdialog.ui \ + qt/forms/voting/pollcard.ui \ + qt/forms/voting/pollcardview.ui \ + qt/forms/voting/polldetails.ui \ + qt/forms/voting/pollresultchoiceitem.ui \ + qt/forms/voting/pollresultdialog.ui \ + qt/forms/voting/polltab.ui \ qt/forms/voting/pollwizard.ui \ qt/forms/voting/pollwizarddetailspage.ui \ qt/forms/voting/pollwizardprojectpage.ui \ qt/forms/voting/pollwizardsummarypage.ui \ qt/forms/voting/pollwizardtypepage.ui \ - qt/forms/askpassphrasedialog.ui \ - qt/forms/sendcoinsentry.ui \ - qt/forms/intro.ui + qt/forms/voting/votingpage.ui QT_MOC_CPP = \ qt/moc_aboutdialog.cpp \ @@ -157,7 +164,6 @@ QT_MOC_CPP = \ qt/moc_transactionfilterproxy.cpp \ qt/moc_transactiontablemodel.cpp \ qt/moc_transactionview.cpp \ - qt/moc_votingdialog.cpp \ qt/moc_walletmodel.cpp \ qt/researcher/moc_projecttablemodel.cpp \ qt/researcher/moc_researchermodel.cpp \ @@ -166,19 +172,26 @@ QT_MOC_CPP = \ qt/researcher/moc_researcherwizardbeaconpage.cpp \ qt/researcher/moc_researcherwizardemailpage.cpp \ qt/researcher/moc_researcherwizardinvestorpage.cpp \ - qt/researcher/moc_researcherwizardmodepage.cpp \ qt/researcher/moc_researcherwizardmodedetailpage.cpp \ + qt/researcher/moc_researcherwizardmodepage.cpp \ qt/researcher/moc_researcherwizardpoolpage.cpp \ qt/researcher/moc_researcherwizardpoolsummarypage.cpp \ qt/researcher/moc_researcherwizardprojectspage.cpp \ qt/researcher/moc_researcherwizardsummarypage.cpp \ + qt/voting/moc_pollcard.cpp \ + qt/voting/moc_pollcardview.cpp \ + qt/voting/moc_polldetails.cpp \ + qt/voting/moc_pollresultchoiceitem.cpp \ + qt/voting/moc_pollresultdialog.cpp \ + qt/voting/moc_polltab.cpp \ qt/voting/moc_polltablemodel.cpp \ qt/voting/moc_pollwizard.cpp \ qt/voting/moc_pollwizarddetailspage.cpp \ qt/voting/moc_pollwizardprojectpage.cpp \ qt/voting/moc_pollwizardsummarypage.cpp \ qt/voting/moc_pollwizardtypepage.cpp \ - qt/voting/moc_votingmodel.cpp + qt/voting/moc_votingmodel.cpp \ + qt/voting/moc_votingpage.cpp GRIDCOIN_MM = \ qt/macdockiconhandler.mm \ @@ -189,7 +202,9 @@ QT_MOC = \ qt/intro.moc \ qt/overviewpage.moc \ qt/rpcconsole.moc \ - qt/voting/pollwizarddetailspage.moc + qt/voting/polltab.moc \ + qt/voting/pollwizarddetailspage.moc \ + qt/voting/votingpage.moc QT_QRC_CPP = qt/qrc_bitcoin.cpp QT_QRC = qt/bitcoin.qrc @@ -244,8 +259,8 @@ GRIDCOINRESEARCH_QT_H = \ qt/researcher/researcherwizardbeaconpage.h \ qt/researcher/researcherwizardemailpage.h \ qt/researcher/researcherwizardinvestorpage.h \ - qt/researcher/researcherwizardmodepage.h \ qt/researcher/researcherwizardmodedetailpage.h \ + qt/researcher/researcherwizardmodepage.h \ qt/researcher/researcherwizardpoolpage.h \ qt/researcher/researcherwizardpoolsummarypage.h \ qt/researcher/researcherwizardprojectspage.h \ @@ -263,6 +278,12 @@ GRIDCOINRESEARCH_QT_H = \ qt/transactionview.h \ qt/upgradeqt.h \ qt/voting/poll_types.h \ + qt/voting/pollcard.h \ + qt/voting/pollcardview.h \ + qt/voting/polldetails.h \ + qt/voting/pollresultchoiceitem.h \ + qt/voting/pollresultdialog.h \ + qt/voting/polltab.h \ qt/voting/polltablemodel.h \ qt/voting/pollwizard.h \ qt/voting/pollwizarddetailspage.h \ @@ -270,7 +291,7 @@ GRIDCOINRESEARCH_QT_H = \ qt/voting/pollwizardsummarypage.h \ qt/voting/pollwizardtypepage.h \ qt/voting/votingmodel.h \ - qt/votingdialog.h \ + qt/voting/votingpage.h \ qt/walletmodel.h \ qt/winshutdownmonitor.h @@ -318,8 +339,8 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/researcher/researcherwizardbeaconpage.cpp \ qt/researcher/researcherwizardemailpage.cpp \ qt/researcher/researcherwizardinvestorpage.cpp \ - qt/researcher/researcherwizardmodepage.cpp \ qt/researcher/researcherwizardmodedetailpage.cpp \ + qt/researcher/researcherwizardmodepage.cpp \ qt/researcher/researcherwizardpoolpage.cpp \ qt/researcher/researcherwizardpoolsummarypage.cpp \ qt/researcher/researcherwizardprojectspage.cpp \ @@ -337,6 +358,12 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/transactionview.cpp \ qt/upgradeqt.cpp \ qt/voting/poll_types.cpp \ + qt/voting/pollcard.cpp \ + qt/voting/pollcardview.cpp \ + qt/voting/polldetails.cpp \ + qt/voting/pollresultchoiceitem.cpp \ + qt/voting/pollresultdialog.cpp \ + qt/voting/polltab.cpp \ qt/voting/polltablemodel.cpp \ qt/voting/pollwizard.cpp \ qt/voting/pollwizarddetailspage.cpp \ @@ -344,7 +371,7 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/voting/pollwizardsummarypage.cpp \ qt/voting/pollwizardtypepage.cpp \ qt/voting/votingmodel.cpp \ - qt/votingdialog.cpp \ + qt/voting/votingpage.cpp \ qt/walletmodel.cpp \ qt/winshutdownmonitor.cpp @@ -381,6 +408,7 @@ RES_ICONS = \ qt/res/icons/menu_active.svg \ qt/res/icons/message.svg \ qt/res/icons/no_result.svg \ + qt/res/icons/open_link.svg \ qt/res/icons/qrcode.png \ qt/res/icons/quit.png \ qt/res/icons/remove.png \ @@ -404,6 +432,13 @@ RES_ICONS = \ qt/res/icons/warning.svg \ qt/res/icons/white_and_red_x.svg \ qt/res/icons/www.png \ + qt/res/icons/icons_light/add.svg \ + qt/res/icons/icons_light/create.svg \ + qt/res/icons/icons_light/edit.svg \ + qt/res/icons/icons_light/hamburger.svg \ + qt/res/icons/icons_light/list_view.svg \ + qt/res/icons/icons_light/refresh.svg \ + qt/res/icons/icons_light/remove.svg \ qt/res/icons/icons_light/search.svg \ qt/res/icons/icons_light/settings.svg \ qt/res/icons/icons_light/settings_action_needed.svg \ @@ -425,6 +460,7 @@ RES_ICONS = \ qt/res/icons/icons_light/sidebar_unlocked_inactive.svg \ qt/res/icons/icons_light/sidebar_voting_active.svg \ qt/res/icons/icons_light/sidebar_voting_inactive.svg \ + qt/res/icons/icons_light/sort_asc.svg \ qt/res/icons/icons_light/status_beacon_green.svg \ qt/res/icons/icons_light/status_beacon_gray.svg \ qt/res/icons/icons_light/status_beacon_red.svg \ @@ -447,6 +483,15 @@ RES_ICONS = \ qt/res/icons/icons_light/status_sync_done.svg \ qt/res/icons/icons_light/status_sync_stalled.svg \ qt/res/icons/icons_light/status_sync_syncing.svg \ + qt/res/icons/icons_light/table_view.svg \ + qt/res/icons/icons_light/vote.svg \ + qt/res/icons/icons_dark/add.svg \ + qt/res/icons/icons_dark/create.svg \ + qt/res/icons/icons_dark/edit.svg \ + qt/res/icons/icons_dark/hamburger.svg \ + qt/res/icons/icons_dark/list_view.svg \ + qt/res/icons/icons_dark/refresh.svg \ + qt/res/icons/icons_dark/remove.svg \ qt/res/icons/icons_dark/search.svg \ qt/res/icons/icons_dark/settings_action_needed.svg \ qt/res/icons/icons_dark/sidebar_favorites_active.svg \ @@ -467,6 +512,7 @@ RES_ICONS = \ qt/res/icons/icons_dark/sidebar_unlocked_inactive.svg \ qt/res/icons/icons_dark/sidebar_voting_active.svg \ qt/res/icons/icons_dark/sidebar_voting_inactive.svg \ + qt/res/icons/icons_dark/sort_asc.svg \ qt/res/icons/icons_dark/status_beacon_green.svg \ qt/res/icons/icons_dark/status_beacon_gray.svg \ qt/res/icons/icons_dark/status_beacon_red.svg \ @@ -489,7 +535,9 @@ RES_ICONS = \ qt/res/icons/icons_dark/status_sync_done.svg \ qt/res/icons/icons_dark/status_sync_stalled.svg \ qt/res/icons/icons_dark/status_sync_syncing.svg \ + qt/res/icons/icons_dark/table_view.svg \ qt/res/icons/icons_dark/transactions.svg \ + qt/res/icons/icons_dark/vote.svg \ qt/res/icons/icons_light/transactions.svg \ qt/res/icons/icons_light/chevron_down.svg \ qt/res/icons/icons_light/chevron_right.svg \ diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 10f40c93b7..b3375cb223 100755 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -12,6 +12,7 @@ #include "clientmodel.h" #include "walletmodel.h" #include "researcher/researchermodel.h" +#include "voting/votingmodel.h" #include "optionsmodel.h" #include "guiutil.h" #include "qt/intro.h" @@ -632,10 +633,12 @@ int StartGridcoinQt(int argc, char *argv[], QApplication& app, OptionsModel& opt ClientModel clientModel(&optionsModel); WalletModel walletModel(pwalletMain, &optionsModel); ResearcherModel researcherModel; + VotingModel votingModel(clientModel, optionsModel, walletModel); window.setResearcherModel(&researcherModel); window.setClientModel(&clientModel); window.setWalletModel(&walletModel); + window.setVotingModel(&votingModel); // If -min option passed, start window minimized. if(gArgs.GetBoolArg("-min")) diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index d72c35728a..1e9e62e37e 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -88,12 +88,22 @@ res/icons/icons_light/status_encryption_unlocked.svg + res/icons/icons_light/add.svg res/icons/icons_light/chevron_down.svg res/icons/icons_light/chevron_right.svg res/icons/icons_light/chevron_up.svg + res/icons/icons_light/create.svg + res/icons/icons_light/edit.svg + res/icons/icons_light/hamburger.svg + res/icons/icons_light/list_view.svg + res/icons/icons_light/refresh.svg + res/icons/icons_light/remove.svg res/icons/icons_light/search.svg res/icons/icons_light/settings.svg res/icons/icons_light/settings_action_needed.svg + res/icons/icons_light/sort_asc.svg + res/icons/icons_light/table_view.svg + res/icons/icons_light/vote.svg + res/icons/icons_dark/add.svg res/icons/icons_dark/chevron_down.svg res/icons/icons_dark/chevron_right.svg res/icons/icons_dark/chevron_up.svg + res/icons/icons_dark/create.svg + res/icons/icons_dark/edit.svg + res/icons/icons_dark/hamburger.svg + res/icons/icons_dark/list_view.svg + res/icons/icons_dark/refresh.svg + res/icons/icons_dark/remove.svg res/icons/icons_dark/search.svg res/icons/icons_dark/settings_action_needed.svg + res/icons/icons_dark/sort_asc.svg + res/icons/icons_dark/table_view.svg + res/icons/icons_dark/vote.svg res/icons/tx_pos_ss.svg res/icons/tx_por.svg @@ -180,6 +200,7 @@ res/icons/light_mode.svg res/icons/light_mode_active.svg res/icons/no_result.svg + res/icons/open_link.svg diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 528e9d6c8e..f2471c43b6 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -20,7 +20,7 @@ #include "signverifymessagedialog.h" #include "optionsdialog.h" #include "aboutdialog.h" -#include "votingdialog.h" +#include "voting/votingpage.h" #include "clientmodel.h" #include "walletmodel.h" #include "researcher/researchermodel.h" @@ -48,6 +48,7 @@ #endif #include +#include #include #include #include @@ -55,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -186,7 +188,7 @@ BitcoinGUI::BitcoinGUI(QWidget *parent): receiveCoinsPage = new ReceiveCoinsPage(this); transactionView = new TransactionView(this); addressBookPage = new FavoritesPage(this); - votingPage = new VotingDialog(this); + votingPage = new VotingPage(this); signVerifyMessageDialog = new SignVerifyMessageDialog(this); @@ -750,6 +752,7 @@ void BitcoinGUI::setClientModel(ClientModel *clientModel) rpcConsole->setClientModel(clientModel); addressBookPage->setOptionsModel(clientModel->getOptionsModel()); receiveCoinsPage->setOptionsModel(clientModel->getOptionsModel()); + votingPage->setOptionsModel(clientModel->getOptionsModel()); } } @@ -772,7 +775,6 @@ void BitcoinGUI::setWalletModel(WalletModel *walletModel) addressBookPage->setAddressTableModel(walletModel->getAddressTableModel()); receiveCoinsPage->setAddressTableModel(walletModel->getAddressTableModel()); sendCoinsPage->setModel(walletModel); - votingPage->setModel(walletModel); signVerifyMessageDialog->setModel(walletModel); setEncryptionStatus(walletModel->getEncryptionStatus()); @@ -802,6 +804,11 @@ void BitcoinGUI::setResearcherModel(ResearcherModel *researcherModel) connect(researcherModel, SIGNAL(beaconChanged()), this, SLOT(updateBeaconIcon())); } +void BitcoinGUI::setVotingModel(VotingModel *votingModel) +{ + votingPage->setVotingModel(votingModel); +} + void BitcoinGUI::createTrayIcon() { #ifndef Q_OS_MAC diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index f7520e0692..7ef1163cc8 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -16,12 +16,13 @@ class TransactionTableModel; class ClientModel; class WalletModel; class ResearcherModel; +class VotingModel; class TransactionView; class OverviewPage; class FavoritesPage; class ReceiveCoinsPage; class SendCoinsDialog; -class VotingDialog; +class VotingPage; class SignVerifyMessageDialog; class Notificator; class RPCConsole; @@ -65,6 +66,11 @@ class BitcoinGUI : public QMainWindow */ void setResearcherModel(ResearcherModel *researcherModel); + /** Set the voting model. + The voting model facilitates presentation of and interaction with network polls and votes. + */ + void setVotingModel(VotingModel *votingModel); + protected: void changeEvent(QEvent *e); void closeEvent(QCloseEvent *event); @@ -83,7 +89,7 @@ class BitcoinGUI : public QMainWindow ReceiveCoinsPage *receiveCoinsPage; SendCoinsDialog *sendCoinsPage; TransactionView *transactionView; - VotingDialog *votingPage; + VotingPage *votingPage; SignVerifyMessageDialog *signVerifyMessageDialog; std::unique_ptr updateMessageDialog; diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index 0e9380de30..fd5acfad22 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -923,7 +923,11 @@ - + + + true + + diff --git a/src/qt/forms/voting/pollcard.ui b/src/qt/forms/voting/pollcard.ui new file mode 100644 index 0000000000..894c6925c3 --- /dev/null +++ b/src/qt/forms/voting/pollcard.ui @@ -0,0 +1,235 @@ + + + PollCard + + + + 0 + 0 + 666 + 146 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 16 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Title + + + Qt::PlainText + + + true + + + + + + + 3 + + + + + Votes: + + + + + + + + + + + + + + + + + + + + + Expiration: + + + + + + + Top Answer: + + + + + + + + + + Qt::PlainText + + + true + + + + + + + Total Weight: + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + true + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Balance + + + + + + + Magnitude + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Voting finished. + + + + + + + Vote + + + + + + + Details + + + + + + + + + + + diff --git a/src/qt/forms/voting/pollcardview.ui b/src/qt/forms/voting/pollcardview.ui new file mode 100644 index 0000000000..3f931a54c9 --- /dev/null +++ b/src/qt/forms/voting/pollcardview.ui @@ -0,0 +1,167 @@ + + + PollCardView + + + + 0 + 0 + 697 + 228 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + 0 + 0 + 695 + 226 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 12 + + + 12 + + + 12 + + + 12 + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 1 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 12 + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + + diff --git a/src/qt/forms/voting/polldetails.ui b/src/qt/forms/voting/polldetails.ui new file mode 100644 index 0000000000..9bdae8e4b3 --- /dev/null +++ b/src/qt/forms/voting/polldetails.ui @@ -0,0 +1,137 @@ + + + PollDetails + + + + 0 + 0 + 599 + 103 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Date Range + + + Qt::TextSelectableByMouse + + + + + + + Title + + + Qt::PlainText + + + true + + + Qt::TextSelectableByMouse + + + + + + + Question + + + Qt::PlainText + + + true + + + Qt::TextSelectableByMouse + + + + + + + + + + + + + 1 + 0 + + + + URL + + + Qt::RichText + + + true + + + Qt::TextBrowserInteraction + + + + + + + + + 0 + + + + + Top Answer: + + + + + + + + 1 + 0 + + + + Qt::PlainText + + + true + + + Qt::TextSelectableByMouse + + + + + + + + + + diff --git a/src/qt/forms/voting/pollresultchoiceitem.ui b/src/qt/forms/voting/pollresultchoiceitem.ui new file mode 100644 index 0000000000..b513af6856 --- /dev/null +++ b/src/qt/forms/voting/pollresultchoiceitem.ui @@ -0,0 +1,106 @@ + + + PollResultChoiceItem + + + + 0 + 0 + 666 + 90 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Choice Text + + + Qt::PlainText + + + true + + + Qt::TextSelectableByMouse + + + + + + + 1000 + + + false + + + false + + + + + + + + + Weight: + + + + + + + 123 + + + Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 1% + + + Qt::TextSelectableByMouse + + + + + + + + + + diff --git a/src/qt/forms/voting/pollresultdialog.ui b/src/qt/forms/voting/pollresultdialog.ui new file mode 100644 index 0000000000..c3fa87134c --- /dev/null +++ b/src/qt/forms/voting/pollresultdialog.ui @@ -0,0 +1,157 @@ + + + PollResultDialog + + + + 0 + 0 + 732 + 524 + + + + Poll Details + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::Horizontal + + + + + + + true + + + + + 0 + 0 + 710 + 430 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 12 + + + 9 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + + Poll ID + + + ID + + + Qt::TextSelectableByMouse + + + + + + + QDialogButtonBox::Close + + + + + + + + + + PollDetails + QWidget +
voting/polldetails.h
+ 1 +
+
+ + +
diff --git a/src/qt/forms/voting/polltab.ui b/src/qt/forms/voting/polltab.ui new file mode 100644 index 0000000000..130bbf251b --- /dev/null +++ b/src/qt/forms/voting/polltab.ui @@ -0,0 +1,135 @@ + + + PollTab + + + + 0 + 0 + 666 + 424 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + false + + + true + + + false + + + true + + + false + + + + + + + + + + + + + + + PollCardView + QWidget +
voting/pollcardview.h
+ 1 +
+
+ + +
diff --git a/src/qt/forms/voting/votingpage.ui b/src/qt/forms/voting/votingpage.ui new file mode 100644 index 0000000000..b056d81d37 --- /dev/null +++ b/src/qt/forms/voting/votingpage.ui @@ -0,0 +1,235 @@ + + + VotingPage + + + + 0 + 0 + 899 + 456 + + + + Voting + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 4 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Polls + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Search by title + + + true + + + + + + + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + View as list. + + + Alt+T + + + + + + + View as table. + + + Alt+T + + + + + + + Sort by... + + + Alt+S + + + QToolButton::InstantPopup + + + + + + + &Refresh + + + + + + + Create &Poll + + + + + + + + + + A new poll is available. Press "Refresh" to load it. + + + Qt::AlignCenter + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + &Active + + + + + &Completed + + + + + + + + + + + + PollTab + QWidget +
voting/polltab.h
+ 1 +
+
+ + +
diff --git a/src/qt/noresult.cpp b/src/qt/noresult.cpp index 0eddffd745..44aac0888b 100644 --- a/src/qt/noresult.cpp +++ b/src/qt/noresult.cpp @@ -45,3 +45,18 @@ void NoResult::setContentWidget(QWidget* widget) ui->verticalLayout->insertWidget(index, widget, 0, Qt::AlignHCenter); } } + +void NoResult::showDefaultNothingHereTitle() +{ + setTitle(tr("Nothing here yet...")); +} + +void NoResult::showDefaultNoResultTitle() +{ + setTitle(tr("No results available.")); +} + +void NoResult::showDefaultLoadingTitle() +{ + setTitle(tr("Loading...")); +} diff --git a/src/qt/noresult.h b/src/qt/noresult.h index 3be380156c..523a6f5a66 100644 --- a/src/qt/noresult.h +++ b/src/qt/noresult.h @@ -21,9 +21,19 @@ class NoResult : public QWidget QWidget* contentWidget(); + template + ContentWidget* contentWidgetAs() + { + return qobject_cast(m_content_widget); + } + + public slots: void setTitle(const QString& title); void setContentWidget(QWidget* widget); + void showDefaultNothingHereTitle(); + void showDefaultNoResultTitle(); + void showDefaultLoadingTitle(); private: Ui::NoResult *ui; diff --git a/src/qt/res/icons/icons_dark/add.svg b/src/qt/res/icons/icons_dark/add.svg new file mode 100644 index 0000000000..6e0ddaf489 --- /dev/null +++ b/src/qt/res/icons/icons_dark/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_dark/create.svg b/src/qt/res/icons/icons_dark/create.svg new file mode 100644 index 0000000000..082f89a4fc --- /dev/null +++ b/src/qt/res/icons/icons_dark/create.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_dark/edit.svg b/src/qt/res/icons/icons_dark/edit.svg new file mode 100644 index 0000000000..dd6416c3ac --- /dev/null +++ b/src/qt/res/icons/icons_dark/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_dark/hamburger.svg b/src/qt/res/icons/icons_dark/hamburger.svg new file mode 100644 index 0000000000..4e8bbcfbfe --- /dev/null +++ b/src/qt/res/icons/icons_dark/hamburger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_dark/list_view.svg b/src/qt/res/icons/icons_dark/list_view.svg new file mode 100644 index 0000000000..97ac228b9d --- /dev/null +++ b/src/qt/res/icons/icons_dark/list_view.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qt/res/icons/icons_dark/refresh.svg b/src/qt/res/icons/icons_dark/refresh.svg new file mode 100644 index 0000000000..b7e74dbb12 --- /dev/null +++ b/src/qt/res/icons/icons_dark/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_dark/remove.svg b/src/qt/res/icons/icons_dark/remove.svg new file mode 100644 index 0000000000..7095293dad --- /dev/null +++ b/src/qt/res/icons/icons_dark/remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_dark/sort_asc.svg b/src/qt/res/icons/icons_dark/sort_asc.svg new file mode 100644 index 0000000000..955dbc39d0 --- /dev/null +++ b/src/qt/res/icons/icons_dark/sort_asc.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qt/res/icons/icons_dark/table_view.svg b/src/qt/res/icons/icons_dark/table_view.svg new file mode 100644 index 0000000000..9dcb8e7d5c --- /dev/null +++ b/src/qt/res/icons/icons_dark/table_view.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/qt/res/icons/icons_dark/vote.svg b/src/qt/res/icons/icons_dark/vote.svg new file mode 100644 index 0000000000..540180830c --- /dev/null +++ b/src/qt/res/icons/icons_dark/vote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_light/add.svg b/src/qt/res/icons/icons_light/add.svg new file mode 100644 index 0000000000..1e793cb174 --- /dev/null +++ b/src/qt/res/icons/icons_light/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_light/create.svg b/src/qt/res/icons/icons_light/create.svg new file mode 100644 index 0000000000..4636571101 --- /dev/null +++ b/src/qt/res/icons/icons_light/create.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_light/edit.svg b/src/qt/res/icons/icons_light/edit.svg new file mode 100644 index 0000000000..8b954dd05f --- /dev/null +++ b/src/qt/res/icons/icons_light/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_light/hamburger.svg b/src/qt/res/icons/icons_light/hamburger.svg new file mode 100644 index 0000000000..c23ab15767 --- /dev/null +++ b/src/qt/res/icons/icons_light/hamburger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_light/list_view.svg b/src/qt/res/icons/icons_light/list_view.svg new file mode 100644 index 0000000000..824625d3d3 --- /dev/null +++ b/src/qt/res/icons/icons_light/list_view.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qt/res/icons/icons_light/refresh.svg b/src/qt/res/icons/icons_light/refresh.svg new file mode 100644 index 0000000000..d972827332 --- /dev/null +++ b/src/qt/res/icons/icons_light/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_light/remove.svg b/src/qt/res/icons/icons_light/remove.svg new file mode 100644 index 0000000000..a32a0ed642 --- /dev/null +++ b/src/qt/res/icons/icons_light/remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/icons_light/sort_asc.svg b/src/qt/res/icons/icons_light/sort_asc.svg new file mode 100644 index 0000000000..2e44e5c6fa --- /dev/null +++ b/src/qt/res/icons/icons_light/sort_asc.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qt/res/icons/icons_light/table_view.svg b/src/qt/res/icons/icons_light/table_view.svg new file mode 100644 index 0000000000..677a67d94d --- /dev/null +++ b/src/qt/res/icons/icons_light/table_view.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/qt/res/icons/icons_light/vote.svg b/src/qt/res/icons/icons_light/vote.svg new file mode 100644 index 0000000000..94fed95529 --- /dev/null +++ b/src/qt/res/icons/icons_light/vote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/icons/open_link.svg b/src/qt/res/icons/open_link.svg new file mode 100644 index 0000000000..78bb7afb86 --- /dev/null +++ b/src/qt/res/icons/open_link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qt/res/stylesheets/dark_stylesheet.qss b/src/qt/res/stylesheets/dark_stylesheet.qss index a49411c7db..cf00407ff8 100644 --- a/src/qt/res/stylesheets/dark_stylesheet.qss +++ b/src/qt/res/stylesheets/dark_stylesheet.qss @@ -44,7 +44,8 @@ QTabWidget::pane, padding: 0.75em 1em; } -AddressBookPage #wrapperFrame { +AddressBookPage #wrapperFrame, +PollCard #detailsFrame { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } @@ -465,6 +466,14 @@ QTabBar::tab:!selected:hover { border-bottom-color: rgb(75, 143, 226); } +#linkIconLabel { + image: url(:/icons/open_link); + min-width: 1em; + max-width: 1em; + min-height: 1em; + max-height: 1em; +} + /* RPC Console */ #messagesWidget, #lineEdit, #scraper_log { font-family: "Inconsolata"; @@ -823,12 +832,103 @@ TransactionView #filterFrame { /* Voting page */ +VotingPage #tabWrapperWidget { + background: rgba(0, 0, 0, 0.1); +} + +VotingPage QTabWidget::tab-bar { + left: 0; +} + +VotingPage QTabBar::tab { + margin-bottom: 0; + margin: 0 0.75em; +} + +VotingPage QTabBar::tab:first { + margin-left: 1em; +} + +VotingPage QTabWidget::pane { + background-color: rgb(26, 38, 50); + border-top: 0.065em solid rgba(0, 0, 0, 0.3); + border-radius: 0; + padding: 0; +} + +VotingPage QStackedWidget > QWidget { + padding: 0.75em 1em; +} + +VotingPage #tabButtonFrame { + background: transparent; + padding: 0.4em 1em; +} + +VotingPage #cardsToggleButton, +VotingPage #sortButton, +VotingPage #tableToggleButton { + background: none; + border: none; + padding: 0.1em; + width: 1em; + height: 1em; +} + +VotingPage #cardsToggleButton:hover, +VotingPage #cardsToggleButton:focus, +VotingPage #sortButton:hover, +VotingPage #sortButton:focus, +VotingPage #tableToggleButton:hover, +VotingPage #tableToggleButton:focus { + background-color: rgb(21, 126, 205); +} + +VotingPage QProgressBar { + background: none; + border: none; + min-height: 0.25em; + max-height: 0.25em; + padding: 0; +} + +VotingPage QProgressBar::chunk { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 rgb(26, 145, 235), stop: 1 rgb(160, 150, 255)); + border: none; + border-radius: 0.125em; + padding: 0; +} + +VotingPage #pollReceivedLabel { + background: rgb(25, 90, 240); + padding: 0.25em 1em; + color: white; +} + +PollCard #titleLabel, +PollDetails #titleLabel, PollWizard #pageTitleLabel, -PollWizardTypePage #typeTextLabel { +PollWizardTypePage #typeTextLabel, +VoteWizard #pageTitleLabel { font-weight: bold; color: white; } +PollCard #balanceLabel, +PollCard #magnitudeLabel { + border: 0.065em solid rgb(115, 131, 161); + border-radius: 0.65em; + padding: 0.1em 0.3em; + color: rgb(115, 131, 161); +} + +PollCard #remainingLabel, +PollResultChoiceItem #percentageLabel, +PollResultChoiceItem #weightLabel, +PollResultChoiceItem #weightTextLabel { + color: rgba(204, 208, 209, 0.6); +} + PollResultChoiceItem QProgressBar { background: rgba(0, 0, 0, 0.2); border: none; @@ -838,6 +938,13 @@ PollResultChoiceItem QProgressBar { padding: 0; } +PollDetails #dateRangeLabel, +PollResultDialog #idLabel, +PollWizardSummaryPage #pollIdLabel, +VoteWizardSummaryPage #voteIdLabel { + color: rgba(255, 255, 255, 0.4); +} + PollWizardDetailsPage #pollTypeAlert { background: rgba(255, 240, 0, 0.4); border-radius: 0.25em; @@ -845,7 +952,8 @@ PollWizardDetailsPage #pollTypeAlert { color: white; } -PollWizardDetailsPage #errorLabel { +PollWizardDetailsPage #errorLabel, +VoteWizardBallotPage #errorLabel { background: rgba(255, 0, 0, 0.4); border-radius: 0.25em; padding: 0.1em 0.2em; diff --git a/src/qt/res/stylesheets/light_stylesheet.qss b/src/qt/res/stylesheets/light_stylesheet.qss index 1b7774cbbd..270ce2387b 100644 --- a/src/qt/res/stylesheets/light_stylesheet.qss +++ b/src/qt/res/stylesheets/light_stylesheet.qss @@ -40,7 +40,8 @@ QTabWidget::pane, padding: 0.75em 1em; } -AddressBookPage #wrapperFrame { +AddressBookPage #wrapperFrame, +PollCard #detailsFrame { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } @@ -456,6 +457,14 @@ QTabBar::tab:!selected:hover { border-bottom-color: rgb(113, 121, 140); } +#linkIconLabel { + image: url(:/icons/open_link); + min-width: 1em; + max-width: 1em; + min-height: 1em; + max-height: 1em; +} + /* RPC Console */ #messagesWidget, #lineEdit, #scraper_log { font-family: "Inconsolata"; @@ -803,12 +812,114 @@ TransactionView #filterFrame { /* Voting page */ +VotingPage #tabWrapperWidget { + background: rgb(244, 247, 249); +} + +VotingPage QTabWidget::tab-bar { + left: 0; +} + +VotingPage QTabBar::tab { + margin-bottom: 0; + margin: 0 0.75em; +} + +VotingPage QTabBar::tab:first { + margin-left: 1em; +} + +VotingPage QTabWidget::pane { + background-color: rgb(235, 236, 238); + border-top: 0.065em solid rgba(0, 0, 0, 0.1); + border-radius: 0; + padding: 0; +} + +VotingPage #tabButtonFrame { + background: transparent; + padding: 0.4em 1em; +} + +VotingPage #cardsToggleButton, +VotingPage #sortButton, +VotingPage #tableToggleButton { + background: none; + border: none; + padding: 0.1em; + width: 1em; + height: 1em; +} + +VotingPage #cardsToggleButton:hover, +VotingPage #cardsToggleButton:focus, +VotingPage #sortButton:hover, +VotingPage #sortButton:focus, +VotingPage #tableToggleButton:hover, +VotingPage #tableToggleButton:focus { + background: rgba(0, 0, 0, 0.1); +} + +VotingPage QProgressBar { + background: none; + border: none; + min-height: 0.25em; + max-height: 0.25em; + padding: 0; +} + +VotingPage QProgressBar::chunk { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 rgb(100, 20, 255), stop: 1 rgb(180, 40, 255)); + border: none; + border-radius: 0.125em; + padding: 0; +} + +VotingPage #pollReceivedLabel { + background: rgb(75, 30, 250); + padding: 0.25em 1em; + color: white; +} + +PollCard #titleLabel, +PollDetails #titleLabel, PollWizard #pageTitleLabel, PollWizardTypePage #typeTextLabel { color: rgb(43, 52, 69); font-weight: bold; } +PollCard #balanceLabel, +PollCard #magnitudeLabel { + border: 0.065em solid rgb(115, 131, 161); + border-radius: 0.65em; + padding: 0.1em 0.3em; + color: rgb(115, 131, 161); +} + +PollCard #remainingLabel, +PollResultChoiceItem #percentageLabel, +PollResultChoiceItem #weightLabel, +PollResultChoiceItem #weightTextLabel { + color: rgba(58, 70, 93, 0.6); +} + +PollResultChoiceItem QProgressBar { + background: rgba(0, 0, 0, 0.1); + border: none; + border-radius: 0.25em; + min-height: 0.5em; + max-height: 0.5em; + padding: 0; +} + +PollDetails #dateRangeLabel, +PollResultDialog #idLabel, +PollWizardSummaryPage #pollIdLabel, +VoteWizardSummaryPage #voteIdLabel { + color: rgba(0, 0, 0, 0.4); +} + PollWizardDetailsPage #pollTypeAlert { background: rgba(255, 240, 0, 0.6); border-radius: 0.25em; @@ -816,7 +927,8 @@ PollWizardDetailsPage #pollTypeAlert { color: black; } -PollWizardDetailsPage #errorLabel { +PollWizardDetailsPage #errorLabel, +VoteWizardBallotPage #errorLabel { background: rgba(255, 0, 0, 0.4); border-radius: 0.25em; padding: 0.1em 0.2em; diff --git a/src/qt/voting/pollcard.cpp b/src/qt/voting/pollcard.cpp new file mode 100644 index 0000000000..e4776aa908 --- /dev/null +++ b/src/qt/voting/pollcard.cpp @@ -0,0 +1,74 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/forms/voting/ui_pollcard.h" +#include "qt/guiutil.h" +#include "qt/voting/pollcard.h" +#include "qt/voting/votingmodel.h" + +// ----------------------------------------------------------------------------- +// Class: PollCard +// ----------------------------------------------------------------------------- + +PollCard::PollCard(const PollItem& poll_item, QWidget* parent) + : QWidget(parent) + , ui(new Ui::PollCard) + , m_expiration(poll_item.m_expiration) +{ + ui->setupUi(this); + + ui->titleLabel->setText(poll_item.m_title); + ui->expirationLabel->setText(GUIUtil::dateTimeStr(poll_item.m_expiration)); + ui->voteCountLabel->setText(QString::number(poll_item.m_total_votes)); + ui->totalWeightLabel->setText(QString::number(poll_item.m_total_weight)); + ui->topAnswerLabel->setText(poll_item.m_top_answer); + + if (!poll_item.m_finished) { + connect(ui->voteButton, &QPushButton::clicked, this, &PollCard::voteRequested); + } else { + delete ui->voteButton; + ui->voteButton = nullptr; + } + + connect(ui->detailsButton, &QPushButton::clicked, this, &PollCard::detailsRequested); +} + +PollCard::~PollCard() +{ + delete ui; +} + +void PollCard::updateRemainingTime(const QDateTime& now) +{ + if (ui->voteButton == nullptr) { + return; + } + + if (m_expiration < now) { + delete ui->voteButton; + ui->voteButton = nullptr; + ui->remainingLabel->setText(tr("Voting finished.")); + + return; + } + + constexpr int64_t three_days = 60 * 60 * 24 * 3; + const int64_t remaining_secs = now.secsTo(m_expiration); + + ui->remainingLabel->setText(QObject::tr("%1 remaining.") + .arg(GUIUtil::formatNiceTimeOffset(remaining_secs))); + + if (remaining_secs < three_days) { + ui->remainingLabel->setStyleSheet(QStringLiteral("color: red")); + } +} + +void PollCard::updateIcons(const QString& theme) +{ + if (ui->voteButton != nullptr) { + ui->voteButton->setIcon(QIcon(":/icons/" + theme + "_vote")); + } + + ui->detailsButton->setIcon(QIcon(":/icons/" + theme + "_hamburger")); +} diff --git a/src/qt/voting/pollcard.h b/src/qt/voting/pollcard.h new file mode 100644 index 0000000000..2cc8b2bbba --- /dev/null +++ b/src/qt/voting/pollcard.h @@ -0,0 +1,38 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLCARD_H +#define VOTING_POLLCARD_H + +#include +#include + +namespace Ui { +class PollCard; +} + +class PollItem; + +class PollCard : public QWidget +{ + Q_OBJECT + +public: + explicit PollCard(const PollItem& poll_item, QWidget* parent = nullptr); + ~PollCard(); + +signals: + void voteRequested(); + void detailsRequested(); + +public slots: + void updateRemainingTime(const QDateTime& now); + void updateIcons(const QString& theme); + +private: + Ui::PollCard* ui; + QDateTime m_expiration; +}; + +#endif // VOTING_POLLCARD_H diff --git a/src/qt/voting/pollcardview.cpp b/src/qt/voting/pollcardview.cpp new file mode 100644 index 0000000000..d6bc7a62b7 --- /dev/null +++ b/src/qt/voting/pollcardview.cpp @@ -0,0 +1,144 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/forms/voting/ui_pollcardview.h" +#include "qt/voting/pollcard.h" +#include "qt/voting/pollcardview.h" +#include "qt/voting/polltablemodel.h" +#include "qt/voting/votingmodel.h" + +#include +#include + +namespace { +constexpr int REFRESH_TIMER_INTERVAL_MSECS = 60 * 1000; +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: PollCardView +// ----------------------------------------------------------------------------- + +PollCardView::PollCardView(QWidget* parent) + : QWidget(parent) + , ui(new Ui::PollCardView) +{ + ui->setupUi(this); +} + +PollCardView::~PollCardView() +{ + delete ui; +} + +void PollCardView::setModel(PollTableModel* model) +{ + m_model = model; + + if (!model) { + return; + } + + connect(model, &PollTableModel::layoutChanged, this, &PollCardView::redraw); + + if (!m_refresh_timer && m_model->includesActivePolls()) { + m_refresh_timer.reset(new QTimer(this)); + m_refresh_timer->setTimerType(Qt::VeryCoarseTimer); + + connect( + m_refresh_timer.get(), &QTimer::timeout, + this, &PollCardView::updateRemainingTime); + } +} + +void PollCardView::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + + if (m_refresh_timer) { + updateRemainingTime(); + m_refresh_timer->start(REFRESH_TIMER_INTERVAL_MSECS); + } +} + +void PollCardView::hideEvent(QHideEvent* event) +{ + QWidget::hideEvent(event); + + if (m_refresh_timer) { + m_refresh_timer->stop(); + } +} + +void PollCardView::redraw() +{ + // TODO: destroying and re-creating the widgets is not very efficient for + // sorting and filtering. Hook up model events for these operations. + clear(); + + if (!m_model) { + return; + } + + const QDateTime now = QDateTime::currentDateTimeUtc(); + const QModelIndex dummy_parent; + + for (int i = 0; i < m_model->rowCount(dummy_parent); ++i) { + if (const PollItem* poll_item = m_model->rowItem(i)) { + PollCard* card = new PollCard(*poll_item, this); + card->updateRemainingTime(now); + card->updateIcons(m_theme); + + ui->cardsLayout->addWidget(card); + + if (!poll_item->m_finished) { + connect(card, &PollCard::voteRequested, [this, i]() { + emit voteRequested(i); + }); + } + + connect(card, &PollCard::detailsRequested, [this, i]() { + emit detailsRequested(i); + }); + } + } +} + +void PollCardView::clear() +{ + while (ui->cardsLayout->count() > 0) { + delete ui->cardsLayout->takeAt(0)->widget(); + } + + ui->scrollArea->verticalScrollBar()->setValue(0); +} + +void PollCardView::updateRemainingTime() +{ + if (ui->cardsLayout->count() == 0) { + return; + } + + const QDateTime now = QDateTime::currentDateTimeUtc(); + + for (int i = 0; i < ui->cardsLayout->count(); ++i) { + QLayoutItem* item = ui->cardsLayout->itemAt(i); + + if (auto* card = qobject_cast(item->widget())) { + card->updateRemainingTime(now); + } + } +} + +void PollCardView::updateIcons(const QString& theme) +{ + m_theme = theme; + + for (int i = 0; i < ui->cardsLayout->count(); ++i) { + QLayoutItem* item = ui->cardsLayout->itemAt(i); + + if (auto* card = qobject_cast(item->widget())) { + card->updateIcons(theme); + } + } +} diff --git a/src/qt/voting/pollcardview.h b/src/qt/voting/pollcardview.h new file mode 100644 index 0000000000..9b30920f99 --- /dev/null +++ b/src/qt/voting/pollcardview.h @@ -0,0 +1,55 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLCARDVIEW_H +#define VOTING_POLLCARDVIEW_H + +#include +#include + +namespace Ui { +class PollCardView; +} + +class PollTableModel; + +QT_BEGIN_NAMESPACE +class QHideEvent; +class QShowEvent; +class QTimer; +QT_END_NAMESPACE + +class PollCardView : public QWidget +{ + Q_OBJECT + +public: + explicit PollCardView(QWidget* parent = nullptr); + ~PollCardView(); + + void setModel(PollTableModel* model); + + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + +signals: + void voteRequested(int row); + void detailsRequested(int row); + +public slots: + void updateRemainingTime(); + void updateIcons(const QString& theme); + +private: + Ui::PollCardView* ui; + PollTableModel* m_model; + std::unique_ptr m_refresh_timer; + QString m_theme; + +private slots: + void redraw(); + void clear(); +}; + +#endif // VOTING_POLLCARDVIEW_H diff --git a/src/qt/voting/polldetails.cpp b/src/qt/voting/polldetails.cpp new file mode 100644 index 0000000000..8ea397ba63 --- /dev/null +++ b/src/qt/voting/polldetails.cpp @@ -0,0 +1,42 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/guiutil.h" +#include "qt/forms/voting/ui_polldetails.h" +#include "qt/voting/polldetails.h" +#include "qt/voting/votingmodel.h" + +PollDetails::PollDetails(QWidget* parent) + : QWidget(parent) + , ui(new Ui::PollDetails) +{ + ui->setupUi(this); + + GRC::ScaleFontPointSize(ui->dateRangeLabel, 9); + GRC::ScaleFontPointSize(ui->titleLabel, 12); + GRC::ScaleFontPointSize(ui->questionLabel, 11); +} + +PollDetails::~PollDetails() +{ + delete ui; +} + +void PollDetails::setItem(const PollItem& poll_item) +{ + ui->dateRangeLabel->setText(QStringLiteral("%1 → %2") + .arg(GUIUtil::dateTimeStr(poll_item.m_start_time)) + .arg(GUIUtil::dateTimeStr(poll_item.m_expiration))); + + ui->titleLabel->setText(poll_item.m_title); + ui->urlLabel->setText(QStringLiteral("%1").arg(poll_item.m_url)); + + ui->questionLabel->setVisible(!poll_item.m_question.isEmpty()); + ui->questionLabel->setText(poll_item.m_question); + + ui->topAnswerTextLabel->setVisible(!poll_item.m_top_answer.isEmpty()); + ui->topAnswerLabel->setVisible(!poll_item.m_top_answer.isEmpty()); + ui->topAnswerLabel->setText(poll_item.m_top_answer); +} diff --git a/src/qt/voting/polldetails.h b/src/qt/voting/polldetails.h new file mode 100644 index 0000000000..5eba695768 --- /dev/null +++ b/src/qt/voting/polldetails.h @@ -0,0 +1,30 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLDETAILS_H +#define VOTING_POLLDETAILS_H + +#include + +namespace Ui { +class PollDetails; +} + +class PollItem; + +class PollDetails : public QWidget +{ + Q_OBJECT + +public: + explicit PollDetails(QWidget* parent = nullptr); + ~PollDetails(); + + void setItem(const PollItem& poll_item); + +private: + Ui::PollDetails* ui; +}; + +#endif // VOTING_POLLDETAILS_H diff --git a/src/qt/voting/pollresultchoiceitem.cpp b/src/qt/voting/pollresultchoiceitem.cpp new file mode 100644 index 0000000000..eb32889761 --- /dev/null +++ b/src/qt/voting/pollresultchoiceitem.cpp @@ -0,0 +1,77 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/forms/voting/ui_pollresultchoiceitem.h" +#include "qt/voting/pollresultchoiceitem.h" +#include "qt/voting/votingmodel.h" + +namespace { +constexpr char CHUNK_STYLE_TEMPLATE[] = + "QProgressBar::chunk {" + "border-radius: 0.25em;" + "background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 %1, stop: 1 %2);" + "}"; + +const QColor BAR_START_COLOR(0, 219, 222); +const QColor BAR_END_COLOR(252, 0, 255); + +QString CalculateBarStyle(const double ratio) +{ + const QColor end_color( + (1 - ratio) * BAR_START_COLOR.red() + ratio * BAR_END_COLOR.red(), + (1 - ratio) * BAR_START_COLOR.green() + ratio * BAR_END_COLOR.green(), + (1 - ratio) * BAR_START_COLOR.blue() + ratio * BAR_END_COLOR.blue()); + + return QString(CHUNK_STYLE_TEMPLATE) + .arg(BAR_START_COLOR.name()) + .arg(end_color.name()); +} +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: PollResultChoiceItem +// ----------------------------------------------------------------------------- + +PollResultChoiceItem::PollResultChoiceItem( + const VoteResultItem& choice, + const double total_poll_weight, + const double top_choice_weight, + QWidget* parent) + : QFrame(parent) + , ui(new Ui::PollResultChoiceItem) +{ + ui->setupUi(this); + + ui->choiceLabel->setText(choice.m_label); + ui->weightLabel->setText(QString::number(choice.m_weight)); + + // If the poll has no responses yet, skip the rest of the ratio-dependent + // calculations and formatting: + // + if (total_poll_weight == 0) { + ui->percentageLabel->hide(); + return; + } + + const double relative_ratio = choice.m_weight / top_choice_weight; + const double percentage = 100.0 * choice.m_weight / total_poll_weight; + + ui->percentageLabel->setText(QStringLiteral("%1%").arg(percentage, 0, 'f', 2)); + + // Limit the lower bound to the smallest degree that Qt will draw the + // progress bar's rounded border at for the default dialog size: + // + if (percentage < 0.5) { + ui->weightBar->setValue(0); + } else { + ui->weightBar->setValue(std::max(12.0, 1000.0 * relative_ratio)); + } + + ui->weightBar->setStyleSheet(CalculateBarStyle(relative_ratio)); +} + +PollResultChoiceItem::~PollResultChoiceItem() +{ + delete ui; +} diff --git a/src/qt/voting/pollresultchoiceitem.h b/src/qt/voting/pollresultchoiceitem.h new file mode 100644 index 0000000000..9d2c51c8c5 --- /dev/null +++ b/src/qt/voting/pollresultchoiceitem.h @@ -0,0 +1,32 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLRESULTCHOICEITEM_H +#define VOTING_POLLRESULTCHOICEITEM_H + +#include + +namespace Ui { +class PollResultChoiceItem; +} + +class VoteResultItem; + +class PollResultChoiceItem : public QFrame +{ + Q_OBJECT + +public: + explicit PollResultChoiceItem( + const VoteResultItem& choice, + const double total_poll_weight, + const double top_choice_weight, + QWidget* parent = nullptr); + ~PollResultChoiceItem(); + +private: + Ui::PollResultChoiceItem* ui; +}; + +#endif // VOTING_POLLRESULTCHOICEITEM_H diff --git a/src/qt/voting/pollresultdialog.cpp b/src/qt/voting/pollresultdialog.cpp new file mode 100644 index 0000000000..c73f1b4126 --- /dev/null +++ b/src/qt/voting/pollresultdialog.cpp @@ -0,0 +1,77 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_pollresultdialog.h" +#include "qt/voting/pollresultchoiceitem.h" +#include "qt/voting/pollresultdialog.h" +#include "qt/voting/votingmodel.h" + +#include + +namespace { +std::vector +SortResultsByWeight(const std::vector& choices) +{ + std::vector sorted; + + for (const auto& choice_result : choices) { + sorted.emplace_back(&choice_result); + } + + constexpr auto descending = [](const VoteResultItem* a, const VoteResultItem* b) { + return *b < *a; + }; + + std::stable_sort(sorted.begin(), sorted.end(), descending); + + return sorted; +} +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: PollResultChoiceItem +// ----------------------------------------------------------------------------- + +PollResultDialog::PollResultDialog(const PollItem& poll_item, QWidget* parent) + : QDialog(parent) + , ui(new Ui::PollResultDialog) +{ + ui->setupUi(this); + + setModal(true); + setAttribute(Qt::WA_DeleteOnClose, true); + resize(GRC::ScaleSize(this, 740, 580)); + + GRC::ScaleFontPointSize(ui->idLabel, 8); + + ui->details->setItem(poll_item); + ui->idLabel->setText(poll_item.m_id); + + const auto sorted = SortResultsByWeight(poll_item.m_choices); + + ui->choicesLayout->addWidget(new PollResultChoiceItem( + *sorted.front(), + poll_item.m_total_weight, + sorted.front()->m_weight)); + + for (size_t i = 1; i < poll_item.m_choices.size(); ++i) { + QFrame* line = new QFrame(this); + line->setFrameShape(QFrame::HLine); + ui->choicesLayout->addWidget(line); + + ui->choicesLayout->addWidget(new PollResultChoiceItem( + *sorted[i], + poll_item.m_total_weight, + sorted.front()->m_weight)); + } + + ui->buttonBox->button(QDialogButtonBox::Close)->setIcon(QIcon()); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +PollResultDialog::~PollResultDialog() +{ + delete ui; +} diff --git a/src/qt/voting/pollresultdialog.h b/src/qt/voting/pollresultdialog.h new file mode 100644 index 0000000000..583b2e5f0d --- /dev/null +++ b/src/qt/voting/pollresultdialog.h @@ -0,0 +1,28 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLRESULTDIALOG_H +#define VOTING_POLLRESULTDIALOG_H + +#include + +namespace Ui { +class PollResultDialog; +} + +class PollItem; + +class PollResultDialog : public QDialog +{ + Q_OBJECT + +public: + explicit PollResultDialog(const PollItem& poll_item, QWidget* parent = nullptr); + ~PollResultDialog(); + +private: + Ui::PollResultDialog* ui; +}; + +#endif // VOTING_POLLRESULTDIALOG_H diff --git a/src/qt/voting/polltab.cpp b/src/qt/voting/polltab.cpp new file mode 100644 index 0000000000..e612d8dd4e --- /dev/null +++ b/src/qt/voting/polltab.cpp @@ -0,0 +1,292 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/forms/voting/ui_polltab.h" +#include "qt/noresult.h" +#include "qt/voting/pollresultdialog.h" +#include "qt/voting/polltab.h" +#include "qt/voting/polltablemodel.h" +#include "qt/voting/votewizard.h" +#include "qt/voting/votingmodel.h" + +#include +#include +#include +#include +#include +#include + +using namespace GRC; + +namespace { +QString RefreshMessage() +{ + return QCoreApplication::translate("PollTab", "Press \"Refresh\" to update the list."); +} + +QString WaitMessage() +{ + return QCoreApplication::translate("PollTab", "This may take several minutes."); +} + +QString FullRefreshMessage() +{ + return QStringLiteral("%1 %2").arg(RefreshMessage()).arg(WaitMessage()); +} +} // Anonymous namespace + +//! +//! \brief An infinite progress bar that provides a loading animation while +//! refreshing the polls lists. +//! +class LoadingBar : public QProgressBar +{ + Q_OBJECT + + static constexpr int MAX = std::numeric_limits::max(); + +public: + LoadingBar(QWidget* parent = nullptr) + : QProgressBar(parent) + , m_active(false) + { + setRange(0, MAX); + setGraphicsEffect(&m_opacity_effect); + hide(); + + m_fade_anim.setTargetObject(&m_opacity_effect); + m_fade_anim.setPropertyName("opacity"); + m_fade_anim.setDuration(1000); + m_fade_anim.setStartValue(1); + m_fade_anim.setEndValue(0); + m_fade_anim.setEasingCurve(QEasingCurve::OutQuad); + + m_size_anim.setTargetObject(this); + m_size_anim.setPropertyName("value"); + m_size_anim.setDuration(1000); + m_size_anim.setStartValue(0); + m_size_anim.setEndValue(MAX); + m_size_anim.setEasingCurve(QEasingCurve::OutQuad); + + connect(&m_fade_anim, &QPropertyAnimation::finished, this, &QProgressBar::hide); + } + + void start() + { + if (m_active) { + return; + } + + m_active = true; + m_fade_anim.stop(); + m_opacity_effect.setOpacity(1); + + m_size_anim.stop(); + m_size_anim.setLoopCount(-1); // Infinite + m_size_anim.setStartValue(0); + m_size_anim.setEndValue(MAX); + m_size_anim.start(); + + connect( + &m_size_anim, &QAbstractAnimation::currentLoopChanged, + this, &LoadingBar::invertBarAnimation); + + setInvertedAppearance(false); + reset(); + raise(); + show(); + } + + void finish() + { + m_active = false; + m_size_anim.setLoopCount(m_size_anim.currentLoop() + 1); + m_fade_anim.start(); + + disconnect( + &m_size_anim, &QAbstractAnimation::currentLoopChanged, + this, &LoadingBar::invertBarAnimation); + } + +private: + bool m_active; + QGraphicsOpacityEffect m_opacity_effect; + QPropertyAnimation m_fade_anim; + QPropertyAnimation m_size_anim; + +private slots: + void invertBarAnimation() + { + const bool inverted = invertedAppearance(); + + m_size_anim.setStartValue(inverted ? 0 : MAX); + m_size_anim.setEndValue(inverted ? MAX : 0); + setInvertedAppearance(!inverted); + } +}; // LoadingBar + +// ----------------------------------------------------------------------------- +// Class: PollTab +// ----------------------------------------------------------------------------- + +PollTab::PollTab(QWidget* parent) + : QWidget(parent) + , ui(new Ui::PollTab) + , m_model(new PollTableModel(this)) + , m_no_result(new NoResult(this)) + , m_loading(new LoadingBar(this)) +{ + ui->setupUi(this); + + ui->tabLayout->addWidget(m_no_result.get()); + ui->stack->hide(); + + ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->table->sortByColumn(PollTableModel::Expiration, Qt::AscendingOrder); + + m_no_result->setContentWidget(new QLabel(FullRefreshMessage())); + + connect(ui->cards, &PollCardView::voteRequested, this, &PollTab::showVoteRowDialog); + connect(ui->cards, &PollCardView::detailsRequested, this, &PollTab::showDetailsRowDialog); + connect(ui->table, &QAbstractItemView::doubleClicked, this, &PollTab::showPreferredDialog); + connect(ui->table, &QWidget::customContextMenuRequested, this, &PollTab::showTableContextMenu); + connect(m_model.get(), &PollTableModel::layoutChanged, this, &PollTab::finishRefresh); +} + +PollTab::~PollTab() +{ + delete ui; +} + +void PollTab::setVotingModel(VotingModel* model) +{ + m_voting_model = model; + m_model->setModel(model); + + ui->cards->setModel(m_model.get()); + ui->table->setModel(m_model.get()); +} + +void PollTab::setPollFilterFlags(PollFilterFlag flags) +{ + m_model->setPollFilterFlags(flags); +} + +void PollTab::changeViewMode(const ViewId view_id) +{ + ui->stack->setCurrentIndex(view_id); +} + +void PollTab::refresh() +{ + if (m_model->empty()) { + m_no_result->showDefaultLoadingTitle(); + m_no_result->contentWidgetAs()->setText(WaitMessage()); + } + + m_loading->start(); + m_model->refresh(); +} + +void PollTab::filter(const QString& needle) +{ + if (needle != m_last_filter) { + m_model->changeTitleFilter(needle); + m_last_filter = needle; + } +} + +void PollTab::sort(const int column) +{ + const Qt::SortOrder order = m_model->sort(column); + ui->table->horizontalHeader()->setSortIndicator(column, order); +} + +void PollTab::updateIcons(const QString& theme) +{ + ui->cards->updateIcons(theme); +} + +const PollItem* PollTab::selectedTableItem() const +{ + if (!ui->table->selectionModel()->hasSelection()) { + return nullptr; + } + + return m_model->rowItem( + ui->table->selectionModel()->selectedIndexes().first().row()); +} + +void PollTab::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + m_loading->setFixedWidth(event->size().width()); +} + +void PollTab::finishRefresh() +{ + m_loading->finish(); + ui->stack->setVisible(!m_model->empty()); + m_no_result->setVisible(m_model->empty()); + + if (m_model->empty()) { + m_no_result->showDefaultNoResultTitle(); + m_no_result->contentWidgetAs()->setText(FullRefreshMessage()); + } +} + +void PollTab::showVoteRowDialog(int row) +{ + if (const PollItem* const poll_item = m_model->rowItem(row)) { + showVoteDialog(*poll_item); + } +} + +void PollTab::showVoteDialog(const PollItem& poll_item) +{ + (new VoteWizard(poll_item, *m_voting_model, this))->show(); +} + +void PollTab::showDetailsRowDialog(int row) +{ + if (const PollItem* const poll_item = m_model->rowItem(row)) { + showDetailsDialog(*poll_item); + } +} + +void PollTab::showDetailsDialog(const PollItem& poll_item) +{ + (new PollResultDialog(poll_item, this))->show(); +} + +void PollTab::showPreferredDialog(const QModelIndex& index) +{ + if (const PollItem* const poll_item = m_model->rowItem(index.row())) { + if (poll_item->m_finished) { + showDetailsDialog(*poll_item); + } else { + showVoteDialog(*poll_item); + } + } +} + +void PollTab::showTableContextMenu(const QPoint& pos) +{ + if (const PollItem* const poll_item = selectedTableItem()) { + QMenu menu; + menu.addAction(tr("Show Results"), [this, poll_item]() { + showDetailsDialog(*poll_item); + }); + + if (!poll_item->m_finished) { + menu.addAction(tr("Vote"), [this, poll_item]() { + showVoteDialog(*poll_item); + }); + } + + menu.exec(ui->table->viewport()->mapToGlobal(pos)); + } +} + +#include "qt/voting/polltab.moc" diff --git a/src/qt/voting/polltab.h b/src/qt/voting/polltab.h new file mode 100644 index 0000000000..93675ac389 --- /dev/null +++ b/src/qt/voting/polltab.h @@ -0,0 +1,81 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_POLLTAB_H +#define VOTING_POLLTAB_H + +#include "gridcoin/voting/filter.h" + +#include +#include +#include + +namespace Ui { +class PollTab; +} + +class LoadingBar; +class NoResult; +class PollItem; +class PollTableModel; +class VotingModel; + +class PollTab : public QWidget +{ + Q_OBJECT + +public: + //! + //! \brief Represents the offsets of the main tabs on the voting page. + //! + enum TabId + { + TabActive, + TabFinished, + }; + + //! + //! \brief Represents the data views available in the stack for each tab. + //! + enum ViewId + { + ViewCards, + ViewTable, + }; + + explicit PollTab(QWidget* parent = nullptr); + ~PollTab(); + + void setVotingModel(VotingModel* voting_model); + void setPollFilterFlags(GRC::PollFilterFlag flags); + +public slots: + void changeViewMode(const ViewId view_id); + void refresh(); + void filter(const QString& needle); + void sort(const int column); + void updateIcons(const QString& theme); + +private: + Ui::PollTab* ui; + VotingModel* m_voting_model; + std::unique_ptr m_model; + std::unique_ptr m_no_result; + std::unique_ptr m_loading; + QString m_last_filter; + + const PollItem* selectedTableItem() const; + void resizeEvent(QResizeEvent* event) override; + +private slots: + void finishRefresh(); + void showVoteRowDialog(int row); + void showVoteDialog(const PollItem& poll_item); + void showDetailsRowDialog(int row); + void showDetailsDialog(const PollItem& poll_item); + void showPreferredDialog(const QModelIndex& index); + void showTableContextMenu(const QPoint& pos); +}; + +#endif // VOTING_POLLTAB_H diff --git a/src/qt/voting/votingpage.cpp b/src/qt/voting/votingpage.cpp new file mode 100644 index 0000000000..129105eb3c --- /dev/null +++ b/src/qt/voting/votingpage.cpp @@ -0,0 +1,166 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_votingpage.h" +#include "qt/optionsmodel.h" +#include "qt/voting/polltab.h" +#include "qt/voting/polltablemodel.h" +#include "qt/voting/pollwizard.h" +#include "qt/voting/votingmodel.h" +#include "qt/voting/votingpage.h" + +#include +#include +#include + +using namespace GRC; + +namespace { +//! +//! \brief Provides a dropdown used to sort polls, especially for the card view. +//! +class PollSortMenu : public QMenu +{ + Q_OBJECT + +public: + PollSortMenu(VotingPage* parent) : QMenu(parent) + { + const PollTableModel model; + const QModelIndex dummy; + + for (int column = 0; column < model.columnCount(dummy); ++column) { + addAction(model.columnName(column), [parent, column]() { + parent->currentTab().sort(column); + }); + } + } +}; // PollSortMenu +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: VotingPage +// ----------------------------------------------------------------------------- + +VotingPage::VotingPage(QWidget* parent) + : QWidget(parent) + , ui(new Ui::VotingPage) + , m_filter_action(new QAction()) +{ + ui->setupUi(this); + + GRC::ScaleFontPointSize(ui->headerTitleLabel, 15); + + m_tabs = { + ui->activePollsTab, + ui->finishedPollsTab + }; + + ui->activePollsTab->setPollFilterFlags(PollFilterFlag::ACTIVE); + ui->finishedPollsTab->setPollFilterFlags(PollFilterFlag::FINISHED); + + m_filter_action->setShortcut(Qt::CTRL + Qt::Key_F); + ui->filterLineEdit->addAction(m_filter_action.get(), QLineEdit::LeadingPosition); + ui->cardsToggleButton->hide(); + ui->sortButton->setMenu(new PollSortMenu(this)); + + // Move the buttons from the form into the tab widget's tab bar: + ui->tabWidget->setCornerWidget(ui->tabButtonFrame); + + // Move the notification label from the form into the active polls tab: + qobject_cast(ui->activePollsTab->layout())->insertWidget(0, ui->pollReceivedLabel); + ui->pollReceivedLabel->hide(); + + connect(m_filter_action.get(), &QAction::triggered, [this]() { + ui->filterLineEdit->setFocus(); + ui->filterLineEdit->selectAll(); + }); + connect(ui->filterLineEdit, &QLineEdit::textChanged, [this](const QString& pattern) { + currentTab().filter(pattern); + m_current_filter = pattern; + }); + connect(ui->cardsToggleButton, &QToolButton::clicked, [this]() { + for (auto* tab : m_tabs) { + tab->changeViewMode(PollTab::ViewCards); + } + + ui->cardsToggleButton->hide(); + ui->tableToggleButton->show(); + }); + connect(ui->tableToggleButton, &QToolButton::clicked, [this]() { + for (auto* tab : m_tabs) { + tab->changeViewMode(PollTab::ViewTable); + } + + ui->tableToggleButton->hide(); + ui->cardsToggleButton->show(); + }); + connect(ui->refreshButton, &QPushButton::clicked, [this]() { + currentTab().refresh(); + + if (ui->tabWidget->currentIndex() == PollTab::TabActive) { + ui->pollReceivedLabel->hide(); + } + }); + connect(ui->createPollButton, &QPushButton::clicked, [this]() { + (new PollWizard(*m_voting_model, this))->show(); + }); + connect(ui->tabWidget, &QTabWidget::currentChanged, [this](int tab_id) { + currentTab().filter(m_current_filter); + }); +} + +VotingPage::~VotingPage() +{ + delete ui; +} + +void VotingPage::setVotingModel(VotingModel* model) +{ + m_voting_model = model; + + for (auto tab : m_tabs) { + tab->setVotingModel(model); + } + + if (!model) { + return; + } + + connect(model, &VotingModel::newPollReceived, [this]() { + ui->pollReceivedLabel->show(); + }); +} + +void VotingPage::setOptionsModel(OptionsModel* model) +{ + if (!model) { + return; + } + + connect(model, &OptionsModel::walletStylesheetChanged, this, &VotingPage::updateIcons); + updateIcons(model->getCurrentStyle()); +} + +PollTab& VotingPage::currentTab() +{ + return *qobject_cast(ui->tabWidget->currentWidget()); +} + +void VotingPage::updateIcons(const QString& theme) +{ + m_filter_action->setIcon(QIcon(":/icons/" + theme + "_search")); + ui->cardsToggleButton->setIcon(QIcon(":/icons/" + theme + "_list_view")); + ui->tableToggleButton->setIcon(QIcon(":/icons/" + theme + "_table_view")); + ui->sortButton->setIcon(QIcon(":/icons/" + theme + "_sort_asc")); + ui->refreshButton->setIcon(QIcon(":/icons/" + theme + "_refresh")); + ui->createPollButton->setIcon(QIcon(":/icons/" + theme + "_create")); + + for (auto* tab : m_tabs) { + tab->updateIcons(theme); + } +} + +#include "qt/voting/votingpage.moc" diff --git a/src/qt/voting/votingpage.h b/src/qt/voting/votingpage.h new file mode 100644 index 0000000000..93ee245495 --- /dev/null +++ b/src/qt/voting/votingpage.h @@ -0,0 +1,51 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_VOTINGPAGE_H +#define VOTING_VOTINGPAGE_H + +#include +#include +#include + +namespace Ui { + class VotingPage; +} + +class OptionsModel; +class PollTab; +class VotingModel; + +QT_BEGIN_NAMESPACE +class QAction; +class QResizeEvent; +class QString; +QT_END_NAMESPACE + +class VotingPage : public QWidget +{ + Q_OBJECT + +public: + explicit VotingPage(QWidget* parent = nullptr); + ~VotingPage(); + + void setVotingModel(VotingModel* model); + void setOptionsModel(OptionsModel* model); + + PollTab& currentTab(); + +private: + Ui::VotingPage* ui; + VotingModel* m_voting_model; + std::array m_tabs; + std::unique_ptr m_filter_action; + QString m_current_filter; + + +private slots: + void updateIcons(const QString& theme); +}; + +#endif // VOTING_VOTINGPAGE_H diff --git a/src/qt/votingdialog.cpp b/src/qt/votingdialog.cpp deleted file mode 100644 index 483d47cd4a..0000000000 --- a/src/qt/votingdialog.cpp +++ /dev/null @@ -1,1228 +0,0 @@ -// Copyright (c) 2014-2021 The Gridcoin developers -// Distributed under the MIT/X11 software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#include -#include -#include -#include - -#include -#include -#include - -#ifdef QT_CHARTS_LIB - #include - #include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "util.h" -#include "gridcoin/voting/builders.h" -#include "gridcoin/voting/poll.h" -#include "gridcoin/voting/registry.h" -#include "gridcoin/voting/result.h" -#include "qt/walletmodel.h" -#include "votingdialog.h" -#include "rpc/protocol.h" -#include "sync.h" - -using namespace GRC; - -extern CCriticalSection cs_main; - -static int column_alignments[] = { - Qt::AlignRight|Qt::AlignVCenter, // RowNumber - Qt::AlignLeft|Qt::AlignVCenter, // Expiration - Qt::AlignLeft|Qt::AlignVCenter, // Title - Qt::AlignLeft|Qt::AlignVCenter, // TopAnswer - Qt::AlignRight|Qt::AlignVCenter, // TotalParticipants - Qt::AlignRight|Qt::AlignVCenter, // TotalShares - Qt::AlignLeft|Qt::AlignVCenter, // ShareType - }; - -// VotingTableModel -// -VotingTableModel::VotingTableModel(void) -{ - columns_ - << tr("#") - << tr("Expiration") - << tr("Title") - << tr("Top Answer") - << tr("# Voters") // Total Participants - << tr("Total Shares") - << tr("Share Type") - ; -} - -VotingTableModel::~VotingTableModel(void) -{ - for(ssize_t i=0; i < data_.size(); i++) - if (data_.at(i)) - delete data_.at(i); -} - -int VotingTableModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - return data_.size(); -} - -int VotingTableModel::columnCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - return columns_.length(); -} - -QVariant VotingTableModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - const VotingItem *item = (const VotingItem *) index.internalPointer(); - - switch (role) - { - case Qt::DisplayRole: - switch (index.column()) - { - case RowNumber: - return QVariant(item->rowNumber_); - case Title: - return item->title_; - case Expiration: - return item->expiration_.toString(); - case ShareType: - return item->shareType_; - case TotalParticipants: - return item->totalParticipants_; - case TotalShares: - return item->totalShares_; - case TopAnswer: - return item->topAnswer_; - default: - ; - } - break; - - case SortRole: - switch (index.column()) - { - case RowNumber: - return QVariant(item->rowNumber_); - case Title: - return item->title_; - case Expiration: - return item->expiration_; - case ShareType: - return item->shareType_; - case TotalParticipants: - return item->totalParticipants_; - case TotalShares: - return item->totalShares_; - case TopAnswer: - return item->topAnswer_; - default: - ; - } - break; - - case RowNumberRole: - return QVariant(item->rowNumber_); - - case TitleRole: - return item->title_; - - case ExpirationRole: - return item->expiration_; - - case ShareTypeRole: - return item->shareType_; - - case TotalParticipantsRole: - return item->totalParticipants_; - - case TotalSharesRole: - return item->totalShares_; - - case TopAnswerRole: - return item->topAnswer_; - - case Qt::TextAlignmentRole: - return column_alignments[index.column()]; - - default: - ; - } - - return QVariant(); -} - -// section corresponds to column number for horizontal headers -QVariant VotingTableModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - if (orientation == Qt::Horizontal && section >= 0) - { - if (role == Qt::DisplayRole) - return columns_[section]; - else - if (role == Qt::TextAlignmentRole) - return column_alignments[section]; - else - if (role == Qt::ToolTipRole) - { - switch (section) - { - case RowNumber: - return tr("Row Number."); - case Title: - return tr("Title."); - case Expiration: - return tr("Expiration."); - case ShareType: - return tr("Share Type."); - case TotalParticipants: - return tr("Total Participants."); - case TotalShares: - return tr("Total Shares."); - case TopAnswer: - return tr("Top Answer."); - } - } - } - return QVariant(); -} - -const VotingItem *VotingTableModel::index(int row) const -{ - if ((row >= 0) && (row < data_.size())) - return data_[row]; - return 0; -} - -QModelIndex VotingTableModel::index(int row, int column, const QModelIndex &parent) const -{ - Q_UNUSED(parent); - const VotingItem *item = index(row); - if (item) - return createIndex(row, column, (void *)item); - return QModelIndex(); -} - -Qt::ItemFlags VotingTableModel::flags(const QModelIndex &index) const -{ - if (!index.isValid()) - return Qt::ItemIsEnabled; - - return Qt::ItemIsEnabled | Qt::ItemIsSelectable; -} - -namespace { -VotingItem* BuildPollItem(const PollRegistry::Sequence::Iterator& iter) -{ - const PollResultOption result = PollResult::BuildFor(iter->Ref()); - - if (!result) { - return nullptr; - } - - const Poll& poll = result->m_poll; - - VotingItem *item = new VotingItem; - item->pollTxid_ = iter->Ref().Txid(); - item->expiration_ = QDateTime::fromMSecsSinceEpoch(poll.Expiration() * 1000); - item->shareType_ = QString::fromStdString(poll.WeightTypeToString()); - item->responseType_ = QString::fromStdString(poll.ResponseTypeToString()); - item->totalParticipants_ = result->m_votes.size(); - item->totalShares_ = result->m_total_weight / (double)COIN; - - item->title_ = QString::fromStdString(poll.m_title).replace("_"," "); - item->question_ = QString::fromStdString(poll.m_question).replace("_"," "); - item->url_ = QString::fromStdString(poll.m_url).trimmed(); - - if (!item->url_.startsWith("http://") && !item->url_.startsWith("https://")) { - item->url_.prepend("http://"); - } - - for (size_t i = 0; i < result->m_responses.size(); ++i) { - item->vectorOfAnswers_.emplace_back( - poll.Choices().At(i)->m_label, - result->m_responses[i].m_weight / (double)COIN, - result->m_responses[i].m_votes); - } - - if (!result->m_votes.empty()) { - item->topAnswer_ = QString::fromStdString(result->WinnerLabel()).replace("_"," "); - } - - return item; -} -} // Anonymous namespace - -void VotingTableModel::resetData(bool history) -{ - std::string function = __func__; - function += ": "; - - // data: erase - if (data_.size()) { - beginRemoveRows(QModelIndex(), 0, data_.size() - 1); - for(ssize_t i=0; i < data_.size(); i++) - if (data_.at(i)) - delete data_.at(i); - data_.clear(); - endRemoveRows(); - } - - g_timer.GetTimes(function + "erase data", "votedialog"); - - // retrieve data - std::vector items; - - { - LOCK(cs_main); - - for (const auto iter : GetPollRegistry().Polls().OnlyActive(!history)) { - if (VotingItem* item = BuildPollItem(iter)) { - item->rowNumber_ = items.size() + 1; - items.push_back(item); - - } - } - - g_timer.GetTimes(function + "populate poll results (cs_main lock)", "votedialog"); - } - - // data: populate - if (items.size()) { - beginInsertRows(QModelIndex(), 0, items.size() - 1); - for(size_t i=0; i < items.size(); i++) - data_.append(items[i]); - endInsertRows(); - - g_timer.GetTimes(function + "insert data in display table", "votedialog"); - } -} - -// VotingProxyModel -// - -VotingProxyModel::VotingProxyModel(QObject *parent) - : QSortFilterProxyModel(parent) -{ - setSortRole(VotingTableModel::SortRole); -} - -void VotingProxyModel::setFilterTQAU(const QString &str) -{ - filterTQAU_ = str; - invalidateFilter(); -} - -bool VotingProxyModel::filterAcceptsRow(int row, const QModelIndex &sourceParent) const -{ - QModelIndex index = sourceModel()->index(row, 0, sourceParent); - - QString title = index.data(VotingTableModel::TitleRole).toString(); - - if (!title.contains(filterTQAU_, Qt::CaseInsensitive)){ - return false; - } - return true; -} - -// VotingDialog -// -VotingDialog::VotingDialog(QWidget *parent) - : QWidget(parent), - tableView_(0), - tableModel_(0), - proxyModel_(0), - chartDialog_(0), - voteDialog_(0) -{ - // data - tableModel_ = new VotingTableModel(); - proxyModel_ = new VotingProxyModel(this); - proxyModel_->setSourceModel(tableModel_); - - QVBoxLayout *vlayout = new QVBoxLayout(this); - - QGroupBox *groupBox = new QGroupBox(tr("Active Polls (Right Click to Vote)")); - vlayout->addWidget(groupBox); - - QVBoxLayout *groupboxvlayout = new QVBoxLayout(); - groupBox->setLayout(groupboxvlayout); - - QHBoxLayout *filterhlayout = new QHBoxLayout(); - groupboxvlayout->addLayout(filterhlayout); - - // Filter by Title with one QLineEdit - QLabel *filterByTQAULabel = new QLabel(tr("Filter: ")); - filterByTQAULabel->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - filterByTQAULabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - filterByTQAULabel->setMaximumWidth(150); - filterhlayout->addWidget(filterByTQAULabel); - filterTQAU = new QLineEdit(); - filterTQAU->setMaximumWidth(350); - filterhlayout->addWidget(filterTQAU); - connect(filterTQAU, SIGNAL(textChanged(QString)), this, SLOT(filterTQAUChanged(QString))); - filterhlayout->addStretch(); - - // buttons in horizontal layout - QHBoxLayout *groupboxhlayout = new QHBoxLayout(); - groupboxvlayout->addLayout(groupboxhlayout); - - QPushButton *resetButton = new QPushButton(); - resetButton->setText(tr("Reload Polls")); - groupboxhlayout->addWidget(resetButton); - connect(resetButton, SIGNAL(clicked()), this, SLOT(resetData())); - - QPushButton *histButton = new QPushButton(); - histButton->setText(tr("Load History")); - groupboxhlayout->addWidget(histButton); - connect(histButton, SIGNAL(clicked()), this, SLOT(loadHistory())); - - QPushButton *newPollButton = new QPushButton(); - newPollButton->setText(tr("Create Poll")); - groupboxhlayout->addWidget(newPollButton); - connect(newPollButton, SIGNAL(clicked()), this, SLOT(showNewPollDialog())); - - groupboxhlayout->addStretch(); - - tableView_ = new QTableView(); - tableView_->installEventFilter(this); - tableView_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - // tableView_->setTabKeyNavigation(false); - tableView_->setContextMenuPolicy(Qt::CustomContextMenu); - connect(tableView_, SIGNAL(customContextMenuRequested(const QPoint &)), - this, SLOT(showContextMenu(const QPoint &))); - tableView_->setAlternatingRowColors(true); - tableView_->setSelectionMode(QAbstractItemView::ExtendedSelection); - tableView_->setSortingEnabled(true); - tableView_->sortByColumn(VotingTableModel::RowNumber, Qt::DescendingOrder); - tableView_->verticalHeader()->hide(); - tableView_->setShowGrid(false); - - tableView_->setModel(proxyModel_); - tableView_->setFont(QFont("Arial", 10)); - tableView_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); - tableView_->horizontalHeader()->setMinimumWidth(VOTINGDIALOG_WIDTH_RowNumber - + VOTINGDIALOG_WIDTH_Title - + VOTINGDIALOG_WIDTH_Expiration - + VOTINGDIALOG_WIDTH_ShareType - + VOTINGDIALOG_WIDTH_TotalParticipants - + VOTINGDIALOG_WIDTH_TotalShares - + VOTINGDIALOG_WIDTH_TopAnswer); - tableView_->verticalHeader()->setDefaultSectionSize(40); - - groupboxvlayout->addWidget(tableView_); - - // loading overlay. Due to a bug in QFutureWatcher for Qt <5.6.0 we - // have to track the running state ourselves. See - // https://bugreports.qt.io/browse/QTBUG-12358 - watcher.setProperty("running", false); - connect(&watcher, SIGNAL(finished()), this, SLOT(onLoadingFinished())); - loadingIndicator = new QLabel(this); - loadingIndicator->setWordWrap(true); - - groupboxvlayout->addWidget(loadingIndicator); - - chartDialog_ = new VotingChartDialog(this); - voteDialog_ = new VotingVoteDialog(this); - pollDialog_ = new NewPollDialog(this); - - loadingIndicator->setText(tr("Press reload to load polls... This can take several minutes, and the wallet may not " - "respond until finished.")); - tableView_->hide(); - loadingIndicator->show(); - - QObject::connect(vote_update_age_timer, SIGNAL(timeout()), this, SLOT(setStale())); -} - -void VotingDialog::setModel(WalletModel *wallet_model) -{ - if (!wallet_model) { - return; - } - - voteDialog_->setModel(wallet_model); - pollDialog_->setModel(wallet_model); -} - -void VotingDialog::loadPolls(bool history) -{ - std::string function = __func__; - function += ": "; - - bool isRunning = watcher.property("running").toBool(); - if (tableModel_&& !isRunning) - { - loadingIndicator->setText(tr("Recalculating voting weights... This can take several minutes, and the wallet may not " - "respond until finished.")); - tableView_->hide(); - loadingIndicator->show(); - - g_timer.InitTimer("votedialog", LogInstance().WillLogCategory(BCLog::LogFlags::MISC)); - vote_update_age_timer->start(STALE); - - QFuture future = QtConcurrent::run(tableModel_, &VotingTableModel::resetData, history); - - g_timer.GetTimes(function + "Post future assignment", "votedialog"); - - watcher.setProperty("running", true); - watcher.setFuture(future); - } -} - -void VotingDialog::resetData(void) -{ - loadPolls(false); -} - -void VotingDialog::loadHistory(void) -{ - loadPolls(true); -} - -void VotingDialog::setStale(void) -{ - LogPrint(BCLog::LogFlags::MISC, "INFO: %s called.", __func__); - - // If the stale flag is not set, but this function is called, it is from the timeout - // trigger of the vote_update_age_timer. Therefore set the loading indicator to stale - // and set the stale flag to true. The stale flag will be reset and the timer restarted - // when the loadPolls is called to refresh. - if (!stale) - { - loadingIndicator->setText(tr("Poll data is more than one hour old. Press reload to update... " - "This can take several minutes, and the wallet may not respond " - "until finished.")); - tableView_->hide(); - loadingIndicator->show(); - - stale = true; - } -} - -void VotingDialog::onLoadingFinished(void) -{ - watcher.setProperty("running", false); - - int rowsCount = tableView_->verticalHeader()->count(); - if (rowsCount > 0) { - loadingIndicator->hide(); - tableView_->show(); - } else { - loadingIndicator->setText(tr("No polls !")); - } - - stale = false; -} - -void VotingDialog::tableColResize(void) -{ - tableView_->setColumnWidth(VotingTableModel::RowNumber, VOTINGDIALOG_WIDTH_RowNumber); - tableView_->setColumnWidth(VotingTableModel::Expiration, VOTINGDIALOG_WIDTH_Expiration); - tableView_->setColumnWidth(VotingTableModel::ShareType, VOTINGDIALOG_WIDTH_ShareType); - tableView_->setColumnWidth(VotingTableModel::TotalParticipants, VOTINGDIALOG_WIDTH_TotalParticipants); - tableView_->setColumnWidth(VotingTableModel::TotalShares, VOTINGDIALOG_WIDTH_TotalShares); - - int fixedColWidth = VOTINGDIALOG_WIDTH_RowNumber - + VOTINGDIALOG_WIDTH_Expiration - + VOTINGDIALOG_WIDTH_ShareType - + VOTINGDIALOG_WIDTH_TotalParticipants - + VOTINGDIALOG_WIDTH_TotalShares; - - int dynamicWidth = tableView_->horizontalHeader()->width() - fixedColWidth; - int nColumns = 2; // 2 dynamic columns - int columns[] = {VotingTableModel::Title,VotingTableModel::TopAnswer}; - int remainingWidth = dynamicWidth % nColumns; - for(int cNum = 0; cNum < nColumns; cNum++) { - if(remainingWidth > 0) - { - tableView_->setColumnWidth(columns[cNum], (dynamicWidth/nColumns) + 1); - remainingWidth -= 1; - } - else - { - tableView_->setColumnWidth(columns[cNum], dynamicWidth/nColumns); - } - } -} - -//customize resize event to allow automatic as well as interactive resizing -void VotingDialog::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - - tableColResize(); -} - -//customize show event for instant table resize -void VotingDialog::showEvent(QShowEvent *event) -{ - QWidget::showEvent(event); - - if (! event->spontaneous()) - tableColResize(); -} - -bool VotingDialog::eventFilter(QObject *obj, QEvent *event) -{ - if (obj == tableView_) - { - if (event->type() == QEvent::KeyPress) - { - QKeyEvent *ke = static_cast(event); - if ((ke->key() == Qt::Key_C) - && (ke->modifiers().testFlag(Qt::ControlModifier))) - { - /* Ctrl-C: copy the selected cells in TableModel */ - QString selected_text; - QItemSelectionModel *selection = tableView_->selectionModel(); - QModelIndexList indexes = selection->selectedIndexes(); - std::sort(indexes.begin(), indexes.end()); - int prev_row = -1; - for(int i=0; i < indexes.size(); i++) { - QModelIndex index = indexes.at(i); - if (i) { - char c = (index.row() != prev_row)? '\n': '\t'; - selected_text.append(c); - } - QVariant data = tableView_->model()->data(index); - selected_text.append( data.toString() ); - prev_row = index.row(); - } - QApplication::clipboard()->setText(selected_text); - return true; - } - } - } - - return QWidget::eventFilter(obj, event); -} - -void VotingDialog::filterTQAUChanged(const QString &str) -{ - if (proxyModel_) - proxyModel_->setFilterTQAU(str); -} - -void VotingDialog::showChartDialog(void) -{ - if (!proxyModel_ || !tableModel_ || !tableView_ || !chartDialog_) - return; - - QItemSelectionModel *selection = tableView_->selectionModel(); - QModelIndexList indexes = selection->selectedIndexes(); - if (!indexes.size()) - return; - - // take the row of the top selected cell - std::sort(indexes.begin(), indexes.end()); - int row = proxyModel_->mapToSource(indexes.at(0)).row(); - - // reset the dialog's data - const VotingItem *item = tableModel_->index(row); - chartDialog_->resetData(item); - - chartDialog_->show(); - chartDialog_->raise(); - chartDialog_->setFocus(); -} - -void VotingDialog::showContextMenu(const QPoint &pos) -{ - QPoint globalPos = tableView_->viewport()->mapToGlobal(pos); - - QMenu menu; - menu.addAction("Show Results", this, SLOT(showChartDialog())); - menu.addAction("Vote", this, SLOT(showVoteDialog())); - menu.exec(globalPos); -} - -void VotingDialog::showVoteDialog(void) -{ - if (!proxyModel_ || !tableModel_ || !tableView_ || !voteDialog_) - return; - - QItemSelectionModel *selection = tableView_->selectionModel(); - QModelIndexList indexes = selection->selectedIndexes(); - if (!indexes.size()) - return; - - // take the row of the top selected cell - std::sort(indexes.begin(), indexes.end()); - int row = proxyModel_->mapToSource(indexes.at(0)).row(); - - // reset the dialog's data - const VotingItem *item = tableModel_->index(row); - voteDialog_->resetData(item); - - voteDialog_->show(); - voteDialog_->raise(); - voteDialog_->setFocus(); -} - -void VotingDialog::showNewPollDialog(void) -{ - if (!proxyModel_ || !tableModel_ || !tableView_ || !voteDialog_) - return; - - // reset the dialog's data - pollDialog_->resetData(); - - pollDialog_->show(); - pollDialog_->raise(); - pollDialog_->setFocus(); -} - -// VotingChartDialog -// -VotingChartDialog::VotingChartDialog(QWidget *parent) - : QDialog(parent) -#ifdef QT_CHARTS_LIB - ,chart_(0) -#endif - ,answerTable_(NULL) -{ - setWindowTitle(tr("Poll Results")); - resize(QDesktopWidget().availableGeometry(this).size() * 0.4); - - QVBoxLayout *vlayout = new QVBoxLayout(this); - - QGridLayout *glayout = new QGridLayout(); - glayout->setHorizontalSpacing(0); - glayout->setVerticalSpacing(0); - glayout->setColumnStretch(0, 1); - glayout->setColumnStretch(1, 3); - glayout->setColumnStretch(2, 5); - - vlayout->addLayout(glayout); - - QLabel *question = new QLabel(tr("Q: ")); - question->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - question->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(question, 0, 0); - - question_ = new QLabel(); - question_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - question_->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(question_, 0, 1); - - QLabel *discussionLabel = new QLabel(tr("Discussion URL: ")); - discussionLabel->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - discussionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(discussionLabel, 1, 0); - - url_ = new QLabel(); - url_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - url_->setTextFormat(Qt::RichText); - url_->setTextInteractionFlags(Qt::TextBrowserInteraction); - url_->setOpenExternalLinks(true); - glayout->addWidget(url_, 1, 1); - - QLabel *topAnswer = new QLabel(tr("Top Answer: ")); - topAnswer->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - topAnswer->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(topAnswer, 3, 0); - - answer_ = new QLabel(); - answer_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - answer_->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(answer_, 3, 1); - - QTabWidget *resTabWidget = new QTabWidget; - -#ifdef QT_CHARTS_LIB - chart_ = new QtCharts::QChart; - chart_->legend()->setVisible(true); - chart_->legend()->setAlignment(Qt::AlignRight); - QtCharts::QChartView *m_chartView = new QtCharts::QChartView(chart_); - m_chartView->setRenderHint(QPainter::Antialiasing); - resTabWidget->addTab(m_chartView, tr("Chart")); -#endif - - answerModel_ = new QStandardItemModel(); - answerModel_->setColumnCount(3); - answerModel_->setRowCount(0); - answerModel_->setHeaderData(0, Qt::Horizontal, tr("Answer")); - answerModel_->setHeaderData(1, Qt::Horizontal, tr("Shares")); - answerModel_->setHeaderData(2, Qt::Horizontal, "%"); - - answerTable_ = new QTableView(this); - answerTable_->setModel(answerModel_); - answerTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - answerTable_->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeMode::ResizeToContents); - answerTable_->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeMode::ResizeToContents); - answerTable_->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); - answerTable_->setEditTriggers( QAbstractItemView::NoEditTriggers ); - resTabWidget->addTab(answerTable_, tr("List")); - vlayout->addWidget(resTabWidget); -} - -void VotingChartDialog::resetData(const VotingItem *item) -{ - if (!item) - return; - - answerModel_->setRowCount(0); - answerTable_->sortByColumn(-1, Qt::AscendingOrder); - answerTable_->setSortingEnabled(false); - -#ifdef QT_CHARTS_LIB - QList oldSeriesList = chart_->series(); - foreach (QtCharts::QAbstractSeries *oldSeries, oldSeriesList) - chart_->removeSeries(oldSeries); - - QtCharts::QPieSeries *series = new QtCharts::QPieSeries(); -#endif - - question_->setText(item->question_); - url_->setText("url_+"\">"+item->url_+""); - answer_->setText(item->topAnswer_); - answer_->setVisible(!item->topAnswer_.isEmpty()); - answerModel_->setRowCount(item->vectorOfAnswers_.size()); - - for (size_t y = 0; y < item->vectorOfAnswers_.size(); y++) - { - const auto& responses = item->vectorOfAnswers_; - const QString answer = QString::fromStdString(responses[y].answer); - - QStandardItem *answerItem = new QStandardItem(answer); - answerItem->setData(answer); - answerModel_->setItem(y, 0, answerItem); - QStandardItem *iSharesItem = new QStandardItem(QString::number(responses[y].shares, 'f', 0)); - iSharesItem->setData(responses[y].shares); - iSharesItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - answerModel_->setItem(y, 1, iSharesItem); - QStandardItem *percentItem = new QStandardItem(); - - if (item->totalShares_ > 0) { - const double ratio = responses[y].shares / (double)item->totalShares_; - percentItem->setText(QString::number(ratio * 100, 'f', 2)); - percentItem->setData(ratio * 100); - percentItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - } - - answerModel_->setItem(y, 2, percentItem); - -#ifdef QT_CHARTS_LIB - QtCharts::QPieSlice *slice = new QtCharts::QPieSlice(answer, responses[y].shares); - series->append(slice); - chart_->addSeries(series); -#endif - } - - answerModel_->setSortRole(Qt::UserRole+1); - answerTable_->setSortingEnabled(true); -} - -// VotingVoteDialog -// -VotingVoteDialog::VotingVoteDialog(QWidget *parent) - : QDialog(parent) - , m_wallet_model(nullptr) -{ - setWindowTitle(tr("PlaceVote")); - resize(QDesktopWidget().availableGeometry(this).size() * 0.4); - - QVBoxLayout *vlayout = new QVBoxLayout(this); - - QGridLayout *glayout = new QGridLayout(); - glayout->setHorizontalSpacing(0); - glayout->setVerticalSpacing(0); - glayout->setColumnStretch(0, 1); - glayout->setColumnStretch(1, 3); - glayout->setColumnStretch(2, 5); - - vlayout->addLayout(glayout); - - QLabel *question = new QLabel(tr("Q: ")); - question->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - question->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(question, 0, 0); - - question_ = new QLabel(); - question_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - question_->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(question_, 0, 1); - - QLabel *discussionLabel = new QLabel(tr("Discussion URL: ")); - discussionLabel->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - discussionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(discussionLabel, 1, 0); - - url_ = new QLabel(); - url_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - url_->setTextFormat(Qt::RichText); - url_->setTextInteractionFlags(Qt::TextBrowserInteraction); - url_->setOpenExternalLinks(true); - glayout->addWidget(url_, 1, 1); - - QLabel *responseTypeLabel = new QLabel(tr("Response Type: ")); - responseTypeLabel->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - responseTypeLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(responseTypeLabel, 3, 0); - - responseType_ = new QLabel(); - responseType_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - responseType_->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(responseType_, 3, 1); - - QLabel *topAnswer = new QLabel(tr("Top Answer: ")); - topAnswer->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - topAnswer->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(topAnswer, 4, 0); - - answer_ = new QLabel(); - answer_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - answer_->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(answer_, 4, 1); - - answerList_ = new QListWidget(this); - vlayout->addWidget(answerList_); - - QHBoxLayout *hlayout = new QHBoxLayout(); - vlayout->addLayout(hlayout); - - QPushButton *voteButton = new QPushButton(); - voteButton->setText(tr("Vote")); - hlayout->addWidget(voteButton); - connect(voteButton, SIGNAL(clicked()), this, SLOT(vote())); - - voteNote_ = new QLabel(); - voteNote_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - voteNote_->setTextInteractionFlags(Qt::TextSelectableByMouse); - voteNote_->setWordWrap(true); - hlayout->addWidget(voteNote_); - -} - -void VotingVoteDialog::setModel(WalletModel *wallet_model) -{ - if (!wallet_model) { - return; - } - - m_wallet_model = wallet_model; -} - -void VotingVoteDialog::resetData(const VotingItem *item) -{ - if (!item) - return; - - answerList_->clear(); - voteNote_->clear(); - question_->setText(item->question_); - url_->setText("url_+"\">"+item->url_+""); - responseType_->setText(item->responseType_); - answer_->setText(item->topAnswer_); - pollTxid_ = item->pollTxid_; - - for (const auto& choice : item->vectorOfAnswers_) { - QListWidgetItem *answerItem = new QListWidgetItem(QString::fromStdString(choice.answer).replace("_", " "), answerList_); - answerItem->setCheckState(Qt::Unchecked); - } -} - -void VotingVoteDialog::vote(void) -{ - // This overall try-catch is needed to properly catch the VoteBuilder builder move constructor and assignment, - // otherwise an expired poll bubbles up all the way to the app level and ends execution with the exception handler - // in bitcoin.cpp, which is not what is intended here. It also catches any thrown VotingError exceptions in - // builder.AddResponse() and SendVoteContract(). - try { - voteNote_->setStyleSheet("QLabel { color : red; }"); - - LOCK(cs_main); - - const PollReference* ref = GetPollRegistry().TryByTxid(pollTxid_); - - if (!ref) { - voteNote_->setText(tr("Poll not found.")); - return; - } - - const PollOption poll = ref->TryReadFromDisk(); - - if (!poll) { - voteNote_->setText(tr("Failed to load poll from disk")); - return; - } - - VoteBuilder builder = VoteBuilder::ForPoll(*poll, ref->Txid()); - - for (int row = 0; row < answerList_->count(); ++row) { - if (answerList_->item(row)->checkState() == Qt::Checked) { - builder = builder.AddResponse(row); - } - } - - const WalletModel::UnlockContext unlock_context(m_wallet_model->requestUnlock()); - - if (!unlock_context.isValid()) { - voteNote_->setText(tr("Please unlock the wallet.")); - return; - } - - SendVoteContract(std::move(builder)); - - voteNote_->setStyleSheet("QLabel { color : green; }"); - voteNote_->setText(tr("Success. Vote will activate with the next block.")); - } catch (const VotingError& e){ - voteNote_->setText(e.what()); - return; - } -} - -NewPollDialog::NewPollDialog(QWidget *parent) - : QDialog(parent) - , m_wallet_model(nullptr) -{ - setWindowTitle(tr("Create Poll")); - resize(QDesktopWidget().availableGeometry(this).size() * 0.4); - - QVBoxLayout *vlayout = new QVBoxLayout(this); - - QGridLayout *glayout = new QGridLayout(); - glayout->setHorizontalSpacing(0); - glayout->setVerticalSpacing(5); - glayout->setColumnStretch(0, 1); - glayout->setColumnStretch(1, 3); - glayout->setColumnStretch(2, 5); - - vlayout->addLayout(glayout); - - //title - QLabel *title = new QLabel(tr("Title: ")); - title->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - title->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(title, 0, 0); - - title_ = new QLineEdit(); - title_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - glayout->addWidget(title_, 0, 1); - - //days - QLabel *days = new QLabel(tr("Days: ")); - days->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - days->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(days, 1, 0); - - days_ = new QLineEdit(); - days_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - glayout->addWidget(days_, 1, 1); - - //question - QLabel *question = new QLabel(tr("Question: ")); - question->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - question->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(question, 2, 0); - - question_ = new QLineEdit(); - question_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - glayout->addWidget(question_, 2, 1); - - //url - QLabel *discussionLabel = new QLabel(tr("Discussion URL: ")); - discussionLabel->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - discussionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(discussionLabel, 3, 0); - - url_ = new QLineEdit(); - url_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - glayout->addWidget(url_, 3, 1); - - //share type - QLabel *shareType = new QLabel(tr("Share Type: ")); - shareType->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - shareType->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(shareType, 4, 0); - - shareTypeBox_ = new QComboBox(this); - QStringList shareTypeBoxItems; - shareTypeBoxItems << tr("Balance") << tr("Magnitude+Balance"); - shareTypeBox_->addItems(shareTypeBoxItems); - glayout->addWidget(shareTypeBox_, 4, 1); - - // response type - QLabel *responseTypeLabel = new QLabel(tr("Response Type: ")); - responseTypeLabel->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - responseTypeLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(responseTypeLabel, 5, 0); - - responseTypeBox_ = new QComboBox(this); - QStringList responseTypeBoxItems; - responseTypeBoxItems - << tr("Yes/No/Abstain") - << tr("Single Choice") - << tr("Multiple Choice"); - responseTypeBox_->addItems(responseTypeBoxItems); - glayout->addWidget(responseTypeBox_, 5, 1); - - // cost - QLabel *costLabelLabel = new QLabel(tr("Cost:")); - costLabelLabel->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - costLabelLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(costLabelLabel, 6, 0); - - // TODO: make this dynamic when rewriting the voting GUI: - QLabel *costLabel = new QLabel(tr("50 GRC + transaction fee")); - costLabel->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - costLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - glayout->addWidget(costLabel, 6, 1); - - //answers - answerList_ = new QListWidget(this); - answerList_->setContextMenuPolicy(Qt::CustomContextMenu); - connect(answerList_, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(showContextMenu(const QPoint &))); - vlayout->addWidget(answerList_); - connect (answerList_, SIGNAL (itemDoubleClicked (QListWidgetItem *)), this, SLOT (editItem (QListWidgetItem *))); - - QHBoxLayout *hlayoutTools = new QHBoxLayout(); - vlayout->addLayout(hlayoutTools); - - QPushButton *addItemButton = new QPushButton(); - addItemButton->setText(tr("Add Item")); - hlayoutTools->addWidget(addItemButton); - connect(addItemButton, SIGNAL(clicked()), this, SLOT(addItem())); - - QPushButton *removeItemButton = new QPushButton(); - removeItemButton->setText(tr("Remove Item")); - hlayoutTools->addWidget(removeItemButton); - connect(removeItemButton, SIGNAL(clicked()), this, SLOT(removeItem())); - - QPushButton *clearAllButton = new QPushButton(); - clearAllButton->setText(tr("Clear All")); - hlayoutTools->addWidget(clearAllButton); - connect(clearAllButton, SIGNAL(clicked()), this, SLOT(resetData())); - - QHBoxLayout *hlayoutBottom = new QHBoxLayout(); - vlayout->addLayout(hlayoutBottom); - - QPushButton *pollButton = new QPushButton(); - pollButton->setText(tr("Create Poll")); - hlayoutBottom->addWidget(pollButton); - connect(pollButton, SIGNAL(clicked()), this, SLOT(createPoll())); - - pollNote_ = new QLabel(); - pollNote_->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); - pollNote_->setTextInteractionFlags(Qt::TextSelectableByMouse); - pollNote_->setWordWrap(true); - hlayoutBottom->addWidget(pollNote_); - -} - -void NewPollDialog::setModel(WalletModel *wallet_model) -{ - if (!wallet_model) { - return; - } - - m_wallet_model = wallet_model; -} - -void NewPollDialog::resetData() -{ - answerList_->clear(); - pollNote_->clear(); - title_->clear(); - days_->clear(); - question_->clear(); - url_->clear(); -} - -void NewPollDialog::createPoll(void) -{ - pollNote_->setStyleSheet("QLabel { color : red; }"); - PollBuilder builder = PollBuilder(); - - try { - builder = builder - .SetType(PollType::SURVEY) - .SetTitle(title_->text().toStdString()) - .SetDuration(days_->text().toInt()) - .SetQuestion(question_->text().toStdString()) - // The dropdown list only contains non-deprecated weight type - // options which start from offset 2: - .SetWeightType(shareTypeBox_->currentIndex() + 2) - .SetResponseType(responseTypeBox_->currentIndex() + 1) - .SetUrl(url_->text().toStdString()); - - for (int row = 0; row < answerList_->count(); ++row) { - const QListWidgetItem* const item = answerList_->item(row); - builder = builder.AddChoice(item->text().toStdString()); - } - } catch (const VotingError& e) { - pollNote_->setText(e.what()); - return; - } - - const WalletModel::UnlockContext unlock_context(m_wallet_model->requestUnlock()); - - if (!unlock_context.isValid()) { - pollNote_->setText(tr("Please unlock the wallet.")); - return; - } - - try { - SendPollContract(std::move(builder)); - } catch (const VotingError& e) { - pollNote_->setText(e.what()); - return; - } - - pollNote_->setStyleSheet("QLabel { color : green; }"); - pollNote_->setText("Success. The poll will activate with the next block."); -} - -void NewPollDialog::addItem (void) -{ - QListWidgetItem *answerItem = new QListWidgetItem("New Item",answerList_); - answerItem->setFlags (answerItem->flags() | Qt::ItemIsEditable); -} - -void NewPollDialog::editItem (QListWidgetItem *item) -{ - answerList_->editItem(item); -} - -void NewPollDialog::removeItem(void) -{ - QList items = answerList_->selectedItems(); - foreach(QListWidgetItem * item, items) - { - delete answerList_->takeItem(answerList_->row(item)); - } - -} - -void NewPollDialog::showContextMenu(const QPoint &pos) -{ - QPoint globalPos = answerList_->viewport()->mapToGlobal(pos); - - QMenu menu; - menu.addAction("Add Item", this, SLOT(addItem())); - menu.addAction("Remove Item", this, SLOT(removeItem())); - menu.exec(globalPos); -} diff --git a/src/qt/votingdialog.h b/src/qt/votingdialog.h deleted file mode 100644 index 7299e7a1ea..0000000000 --- a/src/qt/votingdialog.h +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright (c) 2014-2021 The Gridcoin developers -// Distributed under the MIT/X11 software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#ifndef VOTINGDIALOG_H -#define VOTINGDIALOG_H - -#include "uint256.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef QT_CHARTS_LIB -#include -#include -QT_CHARTS_BEGIN_NAMESPACE -class QChart; -QT_CHARTS_END_NAMESPACE -#endif - -QT_BEGIN_NAMESPACE -class QEvent; -class QObject; -class QResizeEvent; -QT_END_NAMESPACE - -class WalletModel; - -#define VOTINGDIALOG_WIDTH_RowNumber 40 -#define VOTINGDIALOG_WIDTH_Title 225 -#define VOTINGDIALOG_WIDTH_Expiration 175 -#define VOTINGDIALOG_WIDTH_ShareType 80 -#define VOTINGDIALOG_WIDTH_TotalParticipants 80 -#define VOTINGDIALOG_WIDTH_TotalShares 100 -#define VOTINGDIALOG_WIDTH_TopAnswer 80 - -namespace polling { -// TODO: Legacy struct moved here until we redesign the voting GUI. -struct Vote { - std::string answer; - double shares; - double participants; - - Vote(std::string answer, double shares, double participants) - : answer(std::move(answer)) - , shares(shares) - , participants(participants) - { - } -}; -} - -class VotingItem { -public: - unsigned int rowNumber_; - uint256 pollTxid_; - QString title_; - QDateTime expiration_; - QString shareType_; - QString responseType_; - QString question_; - std::vector vectorOfAnswers_; - unsigned int totalParticipants_; - unsigned int totalShares_; - QString url_; - QString topAnswer_; -}; - -// VotingTableModel -// -class VotingTableModel - : public QAbstractTableModel -{ - Q_OBJECT - -public: - explicit VotingTableModel(); - ~VotingTableModel(); - - enum ColumnIndex { - RowNumber = 0, - Expiration = 1, - Title = 2, - TopAnswer = 3, - TotalParticipants = 4, - TotalShares = 5, - ShareType = 6, - }; - - enum Roles { - RowNumberRole = Qt::UserRole, - ExpirationRole, - TitleRole, - TopAnswerRole, - TotalParticipantsRole, - TotalSharesRole, - ShareTypeRole, - - SortRole, - }; - - int rowCount(const QModelIndex &) const; - int columnCount(const QModelIndex &) const; - QVariant data(const QModelIndex &, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role) const; - const VotingItem *index(int row) const; - QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const; - Qt::ItemFlags flags(const QModelIndex &) const; - void resetData(bool history); - -private: - QStringList columns_; - QList data_; -}; - - -// VotingProxyModel -// -class VotingProxyModel - : public QSortFilterProxyModel -{ - Q_OBJECT - -public: - explicit VotingProxyModel(QObject *parent=0); - void setFilterTQAU(const QString &); // filter Title - -protected: - bool filterAcceptsRow(int, const QModelIndex &) const; - -private: - QString filterTQAU_; -}; - -class VotingChartDialog; -class VotingVoteDialog; -class NewPollDialog; - -// VotingDialog -// -class VotingDialog - : public QWidget -{ - Q_OBJECT - -public: - explicit VotingDialog(QWidget *parent=0); - void setModel(WalletModel *wallet_model); - -private: - // The number of milliseconds of age at which the poll data is stale. Currently one hour equivalent. - static constexpr int64_t STALE = 60 * 60 * 1000; - - QLineEdit *filterTQAU; - QPushButton *resetButton; - QPushButton *histButton; - QPushButton *newPollButton; - QTableView *tableView_; - VotingTableModel *tableModel_; - VotingProxyModel *proxyModel_; - VotingChartDialog *chartDialog_; - VotingVoteDialog *voteDialog_; - NewPollDialog *pollDialog_; - QLabel *loadingIndicator; - QFutureWatcher watcher; - QTimer* vote_update_age_timer = new QTimer(this); - bool stale = false; - -private: - virtual void showEvent(QShowEvent *); - virtual void resizeEvent(QResizeEvent *); - void tableColResize(void); - bool eventFilter(QObject *, QEvent *); - -private slots: - void onLoadingFinished(void); - void setStale(void); - -public slots: - void filterTQAUChanged(const QString &); - void loadPolls(bool history); - void resetData(void); - void loadHistory(void); - void showChartDialog(void); - void showContextMenu(const QPoint &); - void showVoteDialog(void); - void showNewPollDialog(void); -}; - -// VotingChartDialog -// -class VotingChartDialog - : public QDialog -{ - Q_OBJECT - -public: - explicit VotingChartDialog(QWidget *parent=0); - void resetData(const VotingItem *); - -private: - QLabel *question_; - QLabel *url_; -#ifdef QT_CHARTS_LIB - QtCharts::QChart *chart_; -#endif - QTableView *answerTable_; - QStandardItemModel *answerModel_; - QStringList answerTableHeader; - QLabel *answer_; -}; - -// VotingVoteDialog -// -class VotingVoteDialog - : public QDialog -{ - Q_OBJECT - -public: - explicit VotingVoteDialog(QWidget *parent=0); - void setModel(WalletModel *wallet_model); - void resetData(const VotingItem *); - -private: - WalletModel *m_wallet_model; - QLabel *question_; - QLabel *url_; - QLabel *responseType_; - QLabel *answer_; - QLabel *voteNote_; - QListWidget *answerList_; - QListWidgetItem *answerItem; - QPushButton *voteButton; - uint256 pollTxid_; - -private slots: - void vote(void); -}; - -// NewPollDialog -// -class NewPollDialog - : public QDialog -{ - Q_OBJECT - -public: - explicit NewPollDialog(QWidget *parent=0); - void setModel(WalletModel *wallet_model); - -public slots: - void resetData(void); - -private: - WalletModel *m_wallet_model; - QLineEdit *title_; - QLineEdit *days_; - QLineEdit *question_; - QLineEdit *url_; - QComboBox *shareTypeBox_; - QComboBox *responseTypeBox_; - QLabel *pollNote_; - QListWidget *answerList_; - QListWidgetItem *answerItem; - QPushButton *addItemButton; - QPushButton *removeItemButton; - QPushButton *clearAllButton; - QPushButton *pollButton; - -private slots: - void createPoll(void); - void editItem (QListWidgetItem *item); - void addItem (void); - void removeItem(void); - void showContextMenu(const QPoint &); -}; - -#endif // VOTINGDIALOG_H From 9588b8c1db6968d94b579438f5755e24798748d4 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:49 -0500 Subject: [PATCH 7/9] Create a vote submission wizard This adds a wizard that replaces the old "vote" dialog and displays a more pleasant voting form. It does not add new features yet. However, the wizard format provides a structure which can enable more advanced voting features in the future. --- gridcoinresearch.pro | 9 + src/Makefile.qt.include | 12 ++ src/qt/forms/voting/votewizard.ui | 53 ++++++ src/qt/forms/voting/votewizardballotpage.ui | 118 +++++++++++++ src/qt/forms/voting/votewizardsummarypage.ui | 175 +++++++++++++++++++ src/qt/voting/votewizard.cpp | 31 ++++ src/qt/voting/votewizard.h | 38 ++++ src/qt/voting/votewizardballotpage.cpp | 113 ++++++++++++ src/qt/voting/votewizardballotpage.h | 44 +++++ src/qt/voting/votewizardsummarypage.cpp | 46 +++++ src/qt/voting/votewizardsummarypage.h | 35 ++++ 11 files changed, 674 insertions(+) create mode 100644 src/qt/forms/voting/votewizard.ui create mode 100644 src/qt/forms/voting/votewizardballotpage.ui create mode 100644 src/qt/forms/voting/votewizardsummarypage.ui create mode 100644 src/qt/voting/votewizard.cpp create mode 100644 src/qt/voting/votewizard.h create mode 100644 src/qt/voting/votewizardballotpage.cpp create mode 100644 src/qt/voting/votewizardballotpage.h create mode 100644 src/qt/voting/votewizardsummarypage.cpp create mode 100644 src/qt/voting/votewizardsummarypage.h diff --git a/gridcoinresearch.pro b/gridcoinresearch.pro index 4e94242a82..5158676698 100755 --- a/gridcoinresearch.pro +++ b/gridcoinresearch.pro @@ -197,6 +197,9 @@ HEADERS += src/qt/bitcoingui.h \ src/qt/voting/pollwizardprojectpage.h \ src/qt/voting/pollwizardsummarypage.h \ src/qt/voting/pollwizardtypepage.h \ + src/qt/voting/votewizard.h \ + src/qt/voting/votewizardballotpage.h \ + src/qt/voting/votewizardsummarypage.h \ src/qt/voting/votingmodel.h \ src/qt/voting/votingpage.h \ src/qt/transactiontablemodel.h \ @@ -317,6 +320,9 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/voting/pollwizardprojectpage.cpp \ src/qt/voting/pollwizardsummarypage.cpp \ src/qt/voting/pollwizardtypepage.cpp \ + src/qt/voting/votewizard.cpp \ + src/qt/voting/votewizardballotpage.cpp \ + src/qt/voting/votewizardsummarypage.cpp \ src/qt/voting/votingmodel.cpp \ src/qt/voting/votingpage.cpp \ src/qt/transactiontablemodel.cpp \ @@ -432,6 +438,9 @@ FORMS += \ src/qt/forms/voting/pollwizardprojectpage.ui \ src/qt/forms/voting/pollwizardsummarypage.ui \ src/qt/forms/voting/pollwizardtypepage.ui \ + src/qt/forms/voting/votewizard.ui \ + src/qt/forms/voting/votewizardballotpage.ui \ + src/qt/forms/voting/votewizardsummarypage.ui \ src/qt/forms/voting/votingpage.ui \ src/qt/forms/receivecoinspage.ui \ src/qt/forms/sendcoinsdialog.ui \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 8a1645f2e6..5da3c14f58 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -116,6 +116,9 @@ QT_FORMS_UI = \ qt/forms/voting/pollwizardprojectpage.ui \ qt/forms/voting/pollwizardsummarypage.ui \ qt/forms/voting/pollwizardtypepage.ui \ + qt/forms/voting/votewizard.ui \ + qt/forms/voting/votewizardballotpage.ui \ + qt/forms/voting/votewizardsummarypage.ui \ qt/forms/voting/votingpage.ui QT_MOC_CPP = \ @@ -190,6 +193,9 @@ QT_MOC_CPP = \ qt/voting/moc_pollwizardprojectpage.cpp \ qt/voting/moc_pollwizardsummarypage.cpp \ qt/voting/moc_pollwizardtypepage.cpp \ + qt/voting/moc_votewizard.cpp \ + qt/voting/moc_votewizardballotpage.cpp \ + qt/voting/moc_votewizardsummarypage.cpp \ qt/voting/moc_votingmodel.cpp \ qt/voting/moc_votingpage.cpp @@ -290,6 +296,9 @@ GRIDCOINRESEARCH_QT_H = \ qt/voting/pollwizardprojectpage.h \ qt/voting/pollwizardsummarypage.h \ qt/voting/pollwizardtypepage.h \ + qt/voting/votewizard.h \ + qt/voting/votewizardballotpage.h \ + qt/voting/votewizardsummarypage.h \ qt/voting/votingmodel.h \ qt/voting/votingpage.h \ qt/walletmodel.h \ @@ -370,6 +379,9 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/voting/pollwizardprojectpage.cpp \ qt/voting/pollwizardsummarypage.cpp \ qt/voting/pollwizardtypepage.cpp \ + qt/voting/votewizard.cpp \ + qt/voting/votewizardballotpage.cpp \ + qt/voting/votewizardsummarypage.cpp \ qt/voting/votingmodel.cpp \ qt/voting/votingpage.cpp \ qt/walletmodel.cpp \ diff --git a/src/qt/forms/voting/votewizard.ui b/src/qt/forms/voting/votewizard.ui new file mode 100644 index 0000000000..03ff98d024 --- /dev/null +++ b/src/qt/forms/voting/votewizard.ui @@ -0,0 +1,53 @@ + + + VoteWizard + + + + 0 + 0 + 500 + 360 + + + + + 0 + 0 + + + + Vote + + + true + + + true + + + QWizard::ClassicStyle + + + QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage|QWizard::NoCancelButtonOnLastPage + + + + + + + VoteWizardBallotPage + QWizardPage +
voting/votewizardballotpage.h
+ 1 +
+ + VoteWizardSummaryPage + QWizardPage +
voting/votewizardsummarypage.h
+ 1 +
+
+ + +
diff --git a/src/qt/forms/voting/votewizardballotpage.ui b/src/qt/forms/voting/votewizardballotpage.ui new file mode 100644 index 0000000000..95c8a96b1c --- /dev/null +++ b/src/qt/forms/voting/votewizardballotpage.ui @@ -0,0 +1,118 @@ + + + VoteWizardBallotPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + + 9 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + + + + Qt::Horizontal + + + + + + + true + + + + + + + true + + + + + 0 + 0 + 596 + 392 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 9 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + PollDetails + QWidget +
voting/polldetails.h
+ 1 +
+
+ + +
diff --git a/src/qt/forms/voting/votewizardsummarypage.ui b/src/qt/forms/voting/votewizardsummarypage.ui new file mode 100644 index 0000000000..0b4d6e295d --- /dev/null +++ b/src/qt/forms/voting/votewizardsummarypage.ui @@ -0,0 +1,175 @@ + + + VoteWizardSummaryPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + + 9 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 60 + + + + + + + + :/icons/round_green_check + + + + + + + Vote Submitted + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Qt::AlignCenter + + + true + + + + + + + Qt::AlignCenter + + + true + + + Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 40 + + + + + + + + Your vote will tally with the next block. + + + Qt::AlignCenter + + + true + + + + + + + Qt::AlignCenter + + + true + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Copy ID + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/qt/voting/votewizard.cpp b/src/qt/voting/votewizard.cpp new file mode 100644 index 0000000000..fcb0869f1d --- /dev/null +++ b/src/qt/voting/votewizard.cpp @@ -0,0 +1,31 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_votewizard.h" +#include "qt/voting/votewizard.h" +#include "qt/voting/votingmodel.h" + +// ----------------------------------------------------------------------------- +// Class: VoteWizard +// ----------------------------------------------------------------------------- + +VoteWizard::VoteWizard(const PollItem& poll_item, VotingModel& voting_model, QWidget* parent) + : QWizard(parent) + , ui(new Ui::VoteWizard) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + resize(GRC::ScaleSize(this, 740, 580)); + + ui->ballotPage->setModel(&voting_model); + ui->ballotPage->setPoll(poll_item); + ui->summaryPage->setPoll(poll_item); +} + +VoteWizard::~VoteWizard() +{ + delete ui; +} diff --git a/src/qt/voting/votewizard.h b/src/qt/voting/votewizard.h new file mode 100644 index 0000000000..fadd9b5498 --- /dev/null +++ b/src/qt/voting/votewizard.h @@ -0,0 +1,38 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_VOTEWIZARD_H +#define VOTING_VOTEWIZARD_H + +#include + +namespace Ui { +class VoteWizard; +} + +class PollItem; +class VotingModel; + +class VoteWizard : public QWizard +{ + Q_OBJECT + +public: + enum Pages + { + PageBallot, + PageSummary, + }; + + explicit VoteWizard( + const PollItem& poll_item, + VotingModel& voting_model, + QWidget* parent = nullptr); + ~VoteWizard(); + +private: + Ui::VoteWizard* ui; +}; + +#endif // VOTING_VOTEWIZARD_H diff --git a/src/qt/voting/votewizardballotpage.cpp b/src/qt/voting/votewizardballotpage.cpp new file mode 100644 index 0000000000..72a9f5128c --- /dev/null +++ b/src/qt/voting/votewizardballotpage.cpp @@ -0,0 +1,113 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_votewizardballotpage.h" +#include "qt/voting/votewizard.h" +#include "qt/voting/votewizardballotpage.h" +#include "qt/voting/votingmodel.h" + +#include +#include +#include + +namespace { +//! +//! \brief Provides for QWizardPage::registerField() without a real widget. +//! +struct DummyField : public QWidget +{ + DummyField(QWidget* parent = nullptr) : QWidget(parent) { hide(); } +}; +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: VoteWizardBallotPage +// ----------------------------------------------------------------------------- + +VoteWizardBallotPage::VoteWizardBallotPage(QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::VoteWizardBallotPage) + , m_choice_buttons(new QButtonGroup(this)) +{ + ui->setupUi(this); + + setCommitPage(true); + setButtonText(QWizard::CommitButton, tr("Submit Vote")); + + registerField("txid", new DummyField(this), "", ""); + registerField("responseLabels", new DummyField(this)); + + connect( + m_choice_buttons.get(), QOverload::of(&QButtonGroup::buttonClicked), + [this](QAbstractButton*) { emit completeChanged(); }); +} + +VoteWizardBallotPage::~VoteWizardBallotPage() +{ + delete ui; +} + +void VoteWizardBallotPage::setModel(VotingModel* voting_model) +{ + m_voting_model = voting_model; +} + +void VoteWizardBallotPage::setPoll(const PollItem& poll_item) +{ + ui->details->setItem(poll_item); + m_choice_buttons->setExclusive(!poll_item.m_multiple_choice); + m_poll_id = poll_item.m_id; + + for (const auto& choice : poll_item.m_choices) { + QAbstractButton* button; + + if (poll_item.m_multiple_choice) { + button = new QCheckBox(choice.m_label); + } else { + button = new QRadioButton(choice.m_label); + } + + ui->choicesLayout->addWidget(button); + m_choice_buttons->addButton(button); + } +} + +void VoteWizardBallotPage::initializePage() +{ + ui->errorLabel->hide(); +} + +bool VoteWizardBallotPage::validatePage() +{ + const QList choice_buttons = m_choice_buttons->buttons(); + std::vector choices; + QStringList labels; + + for (int i = 0; i < choice_buttons.count(); ++i) { + if (choice_buttons[i]->isChecked()) { + choices.emplace_back(i); + labels << choice_buttons[i]->text(); + } + } + + const VotingResult result = m_voting_model->sendVote(m_poll_id, choices); + + if (!result.ok()) { + ui->errorLabel->setText(result.error()); + ui->errorLabel->show(); + + return false; + } + + setField("txid", result.txid()); + setField("responseLabels", labels.join('\n')); + + return true; +} + +bool VoteWizardBallotPage::isComplete() const +{ + return m_choice_buttons->checkedId() != -1; +} diff --git a/src/qt/voting/votewizardballotpage.h b/src/qt/voting/votewizardballotpage.h new file mode 100644 index 0000000000..5e7201f421 --- /dev/null +++ b/src/qt/voting/votewizardballotpage.h @@ -0,0 +1,44 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_VOTEWIZARDBALLOTPAGE_H +#define VOTING_VOTEWIZARDBALLOTPAGE_H + +#include +#include + +namespace Ui { +class VoteWizardBallotPage; +} + +class PollItem; +class VotingModel; + +QT_BEGIN_NAMESPACE +class QButtonGroup; +QT_END_NAMESPACE + +class VoteWizardBallotPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit VoteWizardBallotPage(QWidget* parent = nullptr); + ~VoteWizardBallotPage(); + + void setModel(VotingModel* voting_model); + void setPoll(const PollItem& poll_item); + + void initializePage() override; + bool validatePage() override; + bool isComplete() const override; + +private: + Ui::VoteWizardBallotPage* ui; + VotingModel* m_voting_model; + std::unique_ptr m_choice_buttons; + QString m_poll_id; +}; + +#endif // VOTING_VOTEWIZARDBALLOTPAGE_H diff --git a/src/qt/voting/votewizardsummarypage.cpp b/src/qt/voting/votewizardsummarypage.cpp new file mode 100644 index 0000000000..5d39e6cb6e --- /dev/null +++ b/src/qt/voting/votewizardsummarypage.cpp @@ -0,0 +1,46 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/decoration.h" +#include "qt/forms/voting/ui_votewizardsummarypage.h" +#include "qt/voting/votewizardsummarypage.h" +#include "qt/voting/votingmodel.h" + +#include + +// ----------------------------------------------------------------------------- +// Class: VoteWizardSummaryPage +// ----------------------------------------------------------------------------- + +VoteWizardSummaryPage::VoteWizardSummaryPage(QWidget* parent) + : QWizardPage(parent) + , ui(new Ui::VoteWizardSummaryPage) +{ + ui->setupUi(this); + + GRC::ScaleFontPointSize(ui->pageTitleLabel, 14); + GRC::ScaleFontPointSize(ui->pollTitleLabel, 12); + GRC::ScaleFontPointSize(ui->voteIdLabel, 8); +} + +VoteWizardSummaryPage::~VoteWizardSummaryPage() +{ + delete ui; +} + +void VoteWizardSummaryPage::setPoll(const PollItem& poll_item) +{ + ui->pollTitleLabel->setText(poll_item.m_title); +} + +void VoteWizardSummaryPage::initializePage() +{ + ui->responsesLabel->setText(field("responseLabels").toString()); + ui->voteIdLabel->setText(field("txid").toString()); +} + +void VoteWizardSummaryPage::on_copyToClipboardButton_clicked() const +{ + QApplication::clipboard()->setText(field("txid").toString()); +} diff --git a/src/qt/voting/votewizardsummarypage.h b/src/qt/voting/votewizardsummarypage.h new file mode 100644 index 0000000000..92e855f320 --- /dev/null +++ b/src/qt/voting/votewizardsummarypage.h @@ -0,0 +1,35 @@ +// Copyright (c) 2014-2021 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef VOTING_VOTEWIZARDSUMMARYPAGE_H +#define VOTING_VOTEWIZARDSUMMARYPAGE_H + +#include + +namespace Ui { +class VoteWizardSummaryPage; +} + +class PollItem; + +class VoteWizardSummaryPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit VoteWizardSummaryPage(QWidget* parent = nullptr); + ~VoteWizardSummaryPage(); + + void setPoll(const PollItem& poll_item); + + void initializePage() override; + +private: + Ui::VoteWizardSummaryPage* ui; + +private slots: + void on_copyToClipboardButton_clicked() const; +}; + +#endif // VOTING_VOTEWIZARDSUMMARYPAGE_H From 0d548a5d0cf94dc3a370d1f8b0e802ddbe42d1d3 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:52 -0500 Subject: [PATCH 8/9] Remove Qt Charts dependency This removes the dependency for the Qt Charts submodule and autotools pieces that check for it. Gridcoin used Qt Charts for the voting GUI. The redesigned UI displays poll results with standard widgets now, so we don't need the charting library. --- build-aux/m4/bitcoin_qt.m4 | 7 ------- ci/test/00_setup_env_native.sh | 2 +- depends/packages/qt.mk | 12 ------------ depends/patches/qt/subdirs.pro | 4 ---- doc/build-unix.md | 10 +++------- gridcoinresearch.pro | 6 ------ 6 files changed, 4 insertions(+), 37 deletions(-) diff --git a/build-aux/m4/bitcoin_qt.m4 b/build-aux/m4/bitcoin_qt.m4 index d9a73d7865..96be384bc2 100644 --- a/build-aux/m4/bitcoin_qt.m4 +++ b/build-aux/m4/bitcoin_qt.m4 @@ -372,8 +372,6 @@ AC_DEFUN([_BITCOIN_QT_CHECK_STATIC_LIBS], [ PKG_CHECK_MODULES([QT_THEME], [${qt_lib_prefix}ThemeSupport${qt_lib_suffix}], [QT_LIBS="$QT_THEME_LIBS $QT_LIBS"]) dnl Gridcoin uses Concurrent: PKG_CHECK_MODULES([QT_CONCURRENT], [${qt_lib_prefix}Concurrent${qt_lib_suffix}], [QT_LIBS="$QT_CONCURRENT_LIBS $QT_LIBS"]) - dnl Gridcoin uses Charts: - PKG_CHECK_MODULES([QT_CHARTS], [${qt_lib_prefix}Charts${qt_lib_suffix}], [QT_LIBS="$QT_CONCURRENT_LIBS $QT_LIBS"]) dnl Gridcoin uses SVG: PKG_CHECK_MODULES([QT_SVG], [${qt_lib_prefix}Svg${qt_lib_suffix}], [QT_LIBS="$QT_SVG_LIBS $QT_LIBS"]) if test "x$TARGET_OS" = xlinux; then @@ -420,11 +418,6 @@ AC_DEFUN([_BITCOIN_QT_FIND_LIBS],[ PKG_CHECK_MODULES([QT_CONCURRENT], [${qt_lib_prefix}Concurrent${qt_lib_suffix} $qt_version], [QT_INCLUDES="$QT_CONCURRENT_CFLAGS $QT_INCLUDES" QT_LIBS="$QT_CONCURRENT_LIBS $QT_LIBS"], [BITCOIN_QT_FAIL([${qt_lib_prefix}Concurrent${qt_lib_suffix} $qt_version not found])]) ]) - dnl Gridcoin uses Charts: - BITCOIN_QT_CHECK([ - PKG_CHECK_MODULES([QT_CHARTS], [${qt_lib_prefix}Charts${qt_lib_suffix} $qt_version], [QT_INCLUDES="$QT_CHARTS_CFLAGS $QT_INCLUDES" QT_LIBS="$QT_CHARTS_LIBS $QT_LIBS" CPPFLAGS="$CPPFLAGS -DQT_CHARTS_LIB"], - [AC_MSG_WARN([${qt_lib_prefix}Charts${qt_lib_suffix} $qt_version not found. Poll results will not display charts.])]) - ]) BITCOIN_QT_CHECK([ PKG_CHECK_MODULES([QT_TEST], [${qt_lib_prefix}Test${qt_lib_suffix} $qt_version], [QT_TEST_INCLUDES="$QT_TEST_CFLAGS"; have_qt_test=yes], [have_qt_test=no]) diff --git a/ci/test/00_setup_env_native.sh b/ci/test/00_setup_env_native.sh index 0eefc5d247..cd40b39c9f 100755 --- a/ci/test/00_setup_env_native.sh +++ b/ci/test/00_setup_env_native.sh @@ -8,7 +8,7 @@ export LC_ALL=C.UTF-8 export CONTAINER_NAME=ci_native export DOCKER_NAME_TAG=ubuntu:20.04 -export PACKAGES="libqt5gui5 libqt5core5a qtbase5-dev libqt5dbus5 qttools5-dev qttools5-dev-tools libqt5charts5-dev libssl-dev libevent-dev bsdmainutils libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-iostreams-dev libboost-program-options-dev libboost-test-dev libboost-thread-dev libdb5.3++-dev libminiupnpc-dev libqrencode-dev libzip-dev zlib1g zlib1g-dev libcurl4 libcurl4-openssl-dev" +export PACKAGES="libqt5gui5 libqt5core5a qtbase5-dev libqt5dbus5 qttools5-dev qttools5-dev-tools libssl-dev libevent-dev bsdmainutils libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-iostreams-dev libboost-program-options-dev libboost-test-dev libboost-thread-dev libdb5.3++-dev libminiupnpc-dev libqrencode-dev libzip-dev zlib1g zlib1g-dev libcurl4 libcurl4-openssl-dev" export RUN_UNIT_TESTS=true # export RUN_FUNCTIONAL_TESTS=false # export RUN_SECURITY_TESTS="true" diff --git a/depends/packages/qt.mk b/depends/packages/qt.mk index df8075d0e4..ed39c56691 100644 --- a/depends/packages/qt.mk +++ b/depends/packages/qt.mk @@ -18,17 +18,12 @@ $(package)_qttranslations_sha256_hash=577b0668a777eb2b451c61e8d026d79285371597ce $(package)_qttools_file_name=qttools-$($(package)_suffix) $(package)_qttools_sha256_hash=98b2aaca230458f65996f3534fd471d2ffd038dd58ac997c0589c06dc2385b4f -# Gridcoin uses charts for the voting features: -$(package)_qtcharts_file_name=qtcharts-$($(package)_suffix) -$(package)_qtcharts_sha256_hash=8567502335913a45dbe47c5b493974b48c2049dc07ab5a2a273ddfdcf43c002c - # Gridcoin displays SVG images in the GUI: $(package)_qtsvg_file_name=qtsvg-$($(package)_suffix) $(package)_qtsvg_sha256_hash=7a6857a2f68cfbebb9f791396b401a98e951c9bff9bfeb1b5b01914c3ea1a0ed $(package)_extra_sources = $($(package)_qttranslations_file_name) $(package)_extra_sources += $($(package)_qttools_file_name) -$(package)_extra_sources += $($(package)_qtcharts_file_name) $(package)_extra_sources += $($(package)_qtsvg) define $(package)_set_vars @@ -196,7 +191,6 @@ define $(package)_fetch_cmds $(call fetch_file,$(package),$($(package)_download_path),$($(package)_download_file),$($(package)_file_name),$($(package)_sha256_hash)) && \ $(call fetch_file,$(package),$($(package)_download_path),$($(package)_qttranslations_file_name),$($(package)_qttranslations_file_name),$($(package)_qttranslations_sha256_hash)) && \ $(call fetch_file,$(package),$($(package)_download_path),$($(package)_qttools_file_name),$($(package)_qttools_file_name),$($(package)_qttools_sha256_hash)) && \ -$(call fetch_file,$(package),$($(package)_download_path),$($(package)_qtcharts_file_name),$($(package)_qtcharts_file_name),$($(package)_qtcharts_sha256_hash)) && \ $(call fetch_file,$(package),$($(package)_download_path),$($(package)_qtsvg_file_name),$($(package)_qtsvg_file_name),$($(package)_qtsvg_sha256_hash)) endef @@ -205,7 +199,6 @@ define $(package)_extract_cmds echo "$($(package)_sha256_hash) $($(package)_source)" > $($(package)_extract_dir)/.$($(package)_file_name).hash && \ echo "$($(package)_qttranslations_sha256_hash) $($(package)_source_dir)/$($(package)_qttranslations_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ echo "$($(package)_qttools_sha256_hash) $($(package)_source_dir)/$($(package)_qttools_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ - echo "$($(package)_qtcharts_sha256_hash) $($(package)_source_dir)/$($(package)_qtcharts_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ echo "$($(package)_qtsvg_sha256_hash) $($(package)_source_dir)/$($(package)_qtsvg_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ $(build_SHA256SUM) -c $($(package)_extract_dir)/.$($(package)_file_name).hash && \ mkdir qtbase && \ @@ -214,8 +207,6 @@ define $(package)_extract_cmds tar --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qttranslations_file_name) -C qttranslations && \ mkdir qttools && \ tar --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qttools_file_name) -C qttools && \ - mkdir qtcharts && \ - tar --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qtcharts_file_name) -C qtcharts && \ mkdir qtsvg && \ tar --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qtsvg_file_name) -C qtsvg endef @@ -288,7 +279,6 @@ define $(package)_config_cmds qtbase/bin/qmake -o qttools/src/linguist/lrelease/Makefile qttools/src/linguist/lrelease/lrelease.pro && \ qtbase/bin/qmake -o qttools/src/linguist/lupdate/Makefile qttools/src/linguist/lupdate/lupdate.pro && \ qtbase/bin/qmake -o qttools/src/linguist/lconvert/Makefile qttools/src/linguist/lconvert/lconvert.pro && \ - qtbase/bin/qmake -o qtcharts/src/charts/Makefile qtcharts/src/charts/charts.pro && \ qtbase/bin/qmake -o qtsvg/src/Makefile qtsvg/src/src.pro endef @@ -299,7 +289,6 @@ define $(package)_build_cmds $(MAKE) -C qttools/src/linguist/lupdate && \ $(MAKE) -C qttools/src/linguist/lconvert && \ $(MAKE) -C qttranslations && \ - $(MAKE) -C qtcharts/src/charts && \ $(MAKE) -C qtsvg/src \ ' endef @@ -311,7 +300,6 @@ define $(package)_stage_cmds $(MAKE) -C qttools/src/linguist/lupdate INSTALL_ROOT=$($(package)_staging_dir) install_target && \ $(MAKE) -C qttools/src/linguist/lconvert INSTALL_ROOT=$($(package)_staging_dir) install_target && \ $(MAKE) -C qttranslations INSTALL_ROOT=$($(package)_staging_dir) install_subtargets && \ - $(MAKE) -C qtcharts/src/charts INSTALL_ROOT=$($(package)_staging_dir) install && \ $(MAKE) -C qtsvg/src INSTALL_ROOT=$($(package)_staging_dir) install \ ' endef diff --git a/depends/patches/qt/subdirs.pro b/depends/patches/qt/subdirs.pro index f2ab21932d..2c69c0cbae 100644 --- a/depends/patches/qt/subdirs.pro +++ b/depends/patches/qt/subdirs.pro @@ -11,16 +11,12 @@ TEMPLATE = subdirs SUBDIRS = \ qtbase \ - qtcharts \ qtsvg \ qttools \ qttranslations qtbase.target = module-qtbase -qtcharts.target = module-qtcharts -qtcharts.depends = qtbase - qtsvg.target = module-qtsvg qtsvg.depends = qtbase diff --git a/doc/build-unix.md b/doc/build-unix.md index 662eac7c0d..2a0fa27465 100644 --- a/doc/build-unix.md +++ b/doc/build-unix.md @@ -135,7 +135,7 @@ To build without GUI pass `--without-gui` to configure. To build with Qt 5 (recommended) you need the following: - sudo apt-get install libqt5gui5 libqt5core5a libqt5charts5-dev libqt5dbus5 qttools5-dev qttools5-dev-tools libprotobuf-dev protobuf-compiler + sudo apt-get install libqt5gui5 libqt5core5a libqt5dbus5 qttools5-dev qttools5-dev-tools libprotobuf-dev protobuf-compiler libqrencode (enabled by default, switch off by passing `--without-qrencode` to configure) can be installed with: @@ -188,11 +188,7 @@ To build without GUI pass `--without-gui` to configure. To build with Qt 5 (recommended) you need the following: - sudo zypper install libQt5Gui5 libQt5Core5 libQt5Charts5 libQt5DBus5 libQt5Network-devel libqt5-qttools-devel libqt5-qttools - -Additionally for Tumbleweed: - - sudo zypper install libQt5Charts5-designer + sudo zypper install libQt5Gui5 libQt5Core5 libQt5DBus5 libQt5Network-devel libqt5-qttools-devel libqt5-qttools libqrencode (enabled by default, switch off by passing `--without-qrencode` to configure) can be installed with: @@ -222,7 +218,7 @@ Dependencies for the GUI: Alpine Linux To build the Qt GUI on Alpine Linux, we need these dependencies: - apk add libqrencode-dev protobuf-dev qt5-qtbase-dev qt5-qtcharts-dev qt5-qtsvg-dev qt5-qttools-dev + apk add libqrencode-dev protobuf-dev qt5-qtbase-dev qt5-qtsvg-dev qt5-qttools-dev Setup and Build Example: Arch Linux diff --git a/gridcoinresearch.pro b/gridcoinresearch.pro index 5158676698..b73dde67c7 100755 --- a/gridcoinresearch.pro +++ b/gridcoinresearch.pro @@ -15,12 +15,6 @@ win32 { DEFINES += _WIN32_WINNT=0x0501 WINVER=0x0501 } -lessThan(QT_MAJOR_VERSION, 5) | lessThan(QT_MINOR_VERSION, 8) { - # Qt charts not available -}else{ - QT += charts -} - # for boost 1.37, add -mt to the boost libraries # use: qmake BOOST_LIB_SUFFIX=-mt # for boost thread win32 with _win32 sufix From 74de3ed8f68aded8d19ddb456c233737d5b4d17f Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Mon, 31 May 2021 19:46:55 -0500 Subject: [PATCH 9/9] Add system tray notification for new polls This adds a GUI hook that displays a system notification when a node receives a new poll as well as an accompanying GUI option to disable those notifications. --- src/qt/bitcoingui.cpp | 21 +++++++++++++++++++++ src/qt/bitcoingui.h | 1 + src/qt/forms/optionsdialog.ui | 7 +++++++ src/qt/optionsdialog.cpp | 1 + src/qt/optionsmodel.cpp | 12 ++++++++++++ src/qt/optionsmodel.h | 3 +++ 6 files changed, 45 insertions(+) diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index f2471c43b6..d6776852cc 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -42,6 +42,7 @@ #include "clicklabel.h" #include "univalue.h" #include "upgradeqt.h" +#include "voting/votingmodel.h" #ifdef Q_OS_MAC #include "macdockiconhandler.h" @@ -807,6 +808,12 @@ void BitcoinGUI::setResearcherModel(ResearcherModel *researcherModel) void BitcoinGUI::setVotingModel(VotingModel *votingModel) { votingPage->setVotingModel(votingModel); + + if (!votingModel) { + return; + } + + connect(votingModel, SIGNAL(newPollReceived()), this, SLOT(handleNewPoll())); } void BitcoinGUI::createTrayIcon() @@ -1786,6 +1793,20 @@ void BitcoinGUI::updateBeaconIcon() .arg(researcherModel->formatBeaconStatus())); } +void BitcoinGUI::handleNewPoll() +{ + if (!clientModel || !clientModel->getOptionsModel()) { + return; + } + + if (!clientModel->getOptionsModel()->getDisablePollNotifications()) { + notificator->notify( + Notificator::Information, + tr("New Poll"), + tr("A new poll is available. Open Gridcoin to vote.")); + } +} + // ----------------------------------------------------------------------------- // Class: ToolbarButtonIconFilter // ----------------------------------------------------------------------------- diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 7ef1163cc8..3f6d7bddf7 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -274,6 +274,7 @@ private slots: QString GetEstimatedStakingFrequency(unsigned int nEstimateTime); void updateGlobalStatus(); + void handleNewPoll(); }; //! diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index 2f24fb4e88..43bbb533f0 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -286,6 +286,13 @@ + + + + Disable Poll Notifications + + + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 45749d48ba..2d85a2a12f 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -148,6 +148,7 @@ void OptionsDialog::setMapper() /* Window */ mapper->addMapping(ui->disableTransactionNotifications, OptionsModel::DisableTrxNotifications); + mapper->addMapping(ui->disablePollNotifications, OptionsModel::DisablePollNotifications); #ifndef Q_OS_MAC mapper->addMapping(ui->minimizeToTray, OptionsModel::MinimizeToTray); mapper->addMapping(ui->minimizeOnClose, OptionsModel::MinimizeOnClose); diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 7219246236..a8014f2db8 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -46,6 +46,7 @@ void OptionsModel::Init() fStartMin = settings.value("fStartMin", true).toBool(); fMinimizeToTray = settings.value("fMinimizeToTray", false).toBool(); fDisableTrxNotifications = settings.value("fDisableTrxNotifications", false).toBool(); + fDisablePollNotifications = settings.value("fDisablePollNotifications", false).toBool(); bDisplayAddresses = settings.value("bDisplayAddresses", false).toBool(); fMinimizeOnClose = settings.value("fMinimizeOnClose", false).toBool(); fCoinControlFeatures = settings.value("fCoinControlFeatures", false).toBool(); @@ -97,6 +98,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const return QVariant(fMinimizeToTray); case DisableTrxNotifications: return QVariant(fDisableTrxNotifications); + case DisablePollNotifications: + return QVariant(fDisablePollNotifications); case MapPortUPnP: return settings.value("fUseUPnP", gArgs.GetBoolArg("-upnp", true)); case MinimizeOnClose: @@ -178,6 +181,10 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in fDisableTrxNotifications = value.toBool(); settings.setValue("fDisableTrxNotifications", fDisableTrxNotifications); break; + case DisablePollNotifications: + fDisablePollNotifications = value.toBool(); + settings.setValue("fDisablePollNotifications", fDisablePollNotifications); + break; case MapPortUPnP: fUseUPnP = value.toBool(); settings.setValue("fUseUPnP", fUseUPnP); @@ -338,6 +345,11 @@ bool OptionsModel::getDisableTrxNotifications() return fDisableTrxNotifications; } +bool OptionsModel::getDisablePollNotifications() +{ + return fDisablePollNotifications; +} + bool OptionsModel::getMinimizeOnClose() { return fMinimizeOnClose; diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index 14ed35f5fb..6d77b0a794 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -22,6 +22,7 @@ class OptionsModel : public QAbstractListModel MinimizeToTray, // bool StartMin, // bool DisableTrxNotifications, // bool + DisablePollNotifications,// bool MapPortUPnP, // bool MinimizeOnClose, // bool ProxyUse, // bool @@ -54,6 +55,7 @@ class OptionsModel : public QAbstractListModel bool getStartMin(); bool getMinimizeToTray(); bool getDisableTrxNotifications(); + bool getDisablePollNotifications(); bool getMinimizeOnClose(); int getDisplayUnit(); bool getDisplayAddresses(); @@ -75,6 +77,7 @@ class OptionsModel : public QAbstractListModel bool fStartAtStartup; bool fStartMin; bool fDisableTrxNotifications; + bool fDisablePollNotifications; bool bDisplayAddresses; bool fMinimizeOnClose; bool fCoinControlFeatures;