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