mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
Rename all header files with .h extension Fix function declaration in HamlibTransceiver.h that overrode member function but was not marked override Remove unused variable in Modulator.cpp Remove unused files from source tree Remove duplicate LazyFillComboBox files Reformat codebase to LLVM C++ standard
584 lines
18 KiB
C++
584 lines
18 KiB
C++
#include "StationList.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <utility>
|
|
|
|
#include <QAbstractTableModel>
|
|
#include <QByteArray>
|
|
#include <QDataStream>
|
|
#include <QDebug>
|
|
#include <QDebugStateSaver>
|
|
#include <QMetaType>
|
|
#include <QMimeData>
|
|
#include <QObject>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#include <QTimeZone>
|
|
#include <QVector>
|
|
|
|
#include "DriftingDateTime.h"
|
|
|
|
#include "JS8_Include/pimpl_impl.h"
|
|
|
|
#include "FrequencyList.h"
|
|
#include "JS8_Main/Bands.h"
|
|
#include "Radio.h"
|
|
|
|
#if !defined(QT_NO_DEBUG_STREAM)
|
|
QDebug operator<<(QDebug debug, StationList::Station const &station) {
|
|
QDebugStateSaver saver{debug};
|
|
debug.nospace() << "Station(" << station.band_name_ << ", "
|
|
<< station.frequency_ << ", " << station.switch_at_ << ", "
|
|
<< station.switch_until_ << ", " << station.description_
|
|
<< ')';
|
|
return debug;
|
|
}
|
|
#endif
|
|
|
|
QDataStream &operator<<(QDataStream &os, StationList::Station const &station) {
|
|
return os << station.band_name_ << station.frequency_ << station.switch_at_
|
|
<< station.switch_until_ << station.description_;
|
|
}
|
|
|
|
QDataStream &operator>>(QDataStream &is, StationList::Station &station) {
|
|
is >> station.band_name_ >> station.frequency_ >> station.switch_at_ >>
|
|
station.switch_until_ >> station.description_;
|
|
|
|
// Set the timezones to UTC. Will be local time when read from the saved
|
|
// configuration
|
|
station.switch_at_.setTimeZone(QTimeZone::utc());
|
|
station.switch_until_.setTimeZone(QTimeZone::utc());
|
|
|
|
// Convert to UTC. This is necessary because the TimeSpec needs to be
|
|
// Qt::UTC
|
|
station.switch_at_ = station.switch_at_.toUTC();
|
|
station.switch_until_ = station.switch_until_.toUTC();
|
|
|
|
return is;
|
|
}
|
|
|
|
class StationList::impl final : public QAbstractTableModel {
|
|
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{5};
|
|
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} {
|
|
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);
|
|
}
|
|
|
|
namespace {
|
|
bool row_is_higher(QModelIndex const &lhs, QModelIndex const &rhs) {
|
|
return lhs.row() > rhs.row();
|
|
}
|
|
} // namespace
|
|
|
|
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(), row_is_higher);
|
|
Q_FOREACH (auto index, rows) {
|
|
if (result && !m_->removeRow(index.row())) {
|
|
result = false;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
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{};
|
|
}
|
|
|
|
#if 0
|
|
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].frequency_;
|
|
}
|
|
}
|
|
}
|
|
return 0; // no offset
|
|
}
|
|
#endif
|
|
|
|
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 (band_column == column) {
|
|
result |= Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
|
|
|
|
} else 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::AlignCenter;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case frequency_column: {
|
|
auto frequency_offset = stations_.at(row).frequency_;
|
|
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");
|
|
break;
|
|
|
|
case Qt::TextAlignmentRole:
|
|
item = static_cast<Qt::Alignment::Int>(Qt::AlignRight |
|
|
Qt::AlignVCenter);
|
|
break;
|
|
}
|
|
} break;
|
|
|
|
case switch_at_column:
|
|
switch (role) {
|
|
case SortRole:
|
|
case Qt::EditRole:
|
|
case Qt::DisplayRole:
|
|
case Qt::AccessibleTextRole:
|
|
item = stations_.at(row).switch_at_.time().toString("hh:mm");
|
|
break;
|
|
|
|
case Qt::ToolTipRole:
|
|
case Qt::AccessibleDescriptionRole:
|
|
item = tr("Switch at this time");
|
|
break;
|
|
|
|
case Qt::TextAlignmentRole:
|
|
item = Qt::AlignCenter;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case switch_until_column:
|
|
switch (role) {
|
|
case SortRole:
|
|
case Qt::EditRole:
|
|
case Qt::DisplayRole:
|
|
case Qt::AccessibleTextRole:
|
|
item = stations_.at(row).switch_until_.time().toString("hh:mm");
|
|
break;
|
|
|
|
case Qt::ToolTipRole:
|
|
case Qt::AccessibleDescriptionRole:
|
|
item = tr("Switch until this time\nFrequency will not return "
|
|
"at end time.\nEnter a new line to return to "
|
|
"original or other frequency.");
|
|
break;
|
|
|
|
case Qt::TextAlignmentRole:
|
|
item = Qt::AlignCenter;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case description_column:
|
|
switch (role) {
|
|
case SortRole:
|
|
case Qt::EditRole:
|
|
case Qt::DisplayRole:
|
|
case Qt::AccessibleTextRole:
|
|
item = stations_.at(row).description_;
|
|
break;
|
|
|
|
case Qt::ToolTipRole:
|
|
case Qt::AccessibleDescriptionRole:
|
|
item = tr("Antenna description");
|
|
break;
|
|
|
|
case Qt::TextAlignmentRole:
|
|
item = static_cast<Qt::Alignment::Int>(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 frequency_column:
|
|
header = tr("Freq. (MHz)");
|
|
break;
|
|
case switch_at_column:
|
|
header = tr("Switch at (UTC)");
|
|
break;
|
|
case switch_until_column:
|
|
header = tr("Until (UTC)");
|
|
break;
|
|
case description_column:
|
|
header = tr("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 frequency_column: {
|
|
if (value.canConvert<Frequency>()) {
|
|
Frequency offset{qvariant_cast<Radio::Frequency>(value)};
|
|
|
|
if (offset == 0) {
|
|
Frequency l;
|
|
Frequency h;
|
|
if (bands_->findFreq(stations_[row].band_name_, &l, &h)) {
|
|
offset = l;
|
|
}
|
|
}
|
|
|
|
if (offset != stations_[row].frequency_) {
|
|
stations_[row].frequency_ = offset;
|
|
Q_EMIT dataChanged(model_index, model_index, roles);
|
|
|
|
auto band = bands_->find(offset);
|
|
if (band != stations_[row].band_name_) {
|
|
stations_[row].band_name_ = band;
|
|
auto band_index = model_index.model()->index(
|
|
row, band_column, model_index);
|
|
Q_EMIT dataChanged(band_index, band_index, roles);
|
|
}
|
|
changed = true;
|
|
}
|
|
}
|
|
} break;
|
|
case switch_at_column: {
|
|
QString s = value.toString();
|
|
if (s.length() < 5) {
|
|
s = QString("0").repeated(5 - s.length()) + s;
|
|
}
|
|
auto t = QTime::fromString(s);
|
|
auto at = QDateTime(QDate(2000, 1, 1), t, QTimeZone::utc());
|
|
auto until = stations_[row].switch_until_;
|
|
stations_[row].switch_at_ = at;
|
|
stations_[row].switch_until_ = until;
|
|
|
|
Q_EMIT dataChanged(model_index, model_index, roles);
|
|
|
|
auto switch_until_index = model_index.model()->index(
|
|
row, switch_until_column, model_index);
|
|
Q_EMIT dataChanged(switch_until_index, switch_until_index, roles);
|
|
|
|
changed = true;
|
|
break;
|
|
}
|
|
case switch_until_column: {
|
|
QString s = value.toString();
|
|
if (s.length() < 5) {
|
|
s = QString("0").repeated(5 - s.length()) + s;
|
|
}
|
|
auto t = QTime::fromString(s);
|
|
auto until = QDateTime(QDate(2000, 1, 1), t, QTimeZone::utc());
|
|
auto at = stations_[row].switch_at_;
|
|
stations_[row].switch_at_ = at;
|
|
stations_[row].switch_until_ = until;
|
|
|
|
Q_EMIT dataChanged(model_index, model_index, roles);
|
|
|
|
auto switch_at_index =
|
|
model_index.model()->index(row, switch_at_column, model_index);
|
|
Q_EMIT dataChanged(switch_at_index, switch_at_index, roles);
|
|
|
|
changed = true;
|
|
break;
|
|
}
|
|
case description_column:
|
|
stations_[row].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::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, DriftingDateTime::currentDateTimeUtc(),
|
|
DriftingDateTime::currentDateTimeUtc(), QString{}});
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|