code:
#include "MessageServer.hpp"

#include <stdexcept>

#include <QUdpSocket>
#include <QString>
#include <QTimer>
#include <QHash>

#include "Radio.hpp"
#include "NetworkMessage.hpp"
#include "qt_helpers.hpp"

#include "pimpl_impl.hpp"

#include "moc_MessageServer.cpp"

class MessageServer::impl
  : public QUdpSocket
{
  Q_OBJECT;

public:
  impl (MessageServer * self, QString const& version, QString const& revision)
    : self_ {self}
    , version_ {version}
    , revision_ {revision}
    , port_ {0u}
    , clock_ {new QTimer {this}}
  {
    // register the required types with Qt
    Radio::register_types ();

    connect (this, &QIODevice::readyRead, this, &MessageServer::impl::pending_datagrams);
    connect (this, static_cast<void (impl::*) (SocketError)> (&impl::error)
             , [this] (SocketError /* e */)
             {
               Q_EMIT self_->error (errorString ());
             });
    connect (clock_, &QTimer::timeout, this, &impl::tick);
    clock_->start (NetworkMessage::pulse * 1000);
  }

  enum StreamStatus {Fail, Short, OK};

  void leave_multicast_group ();
  void join_multicast_group ();
  void parse_message (QHostAddress const& sender, port_type sender_port, QByteArray const& msg);
  void tick ();
  void pending_datagrams ();
  StreamStatus check_status (QDataStream const&) const;
  void send_message (QDataStream const& out, QByteArray const& message, QHostAddress const& address, port_type port)
  {
      if (OK == check_status (out))
        {
          writeDatagram (message, address, port);
        }
      else
        {
          Q_EMIT self_->error ("Error creating UDP message");
        }
  }

  MessageServer * self_;
  QString version_;
  QString revision_;
  port_type port_;
  QHostAddress multicast_group_address_;
  static BindMode constexpr bind_mode_ = ShareAddress | ReuseAddressHint;
  struct Client
  {
    Client () = default;
    Client (QHostAddress const& sender_address, port_type const& sender_port)
      : sender_address_ {sender_address}
      , sender_port_ {sender_port}
      , negotiated_schema_number_ {2} // not 1 because it's broken
      , last_activity_ {QDateTime::currentDateTime ()}
    {
    }
    Client (Client const&) = default;
    Client& operator= (Client const&) = default;

    QHostAddress sender_address_;
    port_type sender_port_;
    quint32 negotiated_schema_number_;
    QDateTime last_activity_;
  };
  QHash<QString, Client> clients_; // maps id to Client
  QTimer * clock_;
};

MessageServer::impl::BindMode constexpr MessageServer::impl::bind_mode_;

#include "MessageServer.moc"

void MessageServer::impl::leave_multicast_group ()
{
  if (!multicast_group_address_.isNull () && BoundState == state ())
    {
      leaveMulticastGroup (multicast_group_address_);
    }
}

void MessageServer::impl::join_multicast_group ()
{
  if (BoundState == state ()
      && !multicast_group_address_.isNull ())
    {
      if (IPv4Protocol == multicast_group_address_.protocol ()
          && IPv4Protocol != localAddress ().protocol ())
        {
          close ();
          bind (QHostAddress::AnyIPv4, port_, bind_mode_);
        }
      if (!joinMulticastGroup (multicast_group_address_))
        {
          multicast_group_address_.clear ();
        }
    }
}

void MessageServer::impl::pending_datagrams ()
{
  while (hasPendingDatagrams ())
    {
      QByteArray datagram;
      datagram.resize (pendingDatagramSize ());
      QHostAddress sender_address;
      port_type sender_port;
      if (0 <= readDatagram (datagram.data (), datagram.size (), &sender_address, &sender_port))
        {
          parse_message (sender_address, sender_port, datagram);
        }
    }
}

void MessageServer::impl::parse_message (QHostAddress const& sender, port_type sender_port, QByteArray const& msg)
{
  try
    {
      //
      // message format is described in NetworkMessage.hpp
      //
      NetworkMessage::Reader in {msg};

      auto id = in.id ();
      if (OK == check_status (in))
        {
          if (!clients_.contains (id))
            {
              auto& client = (clients_[id] = {sender, sender_port});
              QByteArray client_version;
              QByteArray client_revision;

              if (NetworkMessage::Heartbeat == in.type ())
                {
                  // negotiate a working schema number
                  in >> client.negotiated_schema_number_;
                  if (OK == check_status (in))
                    {
                      auto sn = NetworkMessage::Builder::schema_number;
                      client.negotiated_schema_number_ = std::min (sn, client.negotiated_schema_number_);

                      // reply to the new client informing it of the
                      // negotiated schema number
                      QByteArray message;
                      NetworkMessage::Builder hb {&message, NetworkMessage::Heartbeat, id, client.negotiated_schema_number_};
                      hb << NetworkMessage::Builder::schema_number // maximum schema number accepted
                         << version_.toUtf8 () << revision_.toUtf8 ();
                      if (impl::OK == check_status (hb))
                        {
                          writeDatagram (message, client.sender_address_, client.sender_port_);
                        }
                      else
                        {
                          Q_EMIT self_->error ("Error creating UDP message");
                        }
                    }
                  // we don't care if this fails to read
                  in >> client_version >> client_revision;
                }
              Q_EMIT self_->client_opened (id, QString::fromUtf8 (client_version),
                                           QString::fromUtf8 (client_revision));
            }
          clients_[id].last_activity_ = QDateTime::currentDateTime ();

          //
          // message format is described in NetworkMessage.hpp
          //
          switch (in.type ())
            {
            case NetworkMessage::Heartbeat:
              //nothing to do here as time out handling deals with lifetime
              break;

            case NetworkMessage::Clear:
              Q_EMIT self_->clear_decodes (id);
              break;

            case NetworkMessage::Status:
              {
                // unpack message
                Frequency f;
                QByteArray mode;
                QByteArray dx_call;
                QByteArray report;
                QByteArray tx_mode;
                bool tx_enabled {false};
                bool transmitting {false};
                bool decoding {false};
                qint32 rx_df {-1};
                qint32 tx_df {-1};
                QByteArray de_call;
                QByteArray de_grid;
                QByteArray dx_grid;
                bool watchdog_timeout {false};
                QByteArray sub_mode;
                bool fast_mode {false};
                in >> f >> mode >> dx_call >> report >> tx_mode >> tx_enabled >> transmitting >> decoding
                   >> rx_df >> tx_df >> de_call >> de_grid >> dx_grid >> watchdog_timeout >> sub_mode
                   >> fast_mode;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->status_update (id, f, QString::fromUtf8 (mode), QString::fromUtf8 (dx_call)
                                                 , QString::fromUtf8 (report), QString::fromUtf8 (tx_mode)
                                                 , tx_enabled, transmitting, decoding, rx_df, tx_df
                                                 , QString::fromUtf8 (de_call), QString::fromUtf8 (de_grid)
                                                 , QString::fromUtf8 (dx_grid), watchdog_timeout
                                                 , QString::fromUtf8 (sub_mode), fast_mode);
                  }
              }
              break;

            case NetworkMessage::Decode:
              {
                // unpack message
                bool is_new {true};
                QTime time;
                qint32 snr;
                float delta_time;
                quint32 delta_frequency;
                QByteArray mode;
                QByteArray message;
                bool low_confidence {false};
                bool off_air {false};
                in >> is_new >> time >> snr >> delta_time >> delta_frequency >> mode
                   >> message >> low_confidence >> off_air;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->decode (is_new, id, time, snr, delta_time, delta_frequency
                                          , QString::fromUtf8 (mode), QString::fromUtf8 (message)
                                          , low_confidence, off_air);
                  }
              }
              break;

            case NetworkMessage::WSPRDecode:
              {
                // unpack message
                bool is_new {true};
                QTime time;
                qint32 snr;
                float delta_time;
                Frequency frequency;
                qint32 drift;
                QByteArray callsign;
                QByteArray grid;
                qint32 power;
                bool off_air {false};
                in >> is_new >> time >> snr >> delta_time >> frequency >> drift >> callsign >> grid >> power
                   >> off_air;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->WSPR_decode (is_new, id, time, snr, delta_time, frequency, drift
                                               , QString::fromUtf8 (callsign), QString::fromUtf8 (grid)
                                               , power, off_air);
                  }
              }
              break;

            case NetworkMessage::QSOLogged:
              {
                QDateTime time_off;
                QByteArray dx_call;
                QByteArray dx_grid;
                Frequency dial_frequency;
                QByteArray mode;
                QByteArray report_sent;
                QByteArray report_received;
                QByteArray tx_power;
                QByteArray comments;
                QByteArray name;
                QDateTime time_on; // Note: LOTW uses TIME_ON for their +/- 30-minute time window
                QByteArray operator_call;
                QByteArray my_call;
                QByteArray my_grid;
                in >> time_off >> dx_call >> dx_grid >> dial_frequency >> mode >> report_sent >> report_received
                   >> tx_power >> comments >> name >> time_on >> operator_call >> my_call >> my_grid;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->qso_logged (id, time_off, QString::fromUtf8 (dx_call), QString::fromUtf8 (dx_grid)
                                              , dial_frequency, QString::fromUtf8 (mode), QString::fromUtf8 (report_sent)
                                              , QString::fromUtf8 (report_received), QString::fromUtf8 (tx_power)
                                              , QString::fromUtf8 (comments), QString::fromUtf8 (name), time_on
                                              , QString::fromUtf8 (operator_call), QString::fromUtf8 (my_call)
                                              , QString::fromUtf8 (my_grid));
                  }
              }
              break;

            case NetworkMessage::Close:
              Q_EMIT self_->client_closed (id);
              clients_.remove (id);
              break;

            case NetworkMessage::LoggedADIF:
              {
                QByteArray ADIF;
                in >> ADIF;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->logged_ADIF (id, ADIF);
                  }
              }
              break;

            default:
              // Ignore
              break;
            }
        }
      else
        {
          Q_EMIT self_->error ("MessageServer warning: invalid UDP message received");
        }
    }
  catch (std::exception const& e)
    {
      Q_EMIT self_->error (QString {"MessageServer exception: %1"}.arg (e.what ()));
    }
  catch (...)
    {
      Q_EMIT self_->error ("Unexpected exception in MessageServer");
    }
}

