From dad4863e84ff591001149517f76553192aacce44 Mon Sep 17 00:00:00 2001 From: Bill Somerville Date: Mon, 6 Apr 2015 01:57:47 +0000 Subject: [PATCH] Ensure all model proxy caches are flushed before access This fixes a defect where station detail changes are not saved. The Qt sort and filter proxy models utilize an item cache that must be flushed by callig submit() before accessing the underlying model if the proxy model has been used for updates. Also separated the item model candidate key filter from the implementation internals of the foreign key item delegate so that candidate key filtered models can be used directly as view models. Make the insert new station details band combo box use a candidate key filtered item model to avoid constraint violations. Constraint is zero or one station records per band. git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@5161 ab8295b8-cf94-4d9e-aec4-7959e3be5d79 --- CMakeLists.txt | 1 + CandidateKeyFilter.cpp | 70 +++ CandidateKeyFilter.hpp | 36 ++ Configuration.cpp | 20 +- ForeignKeyDelegate.cpp | 109 ++-- ForeignKeyDelegate.hpp | 69 +-- FrequencyList.cpp | 717 +++++++++++++------------- FrequencyList.hpp | 118 ++--- StationList.cpp | 1103 ++++++++++++++++++++-------------------- StationList.hpp | 198 ++++---- 10 files changed, 1260 insertions(+), 1181 deletions(-) create mode 100644 CandidateKeyFilter.cpp create mode 100644 CandidateKeyFilter.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f90159a5..7b2b9503c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,6 +184,7 @@ set (wsjt_qt_CXXSRCS StationList.cpp FrequencyLineEdit.cpp FrequencyItemDelegate.cpp + CandidateKeyFilter.cpp ForeignKeyDelegate.cpp LiveFrequencyValidator.cpp GetUserId.cpp diff --git a/CandidateKeyFilter.cpp b/CandidateKeyFilter.cpp new file mode 100644 index 000000000..c0e048563 --- /dev/null +++ b/CandidateKeyFilter.cpp @@ -0,0 +1,70 @@ +#include "CandidateKeyFilter.hpp" + +#include +#include + +#include "pimpl_impl.hpp" + +class CandidateKeyFilter::impl final +{ +public: + explicit impl (QAbstractItemModel const * referencing_model + , int referencing_key_column + , int referenced_key_column + , int referencing_key_role + , int referenced_key_role) + : referencing_ {referencing_model} + , referencing_key_column_ {referencing_key_column} + , referencing_key_role_ {referencing_key_role} + , referenced_key_column_ {referenced_key_column} + , referenced_key_role_ {referenced_key_role} + { + } + + QAbstractItemModel const * referencing_; + int referencing_key_column_; + int referencing_key_role_; + int referenced_key_column_; + int referenced_key_role_; + QModelIndex active_key_; +}; + +CandidateKeyFilter::CandidateKeyFilter (QAbstractItemModel const * referencing_model + , QAbstractItemModel * referenced_model + , int referencing_key_column + , int referenced_key_column + , int referencing_key_role + , int referenced_key_role) + : QSortFilterProxyModel {nullptr} // ForeignKeyDelegate owns us + , m_ {referencing_model, referencing_key_column, referenced_key_column, referencing_key_role, referenced_key_role} +{ + setSourceModel (referenced_model); +} + +CandidateKeyFilter::~CandidateKeyFilter () +{ +} + +void CandidateKeyFilter::set_active_key (QModelIndex const& index) +{ + if (index.isValid () ) + { + Q_ASSERT (index.column () == m_->referencing_key_column_); + m_->active_key_ = index; + } + invalidateFilter (); +} + +bool CandidateKeyFilter::filterAcceptsRow (int candidate_row, QModelIndex const& candidate_parent) const +{ + auto candidate_key = sourceModel ()->index (candidate_row, m_->referenced_key_column_, candidate_parent).data (m_->referenced_key_role_); + + // Include the current key. + if (m_->active_key_.isValid () && candidate_key == m_->active_key_.data (m_->referencing_key_role_)) + { + return true; + } + + // Filter out any candidates already in the referencing key rows. + return m_->referencing_->match (m_->referencing_->index (0, m_->referencing_key_column_), m_->referencing_key_role_, candidate_key, 1, Qt::MatchExactly).isEmpty (); +} diff --git a/CandidateKeyFilter.hpp b/CandidateKeyFilter.hpp new file mode 100644 index 000000000..1a5304b20 --- /dev/null +++ b/CandidateKeyFilter.hpp @@ -0,0 +1,36 @@ +#ifndef CANDIDATE_KEY_FILTER_HPP_ +#define CANDIDATE_KEY_FILTER_HPP_ + +#include +#include + +#include "pimpl_h.hpp" + +class QAbstractItemModel; + +class CandidateKeyFilter final + : public QSortFilterProxyModel +{ +public: + explicit CandidateKeyFilter (QAbstractItemModel const * referencing_model + , QAbstractItemModel * referenced_model + , int referencing_key_column = 0 + , int referenced_key_column = 0 + , int referencing_key_role = Qt::EditRole + , int referenced_key_role = Qt::EditRole); + ~CandidateKeyFilter (); + + // this key is not to be filtered, usually because we want to allow + // it since we are editing the row that contains it this it is valid + // even though it is in use + void set_active_key (QModelIndex const& index = QModelIndex {}); + +protected: + bool filterAcceptsRow (int candidate_row, QModelIndex const& candidate_parent) const override; + +private: + class impl; + pimpl m_; +}; + +#endif diff --git a/Configuration.cpp b/Configuration.cpp index c9c3e3c51..fb0ea3ea8 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -155,12 +155,14 @@ #include #include #include +#include #include #include "qt_helpers.hpp" #include "SettingsGroup.hpp" #include "FrequencyLineEdit.hpp" #include "FrequencyItemDelegate.hpp" +#include "CandidateKeyFilter.hpp" #include "ForeignKeyDelegate.hpp" #include "TransceiverFactory.hpp" #include "Transceiver.hpp" @@ -240,13 +242,13 @@ class StationDialog final : public QDialog { public: - explicit StationDialog (Bands * bands, QWidget * parent = nullptr) + explicit StationDialog (StationList const * stations, Bands * bands, QWidget * parent = nullptr) : QDialog {parent} - , bands_ {bands} + , filtered_bands_ {new CandidateKeyFilter {stations, bands}} { setWindowTitle (QApplication::applicationName () + " - " + tr ("Add Station")); - band_.setModel (bands_); + band_.setModel (filtered_bands_.data ()); auto form_layout = new QFormLayout (); form_layout->addRow (tr ("&Band:"), &band_); @@ -273,8 +275,14 @@ public: return {band_.currentText (), delta_.frequency_delta (), description_.text ()}; } + int exec () override + { + filtered_bands_->set_active_key (); + return QDialog::exec (); + } + private: - Bands * bands_; + QScopedPointer filtered_bands_; QComboBox band_; FrequencyDeltaLineEdit delta_; @@ -724,7 +732,7 @@ Configuration::impl::impl (Configuration * self, QSettings * settings, QWidget * , stations_ {&bands_} , next_stations_ {&bands_} , frequency_dialog_ {new FrequencyDialog {this}} - , station_dialog_ {new StationDialog {&bands_, this}} + , station_dialog_ {new StationDialog {&next_stations_, &bands_, this}} , rig_active_ {false} , have_rig_ {false} , rig_changed_ {false} @@ -922,7 +930,7 @@ Configuration::impl::impl (Configuration * self, QSettings * settings, QWidget * ui_->stations_table_view->setModel (&next_stations_); ui_->stations_table_view->sortByColumn (0, Qt::AscendingOrder); ui_->stations_table_view->setColumnWidth (1, 150); - ui_->stations_table_view->setItemDelegateForColumn (0, new ForeignKeyDelegate {&next_stations_, &bands_, 0, this}); + ui_->stations_table_view->setItemDelegateForColumn (0, new ForeignKeyDelegate {&next_stations_, &bands_, 0, 0, this}); ui_->stations_table_view->setItemDelegateForColumn (1, new FrequencyDeltaItemDelegate {this}); station_delete_action_ = new QAction {tr ("&Delete"), ui_->stations_table_view}; diff --git a/ForeignKeyDelegate.cpp b/ForeignKeyDelegate.cpp index e3c717299..18817e74f 100644 --- a/ForeignKeyDelegate.cpp +++ b/ForeignKeyDelegate.cpp @@ -1,77 +1,32 @@ -#include "ForeignKeyDelegate.hpp" - -#include -#include - -class CandidateKeyFilter final - : public QSortFilterProxyModel -{ -public: - explicit CandidateKeyFilter (QAbstractItemModel const * referencing_model - , QAbstractItemModel * referenced_model - , int referenced_key_column - , int referencing_key_role - , int referenced_key_role) - : QSortFilterProxyModel {nullptr} // ForeignKeyDelegate owns us - , referencing_ {referencing_model} - , referencing_key_role_ {referencing_key_role} - , referenced_key_column_ {referenced_key_column} - , referenced_key_role_ {referenced_key_role} - { - setSourceModel (referenced_model); - } - - void set_active_key (QModelIndex const& index) - { - active_key_ = index; - invalidateFilter (); - } - -protected: - bool filterAcceptsRow (int candidate_row, QModelIndex const& candidate_parent) const override - { - auto candidate_key = sourceModel ()->index (candidate_row, referenced_key_column_, candidate_parent).data (referenced_key_role_); - - // Include the current key. - if (candidate_key == active_key_.data (referencing_key_role_)) - { - return true; - } - - // Filter out any candidates already in the referencing key rows. - return referencing_->match (referencing_->index (0, active_key_.column ()), referencing_key_role_, candidate_key, 1, Qt::MatchExactly).isEmpty (); - } - -private: - QAbstractItemModel const * referencing_; - int referencing_key_role_; - int referenced_key_column_; - int referenced_key_role_; - QModelIndex active_key_; -}; - -ForeignKeyDelegate::ForeignKeyDelegate (QAbstractItemModel const * referencing_model - , QAbstractItemModel * referenced_model - , int referenced_key_column - , QObject * parent - , int referencing_key_role - , int referenced_key_role) - : QStyledItemDelegate {parent} - , candidate_key_filter_ {new CandidateKeyFilter {referencing_model, referenced_model, referenced_key_column, referencing_key_role, referenced_key_role}} -{ -} - -ForeignKeyDelegate::~ForeignKeyDelegate () -{ -} - -QWidget * ForeignKeyDelegate::createEditor (QWidget * parent - , QStyleOptionViewItem const& /* option */ - , QModelIndex const& index) const -{ - auto editor = new QComboBox {parent}; - editor->setFrame (false); - candidate_key_filter_->set_active_key (index); - editor->setModel (candidate_key_filter_.data ()); - return editor; -} +#include "ForeignKeyDelegate.hpp" + +#include + +#include "CandidateKeyFilter.hpp" + +ForeignKeyDelegate::ForeignKeyDelegate (QAbstractItemModel const * referencing_model + , QAbstractItemModel * referenced_model + , int referencing_key_column + , int referenced_key_column + , QObject * parent + , int referencing_key_role + , int referenced_key_role) + : QStyledItemDelegate {parent} + , candidate_key_filter_ {new CandidateKeyFilter {referencing_model, referenced_model, referencing_key_column, referenced_key_column, referencing_key_role, referenced_key_role}} +{ +} + +ForeignKeyDelegate::~ForeignKeyDelegate () +{ +} + +QWidget * ForeignKeyDelegate::createEditor (QWidget * parent + , QStyleOptionViewItem const& /* option */ + , QModelIndex const& index) const +{ + auto editor = new QComboBox {parent}; + editor->setFrame (false); + candidate_key_filter_->set_active_key (index); + editor->setModel (candidate_key_filter_.data ()); + return editor; +} diff --git a/ForeignKeyDelegate.hpp b/ForeignKeyDelegate.hpp index 6e6797079..9e0dc06ab 100644 --- a/ForeignKeyDelegate.hpp +++ b/ForeignKeyDelegate.hpp @@ -1,34 +1,35 @@ -#ifndef FOREIGN_KEY_DELEGATE_HPP_ -#define FOREIGN_KEY_DELEGATE_HPP_ - -#include -#include - -class CandidateKeyFilter; - -// -// Class ForeignKeyDelegate -// -// Item delegate for editing a foreign key item in a one or many -// to one relationship. A QComboBox is used as an item delegate -// for the edit role. -// -class ForeignKeyDelegate final - : public QStyledItemDelegate -{ -public: - explicit ForeignKeyDelegate (QAbstractItemModel const * referencing_model - , QAbstractItemModel * referenced_model - , int referenced_key_column = 0 - , QObject * parent = nullptr - , int referencing_key_role = Qt::EditRole - , int referenced_key_role = Qt::EditRole); - ~ForeignKeyDelegate (); - - QWidget * createEditor (QWidget * parent, QStyleOptionViewItem const&, QModelIndex const&) const override; - -private: - QScopedPointer candidate_key_filter_; -}; - -#endif +#ifndef FOREIGN_KEY_DELEGATE_HPP_ +#define FOREIGN_KEY_DELEGATE_HPP_ + +#include +#include + +class CandidateKeyFilter; + +// +// Class ForeignKeyDelegate +// +// Item delegate for editing a foreign key item in a one or many +// to one relationship. A QComboBox is used as an item delegate +// for the edit role. +// +class ForeignKeyDelegate final + : public QStyledItemDelegate +{ +public: + explicit ForeignKeyDelegate (QAbstractItemModel const * referencing_model + , QAbstractItemModel * referenced_model + , int referencing_key_column = 0 + , int referenced_key_column = 0 + , QObject * parent = nullptr + , int referencing_key_role = Qt::EditRole + , int referenced_key_role = Qt::EditRole); + ~ForeignKeyDelegate (); + + QWidget * createEditor (QWidget * parent, QStyleOptionViewItem const&, QModelIndex const&) const override; + +private: + QScopedPointer candidate_key_filter_; +}; + +#endif diff --git a/FrequencyList.cpp b/FrequencyList.cpp index b2daf7549..6a864194d 100644 --- a/FrequencyList.cpp +++ b/FrequencyList.cpp @@ -1,358 +1,359 @@ -#include "FrequencyList.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "pimpl_impl.hpp" - -class FrequencyList::impl final - : public QAbstractTableModel -{ -public: - impl (Frequencies frequencies, QObject * parent) - : QAbstractTableModel {parent} - , frequencies_ {frequencies} - { - } - - Frequencies const& frequencies () const {return frequencies_;} - void assign (Frequencies); - QModelIndex add (Frequency); - -protected: - // Implement the QAbstractTableModel interface - int rowCount (QModelIndex const& parent = QModelIndex {}) const override; - int columnCount (QModelIndex const& parent = QModelIndex {}) const override; - Qt::ItemFlags flags (QModelIndex const& = QModelIndex {}) const override; - QVariant data (QModelIndex const&, int role = Qt::DisplayRole) const override; - bool setData (QModelIndex const&, QVariant const& value, int role = Qt::EditRole) override; - QVariant headerData (int section, Qt::Orientation, int = Qt::DisplayRole) const override; - bool removeRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override; - bool insertRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override; - QStringList mimeTypes () const override; - QMimeData * mimeData (QModelIndexList const&) const override; - -private: - static int constexpr num_cols {2}; - static auto constexpr mime_type ="application/wsjt.Frequencies"; - - Frequencies frequencies_; -}; - -FrequencyList::FrequencyList (QObject * parent) - : FrequencyList {{}, parent} -{ -} - -FrequencyList::FrequencyList (Frequencies frequencies, QObject * parent) - : QSortFilterProxyModel {parent} - , m_ {frequencies, parent} -{ - // setDynamicSortFilter (true); - setSourceModel (&*m_); - setSortRole (SortRole); -} - -FrequencyList::~FrequencyList () -{ -} - -FrequencyList& FrequencyList::operator = (Frequencies frequencies) -{ - m_->assign (frequencies); - return *this; -} - -auto FrequencyList::frequencies () const -> Frequencies -{ - return m_->frequencies (); -} - -QModelIndex FrequencyList::add (Frequency f) -{ - return mapFromSource (m_->add (f)); -} - -bool FrequencyList::remove (Frequency f) -{ - auto row = m_->frequencies ().indexOf (f); - - if (0 > row) - { - return false; - } - - return m_->removeRow (row); -} - -namespace -{ - bool row_is_higher (QModelIndex const& lhs, QModelIndex const& rhs) - { - return lhs.row () > rhs.row (); - } -} - -bool FrequencyList::removeDisjointRows (QModelIndexList rows) -{ - bool result {true}; - - // We must work with source model indexes because we don't want row - // removes to invalidate model indexes we haven't yet processed. We - // achieve that by processing them in decending row order. - for (int r = 0; r < rows.size (); ++r) - { - rows[r] = mapToSource (rows[r]); - } - - // reverse sort by row - qSort (rows.begin (), rows.end (), row_is_higher); - Q_FOREACH (auto index, rows) - { - if (result && !m_->removeRow (index.row ())) - { - result = false; - } - } - - return result; -} - - -void FrequencyList::impl::assign (Frequencies frequencies) -{ - beginResetModel (); - std::swap (frequencies_, frequencies); - endResetModel (); -} - -QModelIndex FrequencyList::impl::add (Frequency f) -{ - // Any Frequency that isn't in the list may be added - if (!frequencies_.contains (f)) - { - auto row = frequencies_.size (); - - beginInsertRows (QModelIndex {}, row, row); - frequencies_.append (f); - endInsertRows (); - - return index (row, 0); - } - - return QModelIndex {}; -} - -int FrequencyList::impl::rowCount (QModelIndex const& parent) const -{ - return parent.isValid () ? 0 : frequencies_.size (); -} - -int FrequencyList::impl::columnCount (QModelIndex const& parent) const -{ - return parent.isValid () ? 0 : num_cols; -} - -Qt::ItemFlags FrequencyList::impl::flags (QModelIndex const& index) const -{ - auto result = QAbstractTableModel::flags (index) | Qt::ItemIsDropEnabled; - - auto row = index.row (); - auto column = index.column (); - - if (index.isValid () - && row < frequencies_.size () - && column < num_cols) - { - switch (column) - { - case 0: - result |= Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; - break; - - case 1: - result |= Qt::ItemIsDragEnabled; - break; - } - } - - return result; -} - -QVariant FrequencyList::impl::data (QModelIndex const& index, int role) const -{ - QVariant item; - - auto row = index.row (); - auto column = index.column (); - - if (index.isValid () - && row < frequencies_.size () - && column < num_cols) - { - auto frequency = frequencies_.at (row); - - switch (column) - { - case 0: - switch (role) - { - case SortRole: - case Qt::DisplayRole: - case Qt::EditRole: - case Qt::AccessibleTextRole: - item = frequency; - break; - - case Qt::ToolTipRole: - case Qt::AccessibleDescriptionRole: - item = tr ("Frequency"); - break; - - case Qt::TextAlignmentRole: - item = Qt::AlignRight + Qt::AlignVCenter; - break; - } - break; - - case 1: - switch (role) - { - case Qt::DisplayRole: - case Qt::EditRole: - case Qt::AccessibleTextRole: - item = static_cast (frequency / 1.e6); - break; - - case SortRole: // use the underlying Frequency value - item = frequency; - break; - - case Qt::ToolTipRole: - case Qt::AccessibleDescriptionRole: - item = tr ("Frequency MHz"); - break; - - case Qt::TextAlignmentRole: - item = Qt::AlignRight + Qt::AlignVCenter; - break; - } - break; - } - } - - return item; -} - -bool FrequencyList::impl::setData (QModelIndex const& model_index, QVariant const& value, int role) -{ - bool changed {false}; - - auto row = model_index.row (); - if (model_index.isValid () - && Qt::EditRole == role - && row < frequencies_.size () - && 0 == model_index.column () - && value.canConvert ()) - { - auto frequency = value.value (); - auto original_frequency = frequencies_.at (row); - if (frequency != original_frequency) - { - frequencies_.replace (row, frequency); - Q_EMIT dataChanged (model_index, index (model_index.row (), 1), QVector {} << role); - } - changed = true; - } - - return changed; -} - -QVariant FrequencyList::impl::headerData (int section, Qt::Orientation orientation, int role) const -{ - QVariant header; - - if (Qt::DisplayRole == role - && Qt::Horizontal == orientation - && section < num_cols) - { - switch (section) - { - case 0: header = tr ("Frequency"); break; - case 1: header = tr ("Frequency (MHz)"); break; - } - } - else - { - header = QAbstractTableModel::headerData (section, orientation, role); - } - - return header; -} - -bool FrequencyList::impl::removeRows (int row, int count, QModelIndex const& parent) -{ - if (0 < count && (row + count) <= rowCount (parent)) - { - beginRemoveRows (parent, row, row + count - 1); - for (auto r = 0; r < count; ++r) - { - frequencies_.removeAt (row); - } - endRemoveRows (); - return true; - } - - return false; -} - -bool FrequencyList::impl::insertRows (int row, int count, QModelIndex const& parent) -{ - if (0 < count) - { - beginInsertRows (parent, row, row + count - 1); - for (auto r = 0; r < count; ++r) - { - frequencies_.insert (row, Frequency {}); - } - endInsertRows (); - return true; - } - - return false; -} - -QStringList FrequencyList::impl::mimeTypes () const -{ - QStringList types; - types << mime_type; - return types; -} - -QMimeData * FrequencyList::impl::mimeData (QModelIndexList const& items) const -{ - QMimeData * mime_data = new QMimeData {}; - QByteArray encoded_data; - QDataStream stream {&encoded_data, QIODevice::WriteOnly}; - - Q_FOREACH (auto const& item, items) - { - if (item.isValid ()) - { - stream << QString {data (item, Qt::DisplayRole).toString ()}; - } - } - - mime_data->setData (mime_type, encoded_data); - return mime_data; -} +#include "FrequencyList.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pimpl_impl.hpp" + +class FrequencyList::impl final + : public QAbstractTableModel +{ +public: + impl (Frequencies frequencies, QObject * parent) + : QAbstractTableModel {parent} + , frequencies_ {frequencies} + { + } + + Frequencies const& frequencies () const {return frequencies_;} + void assign (Frequencies); + QModelIndex add (Frequency); + +protected: + // Implement the QAbstractTableModel interface + int rowCount (QModelIndex const& parent = QModelIndex {}) const override; + int columnCount (QModelIndex const& parent = QModelIndex {}) const override; + Qt::ItemFlags flags (QModelIndex const& = QModelIndex {}) const override; + QVariant data (QModelIndex const&, int role = Qt::DisplayRole) const override; + bool setData (QModelIndex const&, QVariant const& value, int role = Qt::EditRole) override; + QVariant headerData (int section, Qt::Orientation, int = Qt::DisplayRole) const override; + bool removeRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override; + bool insertRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override; + QStringList mimeTypes () const override; + QMimeData * mimeData (QModelIndexList const&) const override; + +private: + static int constexpr num_cols {2}; + static auto constexpr mime_type ="application/wsjt.Frequencies"; + + Frequencies frequencies_; +}; + +FrequencyList::FrequencyList (QObject * parent) + : FrequencyList {{}, parent} +{ +} + +FrequencyList::FrequencyList (Frequencies frequencies, QObject * parent) + : QSortFilterProxyModel {parent} + , m_ {frequencies, parent} +{ + // setDynamicSortFilter (true); + setSourceModel (&*m_); + setSortRole (SortRole); +} + +FrequencyList::~FrequencyList () +{ +} + +FrequencyList& FrequencyList::operator = (Frequencies frequencies) +{ + m_->assign (frequencies); + return *this; +} + +auto FrequencyList::frequencies () -> Frequencies +{ + submit (); + return m_->frequencies (); +} + +QModelIndex FrequencyList::add (Frequency f) +{ + return mapFromSource (m_->add (f)); +} + +bool FrequencyList::remove (Frequency f) +{ + auto row = m_->frequencies ().indexOf (f); + + if (0 > row) + { + return false; + } + + return m_->removeRow (row); +} + +namespace +{ + bool row_is_higher (QModelIndex const& lhs, QModelIndex const& rhs) + { + return lhs.row () > rhs.row (); + } +} + +bool FrequencyList::removeDisjointRows (QModelIndexList rows) +{ + bool result {true}; + + // We must work with source model indexes because we don't want row + // removes to invalidate model indexes we haven't yet processed. We + // achieve that by processing them in decending row order. + for (int r = 0; r < rows.size (); ++r) + { + rows[r] = mapToSource (rows[r]); + } + + // reverse sort by row + qSort (rows.begin (), rows.end (), row_is_higher); + Q_FOREACH (auto index, rows) + { + if (result && !m_->removeRow (index.row ())) + { + result = false; + } + } + + return result; +} + + +void FrequencyList::impl::assign (Frequencies frequencies) +{ + beginResetModel (); + std::swap (frequencies_, frequencies); + endResetModel (); +} + +QModelIndex FrequencyList::impl::add (Frequency f) +{ + // Any Frequency that isn't in the list may be added + if (!frequencies_.contains (f)) + { + auto row = frequencies_.size (); + + beginInsertRows (QModelIndex {}, row, row); + frequencies_.append (f); + endInsertRows (); + + return index (row, 0); + } + + return QModelIndex {}; +} + +int FrequencyList::impl::rowCount (QModelIndex const& parent) const +{ + return parent.isValid () ? 0 : frequencies_.size (); +} + +int FrequencyList::impl::columnCount (QModelIndex const& parent) const +{ + return parent.isValid () ? 0 : num_cols; +} + +Qt::ItemFlags FrequencyList::impl::flags (QModelIndex const& index) const +{ + auto result = QAbstractTableModel::flags (index) | Qt::ItemIsDropEnabled; + + auto row = index.row (); + auto column = index.column (); + + if (index.isValid () + && row < frequencies_.size () + && column < num_cols) + { + switch (column) + { + case 0: + result |= Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; + break; + + case 1: + result |= Qt::ItemIsDragEnabled; + break; + } + } + + return result; +} + +QVariant FrequencyList::impl::data (QModelIndex const& index, int role) const +{ + QVariant item; + + auto row = index.row (); + auto column = index.column (); + + if (index.isValid () + && row < frequencies_.size () + && column < num_cols) + { + auto frequency = frequencies_.at (row); + + switch (column) + { + case 0: + switch (role) + { + case SortRole: + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::AccessibleTextRole: + item = frequency; + break; + + case Qt::ToolTipRole: + case Qt::AccessibleDescriptionRole: + item = tr ("Frequency"); + break; + + case Qt::TextAlignmentRole: + item = Qt::AlignRight + Qt::AlignVCenter; + break; + } + break; + + case 1: + switch (role) + { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::AccessibleTextRole: + item = static_cast (frequency / 1.e6); + break; + + case SortRole: // use the underlying Frequency value + item = frequency; + break; + + case Qt::ToolTipRole: + case Qt::AccessibleDescriptionRole: + item = tr ("Frequency MHz"); + break; + + case Qt::TextAlignmentRole: + item = Qt::AlignRight + Qt::AlignVCenter; + break; + } + break; + } + } + + return item; +} + +bool FrequencyList::impl::setData (QModelIndex const& model_index, QVariant const& value, int role) +{ + bool changed {false}; + + auto row = model_index.row (); + if (model_index.isValid () + && Qt::EditRole == role + && row < frequencies_.size () + && 0 == model_index.column () + && value.canConvert ()) + { + auto frequency = value.value (); + auto original_frequency = frequencies_.at (row); + if (frequency != original_frequency) + { + frequencies_.replace (row, frequency); + Q_EMIT dataChanged (model_index, index (model_index.row (), 1), QVector {} << role); + } + changed = true; + } + + return changed; +} + +QVariant FrequencyList::impl::headerData (int section, Qt::Orientation orientation, int role) const +{ + QVariant header; + + if (Qt::DisplayRole == role + && Qt::Horizontal == orientation + && section < num_cols) + { + switch (section) + { + case 0: header = tr ("Frequency"); break; + case 1: header = tr ("Frequency (MHz)"); break; + } + } + else + { + header = QAbstractTableModel::headerData (section, orientation, role); + } + + return header; +} + +bool FrequencyList::impl::removeRows (int row, int count, QModelIndex const& parent) +{ + if (0 < count && (row + count) <= rowCount (parent)) + { + beginRemoveRows (parent, row, row + count - 1); + for (auto r = 0; r < count; ++r) + { + frequencies_.removeAt (row); + } + endRemoveRows (); + return true; + } + + return false; +} + +bool FrequencyList::impl::insertRows (int row, int count, QModelIndex const& parent) +{ + if (0 < count) + { + beginInsertRows (parent, row, row + count - 1); + for (auto r = 0; r < count; ++r) + { + frequencies_.insert (row, Frequency {}); + } + endInsertRows (); + return true; + } + + return false; +} + +QStringList FrequencyList::impl::mimeTypes () const +{ + QStringList types; + types << mime_type; + return types; +} + +QMimeData * FrequencyList::impl::mimeData (QModelIndexList const& items) const +{ + QMimeData * mime_data = new QMimeData {}; + QByteArray encoded_data; + QDataStream stream {&encoded_data, QIODevice::WriteOnly}; + + Q_FOREACH (auto const& item, items) + { + if (item.isValid ()) + { + stream << QString {data (item, Qt::DisplayRole).toString ()}; + } + } + + mime_data->setData (mime_type, encoded_data); + return mime_data; +} diff --git a/FrequencyList.hpp b/FrequencyList.hpp index 1ff7427d7..5ad288444 100644 --- a/FrequencyList.hpp +++ b/FrequencyList.hpp @@ -1,59 +1,59 @@ -#ifndef FREQUENCY_LIST_HPP__ -#define FREQUENCY_LIST_HPP__ - -#include "pimpl_h.hpp" - -#include - -#include "Radio.hpp" - -// -// Class FrequencyList -// -// Encapsulates a collection of frequencies. The implementation is a -// table containing the list of Frequency type elements which is -// editable and a second column which is an immutable double -// representation of the corresponding Frequency item scaled to -// mega-Hertz. -// -// The list is ordered. -// -// Responsibilities -// -// Stores internally a list of unique frequencies. Provides methods -// to add and delete list elements. -// -// Collaborations -// -// Implements the QSortFilterProxyModel interface for a list of spot -// frequencies. -// -class FrequencyList final - : public QSortFilterProxyModel -{ -public: - using Frequency = Radio::Frequency; - using Frequencies = Radio::Frequencies; - - explicit FrequencyList (QObject * parent = nullptr); - explicit FrequencyList (Frequencies, QObject * parent = nullptr); - ~FrequencyList (); - - // Load and store contents - FrequencyList& operator = (Frequencies); - Frequencies frequencies () const; - - // Model API - QModelIndex add (Frequency); - bool remove (Frequency); - bool removeDisjointRows (QModelIndexList); - - // Custom roles. - static int constexpr SortRole = Qt::UserRole; - -private: - class impl; - pimpl m_; -}; - -#endif +#ifndef FREQUENCY_LIST_HPP__ +#define FREQUENCY_LIST_HPP__ + +#include "pimpl_h.hpp" + +#include + +#include "Radio.hpp" + +// +// Class FrequencyList +// +// Encapsulates a collection of frequencies. The implementation is a +// table containing the list of Frequency type elements which is +// editable and a second column which is an immutable double +// representation of the corresponding Frequency item scaled to +// mega-Hertz. +// +// The list is ordered. +// +// Responsibilities +// +// Stores internally a list of unique frequencies. Provides methods +// to add and delete list elements. +// +// Collaborations +// +// Implements the QSortFilterProxyModel interface for a list of spot +// frequencies. +// +class FrequencyList final + : public QSortFilterProxyModel +{ +public: + using Frequency = Radio::Frequency; + using Frequencies = Radio::Frequencies; + + explicit FrequencyList (QObject * parent = nullptr); + explicit FrequencyList (Frequencies, QObject * parent = nullptr); + ~FrequencyList (); + + // Load and store contents + FrequencyList& operator = (Frequencies); + Frequencies frequencies (); + + // Model API + QModelIndex add (Frequency); + bool remove (Frequency); + bool removeDisjointRows (QModelIndexList); + + // Custom roles. + static int constexpr SortRole = Qt::UserRole; + +private: + class impl; + pimpl m_; +}; + +#endif diff --git a/StationList.cpp b/StationList.cpp index 069dc0730..7eca4b049 100644 --- a/StationList.cpp +++ b/StationList.cpp @@ -1,551 +1,552 @@ -#include "StationList.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "pimpl_impl.hpp" - -#include "Bands.hpp" - -namespace -{ - struct init - { - init () - { - qRegisterMetaType ("Station"); - qRegisterMetaTypeStreamOperators ("Station"); - qRegisterMetaType ("Stations"); - qRegisterMetaTypeStreamOperators ("Stations"); - } - } static_initializer; -} - -#if !defined (QT_NO_DEBUG_STREAM) -QDebug operator << (QDebug debug, StationList::Station const& station) -{ - debug.nospace () << "Station(" - << station.band_name_ << ", " - << station.offset_ << ", " - << station.antenna_description_ << ')'; - return debug.space (); -} -#endif - -QDataStream& operator << (QDataStream& os, StationList::Station const& station) -{ - return os << station.band_name_ - << station.offset_ - << station.antenna_description_; -} - -QDataStream& operator >> (QDataStream& is, StationList::Station& station) -{ - return is >> station.band_name_ - >> station.offset_ - >> station.antenna_description_; -} - - -class StationList::impl final - : public QAbstractTableModel -{ -public: - impl (Bands const * bands, Stations stations, QObject * parent) - : QAbstractTableModel {parent} - , bands_ {bands} - , stations_ {stations} - { - } - - Stations const& stations () const {return stations_;} - void assign (Stations); - QModelIndex add (Station); - FrequencyDelta offset (Frequency) const; - -protected: - // Implement the QAbstractTableModel interface. - int rowCount (QModelIndex const& parent = QModelIndex {}) const override; - int columnCount (QModelIndex const& parent = QModelIndex {}) const override; - Qt::ItemFlags flags (QModelIndex const& = QModelIndex {}) const override; - QVariant data (QModelIndex const&, int role) const override; - QVariant headerData (int section, Qt::Orientation, int = Qt::DisplayRole) const override; - bool setData (QModelIndex const&, QVariant const& value, int role = Qt::EditRole) override; - bool removeRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override; - bool insertRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override; - Qt::DropActions supportedDropActions () const override; - QStringList mimeTypes () const override; - QMimeData * mimeData (QModelIndexList const&) const override; - bool dropMimeData (QMimeData const *, Qt::DropAction, int row, int column, QModelIndex const& parent) override; - -private: - // Helper method for band validation. - QModelIndex first_matching_band (QString const& band_name) const - { - // find first exact match in bands - auto matches = bands_->match (bands_->index (0, 0) - , Qt::DisplayRole - , band_name - , 1 - , Qt::MatchExactly); - return matches.isEmpty () ? QModelIndex {} : matches.first (); - } - - static int constexpr num_columns {3}; - static auto constexpr mime_type = "application/wsjt.antenna-descriptions"; - - Bands const * bands_; - Stations stations_; -}; - -StationList::StationList (Bands const * bands, QObject * parent) - : StationList {bands, {}, parent} -{ -} - -StationList::StationList (Bands const * bands, Stations stations, QObject * parent) - : QSortFilterProxyModel {parent} - , m_ {bands, stations, parent} -{ - // setDynamicSortFilter (true); - setSourceModel (&*m_); - setSortRole (SortRole); -} - -StationList::~StationList () -{ -} - -StationList& StationList::operator = (Stations stations) -{ - m_->assign (stations); - return *this; -} - -auto StationList::stations () const -> Stations -{ - return m_->stations (); -} - -QModelIndex StationList::add (Station s) -{ - return mapFromSource (m_->add (s)); -} - -bool StationList::remove (Station s) -{ - auto row = m_->stations ().indexOf (s); - - if (0 > row) - { - return false; - } - - return removeRow (row); -} - -namespace -{ - bool row_is_higher (QModelIndex const& lhs, QModelIndex const& rhs) - { - return lhs.row () > rhs.row (); - } -} - -bool StationList::removeDisjointRows (QModelIndexList rows) -{ - bool result {true}; - - // We must work with source model indexes because we don't want row - // removes to invalidate model indexes we haven't yet processed. We - // achieve that by processing them in decending row order. - for (int r = 0; r < rows.size (); ++r) - { - rows[r] = mapToSource (rows[r]); - } - - // reverse sort by row - qSort (rows.begin (), rows.end (), row_is_higher); - Q_FOREACH (auto index, rows) - { - if (result && !m_->removeRow (index.row ())) - { - result = false; - } - } - - return result; -} - -auto StationList::offset (Frequency f) const -> FrequencyDelta -{ - return m_->offset (f); -} - - -void StationList::impl::assign (Stations stations) -{ - beginResetModel (); - std::swap (stations_, stations); - endResetModel (); -} - -QModelIndex StationList::impl::add (Station s) -{ - // Any band that isn't in the list may be added - if (!stations_.contains (s)) - { - auto row = stations_.size (); - - beginInsertRows (QModelIndex {}, row, row); - stations_.append (s); - endInsertRows (); - - return index (row, 0); - } - - return QModelIndex {}; -} - -auto StationList::impl::offset (Frequency f) const -> FrequencyDelta -{ - // Lookup band for frequency - auto band_index = bands_->find (f); - if (band_index.isValid ()) - { - auto band_name = band_index.data ().toString (); - - // Lookup station for band - for (int i = 0; i < stations ().size (); ++i) - { - if (stations_[i].band_name_ == band_name) - { - return stations_[i].offset_; - } - } - } - - return 0; // no offset -} - -int StationList::impl::rowCount (QModelIndex const& parent) const -{ - return parent.isValid () ? 0 : stations_.size (); -} - -int StationList::impl::columnCount (QModelIndex const& parent) const -{ - return parent.isValid () ? 0 : num_columns; -} - -Qt::ItemFlags StationList::impl::flags (QModelIndex const& index) const -{ - auto result = QAbstractTableModel::flags (index); - - auto row = index.row (); - auto column = index.column (); - - if (index.isValid () - && row < stations_.size () - && column < num_columns) - { - if (2 == column) - { - result |= Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; - } - else - { - result |= Qt::ItemIsEditable | Qt::ItemIsDropEnabled; - } - } - else - { - result |= Qt::ItemIsDropEnabled; - } - - return result; -} - -QVariant StationList::impl::data (QModelIndex const& index, int role) const -{ - QVariant item; - - auto row = index.row (); - auto column = index.column (); - - if (index.isValid () - && row < stations_.size ()) - { - switch (column) - { - case 0: // band name - switch (role) - { - case SortRole: - { - // Lookup band. - auto band_index = first_matching_band (stations_.at (row).band_name_); - // Use the sort role value of the band. - item = band_index.data (Bands::SortRole); - } - break; - - case Qt::DisplayRole: - case Qt::EditRole: - case Qt::AccessibleTextRole: - item = stations_.at (row).band_name_; - break; - - case Qt::ToolTipRole: - case Qt::AccessibleDescriptionRole: - item = tr ("Band name"); - break; - - case Qt::TextAlignmentRole: - item = Qt::AlignHCenter + Qt::AlignVCenter; - break; - } - break; - - case 1: // frequency offset - { - auto frequency_offset = stations_.at (row).offset_; - switch (role) - { - case Qt::AccessibleTextRole: - item = frequency_offset; - break; - - case SortRole: - case Qt::DisplayRole: - case Qt::EditRole: - item = frequency_offset; - break; - - case Qt::ToolTipRole: - case Qt::AccessibleDescriptionRole: - item = tr ("Frequency offset"); - break; - - case Qt::TextAlignmentRole: - item = Qt::AlignRight + Qt::AlignVCenter; - break; - } - } - break; - - case 2: // antenna description - switch (role) - { - case SortRole: - case Qt::EditRole: - case Qt::DisplayRole: - case Qt::AccessibleTextRole: - item = stations_.at (row).antenna_description_; - break; - - case Qt::ToolTipRole: - case Qt::AccessibleDescriptionRole: - item = tr ("Antenna description"); - break; - - case Qt::TextAlignmentRole: - item = Qt::AlignLeft + Qt::AlignVCenter; - break; - } - break; - } - } - - return item; -} - -QVariant StationList::impl::headerData (int section, Qt::Orientation orientation, int role) const -{ - QVariant header; - - if (Qt::DisplayRole == role && Qt::Horizontal == orientation) - { - switch (section) - { - case 0: header = tr ("Band"); break; - case 1: header = tr ("Offset"); break; - case 2: header = tr ("Antenna Description"); break; - } - } - else - { - header = QAbstractTableModel::headerData (section, orientation, role); - } - - return header; -} - -bool StationList::impl::setData (QModelIndex const& model_index, QVariant const& value, int role) -{ - bool changed {false}; - - auto row = model_index.row (); - auto size = stations_.size (); - if (model_index.isValid () - && Qt::EditRole == role - && row < size) - { - QVector roles; - roles << role; - - switch (model_index.column ()) - { - case 0: - { - // Check if band name is valid. - auto band_index = first_matching_band (value.toString ()); - if (band_index.isValid ()) - { - stations_[row].band_name_ = band_index.data ().toString (); - Q_EMIT dataChanged (model_index, model_index, roles); - changed = true; - } - } - break; - - case 1: - { - stations_[row].offset_ = value.value (); - Q_EMIT dataChanged (model_index, model_index, roles); - changed = true; - } - break; - - case 2: - stations_[row].antenna_description_ = value.toString (); - Q_EMIT dataChanged (model_index, model_index, roles); - changed = true; - break; - } - } - - return changed; -} - -bool StationList::impl::removeRows (int row, int count, QModelIndex const& parent) -{ - if (0 < count && (row + count) <= rowCount (parent)) - { - beginRemoveRows (parent, row, row + count - 1); - for (auto r = 0; r < count; ++r) - { - stations_.removeAt (row); - } - endRemoveRows (); - return true; - } - - return false; -} - -bool StationList::impl::insertRows (int row, int count, QModelIndex const& parent) -{ - if (0 < count) - { - beginInsertRows (parent, row, row + count - 1); - for (auto r = 0; r < count; ++r) - { - stations_.insert (row, Station ()); - } - endInsertRows (); - return true; - } - - return false; -} - -Qt::DropActions StationList::impl::supportedDropActions () const -{ - return Qt::CopyAction | Qt::MoveAction; -} - -QStringList StationList::impl::mimeTypes () const -{ - QStringList types; - types << mime_type; - types << "application/wsjt.Frequencies"; - return types; -} - -QMimeData * StationList::impl::mimeData (QModelIndexList const& items) const -{ - QMimeData * mime_data = new QMimeData {}; - QByteArray encoded_data; - QDataStream stream {&encoded_data, QIODevice::WriteOnly}; - - Q_FOREACH (auto const& item, items) - { - if (item.isValid ()) - { - stream << QString {data (item, Qt::DisplayRole).toString ()}; - } - } - - mime_data->setData (mime_type, encoded_data); - return mime_data; -} - -bool StationList::impl::dropMimeData (QMimeData const * data, Qt::DropAction action, int /* row */, int /* column */, QModelIndex const& parent) -{ - if (Qt::IgnoreAction == action) - { - return true; - } - - if (parent.isValid () - && 2 == parent.column () - && data->hasFormat (mime_type)) - { - QByteArray encoded_data {data->data (mime_type)}; - QDataStream stream {&encoded_data, QIODevice::ReadOnly}; - auto dest_index = parent; - while (!stream.atEnd ()) - { - QString text; - stream >> text; - setData (dest_index, text); - dest_index = index (dest_index.row () + 1, dest_index.column (), QModelIndex {}); - } - return true; - } - else if (data->hasFormat ("application/wsjt.Frequencies")) - { - QByteArray encoded_data {data->data ("application/wsjt.Frequencies")}; - QDataStream stream {&encoded_data, QIODevice::ReadOnly}; - while (!stream.atEnd ()) - { - QString frequency_string; - stream >> frequency_string; - auto frequency = Radio::frequency (frequency_string, 0); - auto band_index = bands_->find (frequency); - if (stations_.cend () == std::find_if (stations_.cbegin () - , stations_.cend () - , [&band_index] (Station const& s) {return s.band_name_ == band_index.data ().toString ();})) - { - add (Station {band_index.data ().toString (), 0, QString {}}); - } - } - - return true; - } - - return false; -} +#include "StationList.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pimpl_impl.hpp" + +#include "Bands.hpp" + +namespace +{ + struct init + { + init () + { + qRegisterMetaType ("Station"); + qRegisterMetaTypeStreamOperators ("Station"); + qRegisterMetaType ("Stations"); + qRegisterMetaTypeStreamOperators ("Stations"); + } + } static_initializer; +} + +#if !defined (QT_NO_DEBUG_STREAM) +QDebug operator << (QDebug debug, StationList::Station const& station) +{ + debug.nospace () << "Station(" + << station.band_name_ << ", " + << station.offset_ << ", " + << station.antenna_description_ << ')'; + return debug.space (); +} +#endif + +QDataStream& operator << (QDataStream& os, StationList::Station const& station) +{ + return os << station.band_name_ + << station.offset_ + << station.antenna_description_; +} + +QDataStream& operator >> (QDataStream& is, StationList::Station& station) +{ + return is >> station.band_name_ + >> station.offset_ + >> station.antenna_description_; +} + + +class StationList::impl final + : public QAbstractTableModel +{ +public: + impl (Bands const * bands, Stations stations, QObject * parent) + : QAbstractTableModel {parent} + , bands_ {bands} + , stations_ {stations} + { + } + + Stations const& stations () const {return stations_;} + void assign (Stations); + QModelIndex add (Station); + FrequencyDelta offset (Frequency) const; + +protected: + // Implement the QAbstractTableModel interface. + int rowCount (QModelIndex const& parent = QModelIndex {}) const override; + int columnCount (QModelIndex const& parent = QModelIndex {}) const override; + Qt::ItemFlags flags (QModelIndex const& = QModelIndex {}) const override; + QVariant data (QModelIndex const&, int role) const override; + QVariant headerData (int section, Qt::Orientation, int = Qt::DisplayRole) const override; + bool setData (QModelIndex const&, QVariant const& value, int role = Qt::EditRole) override; + bool removeRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override; + bool insertRows (int row, int count, QModelIndex const& parent = QModelIndex {}) override; + Qt::DropActions supportedDropActions () const override; + QStringList mimeTypes () const override; + QMimeData * mimeData (QModelIndexList const&) const override; + bool dropMimeData (QMimeData const *, Qt::DropAction, int row, int column, QModelIndex const& parent) override; + +private: + // Helper method for band validation. + QModelIndex first_matching_band (QString const& band_name) const + { + // find first exact match in bands + auto matches = bands_->match (bands_->index (0, 0) + , Qt::DisplayRole + , band_name + , 1 + , Qt::MatchExactly); + return matches.isEmpty () ? QModelIndex {} : matches.first (); + } + + static int constexpr num_columns {3}; + static auto constexpr mime_type = "application/wsjt.antenna-descriptions"; + + Bands const * bands_; + Stations stations_; +}; + +StationList::StationList (Bands const * bands, QObject * parent) + : StationList {bands, {}, parent} +{ +} + +StationList::StationList (Bands const * bands, Stations stations, QObject * parent) + : QSortFilterProxyModel {parent} + , m_ {bands, stations, parent} +{ + // setDynamicSortFilter (true); + setSourceModel (&*m_); + setSortRole (SortRole); +} + +StationList::~StationList () +{ +} + +StationList& StationList::operator = (Stations stations) +{ + m_->assign (stations); + return *this; +} + +auto StationList::stations () -> Stations +{ + submit (); + return m_->stations (); +} + +QModelIndex StationList::add (Station s) +{ + return mapFromSource (m_->add (s)); +} + +bool StationList::remove (Station s) +{ + auto row = m_->stations ().indexOf (s); + + if (0 > row) + { + return false; + } + + return removeRow (row); +} + +namespace +{ + bool row_is_higher (QModelIndex const& lhs, QModelIndex const& rhs) + { + return lhs.row () > rhs.row (); + } +} + +bool StationList::removeDisjointRows (QModelIndexList rows) +{ + bool result {true}; + + // We must work with source model indexes because we don't want row + // removes to invalidate model indexes we haven't yet processed. We + // achieve that by processing them in decending row order. + for (int r = 0; r < rows.size (); ++r) + { + rows[r] = mapToSource (rows[r]); + } + + // reverse sort by row + qSort (rows.begin (), rows.end (), row_is_higher); + Q_FOREACH (auto index, rows) + { + if (result && !m_->removeRow (index.row ())) + { + result = false; + } + } + + return result; +} + +auto StationList::offset (Frequency f) const -> FrequencyDelta +{ + return m_->offset (f); +} + + +void StationList::impl::assign (Stations stations) +{ + beginResetModel (); + std::swap (stations_, stations); + endResetModel (); +} + +QModelIndex StationList::impl::add (Station s) +{ + // Any band that isn't in the list may be added + if (!stations_.contains (s)) + { + auto row = stations_.size (); + + beginInsertRows (QModelIndex {}, row, row); + stations_.append (s); + endInsertRows (); + + return index (row, 0); + } + + return QModelIndex {}; +} + +auto StationList::impl::offset (Frequency f) const -> FrequencyDelta +{ + // Lookup band for frequency + auto band_index = bands_->find (f); + if (band_index.isValid ()) + { + auto band_name = band_index.data ().toString (); + + // Lookup station for band + for (int i = 0; i < stations ().size (); ++i) + { + if (stations_[i].band_name_ == band_name) + { + return stations_[i].offset_; + } + } + } + + return 0; // no offset +} + +int StationList::impl::rowCount (QModelIndex const& parent) const +{ + return parent.isValid () ? 0 : stations_.size (); +} + +int StationList::impl::columnCount (QModelIndex const& parent) const +{ + return parent.isValid () ? 0 : num_columns; +} + +Qt::ItemFlags StationList::impl::flags (QModelIndex const& index) const +{ + auto result = QAbstractTableModel::flags (index); + + auto row = index.row (); + auto column = index.column (); + + if (index.isValid () + && row < stations_.size () + && column < num_columns) + { + if (2 == column) + { + result |= Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; + } + else + { + result |= Qt::ItemIsEditable | Qt::ItemIsDropEnabled; + } + } + else + { + result |= Qt::ItemIsDropEnabled; + } + + return result; +} + +QVariant StationList::impl::data (QModelIndex const& index, int role) const +{ + QVariant item; + + auto row = index.row (); + auto column = index.column (); + + if (index.isValid () + && row < stations_.size ()) + { + switch (column) + { + case 0: // band name + switch (role) + { + case SortRole: + { + // Lookup band. + auto band_index = first_matching_band (stations_.at (row).band_name_); + // Use the sort role value of the band. + item = band_index.data (Bands::SortRole); + } + break; + + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::AccessibleTextRole: + item = stations_.at (row).band_name_; + break; + + case Qt::ToolTipRole: + case Qt::AccessibleDescriptionRole: + item = tr ("Band name"); + break; + + case Qt::TextAlignmentRole: + item = Qt::AlignHCenter + Qt::AlignVCenter; + break; + } + break; + + case 1: // frequency offset + { + auto frequency_offset = stations_.at (row).offset_; + switch (role) + { + case Qt::AccessibleTextRole: + item = frequency_offset; + break; + + case SortRole: + case Qt::DisplayRole: + case Qt::EditRole: + item = frequency_offset; + break; + + case Qt::ToolTipRole: + case Qt::AccessibleDescriptionRole: + item = tr ("Frequency offset"); + break; + + case Qt::TextAlignmentRole: + item = Qt::AlignRight + Qt::AlignVCenter; + break; + } + } + break; + + case 2: // antenna description + switch (role) + { + case SortRole: + case Qt::EditRole: + case Qt::DisplayRole: + case Qt::AccessibleTextRole: + item = stations_.at (row).antenna_description_; + break; + + case Qt::ToolTipRole: + case Qt::AccessibleDescriptionRole: + item = tr ("Antenna description"); + break; + + case Qt::TextAlignmentRole: + item = Qt::AlignLeft + Qt::AlignVCenter; + break; + } + break; + } + } + + return item; +} + +QVariant StationList::impl::headerData (int section, Qt::Orientation orientation, int role) const +{ + QVariant header; + + if (Qt::DisplayRole == role && Qt::Horizontal == orientation) + { + switch (section) + { + case 0: header = tr ("Band"); break; + case 1: header = tr ("Offset"); break; + case 2: header = tr ("Antenna Description"); break; + } + } + else + { + header = QAbstractTableModel::headerData (section, orientation, role); + } + + return header; +} + +bool StationList::impl::setData (QModelIndex const& model_index, QVariant const& value, int role) +{ + bool changed {false}; + + auto row = model_index.row (); + auto size = stations_.size (); + if (model_index.isValid () + && Qt::EditRole == role + && row < size) + { + QVector roles; + roles << role; + + switch (model_index.column ()) + { + case 0: + { + // Check if band name is valid. + auto band_index = first_matching_band (value.toString ()); + if (band_index.isValid ()) + { + stations_[row].band_name_ = band_index.data ().toString (); + Q_EMIT dataChanged (model_index, model_index, roles); + changed = true; + } + } + break; + + case 1: + { + stations_[row].offset_ = value.value (); + Q_EMIT dataChanged (model_index, model_index, roles); + changed = true; + } + break; + + case 2: + stations_[row].antenna_description_ = value.toString (); + Q_EMIT dataChanged (model_index, model_index, roles); + changed = true; + break; + } + } + + return changed; +} + +bool StationList::impl::removeRows (int row, int count, QModelIndex const& parent) +{ + if (0 < count && (row + count) <= rowCount (parent)) + { + beginRemoveRows (parent, row, row + count - 1); + for (auto r = 0; r < count; ++r) + { + stations_.removeAt (row); + } + endRemoveRows (); + return true; + } + + return false; +} + +bool StationList::impl::insertRows (int row, int count, QModelIndex const& parent) +{ + if (0 < count) + { + beginInsertRows (parent, row, row + count - 1); + for (auto r = 0; r < count; ++r) + { + stations_.insert (row, Station ()); + } + endInsertRows (); + return true; + } + + return false; +} + +Qt::DropActions StationList::impl::supportedDropActions () const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList StationList::impl::mimeTypes () const +{ + QStringList types; + types << mime_type; + types << "application/wsjt.Frequencies"; + return types; +} + +QMimeData * StationList::impl::mimeData (QModelIndexList const& items) const +{ + QMimeData * mime_data = new QMimeData {}; + QByteArray encoded_data; + QDataStream stream {&encoded_data, QIODevice::WriteOnly}; + + Q_FOREACH (auto const& item, items) + { + if (item.isValid ()) + { + stream << QString {data (item, Qt::DisplayRole).toString ()}; + } + } + + mime_data->setData (mime_type, encoded_data); + return mime_data; +} + +bool StationList::impl::dropMimeData (QMimeData const * data, Qt::DropAction action, int /* row */, int /* column */, QModelIndex const& parent) +{ + if (Qt::IgnoreAction == action) + { + return true; + } + + if (parent.isValid () + && 2 == parent.column () + && data->hasFormat (mime_type)) + { + QByteArray encoded_data {data->data (mime_type)}; + QDataStream stream {&encoded_data, QIODevice::ReadOnly}; + auto dest_index = parent; + while (!stream.atEnd ()) + { + QString text; + stream >> text; + setData (dest_index, text); + dest_index = index (dest_index.row () + 1, dest_index.column (), QModelIndex {}); + } + return true; + } + else if (data->hasFormat ("application/wsjt.Frequencies")) + { + QByteArray encoded_data {data->data ("application/wsjt.Frequencies")}; + QDataStream stream {&encoded_data, QIODevice::ReadOnly}; + while (!stream.atEnd ()) + { + QString frequency_string; + stream >> frequency_string; + auto frequency = Radio::frequency (frequency_string, 0); + auto band_index = bands_->find (frequency); + if (stations_.cend () == std::find_if (stations_.cbegin () + , stations_.cend () + , [&band_index] (Station const& s) {return s.band_name_ == band_index.data ().toString ();})) + { + add (Station {band_index.data ().toString (), 0, QString {}}); + } + } + + return true; + } + + return false; +} diff --git a/StationList.hpp b/StationList.hpp index 389719629..f869953fe 100644 --- a/StationList.hpp +++ b/StationList.hpp @@ -1,96 +1,102 @@ -#ifndef STATION_LIST_HPP__ -#define STATION_LIST_HPP__ - -#include -#include -#include - -#include "pimpl_h.hpp" - -#include "Radio.hpp" - -class Bands; - -// -// Class StationList -// -// Encapsulates information about a collection of unique operating -// stations per band. The implementation is a table model with the -// first column being the unique (within the table rows) band name -// and, the second the frequency offset for transverter usage and, -// the third the antenna description. All are editable. -// -// Responsibilities -// -// Stores internally an unordered table of bands. -// -// If an ordered representaion is required then wrapping with an -// appropriate proxy model is sufficient -// e.g. QSortFilterProxyModel. A custom SortRole role is provided for -// the band name column which returns a numeric value (Bands lower -// frequency limit) which gives a strict frequency ordering by band. -// -// Collaborations -// -// Implements the QAbstractTableModel interface for a grid of bands -// with offset frequencies and antenna descriptions. -// -// Uses the QAbstractItemModel interface of the bands model to lookup -// band information. -// -class StationList final - : public QSortFilterProxyModel -{ -public: - using Frequency = Radio::Frequency; - using FrequencyDelta = Radio::FrequencyDelta; - - // - // Struct Station - // - // Aggregation of fields that describe a radio station on a band. - // - struct Station - { - QString band_name_; - FrequencyDelta offset_; - QString antenna_description_; - }; - - using Stations = QList; - - explicit StationList (Bands const * bands, QObject * parent = nullptr); - explicit StationList (Bands const * bands, Stations, QObject * parent = nullptr); - ~StationList (); - - // Load and store contents. - StationList& operator = (Stations); - Stations stations () const; - - // - // Model API - // - QModelIndex add (Station); // Add a new Station - bool remove (Station); // Remove a Station - bool removeDisjointRows (QModelIndexList); // Remove one or more stations - FrequencyDelta offset (Frequency) const; // Return the offset to be used for a Frequency - - // Custom sort role. - static int constexpr SortRole = Qt::UserRole; - -private: - class impl; - pimpl m_; -}; - -// Station equivalence is based on band name alone. -inline -bool operator == (StationList::Station const& lhs, StationList::Station const& rhs) -{ - return lhs.band_name_ == rhs.band_name_; -} - -Q_DECLARE_METATYPE (StationList::Station); -Q_DECLARE_METATYPE (StationList::Stations); - -#endif +#ifndef STATION_LIST_HPP__ +#define STATION_LIST_HPP__ + +#include +#include +#include + +#include "pimpl_h.hpp" + +#include "Radio.hpp" + +class Bands; + +// +// Class StationList +// +// Encapsulates information about a collection of unique operating +// stations per band. The implementation is a table model with the +// first column being the unique (within the table rows) band name +// and, the second the frequency offset for transverter usage and, +// the third the antenna description. All are editable. +// +// Responsibilities +// +// Stores internally an unordered table of bands. +// +// If an ordered representaion is required then wrapping with an +// appropriate proxy model is sufficient +// e.g. QSortFilterProxyModel. A custom SortRole role is provided for +// the band name column which returns a numeric value (Bands lower +// frequency limit) which gives a strict frequency ordering by band. +// +// Collaborations +// +// Implements the QAbstractTableModel interface for a grid of bands +// with offset frequencies and antenna descriptions. +// +// Uses the QAbstractItemModel interface of the bands model to lookup +// band information. +// +class StationList final + : public QSortFilterProxyModel +{ +public: + using Frequency = Radio::Frequency; + using FrequencyDelta = Radio::FrequencyDelta; + + // + // Struct Station + // + // Aggregation of fields that describe a radio station on a band. + // + struct Station + { + QString band_name_; + FrequencyDelta offset_; + QString antenna_description_; + }; + + using Stations = QList; + + explicit StationList (Bands const * bands, QObject * parent = nullptr); + explicit StationList (Bands const * bands, Stations, QObject * parent = nullptr); + ~StationList (); + + // Load and store contents. + StationList& operator = (Stations); + Stations stations (); + + // + // Model API + // + QModelIndex add (Station); // Add a new Station + bool remove (Station); // Remove a Station + bool removeDisjointRows (QModelIndexList); // Remove one or more stations + FrequencyDelta offset (Frequency) const; // Return the offset to be used for a Frequency + + // Custom sort role. + static int constexpr SortRole = Qt::UserRole; + +private: + class impl; + pimpl m_; +}; + +// Station equivalence +inline +bool operator == (StationList::Station const& lhs, StationList::Station const& rhs) +{ + return lhs.band_name_ == rhs.band_name_ + && lhs.offset_ == rhs.offset_ + && lhs.antenna_description_ == rhs.antenna_description_; +} + +Q_DECLARE_METATYPE (StationList::Station); +Q_DECLARE_METATYPE (StationList::Stations); + +#if !defined (QT_NO_DEBUG_STREAM) +QDebug operator << (QDebug debug, StationList::Station const&); +#endif + +#endif