mirror of
				https://github.com/saitohirga/WSJT-X.git
				synced 2025-10-29 20:10:28 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			545 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			545 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include "StationList.hpp"
 | |
| 
 | |
| #include <utility>
 | |
| #include <algorithm>
 | |
| #include <cmath>
 | |
| 
 | |
| #include <QMetaType>
 | |
| #include <QAbstractTableModel>
 | |
| #include <QObject>
 | |
| #include <QString>
 | |
| #include <QVector>
 | |
| #include <QStringList>
 | |
| #include <QMimeData>
 | |
| #include <QDataStream>
 | |
| #include <QByteArray>
 | |
| #include <QTextStream>
 | |
| #include <QDebug>
 | |
| #include <QDebugStateSaver>
 | |
| 
 | |
| #include "pimpl_impl.hpp"
 | |
| 
 | |
| #include "Radio.hpp"
 | |
| #include "Bands.hpp"
 | |
| #include "FrequencyList.hpp"
 | |
| 
 | |
| #if !defined (QT_NO_DEBUG_STREAM)
 | |
| QDebug operator << (QDebug debug, StationList::Station const& station)
 | |
| {
 | |
|   QDebugStateSaver saver {debug};
 | |
|   return debug.nospace () << station.toString ();
 | |
| }
 | |
| #endif
 | |
| 
 | |
| QString StationList::Station::toString () const
 | |
| {
 | |
|   QString string;
 | |
|   QTextStream ots {&string};
 | |
|   ots << "Station("
 | |
|       << band_name_ << ", "
 | |
|       << Radio::frequency_MHz_string (offset_) << ", "
 | |
|       << antenna_description_ << ')';
 | |
|   return string;
 | |
| }
 | |
| 
 | |
| 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
 | |
| {
 | |
|   Q_OBJECT
 | |
| 
 | |
| public:
 | |
|   impl (Bands const * bands, Stations stations, QObject * parent)
 | |
|     : QAbstractTableModel {parent}
 | |
|     , bands_ {bands}
 | |
|     , stations_ {stations}
 | |
|   {
 | |
|   }
 | |
| 
 | |
|   Stations station_list (Stations);
 | |
|   QModelIndex add (Station);
 | |
|   FrequencyDelta offset (Frequency) const;
 | |
| 
 | |
|   // 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;
 | |
| 
 | |
|   // 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_;
 | |
| };
 | |
| 
 | |
| #include "StationList.moc"
 | |
| #include "moc_StationList.cpp"
 | |
| 
 | |
| 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}
 | |
| {
 | |
|   setSourceModel (&*m_);
 | |
|   setSortRole (SortRole);
 | |
| }
 | |
| 
 | |
| StationList::~StationList ()
 | |
| {
 | |
| }
 | |
| 
 | |
| auto StationList::station_list (Stations stations) -> Stations
 | |
| {
 | |
|   return m_->station_list (stations);
 | |
| }
 | |
| 
 | |
| auto StationList::station_list () const -> Stations const&
 | |
| {
 | |
|   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);
 | |
| }
 | |
| 
 | |
| 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
 | |
|   std::sort (rows.begin (), rows.end (), [] (QModelIndex const& lhs, QModelIndex const& rhs)
 | |
|              {
 | |
|                return rhs.row () < lhs.row (); // reverse row ordering
 | |
|              });
 | |
|   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);
 | |
| }
 | |
| 
 | |
| 
 | |
| auto StationList::impl::station_list (Stations stations) -> Stations
 | |
| {
 | |
|   beginResetModel ();
 | |
|   std::swap (stations_, stations);
 | |
|   endResetModel ();
 | |
|   return stations;
 | |
| }
 | |
| 
 | |
| 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 const& band = bands_->find (f);
 | |
|   if (!band.isEmpty ())
 | |
|     {
 | |
|       // Lookup station for band
 | |
|       for (int i = 0; i < stations_.size (); ++i)
 | |
|         {
 | |
|           if (stations_[i].band_name_ == band)
 | |
|             {
 | |
|               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 (description_column == 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 band_column:
 | |
|           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 offset_column:
 | |
|           {
 | |
|             auto frequency_offset = stations_.at (row).offset_;
 | |
|             switch (role)
 | |
|               {
 | |
|               case SortRole:
 | |
|               case Qt::EditRole:
 | |
|               case Qt::AccessibleTextRole:
 | |
|                 item = frequency_offset;
 | |
|                 break;
 | |
| 
 | |
|               case Qt::DisplayRole:
 | |
|                 item = Radio::pretty_frequency_MHz_string (frequency_offset) + " MHz";
 | |
|                 break;
 | |
| 
 | |
|               case Qt::ToolTipRole:
 | |
|               case Qt::AccessibleDescriptionRole:
 | |
|                 item = tr ("Frequency offset");
 | |
|                 break;
 | |
| 
 | |
|               case Qt::TextAlignmentRole:
 | |
|                 item = Qt::AlignRight + Qt::AlignVCenter;
 | |
|                 break;
 | |
|               }
 | |
|           }
 | |
|           break;
 | |
| 
 | |
|         case description_column:
 | |
|           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 band_column: header = tr ("Band"); break;
 | |
|         case offset_column: header = tr ("Offset"); break;
 | |
|         case description_column: 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<int> roles;
 | |
|       roles << role;
 | |
| 
 | |
|       switch (model_index.column ())
 | |
|         {
 | |
|         case band_column:
 | |
|           {
 | |
|             // 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 offset_column:
 | |
|           {
 | |
|             if (value.canConvert<FrequencyDelta> ())
 | |
|               {
 | |
|                 FrequencyDelta offset {qvariant_cast<Radio::FrequencyDelta> (value)};
 | |
|                 if (offset != stations_[row].offset_)
 | |
|                   {
 | |
|                     stations_[row].offset_ = offset;
 | |
|                     Q_EMIT dataChanged (model_index, model_index, roles);
 | |
|                     changed = true;
 | |
|                   }
 | |
|               }
 | |
|           }
 | |
|           break;
 | |
| 
 | |
|         case description_column:
 | |
|           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 ()
 | |
|       && description_column == 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 ())
 | |
|         {
 | |
|           FrequencyList_v2_101::Item item;
 | |
|           stream >> item;
 | |
|           auto const& band = bands_->find (item.frequency_);
 | |
|           if (stations_.cend () == std::find_if (stations_.cbegin ()
 | |
|                                                  , stations_.cend ()
 | |
|                                                  , [&band] (Station const& s) {return s.band_name_ == band;}))
 | |
|             {
 | |
|               // not found so add it
 | |
|               add (Station {band, 0, QString {}});
 | |
|             }
 | |
|         }
 | |
|       return true;
 | |
|     }
 | |
|   return false;
 | |
| }
 |