void MessageServer::impl::tick ()
{
  auto now = QDateTime::currentDateTime ();
  auto iter = std::begin (clients_);
  while (iter != std::end (clients_))
    {
      if (now > (*iter).last_activity_.addSecs (NetworkMessage::pulse))
        {
          Q_EMIT self_->clear_decodes (iter.key ());
          Q_EMIT self_->client_closed (iter.key ());
          iter = clients_.erase (iter); // safe while iterating as doesn't rehash
        }
      else
        {
          ++iter;
        }
    }
}

auto MessageServer::impl::check_status (QDataStream const& stream) const -> StreamStatus
{
  auto stat = stream.status ();
  StreamStatus result {Fail};
  switch (stat)
    {
    case QDataStream::ReadPastEnd:
      result = Short;
      break;

    case QDataStream::ReadCorruptData:
      Q_EMIT self_->error ("Message serialization error: read corrupt data");
      break;

    case QDataStream::WriteFailed:
      Q_EMIT self_->error ("Message serialization error: write error");
      break;

    default:
      result = OK;
      break;
    }
  return result;
}

MessageServer::MessageServer (QObject * parent, QString const& version, QString const& revision)
  : QObject {parent}
  , m_ {this, version, revision}
{
}

void MessageServer::start (port_type port, QHostAddress const& multicast_group_address)
{
  if (port != m_->port_
      || multicast_group_address != m_->multicast_group_address_)
    {
      m_->leave_multicast_group ();
      if (impl::BoundState == m_->state ())
        {
          m_->close ();
        }
      m_->multicast_group_address_ = multicast_group_address;
      auto address = m_->multicast_group_address_.isNull ()
        || impl::IPv4Protocol != m_->multicast_group_address_.protocol () ? QHostAddress::Any : QHostAddress::AnyIPv4;
      if (port && m_->bind (address, port, m_->bind_mode_))
        {
          m_->port_ = port;
          m_->join_multicast_group ();
        }
      else
        {
          m_->port_ = 0;
        }
    }
}

void MessageServer::reply (QString const& id, QTime time, qint32 snr, float delta_time
                           , quint32 delta_frequency, QString const& mode
                           , QString const& message_text, bool low_confidence, quint8 modifiers)
{
  auto iter = m_->clients_.find (id);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::Reply, id, (*iter).negotiated_schema_number_};
      out << time << snr << delta_time << delta_frequency << mode.toUtf8 ()
          << message_text.toUtf8 () << low_confidence << modifiers;
      m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
    }
}

void MessageServer::replay (QString const& id)
{
  auto iter = m_->clients_.find (id);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::Replay, id, (*iter).negotiated_schema_number_};
      m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
    }
}

void MessageServer::halt_tx (QString const& id, bool auto_only)
{
  auto iter = m_->clients_.find (id);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::HaltTx, id, (*iter).negotiated_schema_number_};
      out << auto_only;
      m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
    }
}

void MessageServer::free_text (QString const& id, QString const& text, bool send)
{
  auto iter = m_->clients_.find (id);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::FreeText, id, (*iter).negotiated_schema_number_};
      out << text.toUtf8 () << send;
      m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
    }
}

void MessageServer::location (QString const& id, QString const& loc)
{
  auto iter = m_->clients_.find (id);
  if (iter != std::end (m_->clients_))
  {
    QByteArray message;
    NetworkMessage::Builder out {&message, NetworkMessage::Location, id, (*iter).negotiated_schema_number_};
    out << loc.toUtf8 ();
    m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
  }
}

void MessageServer::highlight_callsign (QString const& id, QString const& callsign
                                        , QColor const& bg, QColor const& fg, bool last_only)
{
  auto iter = m_->clients_.find (id);
  if (iter != std::end (m_->clients_))
  {
    QByteArray message;
    NetworkMessage::Builder out {&message, NetworkMessage::HighlightCallsign, id, (*iter).negotiated_schema_number_};
    out << callsign.toUtf8 () << bg << fg << last_only;
    m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_);
  }
}


code:
#ifndef MESSAGE_SERVER_HPP__
#define MESSAGE_SERVER_HPP__

#include <QObject>
#include <QTime>
#include <QDateTime>
#include <QHostAddress>
#include <QColor>

#include "Radio.hpp"

#include "pimpl_h.hpp"

class QString;

//
// MessageServer - a reference implementation of a message server
//                  matching the MessageClient class at the other end
//                  of the wire
//
// This class is fully functioning and suitable for use in C++
// applications that use the Qt framework. Other applications should
// use this classes' implementation as a reference implementation.
//
class MessageServer
  : public QObject
{
  Q_OBJECT;

public:
  using port_type = quint16;
  using Frequency = Radio::Frequency;

  MessageServer (QObject * parent = nullptr,
                 QString const& version = QString {}, QString const& revision = QString {});

  // start or restart the server, if the multicast_group_address
  // argument is given it is assumed to be a multicast group address
  // which the server will join
  Q_SLOT void start (port_type port,
                     QHostAddress const& multicast_group_address = QHostAddress {});

  // ask the client with identification 'id' to make the same action
  // as a double click on the decode would
  //
  // note that the client is not obliged to take any action and only
  // takes any action if the decode is present and is a CQ or QRZ message
  Q_SLOT void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
                     , QString const& mode, QString const& message, bool low_confidence, quint8 modifiers);

  // ask the client with identification 'id' to replay all decodes
  Q_SLOT void replay (QString const& id);

  // ask the client with identification 'id' to halt transmitting
  // auto_only just disables auto Tx, otherwise halt is immediate
  Q_SLOT void halt_tx (QString const& id, bool auto_only);

  // ask the client with identification 'id' to set the free text
  // message and optionally send it ASAP
  Q_SLOT void free_text (QString const& id, QString const& text, bool send);

  // ask the client with identification 'id' to set the location provided
  Q_SLOT void location (QString const& id, QString const& location);

  // ask the client with identification 'id' to highlight the callsign
  // specified with the given colors
  Q_SLOT void highlight_callsign (QString const& id, QString const& callsign
                                  , QColor const& bg = QColor {}, QColor const& fg = QColor {}
                                  , bool last_only = false);

  // the following signals are emitted when a client broadcasts the
  // matching message
  Q_SIGNAL void client_opened (QString const& id, QString const& version, QString const& revision);
  Q_SIGNAL void status_update (QString const& id, Frequency, QString const& mode, QString const& dx_call
                               , QString const& report, QString const& tx_mode, bool tx_enabled
                               , bool transmitting, bool decoding, qint32 rx_df, qint32 tx_df
                               , QString const& de_call, QString const& de_grid, QString const& dx_grid
                               , bool watchdog_timeout, QString const& sub_mode, bool fast_mode);
  Q_SIGNAL void client_closed (QString const& id);
  Q_SIGNAL void decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time
                        , quint32 delta_frequency, QString const& mode, QString const& message
                        , bool low_confidence, bool off_air);
  Q_SIGNAL void WSPR_decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time, Frequency
                             , qint32 drift, QString const& callsign, QString const& grid, qint32 power
                             , bool off_air);
  Q_SIGNAL void qso_logged (QString const& id, QDateTime time_off, QString const& dx_call, QString const& dx_grid
                            , Frequency dial_frequency, QString const& mode, QString const& report_sent
                            , QString const& report_received, QString const& tx_power, QString const& comments
                            , QString const& name, QDateTime time_on, QString const& operator_call
                            , QString const& my_call, QString const& my_grid);
  Q_SIGNAL void clear_decodes (QString const& id);
  Q_SIGNAL void logged_ADIF (QString const& id, QByteArray const& ADIF);

  // this signal is emitted when a network error occurs
  Q_SIGNAL void error (QString const&) const;

private:
  class impl;
  pimpl<impl> m_;
};

#endif

license: mit


code:
#include "MetaDataRegistry.hpp"

#include <QMetaType>
#include <QItemEditorFactory>
#include <QStandardItemEditorCreator>

#include "Radio.hpp"
#include "FrequencyList.hpp"
#include "AudioDevice.hpp"
#include "Configuration.hpp"
#include "StationList.hpp"
#include "Transceiver.hpp"
#include "TransceiverFactory.hpp"
#include "WFPalette.hpp"
#include "IARURegions.hpp"

#include "FrequencyLineEdit.hpp"

QItemEditorFactory * item_editor_factory ()
{
  static QItemEditorFactory * our_item_editor_factory = new QItemEditorFactory;
  return our_item_editor_factory;
}

void register_types ()
{
  // types in Radio.hpp are registered in their own translation unit
  // as they are needed in the wsjtx_udp shared library too

  // we still have to register the fully qualified names of enum types
  // used as signal/slot connection arguments since the new Qt 5.5
  // Q_ENUM macro only seems to register the unqualified name

  item_editor_factory ()->registerEditor (qMetaTypeId<Radio::Frequency> (), new QStandardItemEditorCreator<FrequencyLineEdit> ());
  //auto frequency_delta_type_id = qRegisterMetaType<Radio::FrequencyDelta> ("FrequencyDelta");
  item_editor_factory ()->registerEditor (qMetaTypeId<Radio::FrequencyDelta> (), new QStandardItemEditorCreator<FrequencyDeltaLineEdit> ());

  // Frequency list model
  qRegisterMetaTypeStreamOperators<FrequencyList_v2::Item> ("Item_v2");
  qRegisterMetaTypeStreamOperators<FrequencyList_v2::FrequencyItems> ("FrequencyItems_v2");

  // defunct old versions
  qRegisterMetaTypeStreamOperators<FrequencyList::Item> ("Item");
  qRegisterMetaTypeStreamOperators<FrequencyList::FrequencyItems> ("FrequencyItems");

  // Audio device
  qRegisterMetaType<AudioDevice::Channel> ("AudioDevice::Channel");

  // Configuration
#if QT_VERSION < 0x050500
  qRegisterMetaType<Configuration::DataMode> ("Configuration::DataMode");
  qRegisterMetaType<Configuration::Type2MsgGen> ("Configuration::Type2MsgGen");
#endif
  qRegisterMetaTypeStreamOperators<Configuration::DataMode> ("Configuration::DataMode");
  qRegisterMetaTypeStreamOperators<Configuration::Type2MsgGen> ("Configuration::Type2MsgGen");

  // Station details
  qRegisterMetaType<StationList::Station> ("Station");
  qRegisterMetaType<StationList::Stations> ("Stations");
  qRegisterMetaTypeStreamOperators<StationList::Station> ("Station");
  qRegisterMetaTypeStreamOperators<StationList::Stations> ("Stations");

  // Transceiver
  qRegisterMetaType<Transceiver::TransceiverState> ("Transceiver::TransceiverState");
  qRegisterMetaType<Transceiver::MODE> ("Transceiver::MODE");

  // Transceiver factory
#if QT_VERSION < 0x050500
  qRegisterMetaType<TransceiverFactory::DataBits> ("TransceiverFactory::DataBits");
  qRegisterMetaType<TransceiverFactory::StopBits> ("TransceiverFactory::StopBits");
  qRegisterMetaType<TransceiverFactory::Handshake> ("TransceiverFactory::Handshake");
  qRegisterMetaType<TransceiverFactory::PTTMethod> ("TransceiverFactory::PTTMethod");
  qRegisterMetaType<TransceiverFactory::TXAudioSource> ("TransceiverFactory::TXAudioSource");
  qRegisterMetaType<TransceiverFactory::SplitMode> ("TransceiverFactory::SplitMode");
#endif
  qRegisterMetaTypeStreamOperators<TransceiverFactory::DataBits> ("TransceiverFactory::DataBits");
  qRegisterMetaTypeStreamOperators<TransceiverFactory::StopBits> ("TransceiverFactory::StopBits");
  qRegisterMetaTypeStreamOperators<TransceiverFactory::Handshake> ("TransceiverFactory::Handshake");
  qRegisterMetaTypeStreamOperators<TransceiverFactory::PTTMethod> ("TransceiverFactory::PTTMethod");
  qRegisterMetaTypeStreamOperators<TransceiverFactory::TXAudioSource> ("TransceiverFactory::TXAudioSource");
  qRegisterMetaTypeStreamOperators<TransceiverFactory::SplitMode> ("TransceiverFactory::SplitMode");

  // Waterfall palette
  qRegisterMetaTypeStreamOperators<WFPalette::Colours> ("Colours");

  // IARURegions
#if QT_VERSION < 0x050500
  qRegisterMetaType<IARURegions::Region> ("IARURegions::Region");
#endif
  qRegisterMetaTypeStreamOperators<IARURegions::Region> ("IARURegions::Region");
}


code:
#ifndef META_DATA_REGISTRY_HPP__
#define META_DATA_REGISTRY_HPP__

class QItemEditorFactory;

QItemEditorFactory * item_editor_factory ();
void register_types ();

#endif


code:
#include "Modes.hpp"

#include <algorithm>

#include <QString>
#include <QVariant>
#include <QModelIndex>

#include "moc_Modes.cpp"

namespace
{
  // human readable strings for each Mode enumeration value
  char const * const mode_names[] =
  {
    "All",
    "JS8",
  };
  std::size_t constexpr mode_names_size = sizeof (mode_names) / sizeof (mode_names[0]);
}

Modes::Modes (QObject * parent)
  : QAbstractListModel {parent}
{
  static_assert (mode_names_size == MODES_END_SENTINAL_AND_COUNT,
                 "mode_names array must match Mode enumeration");
}

char const * Modes::name (Mode m)
{
  return mode_names[static_cast<int> (m)];
}

auto Modes::value (QString const& s) -> Mode
{
  auto end = mode_names + mode_names_size;
  auto p = std::find_if (mode_names, end
                         , [&s] (char const * const name) {
                           return name == s;
                         });
  return p != end ? static_cast<Mode> (p - mode_names) : ALL;
}

QVariant Modes::data (QModelIndex const& index, int role) const
{
  QVariant item;

  if (index.isValid ())
    {
      auto const& row = index.row ();
      switch (role)
        {
        case Qt::ToolTipRole:
        case Qt::AccessibleDescriptionRole:
          item = tr ("Mode");
          break;

        case Qt::EditRole:
          item = static_cast<Mode> (row);
          break;

        case Qt::DisplayRole:
        case Qt::AccessibleTextRole:
          item = mode_names[row];
          break;

        case Qt::TextAlignmentRole:
          item = Qt::AlignHCenter + Qt::AlignVCenter;
          break;
        }
    }

  return item;
}

QVariant Modes::headerData (int section, Qt::Orientation orientation, int role) const
{
  QVariant result;

  if (Qt::DisplayRole == role && Qt::Horizontal == orientation)
    {
      result = tr ("Mode");
    }
  else
    {
      result = QAbstractListModel::headerData (section, orientation, role);
    }

  return result;
}

#if !defined (QT_NO_DEBUG_STREAM)
ENUM_QDEBUG_OPS_IMPL (Modes, Mode);
#endif

ENUM_QDATASTREAM_OPS_IMPL (Modes, Mode);
ENUM_CONVERSION_OPS_IMPL (Modes, Mode);


code:
#ifndef MODES_HPP__
#define MODES_HPP__

#include <QAbstractListModel>

#include "qt_helpers.hpp"

class QString;
class QVariant;
class QModelIndex;

//
// Class Modes - Qt model that implements a list of data modes
//
//
// Responsibilities
//
// 	Provides  a  single column  list  model  that contains  the  human
// 	readable string version of the data mode in the display role. Also
// 	provided  is a  translatable  column header  string  and tool  tip
// 	string.
//
//
// Collaborations
//
// 	Implements a concrete sub-class of the QAbstractListModel class.
//
class Modes final
  : public QAbstractListModel
{
  Q_OBJECT
  Q_ENUMS (Mode)

public:
  //
  // This enumeration contains the supported modes, to complement this
  // an array of human readable strings in the implementation
  // (Modes.cpp) must be maintained in parallel.
  //
  enum Mode
  {
    ALL,                        // matches with all modes
    JS8,
    MODES_END_SENTINAL_AND_COUNT // this must be last
  };
  Q_ENUM (Mode)

  explicit Modes (QObject * parent = nullptr);

  // translate between enumeration and human readable strings
  static char const * name (Mode);
  static Mode value (QString const&);

  // Implement the QAbstractListModel interface
  int rowCount (QModelIndex const& parent = QModelIndex {}) const override
  {
    return parent.isValid () ? 0 : MODES_END_SENTINAL_AND_COUNT; // Number of modes in Mode enumeration class
  }
  QVariant data (QModelIndex const&, int role = Qt::DisplayRole) const override;
  QVariant headerData (int section, Qt::Orientation, int = Qt::DisplayRole) const override;
};

// Qt boilerplate to make the Modes::Mode enumeration a type that can
// be streamed and queued as a signal argument as well as showing the
// human readable string when output to debug streams.
#if QT_VERSION < 0x050500
// Qt 5.5 introduces the Q_ENUM macro which automatically registers
// the meta-type
Q_DECLARE_METATYPE (Modes::Mode);
#endif

#if !defined (QT_NO_DEBUG_STREAM)
ENUM_QDEBUG_OPS_DECL (Modes, Mode);
#endif

ENUM_QDATASTREAM_OPS_DECL (Modes, Mode);
ENUM_CONVERSION_OPS_DECL (Modes, Mode);

#endif


code:
#include "Modulator.hpp"
#include <limits>
#include <qmath.h>
#include <QDateTime>
#include <QDebug>
#include "mainwindow.h"
#include "soundout.h"
#include "commons.h"

#include "DriftingDateTime.h"

#include "moc_Modulator.cpp"

extern float gran();		// Noise generator (for tests only)

#define RAMP_INCREMENT 64  // MUST be an integral factor of 2^16

#if defined (WSJT_SOFT_KEYING)
# define SOFT_KEYING WSJT_SOFT_KEYING
#else
# define SOFT_KEYING 1
#endif

double constexpr Modulator::m_twoPi;

//    float wpm=20.0;
//    unsigned m_nspd=1.2*48000.0/wpm;
//    m_nspd=3072;                           //18.75 WPM

Modulator::Modulator (unsigned frameRate, unsigned periodLengthInSeconds,
                      QObject * parent)
  : AudioDevice {parent}
  , m_quickClose {false}
  , m_phi {0.0}
  , m_toneSpacing {0.0}
  , m_fSpread {0.0}
  , m_frameRate {frameRate}
  , m_period {periodLengthInSeconds}
  , m_state {Idle}
  , m_tuning {false}
  , m_cwLevel {false}
  , m_j0 {-1}
  , m_toneFrequency0 {1500.0}
{
}

void Modulator::start (unsigned symbolsLength, double framesPerSymbol,
                       double frequency, double toneSpacing,
                       SoundOutput * stream, Channel channel,
                       bool synchronize, bool fastMode, double dBSNR, int TRperiod)
{
  Q_ASSERT (stream);
// Time according to this computer which becomes our base time
  qint64 ms0 = DriftingDateTime::currentMSecsSinceEpoch() % 86400000;

  if (m_state != Idle)
    {
      stop ();
    }

  m_quickClose = false;

  m_symbolsLength = symbolsLength;
  m_isym0 = std::numeric_limits<unsigned>::max (); // big number
  m_frequency0 = 0.;
  m_phi = 0.;
  m_addNoise = dBSNR < 0.;
  m_nsps = framesPerSymbol;
  m_frequency = frequency;
  m_amp = std::numeric_limits<qint16>::max ();
  m_toneSpacing = toneSpacing;
  m_bFastMode=fastMode;
  m_TRperiod=TRperiod;
  unsigned delay_ms = 1920 == m_nsps && 15 == m_period ? 500 : 1000;

  // noise generator parameters
  if (m_addNoise) {
    m_snr = qPow (10.0, 0.05 * (dBSNR - 6.0));
    m_fac = 3000.0;
    if (m_snr > 1.0) m_fac = 3000.0 / m_snr;
  }

  unsigned mstr = ms0 % (1000 * m_period); // ms in period

  // round up to an exact portion of a second that allows for startup
  // delays
  m_ic = (mstr / delay_ms) * m_frameRate * delay_ms / 1000;

  if(m_bFastMode) m_ic=0;

  m_silentFrames = 0;
  // calculate number of silent frames to send, so that audio will start at
  // the nominal time "delay_ms" into the Tx sequence.
  if (synchronize && !m_tuning && !m_bFastMode)	{
    m_silentFrames = m_ic + m_frameRate / (1000 / delay_ms) - (mstr * (m_frameRate / 1000));
  }

  initialize (QIODevice::ReadOnly, channel);
  Q_EMIT stateChanged ((m_state = (synchronize && m_silentFrames) ?
                        Synchronizing : Active));
  m_stream = stream;
  if (m_stream) m_stream->restart (this);
}

void Modulator::tune (bool newState)
{
  m_tuning = newState;
  if (!m_tuning) stop (true);
}

void Modulator::stop (bool quick)
{
  m_quickClose = quick;
  close ();
}

void Modulator::close ()
{
  if (m_stream)
    {
      if (m_quickClose)
        {
          m_stream->reset ();
        }
      else
        {
          m_stream->stop ();
        }
    }
  if (m_state != Idle)
    {
      Q_EMIT stateChanged ((m_state = Idle));
    }
  AudioDevice::close ();
}

qint64 Modulator::readData (char * data, qint64 maxSize)
{
  double toneFrequency=1500.0;
  if(m_nsps==6) {
    toneFrequency=1000.0;
    m_frequency=1000.0;
    m_frequency0=1000.0;
  }
  if(maxSize==0) return 0;
  Q_ASSERT (!(maxSize % qint64 (bytesPerFrame ()))); // no torn frames
  Q_ASSERT (isOpen ());

  qint64 numFrames (maxSize / bytesPerFrame ());
  qint16 * samples (reinterpret_cast<qint16 *> (data));
  qint16 * end (samples + numFrames * (bytesPerFrame () / sizeof (qint16)));
  qint64 framesGenerated (0);

  switch (m_state)
    {
    case Synchronizing:
      {
        if (m_silentFrames)	{  // send silence up to first second
          framesGenerated = qMin (m_silentFrames, numFrames);
          for ( ; samples != end; samples = load (0, samples)) { // silence
          }
          m_silentFrames -= framesGenerated;
          return framesGenerated * bytesPerFrame ();
        }

        Q_EMIT stateChanged ((m_state = Active));
        m_cwLevel = false;
        m_ramp = 0;		// prepare for CW wave shaping
      }
      // fall through

    case Active:
      {
        unsigned int isym=0;
//        qDebug() << "Mod A" << m_toneSpacing << m_ic;
        if(!m_tuning) isym=m_ic/(4.0*m_nsps);            // Actual fsample=48000
        bool slowCwId=((isym >= m_symbolsLength) && (icw[0] > 0)) && (!m_bFastMode);
        if(m_TRperiod==3) slowCwId=false;
        bool fastCwId=false;
        static bool bCwId=false;
        qint64 ms = DriftingDateTime::currentMSecsSinceEpoch();
        float tsec=0.001*(ms % (1000*m_TRperiod));
        if(m_bFastMode and (icw[0]>0) and (tsec>(m_TRperiod-5.0))) fastCwId=true;
        if(!m_bFastMode) m_nspd=2560;                 // 22.5 WPM

        if(slowCwId or fastCwId) {     // Transmit CW ID?
          m_dphi = m_twoPi*m_frequency/m_frameRate;
          if(m_bFastMode and !bCwId) {
            m_frequency=1500;          // Set params for CW ID
            m_dphi = m_twoPi*m_frequency/m_frameRate;
            m_symbolsLength=126;
            m_nsps=4096.0*12000.0/11025.0;
            m_ic=2246949;
            m_nspd=2560;               // 22.5 WPM
            if(icw[0]*m_nspd/48000.0 > 4.0) m_nspd=4.0*48000.0/icw[0];  //Faster CW for long calls
          }
          bCwId=true;
          unsigned ic0 = m_symbolsLength * 4 * m_nsps;
          unsigned j(0);

          while (samples != end) {
            j = (m_ic - ic0)/m_nspd + 1; // symbol of this sample
            bool level {bool (icw[j])};
            m_phi += m_dphi;
            if (m_phi > m_twoPi) m_phi -= m_twoPi;
            qint16 sample=0;
            float amp=32767.0;
            float x=0;
            if(m_ramp!=0) {
              x=qSin(float(m_phi));
              if(SOFT_KEYING) {
                amp=qAbs(qint32(m_ramp));
                if(amp>32767.0) amp=32767.0;
              }
              sample=round(amp*x);
            }
            if(m_bFastMode) {
              sample=0;
              if(level) sample=32767.0*x;
            }
            if (int (j) <= icw[0] && j < NUM_CW_SYMBOLS) { // stop condition
              samples = load (postProcessSample (sample), samples);
              ++framesGenerated;
              ++m_ic;
            } else {
              Q_EMIT stateChanged ((m_state = Idle));
              return framesGenerated * bytesPerFrame ();
            }

            // adjust ramp
            if ((m_ramp != 0 && m_ramp != std::numeric_limits<qint16>::min ()) || level != m_cwLevel) {
              // either ramp has terminated at max/min or direction has changed
              m_ramp += RAMP_INCREMENT; // ramp
            }
            m_cwLevel = level;
          }
          return framesGenerated * bytesPerFrame ();
        } else {
          bCwId=false;
        } //End of code for CW ID

        double const baud (12000.0 / m_nsps);
        // fade out parameters (no fade out for tuning)
        unsigned int i0,i1;
        if(m_tuning) {
          i1 = i0 = (m_bFastMode ? 999999 : 9999) * m_nsps;
        } else {
          i0=(m_symbolsLength - 0.017) * 4.0 * m_nsps;
          i1= m_symbolsLength * 4.0 * m_nsps;
        }
        if(m_bFastMode and !m_tuning) {
          i1=m_TRperiod*48000 - 24000;
          i0=i1-816;
        }

        qint16 sample;
        for (unsigned i = 0; i < numFrames && m_ic <= i1; ++i) {
          isym=0;
          if(!m_tuning and m_TRperiod!=3) isym=m_ic / (4.0 * m_nsps);         //Actual
                                                                              //fsample=48000
          if(m_bFastMode) isym=isym%m_symbolsLength;
          if (isym != m_isym0 || m_frequency != m_frequency0) {
            if(itone[0]>=100) {
              m_toneFrequency0=itone[0];
            } else {
              if(m_toneSpacing==0.0) {
                m_toneFrequency0=m_frequency + itone[isym]*baud;
              } else {
                m_toneFrequency0=m_frequency + itone[isym]*m_toneSpacing;
              }
            }
//            qDebug() << "Mod B" << m_bFastMode << m_ic << numFrames << isym << itone[isym]
//                     << m_toneFrequency0 << m_nsps;
            m_dphi = m_twoPi * m_toneFrequency0 / m_frameRate;
            m_isym0 = isym;
            m_frequency0 = m_frequency;         //???
          }

          int j=m_ic/480;
          if(m_fSpread>0.0 and j!=m_j0) {
            float x1=(float)qrand()/RAND_MAX;
            float x2=(float)qrand()/RAND_MAX;
            toneFrequency = m_toneFrequency0 + 0.5*m_fSpread*(x1+x2-1.0);
            m_dphi = m_twoPi * toneFrequency / m_frameRate;
            m_j0=j;
          }

          m_phi += m_dphi;
          if (m_phi > m_twoPi) m_phi -= m_twoPi;
          if (m_ic > i0) m_amp = 0.98 * m_amp;
          if (m_ic > i1) m_amp = 0.0;

          sample=qRound(m_amp*qSin(m_phi));
          samples = load(postProcessSample(sample), samples);
          ++framesGenerated;
          ++m_ic;
        }

        if (m_amp == 0.0) { // TODO G4WJS: compare double with zero might not be wise
          if (icw[0] == 0) {
            // no CW ID to send
            Q_EMIT stateChanged ((m_state = Idle));
            return framesGenerated * bytesPerFrame ();
          }
          m_phi = 0.0;
        }

        m_frequency0 = m_frequency;
        // done for this chunk - continue on next call
        return framesGenerated * bytesPerFrame ();
      }
      // fall through

    case Idle:
      break;
    }

  Q_ASSERT (Idle == m_state);
  return 0;
}

qint16 Modulator::postProcessSample (qint16 sample) const
{
  if (m_addNoise) {  // Test frame, we'll add noise
    qint32 s = m_fac * (gran () + sample * m_snr / 32768.0);
    if (s > std::numeric_limits<qint16>::max ()) {
      s = std::numeric_limits<qint16>::max ();
    }
    if (s < std::numeric_limits<qint16>::min ()) {
      s = std::numeric_limits<qint16>::min ();
    }
    sample = s;
  }
  return sample;
}


code:
#ifndef MODULATOR_HPP__
#define MODULATOR_HPP__

#include <QAudio>
#include <QPointer>

#include "AudioDevice.hpp"

class SoundOutput;

//
// Input device that generates PCM audio frames that encode a message
// and an optional CW ID.
//
// Output can be muted while underway, preserving waveform timing when
// transmission is resumed.
//
class Modulator
  : public AudioDevice
{
  Q_OBJECT;

public:
  enum ModulatorState {Synchronizing, Active, Idle};

  Modulator (unsigned frameRate, unsigned periodLengthInSeconds, QObject * parent = nullptr);

  void close () override;

  bool isTuning () const {return m_tuning;}
  double frequency () const {return m_frequency;}
  bool isActive () const {return m_state != Idle;}
  void setSpread(double s) {m_fSpread=s;}
  void setTRPeriod(unsigned p) {m_period=p;}
  void set_nsym(int n) {m_symbolsLength=n;}

  Q_SLOT void start (unsigned symbolsLength, double framesPerSymbol, double frequency,
                     double toneSpacing, SoundOutput *, Channel = Mono,
                     bool synchronize = true, bool fastMode = false,
                     double dBSNR = 99., int TRperiod=60);
  Q_SLOT void stop (bool quick = false);
  Q_SLOT void tune (bool newState = true);
  Q_SLOT void setFrequency (double newFrequency) {m_frequency = newFrequency;}
  Q_SIGNAL void stateChanged (ModulatorState) const;

protected:
  qint64 readData (char * data, qint64 maxSize) override;
  qint64 writeData (char const * /* data */, qint64 /* maxSize */) override
  {
    return -1;			// we don't consume data
  }

private:
  qint16 postProcessSample (qint16 sample) const;

  QPointer<SoundOutput> m_stream;
  bool m_quickClose;

  unsigned m_symbolsLength;

  static double constexpr m_twoPi = 2.0 * 3.141592653589793238462;
  unsigned m_nspd = 2048 + 512; // CW ID WPM factor = 22.5 WPM

  double m_phi;
  double m_dphi;
  double m_amp;
  double m_nsps;
  double volatile m_frequency;
  double m_frequency0;
  double m_snr;
  double m_fac;
  double m_toneSpacing;
  double m_fSpread;

  qint64 m_silentFrames;
  qint32 m_TRperiod;
  qint16 m_ramp;

  unsigned m_frameRate;
  unsigned m_period;
  ModulatorState volatile m_state;

  bool volatile m_tuning;
  bool m_addNoise;
  bool m_bFastMode;

  bool m_cwLevel;
  unsigned m_ic;
  unsigned m_isym0;
  int m_j0;
  double m_toneFrequency0;
};

#endif


code:
#include "MultiSettings.hpp"

#include <stdexcept>

#include <QObject>
#include <QSettings>
#include <QString>
#include <QStringList>
#include <QDir>
#include <QFont>
#include <QApplication>
#include <QStandardPaths>
#include <QMainWindow>
#include <QMenu>
#include <QAction>
#include <QActionGroup>
#include <QDialog>
#include <QLineEdit>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QFormLayout>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QComboBox>
#include <QLabel>
#include <QList>
#include <QMetaObject>

#include "SettingsGroup.hpp"
#include "qt_helpers.hpp"
#include "SettingsGroup.hpp"
#include "MessageBox.hpp"

#include "pimpl_impl.hpp"

namespace
{
  char const * default_string = QT_TRANSLATE_NOOP ("MultiSettings", "Default");
  char const * multi_settings_root_group = "MultiSettings";
  char const * multi_settings_current_group_key = "CurrentMultiSettingsConfiguration";
  char const * multi_settings_current_name_key = "CurrentName";
  char const * multi_settings_place_holder_key = "MultiSettingsPlaceHolder";

  QString unescape_ampersands (QString s)
  {
    return s.replace ("&&", "&");
  }

  // calculate a useable and unique settings file path
  QString settings_path ()
  {
    auto const& config_directory = QStandardPaths::writableLocation (QStandardPaths::ConfigLocation);
    QDir config_path {config_directory}; // will be "." if config_directory is empty
    if (!config_path.mkpath ("."))
      {
        throw std::runtime_error {"Cannot find a usable configuration path \"" + config_path.path ().toStdString () + '"'};
      }
    return config_path.absoluteFilePath (QApplication::applicationName () + ".ini");
  }

  //
  // Dialog to get a valid new configuration name
  //
  class NameDialog final
    : public QDialog
  {
  public:
    explicit NameDialog (QString const& current_name,
                         QStringList const& current_names,
                         QWidget * parent = nullptr)
      : QDialog {parent}
    {
      setWindowTitle (tr ("New Configuration Name"));

      auto form_layout = new QFormLayout ();
      form_layout->addRow (tr ("Old name:"), &old_name_label_);
      old_name_label_.setText (current_name);
      form_layout->addRow (tr ("&New name:"), &name_line_edit_);

      auto main_layout = new QVBoxLayout (this);
      main_layout->addLayout (form_layout);

      auto button_box = new QDialogButtonBox {QDialogButtonBox::Ok | QDialogButtonBox::Cancel};
      button_box->button (QDialogButtonBox::Ok)->setEnabled (false);
      main_layout->addWidget (button_box);

      auto name_validator = new QRegularExpressionValidator {QRegularExpression {R"([^/\\]+)"}, this};
      name_line_edit_.setValidator (name_validator);

      connect (&name_line_edit_, &QLineEdit::textChanged, [current_names, button_box] (QString const& name) {
          bool valid {!current_names.contains (name.trimmed ())};
          button_box->button (QDialogButtonBox::Ok)->setEnabled (valid);
        });
      connect (button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
      connect (button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
    }

    QString new_name () const
    {
      return name_line_edit_.text ().trimmed ();
    }

  private:
    QLabel old_name_label_;
    QLineEdit name_line_edit_;
  };

  //
  // Dialog to get a valid new existing name
  //
  class ExistingNameDialog final
    : public QDialog
  {
  public:
    explicit ExistingNameDialog (QStringList const& current_names, QWidget * parent = nullptr)
      : QDialog {parent}
    {
      setWindowTitle (tr ("Configuration to Clone From"));

      name_combo_box_.addItems (current_names);

      auto form_layout = new QFormLayout ();
      form_layout->addRow (tr ("&Source Configuration Name:"), &name_combo_box_);

      auto main_layout = new QVBoxLayout (this);
      main_layout->addLayout (form_layout);

      auto button_box = new QDialogButtonBox {QDialogButtonBox::Ok | QDialogButtonBox::Cancel};
      main_layout->addWidget (button_box);

      connect (button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
      connect (button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
    }

    QString name () const
    {
      return name_combo_box_.currentText ();
    }

  private:
    QComboBox name_combo_box_;
  };
}

class MultiSettings::impl final
  : public QObject
{
  Q_OBJECT

public:
  explicit impl (MultiSettings const * parent, QString const& config_name);
  bool reposition ();
  void create_menu_actions (QMainWindow * main_window, QMenu * menu);
  bool exit ();

  QSettings settings_;

private:
  using Dictionary = QMap<QString, QVariant>;

  // create a configuration maintenance sub menu
  QMenu * create_sub_menu (QMainWindow * main_window,
                           QMenu * parent,
                           QString const& menu_title,
                           QActionGroup * = nullptr);

  // extract all settings from the current QSettings group
  Dictionary get_settings () const;

  // write the settings values from the dictionary to the current group
  void load_from (Dictionary const&, bool add_placeholder = true);

  // switch to this configuration
  void select_configuration (QMainWindow *, QMenu const *);

  // clone this configuration
  void clone_configuration (QMainWindow * main_window, QMenu *, QMenu const *);

  // update this configuration from another
  void clone_into_configuration (QMainWindow *, QMenu const *);

  // reset configuration to default values
  void reset_configuration (QMainWindow *, QMenu const *);

  // change configuration name
  void rename_configuration (QMainWindow *, QMenu *);

  // remove a configuration
  void delete_configuration (QMainWindow *, QMenu *);

  MultiSettings const * parent_;  // required for emitting signals
  bool name_change_emit_pending_; // delayed until menu built

  QFont original_font_;
  QString current_;

  // action to take on restart
  enum class RepositionType {unchanged, replace, save_and_replace} reposition_type_;
  Dictionary new_settings_;
  bool exit_flag_;              // false means loop around with new
                                // configuration
  QActionGroup * configurations_group_;
};

#include "MultiSettings.moc"
#include "moc_MultiSettings.cpp"

MultiSettings::MultiSettings (QString const& config_name)
  : m_ {this, config_name}
{
}

MultiSettings::~MultiSettings ()
{
}

QSettings * MultiSettings::settings ()
{
  return &m_->settings_;
}

QVariant MultiSettings::common_value (QString const& key, QVariant const& default_value) const
{
  QVariant value;
  QSettings * mutable_settings {const_cast<QSettings *> (&m_->settings_)};
  auto const& current_group = mutable_settings->group ();
  if (current_group.size ()) mutable_settings->endGroup ();
  {
    SettingsGroup alternatives {mutable_settings, multi_settings_root_group};
    value = mutable_settings->value (key, default_value);
  }
  if (current_group.size ()) mutable_settings->beginGroup (current_group);
  return value;
}

void MultiSettings::set_common_value (QString const& key, QVariant const& value)
{
  auto const& current_group = m_->settings_.group ();
  if (current_group.size ()) m_->settings_.endGroup ();
  {
    SettingsGroup alternatives {&m_->settings_, multi_settings_root_group};
    m_->settings_.setValue (key, value);
  }
  if (current_group.size ()) m_->settings_.beginGroup (current_group);
}

void MultiSettings::remove_common_value (QString const& key)
{
  if (!key.size ()) return;     // we don't allow global delete as it
                                // would break this classes data model
  auto const& current_group = m_->settings_.group ();
  if (current_group.size ()) m_->settings_.endGroup ();
  {
    SettingsGroup alternatives {&m_->settings_, multi_settings_root_group};
    m_->settings_.remove (key);
  }
  if (current_group.size ()) m_->settings_.beginGroup (current_group);
}

void MultiSettings::create_menu_actions (QMainWindow * main_window, QMenu * menu)
{
  m_->create_menu_actions (main_window, menu);
}

bool MultiSettings::exit ()
{
  return m_->exit ();
}

MultiSettings::impl::impl (MultiSettings const * parent, QString const& config_name)
  : settings_ {settings_path (), QSettings::IniFormat}
  , parent_ {parent}
  , name_change_emit_pending_ {true}
  , reposition_type_ {RepositionType::unchanged}
  , exit_flag_ {true}
  , configurations_group_ {new QActionGroup {this}}
{
  if (!settings_.isWritable ())
    {
      throw std::runtime_error {QString {"Cannot access \"%1\" for writing"}
        .arg (settings_.fileName ()).toStdString ()};
    }

  // deal with transient, now defunct, settings key
  if (settings_.contains (multi_settings_current_group_key))
    {
      current_ = settings_.value (multi_settings_current_group_key).toString ();
      settings_.remove (multi_settings_current_group_key);
      if (current_.size ())
        {
          {
            SettingsGroup alternatives {&settings_, multi_settings_root_group};
            {
              SettingsGroup source_group {&settings_, current_};
              new_settings_ = get_settings ();
            }
            settings_.setValue (multi_settings_current_name_key, tr (default_string));
          }
          reposition_type_ = RepositionType::save_and_replace;
          reposition ();
        }
      else
        {
          SettingsGroup alternatives {&settings_, multi_settings_root_group};
          settings_.setValue (multi_settings_current_name_key, tr (default_string));
        }
    }

  // bootstrap
  QStringList available_configurations;
  {
    SettingsGroup alternatives {&settings_, multi_settings_root_group};
    available_configurations = settings_.childGroups ();
    // use last selected configuration
    current_ = settings_.value (multi_settings_current_name_key).toString ();
    if (!current_.size ())
      {
        // no configurations so use default name
        current_ = tr (default_string);
        settings_.setValue (multi_settings_current_name_key, current_);
      }
  }

  if (config_name.size () && available_configurations.contains (config_name) && config_name != current_)
    {
      // switch to specified configuration
      {
        SettingsGroup alternatives {&settings_, multi_settings_root_group};
        // save the target settings
        SettingsGroup target_group {&settings_, config_name};
        new_settings_ = get_settings ();
      }
      current_ = config_name;
      reposition_type_ = RepositionType::save_and_replace;
      reposition ();
    }
  settings_.sync ();
}

// do actions that can only be done once all the windows are closed
bool MultiSettings::impl::reposition ()
{
  auto const& current_group = settings_.group ();
  if (current_group.size ()) settings_.endGroup ();
  switch (reposition_type_)
    {
    case RepositionType::save_and_replace:
      {
        // save the current settings with the other alternatives
        Dictionary saved_settings {get_settings ()};
        SettingsGroup alternatives {&settings_, multi_settings_root_group};
        // get the current configuration name
        auto const& previous_group_name = settings_.value (multi_settings_current_name_key, tr (default_string)).toString ();
        SettingsGroup save_group {&settings_, previous_group_name};
        load_from (saved_settings);
      }
      // fall through
    case RepositionType::replace:
      // and purge current settings
      for (auto const& key: settings_.allKeys ())
        {
          if (!key.contains (multi_settings_root_group))
            {
              settings_.remove (key);
            }
        }
      // insert the new settings
      load_from (new_settings_, false);
      if (!new_settings_.size ())
        {
          // if we are clearing the current settings then we must
          // reset the application font and the font in the
          // application style sheet, this is necessary since the
          // application instance is not recreated
          qApp->setFont (original_font_);
          qApp->setStyleSheet (qApp->styleSheet () + "* {" + font_as_stylesheet (original_font_) + '}');
        }
      // now we have set up the new current we can safely purge it
      // from the alternatives
      {
        SettingsGroup alternatives {&settings_, multi_settings_root_group};
        {
          SettingsGroup purge_group {&settings_, current_};
          settings_.remove (QString {});  // purge entire group
        }
        // switch to the specified configuration name
        settings_.setValue (multi_settings_current_name_key, current_);
      }
      settings_.sync ();
      // fall through
    case RepositionType::unchanged:
      new_settings_.clear ();
      break;
    }
  if (current_group.size ()) settings_.beginGroup (current_group);

  reposition_type_ = RepositionType::unchanged; // reset
  bool exit {exit_flag_};
  exit_flag_ = true;           // reset exit flag so normal exit works

  return exit;
}

// populate a pop up menu with the configurations sub-menus for
// maintenance including select, clone, clone from, delete, rename
// and, reset
void MultiSettings::impl::create_menu_actions (QMainWindow * main_window, QMenu * menu)
{
  auto const& current_group = settings_.group ();
  if (current_group.size ()) settings_.endGroup ();
  SettingsGroup alternatives {&settings_, multi_settings_root_group};
  // get the current configuration name
  auto const& current_configuration_name = settings_.value (multi_settings_current_name_key, tr (default_string)).toString ();
  // add the default configuration sub menu
  QMenu * default_menu = create_sub_menu (main_window, menu, current_configuration_name, configurations_group_);
  // and set as the current configuration
  default_menu->menuAction ()->setChecked (true);

  // get the existing alternatives
  auto const& available_configurations = settings_.childGroups ();

  // add all the other configurations
  for (auto const& configuration_name: available_configurations)
    {
      create_sub_menu (main_window, menu, configuration_name, configurations_group_);
    }

  if (current_group.size ()) settings_.beginGroup (current_group);

  if (name_change_emit_pending_)
    {
      Q_EMIT parent_->configurationNameChanged (unescape_ampersands (current_));
      name_change_emit_pending_ = false;
    }
}

// call this at the end of the main program loop to determine if the
// main window really wants to quit or to run again with a new configuration
bool MultiSettings::impl::exit ()
{
  // ensure that configuration name changed signal gets fired on restart
  name_change_emit_pending_ = true;

  // do any configuration swap required and return exit flag
  return reposition ();
}

QMenu * MultiSettings::impl::create_sub_menu (QMainWindow * main_window,
                                              QMenu * parent_menu,
                                              QString const& menu_title,
                                              QActionGroup * action_group)
{
  auto sub_menu = parent_menu->addMenu (menu_title);
  if (action_group) action_group->addAction (sub_menu->menuAction ());
  sub_menu->menuAction ()->setCheckable (true);

  // populate sub-menu actions before showing
  connect (sub_menu, &QMenu::aboutToShow, [this, main_window, parent_menu, sub_menu] () {
      // depopulate before populating and showing because on Mac OS X
      // there is an issue with depopulating in QMenu::aboutToHide()
      // with connections being disconnected before they are actioned
      while (!sub_menu->actions ().isEmpty ())
        {
          sub_menu->removeAction (sub_menu->actions ().last ());
        }

      bool is_current {sub_menu->menuAction ()->text () == current_};
      if (!is_current)
        {
          auto select_action = new QAction {tr ("&Switch To"), this};
          sub_menu->addAction (select_action);
          connect (select_action, &QAction::triggered, [this, main_window, sub_menu] (bool) {
              select_configuration (main_window, sub_menu);
            });
          sub_menu->addSeparator ();
        }

      auto clone_action = new QAction {tr ("&Clone"), this};
      sub_menu->addAction (clone_action);
      connect (clone_action, &QAction::triggered, [this, main_window, parent_menu, sub_menu] (bool) {
          clone_configuration (main_window, parent_menu, sub_menu);
        });

      auto const& current_group = settings_.group ();
      if (current_group.size ()) settings_.endGroup ();
      SettingsGroup alternatives {&settings_, multi_settings_root_group};
      if (settings_.childGroups ().size ())
        {
          auto clone_into_action = new QAction {tr ("Clone &Into ..."), this};
          sub_menu->addAction (clone_into_action);
          connect (clone_into_action, &QAction::triggered, [this, main_window, sub_menu] (bool) {
              clone_into_configuration (main_window, sub_menu);
            });
        }
      if (current_group.size ()) settings_.beginGroup (current_group);
      auto reset_action = new QAction {tr ("R&eset"), this};
      sub_menu->addAction (reset_action);
      connect (reset_action, &QAction::triggered, [this, main_window, sub_menu] (bool) {
          reset_configuration (main_window, sub_menu);
        });

      auto rename_action = new QAction {tr ("&Rename ..."), this};
      sub_menu->addAction (rename_action);
      connect (rename_action, &QAction::triggered, [this, main_window, sub_menu] (bool) {
          rename_configuration (main_window, sub_menu);
        });

      if (!is_current)
        {
          auto delete_action = new QAction {tr ("&Delete"), this};
          sub_menu->addAction (delete_action);
          connect (delete_action, &QAction::triggered, [this, main_window, sub_menu] (bool) {
              delete_configuration (main_window, sub_menu);
            });
        }
    });

  return sub_menu;
}

auto MultiSettings::impl::get_settings () const -> Dictionary
{
  Dictionary settings;
  for (auto const& key: settings_.allKeys ())
    {
      // filter out multi settings group and empty settings
      // placeholder
      if (!key.contains (multi_settings_root_group)
          && !key.contains (multi_settings_place_holder_key))
        {
          settings[key] = settings_.value (key);
        }
    }
  return settings;
}

void MultiSettings::impl::load_from (Dictionary const& dictionary, bool add_placeholder)
{
  if (dictionary.size ())
    {
      for (Dictionary::const_iterator iter = dictionary.constBegin ();
           iter != dictionary.constEnd (); ++iter)
        {
          settings_.setValue (iter.key (), iter.value ());
        }
    }
  else if (add_placeholder)
    {
      // add a placeholder key to stop the alternative configuration
      // name from disappearing
      settings_.setValue (multi_settings_place_holder_key, QVariant {});
    }
  settings_.sync ();
}

void MultiSettings::impl::select_configuration (QMainWindow * main_window, QMenu const * menu)
{
  auto const& target_name = menu->title ();

  if (target_name != current_)
    {
      {
        auto const& current_group = settings_.group ();
        if (current_group.size ()) settings_.endGroup ();
        // position to the alternative settings
        SettingsGroup alternatives {&settings_, multi_settings_root_group};
        // save the target settings
        SettingsGroup target_group {&settings_, target_name};
        new_settings_ = get_settings ();
        if (current_group.size ()) settings_.beginGroup (current_group);
      }
      // and set up the restart
      current_ = target_name;
      Q_EMIT parent_->configurationNameChanged (unescape_ampersands (current_));
      reposition_type_ = RepositionType::save_and_replace;
      exit_flag_ = false;
      main_window->close ();
    }
}

void MultiSettings::impl::clone_configuration (QMainWindow * main_window, QMenu * parent_menu, QMenu const * menu)
{
  auto const& current_group = settings_.group ();
  if (current_group.size ()) settings_.endGroup ();
  auto const& source_name = menu->title ();

  // settings to clone
  Dictionary source_settings;
  if (source_name == current_)
    {
      // grab the data to clone from the current settings
      source_settings = get_settings ();
    }
  SettingsGroup alternatives {&settings_, multi_settings_root_group};
  if (source_name != current_)
    {
      SettingsGroup source_group {&settings_, source_name};
      source_settings = get_settings ();
    }

  // find a new unique name
  QString new_name_root {source_name + " - Copy"};;
  QString new_name {new_name_root};
  unsigned index {0};
  do
    {
      if (index++) new_name = new_name_root + '(' + QString::number (index) + ')';
    }
  while (settings_.childGroups ().contains (new_name) || new_name == current_);
  SettingsGroup new_group {&settings_, new_name};
  load_from (source_settings);

  // insert the new configuration sub menu in the parent menu
  create_sub_menu (main_window, parent_menu, new_name, configurations_group_);
  if (current_group.size ()) settings_.beginGroup (current_group);
}

void MultiSettings::impl::clone_into_configuration (QMainWindow * main_window, QMenu const * menu)
{
  auto const& current_group = settings_.group ();
  if (current_group.size ()) settings_.endGroup ();
  auto const& target_name = menu->title ();

  // get the current configuration name
  QString current_group_name;
  QStringList sources;
  {
    SettingsGroup alternatives {&settings_, multi_settings_root_group};
    current_group_name = settings_.value (multi_settings_current_name_key).toString ();

    {
      // get the source configuration name for the clone
      sources = settings_.childGroups ();
      sources << current_group_name;
      sources.removeOne (target_name);
    }
  }

  // pick a source configuration
  ExistingNameDialog dialog {sources, main_window};
  if (sources.size () && (1 == sources.size () || QDialog::Accepted == dialog.exec ()))
    {
      QString source_name {1 == sources.size () ? sources.at (0) : dialog.name ()};
      if (MessageBox::Yes == MessageBox::query_message (main_window,
                                                        tr ("Clone Into Configuration"),
                                                        tr ("Confirm overwrite of all values for configuration \"%1\" with values from \"%2\"?")
                                                        .arg (unescape_ampersands (target_name))
                                                        .arg (unescape_ampersands (source_name))))
        {
          // grab the data to clone from
          if (source_name == current_group_name)
            {
              // grab the data to clone from the current settings
              new_settings_ = get_settings ();
            }
          else
            {
              SettingsGroup alternatives {&settings_, multi_settings_root_group};
              SettingsGroup source_group {&settings_, source_name};
              new_settings_ = get_settings ();
            }

          // purge target settings and replace
          if (target_name == current_)
            {
              // restart with new settings
              reposition_type_ = RepositionType::replace;
              exit_flag_ = false;
              main_window->close ();
            }
          else
            {
              SettingsGroup alternatives {&settings_, multi_settings_root_group};
              SettingsGroup target_group {&settings_, target_name};
              settings_.remove (QString {}); // purge entire group
              load_from (new_settings_);
              new_settings_.clear ();
            }
        }
    }
  if (current_group.size ()) settings_.beginGroup (current_group);
}

void MultiSettings::impl::reset_configuration (QMainWindow * main_window, QMenu const * menu)
{
  auto const& target_name = menu->title ();

  if (MessageBox::Yes != MessageBox::query_message (main_window,
                                                    tr ("Reset Configuration"),
                                                    tr ("Confirm reset to default values for configuration \"%1\"?")
                                                    .arg (unescape_ampersands (target_name))))
    {
      return;
    }

  if (target_name == current_)
    {
      // restart with default settings
      reposition_type_ = RepositionType::replace;
      new_settings_.clear ();
      exit_flag_ = false;
      main_window->close ();
    }
  else
    {
      auto const& current_group = settings_.group ();
      if (current_group.size ()) settings_.endGroup ();
      SettingsGroup alternatives {&settings_, multi_settings_root_group};
      SettingsGroup target_group {&settings_, target_name};
      settings_.remove (QString {}); // purge entire group
      // add a placeholder to stop alternative configuration name
      // being lost
      settings_.setValue (multi_settings_place_holder_key, QVariant {});
      settings_.sync ();
      if (current_group.size ()) settings_.beginGroup (current_group);
    }
}

void MultiSettings::impl::rename_configuration (QMainWindow * main_window, QMenu * menu)
{
  auto const& current_group = settings_.group ();
  if (current_group.size ()) settings_.endGroup ();
  auto const& target_name = menu->title ();

  // gather names we cannot use
  SettingsGroup alternatives {&settings_, multi_settings_root_group};
  auto invalid_names = settings_.childGroups ();
  invalid_names << settings_.value (multi_settings_current_name_key).toString ();

  // get the new name
  NameDialog dialog {target_name, invalid_names, main_window};
  if (QDialog::Accepted == dialog.exec ())
    {
      if (target_name == current_)
        {
          settings_.setValue (multi_settings_current_name_key, dialog.new_name ());
          settings_.sync ();
          current_ = dialog.new_name ();
          Q_EMIT parent_->configurationNameChanged (unescape_ampersands (current_));
        }
      else
        {
          // switch to the target group and fetch the configuration data
          Dictionary target_settings;
          {
            // grab the target configuration settings
            SettingsGroup target_group {&settings_, target_name};
            target_settings = get_settings ();
            // purge the old configuration data
            settings_.remove (QString {}); // purge entire group
          }
          // load into new configuration group name
          SettingsGroup target_group {&settings_, dialog.new_name ()};
          load_from (target_settings);
        }
      // change the action text in the menu
      menu->setTitle (dialog.new_name ());
    }
  if (current_group.size ()) settings_.beginGroup (current_group);
}

void MultiSettings::impl::delete_configuration (QMainWindow * main_window, QMenu * menu)
{
  auto const& target_name = menu->title ();

  if (target_name == current_)
    {
      return;               // suicide not allowed here
    }
  else
    {
      if (MessageBox::Yes != MessageBox::query_message (main_window,
                                                        tr ("Delete Configuration"),
                                                        tr ("Confirm deletion of configuration \"%1\"?")
                                                        .arg (unescape_ampersands (target_name))))
        {
          return;
        }
      auto const& current_group = settings_.group ();
      if (current_group.size ()) settings_.endGroup ();
      SettingsGroup alternatives {&settings_, multi_settings_root_group};
      SettingsGroup target_group {&settings_, target_name};
      // purge the configuration data
      settings_.remove (QString {}); // purge entire group
      settings_.sync ();
      if (current_group.size ()) settings_.beginGroup (current_group);
    }
  // update the menu
  menu->deleteLater ();
}


code:
#ifndef MULTISETTINGS_HPP__
#define MULTISETTINGS_HPP__

#include <QObject>
#include <QVariant>
#include <QString>

#include "pimpl_h.hpp"

class QSettings;
class QMainWindow;
class QMenu;

//
// MultiSettings - Manage multiple configuration names
//
// Responsibilities:
//
//  MultiSettings allows a  Qt application to be  run with alternative
//  settings as  stored in a QSettings  INI style file. As  far as the
//  application is  concerned it uses the  QSettings instance returned
//  by the MultiSettings::settings() method as  if it were the one and
//  only  QSettings object.   The alternative  settings are  stored as
//  QSettings groups which  are children of a root  level group called
//  MultiSettings. The  current settings are themselves  stored at the
//  root so the QSettings group  name MultiSettings is reserved.  Also
//  at the  root level a key  called CurrentMultiSettingsConfiguration
//  is reserved to store the current configuration name.
//
//
// Example Usage:
//
//  #include <QApplication>
//  #include "MultiSettings.hpp"
//  #include "MyMainWindow.hpp"
//
//  int main (int argc, char * argv[]) {
//    QApplication a {argc, argv};
//    MultiSettings multi_settings;
//    int result;
//    do {
//      MyMainWindow main_window {&multi_settings};
//      main_window.show ();
//      result = a.exec ();
//    } while (!result && !multi_settings.exit ());
//    return result;
//  }
//
//  In  the main  window call  MultiSettings::create_menu_actions() to
//  populate an existing QMenu widget with the configuration switching
//  and maintenance actions.  This would normally be done  in the main
//  window class constructor:
//
//  MyMainWindow::MyMainWindow (MultiSettings * multi_settings) {
//    QSettings * settings {multi_settings->settings ()};
//    // ...
//    multi_settings->create_menu_actions (this, ui->configurations_menu);
//    // ...
//  }
//

class MultiSettings
  : public QObject
{
  Q_OBJECT

public:
  // config_name will be  selected if it is  an existing configuration
  // name otherwise  the last used  configuration will be  selected or
  // the default configuration if none exist
  explicit MultiSettings (QString const& config_name = QString {});

  MultiSettings (MultiSettings const&) = delete;
  MultiSettings& operator = (MultiSettings const&) = delete;
  ~MultiSettings ();

  // Add multiple configurations navigation and maintenance actions to
  // a provided  menu. The provided  main window object  instance will
  // have its close() function called when a "Switch To" configuration
  // action is triggered.
  void create_menu_actions (QMainWindow *, QMenu *);

  // Access to the QSettings object instance.
  QSettings * settings ();

  // Access to values in a common section
  QVariant common_value (QString const& key, QVariant const& default_value = QVariant {}) const;
  void set_common_value (QString const& key, QVariant const& value);
  void remove_common_value (QString const& key);

  // Call this to  determine if the application is  terminating, if it
  // returns  false  then  the   application  main  window  should  be
  // recreated,  shown  and  the application  exec()  function  called
  // again.
  bool exit ();

  // emitted when the name of the current configuration changes
  Q_SIGNAL void configurationNameChanged (QString name) const;

private:
  class impl;
  pimpl<impl> m_;
};

#endif


code:
#ifndef NETWORK_ACCESS_MANAGER_HPP__
#define NETWORK_ACCESS_MANAGER_HPP__

#include <QNetworkAccessManager>
#include <QList>
#include <QSslError>
#include <QNetworkReply>
#include <QString>

#include "MessageBox.hpp"

class QNetworkRequest;
class QIODevice;
class QWidget;

// sub-class QNAM to keep a list of accepted SSL errors and allow
// them in future replies
class NetworkAccessManager
  : public QNetworkAccessManager
{
public:
  NetworkAccessManager (QWidget * parent)
    : QNetworkAccessManager (parent)
  {
    // handle SSL errors that have not been cached as allowed
    // exceptions and offer them to the user to add to the ignored
    // exception cache
    connect (this, &QNetworkAccessManager::sslErrors, [this, &parent] (QNetworkReply * reply, QList<QSslError> const& errors) {
        QString message;
        QList<QSslError> new_errors;
        for (auto const& error: errors)
          {
            if (!allowed_ssl_errors_.contains (error))
              {
                new_errors << error;
                message += '\n' + reply->request ().url ().toDisplayString () + ": "
                  + error.errorString ();
              }
          }
        if (new_errors.size ())
          {
            QString certs;
            for (auto const& cert : reply->sslConfiguration ().peerCertificateChain ())
              {
                certs += cert.toText () + '\n';
              }
            if (MessageBox::Ignore == MessageBox::query_message (parent, tr ("Network SSL Errors"), message, certs, MessageBox::Abort | MessageBox::Ignore))
              {
                // accumulate new SSL error exceptions that have been allowed
                allowed_ssl_errors_.append (new_errors);
                reply->ignoreSslErrors (allowed_ssl_errors_);
              }
          }
        else
          {
            // no new exceptions so silently ignore the ones already allowed
            reply->ignoreSslErrors (allowed_ssl_errors_);
          }
      });
  }

protected:
  QNetworkReply * createRequest (Operation operation, QNetworkRequest const& request, QIODevice * outgoing_data = nullptr) override
  {
    auto reply = QNetworkAccessManager::createRequest (operation, request, outgoing_data);
    // errors are usually certificate specific so passing all cached
    // exceptions here is ok
    reply->ignoreSslErrors (allowed_ssl_errors_);
    return reply;
  }

private:
  QList<QSslError> allowed_ssl_errors_;
};

#endif


code:
#include "NetworkMessage.hpp"

#include <exception>

#include <QString>
#include <QByteArray>
#include <QDebug>

#include "pimpl_impl.hpp"

namespace NetworkMessage
{
  Builder::Builder (QIODevice * device, Type type, QString const& id, quint32 schema)
    : QDataStream {device}
  {
    common_initialization (type, id, schema);
  }

  Builder::Builder (QByteArray * a, Type type, QString const& id, quint32 schema)
    : QDataStream {a, QIODevice::WriteOnly}
  {
    common_initialization (type, id, schema);
  }

  void Builder::common_initialization (Type type, QString const& id, quint32 schema)
  {
    if (schema <= 1)
      {
        setVersion (QDataStream::Qt_5_0); // Qt schema version
      }
#if QT_VERSION >= 0x050200
    else if (schema <= 2)
      {
        setVersion (QDataStream::Qt_5_2); // Qt schema version
      }
#endif
#if QT_VERSION >= 0x050400
    else if (schema <= 3)
      {
        setVersion (QDataStream::Qt_5_4); // Qt schema version
      }
#endif
    else
      {
        throw std::runtime_error {"Unrecognized message schema"};
      }

    // the following two items assume that the quint32 encoding is
    // unchanged over QDataStream versions
    *this << magic;
    *this << schema;

    *this << static_cast<quint32> (type) << id.toUtf8 ();
  }

  class Reader::impl
  {
  public:
    void common_initialization (Reader * parent)
    {
      quint32 magic;
      *parent >> magic;
      if (magic != Builder::magic)
        {
          throw std::runtime_error {"Invalid message format"};
        }
      *parent >> schema_;
      if (schema_ > Builder::schema_number)
        {
          throw std::runtime_error {"Unrecognized message schema"};
        }
      if (schema_ <= 1)
        {
          parent->setVersion (QDataStream::Qt_5_0);
        }
#if QT_VERSION >= 0x050200
      else if (schema_ <= 2)
        {
          parent->setVersion (QDataStream::Qt_5_2);
        }
#endif
#if QT_VERSION >= 0x050400
      else if (schema_ <= 3)
        {
          parent->setVersion (QDataStream::Qt_5_4);
        }
#endif
      quint32 type;
      *parent >> type >> id_;
      if (type >= maximum_message_type_)
        {
          qDebug () << "Unrecognized message type:" << type << "from id:" << id_;
          type_ = maximum_message_type_;
        }
      else
        {
          type_ = static_cast<Type> (type);
        }
    }

    quint32 schema_;
    Type type_;
    QByteArray id_;
  };

  Reader::Reader (QIODevice * device)
    : QDataStream {device}
  {
    m_->common_initialization (this);
  }

  Reader::Reader (QByteArray const& a)
    : QDataStream {a}
  {
    m_->common_initialization (this);
  }

  Reader::~Reader ()
  {
  }

  quint32 Reader::schema () const
  {
    return m_->schema_;
  }

  Type Reader::type () const
  {
    return static_cast<Type> (m_->type_);
  }

  QString Reader::id () const
  {
    return QString::fromUtf8 (m_->id_);
  }
}