mirror of
				https://github.com/saitohirga/WSJT-X.git
				synced 2025-11-03 21:40:52 -05:00 
			
		
		
		
	The UDP "Free text" message comamnd should not expose implementation differences between tab one and tab two. The "send next" capability is not available on tab two so use of it has been removed from the UDP interface. The ability to trigger the immediate (or in next Tx period if the current period is Rx) sending of the current free text message has been added to the UDP interface. This is done by sending a "Free text" UDP message containing an empty text message with the "Send" flag set. git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@5725 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
		
			
				
	
	
		
			509 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			509 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
//
 | 
						|
// MessageAggregator - an example application that utilizes the WSJT-X
 | 
						|
//                     messaging facility
 | 
						|
//
 | 
						|
// This  application is  only  provided as  a  simple GUI  application
 | 
						|
// example to demonstrate the WSJT-X messaging facility. It allows the
 | 
						|
// user to set  the server details either as a  unicast UDP server or,
 | 
						|
// if a  multicast group address  is provided, as a  multicast server.
 | 
						|
// The benefit of the multicast server is that multiple servers can be
 | 
						|
// active at  once each  receiving all  WSJT-X broadcast  messages and
 | 
						|
// each able to  respond to individual WSJT_X clients.  To utilize the
 | 
						|
// multicast  group features  each  WSJT-X client  must  set the  same
 | 
						|
// multicast  group address  as  the UDP  server  address for  example
 | 
						|
// 239.255.0.0 for a site local multicast group.
 | 
						|
//
 | 
						|
// The  UI is  a small  panel  to input  the service  port number  and
 | 
						|
// optionally  the  multicast  group  address.   Below  that  a  table
 | 
						|
// representing  the  log  entries   where  any  QSO  logged  messages
 | 
						|
// broadcast  from WSJT-X  clients are  displayed. The  bottom of  the
 | 
						|
// application main  window is a  dock area  where a dock  window will
 | 
						|
// appear for each WSJT-X client, this  window contains a table of the
 | 
						|
// current decode  messages broadcast  from that  WSJT-X client  and a
 | 
						|
// status line showing  the status update messages  broadcast from the
 | 
						|
// WSJT_X client. The dock windows may  be arranged in a tab bar, side
 | 
						|
// by side,  below each  other or, completely  detached from  the dock
 | 
						|
// area as floating windows. Double clicking the dock window title bar
 | 
						|
// or  dragging and  dropping with  the mouse  allows these  different
 | 
						|
// arrangements.
 | 
						|
//
 | 
						|
// The application  also provides a  simple menu bar including  a view
 | 
						|
// menu that allows each dock window to be hidden or revealed.
 | 
						|
//
 | 
						|
 | 
						|
#include <iostream>
 | 
						|
#include <exception>
 | 
						|
 | 
						|
#include <QtWidgets>
 | 
						|
#include <QFile>
 | 
						|
#include <QStandardItemModel>
 | 
						|
#include <QStandardItem>
 | 
						|
#include <QSortFilterProxyModel>
 | 
						|
#include <QFont>
 | 
						|
#include <QDateTime>
 | 
						|
#include <QTime>
 | 
						|
#include <QHash>
 | 
						|
 | 
						|
#include "MessageServer.hpp"
 | 
						|
#include "NetworkMessage.hpp"
 | 
						|
 | 
						|
#include "qt_helpers.hpp"
 | 
						|
 | 
						|
using port_type = MessageServer::port_type;
 | 
						|
using Frequency = MessageServer::Frequency;
 | 
						|
 | 
						|
QRegExp message_alphabet {"[- A-Za-z0-9+./?]*"};
 | 
						|
 | 
						|
//
 | 
						|
// Decodes Model - simple data model for all decodes
 | 
						|
//
 | 
						|
// The model is a basic table with uniform row format. Rows consist of
 | 
						|
// QStandardItem instances containing the string representation of the
 | 
						|
// column data  and if the underlying  field is not a  string then the
 | 
						|
// UserRole+1 role contains the underlying data item.
 | 
						|
//
 | 
						|
// Three slots  are provided to add  a new decode, remove  all decodes
 | 
						|
// for a client  and, to build a  reply to CQ message for  a given row
 | 
						|
// which is emitted as a signal respectively.
 | 
						|
//
 | 
						|
class DecodesModel
 | 
						|
  : public QStandardItemModel
 | 
						|
{
 | 
						|
  Q_OBJECT;
 | 
						|
 | 
						|
public:
 | 
						|
  DecodesModel (QObject * parent = nullptr)
 | 
						|
    : QStandardItemModel {0, 7, parent}
 | 
						|
    , text_font_ {"Courier", 10}
 | 
						|
  {
 | 
						|
    setHeaderData (0, Qt::Horizontal, tr ("Client"));
 | 
						|
    setHeaderData (1, Qt::Horizontal, tr ("Time"));
 | 
						|
    setHeaderData (2, Qt::Horizontal, tr ("Snr"));
 | 
						|
    setHeaderData (3, Qt::Horizontal, tr ("DT"));
 | 
						|
    setHeaderData (4, Qt::Horizontal, tr ("DF"));
 | 
						|
    setHeaderData (5, Qt::Horizontal, tr ("Md"));
 | 
						|
    setHeaderData (6, Qt::Horizontal, tr ("Message"));
 | 
						|
  }
 | 
						|
 | 
						|
  Q_SLOT void add_decode (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time
 | 
						|
                          , quint32 delta_frequency, QString const& mode, QString const& message)
 | 
						|
  {
 | 
						|
    if (!is_new)
 | 
						|
      {
 | 
						|
        int target_row {-1};
 | 
						|
        for (auto row = 0; row < rowCount (); ++row)
 | 
						|
          {
 | 
						|
            if (data (index (row, 0)).toString () == client_id)
 | 
						|
              {
 | 
						|
                auto row_time = item (row, 1)->data ().toTime ();
 | 
						|
                if (row_time == time
 | 
						|
                    && item (row, 2)->data ().toInt () == snr
 | 
						|
                    && item (row, 3)->data ().toFloat () == delta_time
 | 
						|
                    && item (row, 4)->data ().toUInt () == delta_frequency
 | 
						|
                    && data (index (row, 5)).toString () == mode
 | 
						|
                    && data (index (row, 6)).toString () == message)
 | 
						|
                  {
 | 
						|
                    return;
 | 
						|
                  }
 | 
						|
                if (time <= row_time)
 | 
						|
                  {
 | 
						|
                    target_row = row; // last row with same time
 | 
						|
                  }
 | 
						|
              }
 | 
						|
          }
 | 
						|
        if (target_row >= 0)
 | 
						|
          {
 | 
						|
            insertRow (target_row + 1, make_row (client_id, time, snr, delta_time, delta_frequency, mode, message));
 | 
						|
            return;
 | 
						|
          }
 | 
						|
      }
 | 
						|
 | 
						|
    appendRow (make_row (client_id, time, snr, delta_time, delta_frequency, mode, message));
 | 
						|
  }
 | 
						|
 | 
						|
  QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time
 | 
						|
                                   , quint32 delta_frequency, QString const& mode, QString const& message) const
 | 
						|
  {
 | 
						|
    auto time_item = new QStandardItem {time.toString ("hh:mm")};
 | 
						|
    time_item->setData (time);
 | 
						|
    time_item->setTextAlignment (Qt::AlignRight);
 | 
						|
 | 
						|
    auto snr_item = new QStandardItem {QString::number (snr)};
 | 
						|
    snr_item->setData (snr);
 | 
						|
    snr_item->setTextAlignment (Qt::AlignRight);
 | 
						|
 | 
						|
    auto dt = new QStandardItem {QString::number (delta_time)};
 | 
						|
    dt->setData (delta_time);
 | 
						|
    dt->setTextAlignment (Qt::AlignRight);
 | 
						|
 | 
						|
    auto df = new QStandardItem {QString::number (delta_frequency)};
 | 
						|
    df->setData (delta_frequency);
 | 
						|
    df->setTextAlignment (Qt::AlignRight);
 | 
						|
 | 
						|
    auto md = new QStandardItem {mode};
 | 
						|
    md->setTextAlignment (Qt::AlignHCenter);
 | 
						|
 | 
						|
    QList<QStandardItem *> row {
 | 
						|
      new QStandardItem {client_id}, time_item, snr_item, dt, df, md, new QStandardItem {message}};
 | 
						|
    Q_FOREACH (auto& item, row)
 | 
						|
      {
 | 
						|
        item->setEditable (false);
 | 
						|
        item->setFont (text_font_);
 | 
						|
        item->setTextAlignment (item->textAlignment () | Qt::AlignVCenter);
 | 
						|
      }
 | 
						|
    return row;
 | 
						|
  }
 | 
						|
 | 
						|
  Q_SLOT void clear_decodes (QString const& client_id)
 | 
						|
  {
 | 
						|
    for (auto row = rowCount () - 1; row >= 0; --row)
 | 
						|
      {
 | 
						|
        if (data (index (row, 0)).toString () == client_id)
 | 
						|
          {
 | 
						|
            removeRow (row);
 | 
						|
          }
 | 
						|
      }
 | 
						|
  }
 | 
						|
 | 
						|
  Q_SLOT void do_reply (QModelIndex const& source)
 | 
						|
  {
 | 
						|
    auto row = source.row ();
 | 
						|
    Q_EMIT reply (data (index (row, 0)).toString ()
 | 
						|
                  , item (row, 1)->data ().toTime ()
 | 
						|
                  , item (row, 2)->data ().toInt ()
 | 
						|
                  , item (row, 3)->data ().toFloat ()
 | 
						|
                  , item (row, 4)->data ().toInt ()
 | 
						|
                  , data (index (row, 5)).toString ()
 | 
						|
                  , data (index (row, 6)).toString ());
 | 
						|
  }
 | 
						|
 | 
						|
  Q_SIGNAL void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
 | 
						|
                       , QString const& mode, QString const& message);
 | 
						|
 | 
						|
private:
 | 
						|
  QFont text_font_;
 | 
						|
};
 | 
						|
 | 
						|
class ClientWidget
 | 
						|
  : public QDockWidget
 | 
						|
{
 | 
						|
  Q_OBJECT;
 | 
						|
 | 
						|
public:
 | 
						|
  explicit ClientWidget (QAbstractItemModel * decodes_model, QString const& id, QWidget * parent = 0)
 | 
						|
    : QDockWidget {id, parent}
 | 
						|
    , id_ {id}
 | 
						|
    , decodes_table_view_ {new QTableView}
 | 
						|
    , message_line_edit_ {new QLineEdit}
 | 
						|
    , auto_off_button_ {new QPushButton {tr ("&Auto Off")}}
 | 
						|
    , halt_tx_button_ {new QPushButton {tr ("&Halt Tx")}}
 | 
						|
    , mode_label_ {new QLabel}
 | 
						|
    , dx_call_label_ {new QLabel}
 | 
						|
    , frequency_label_ {new QLabel}
 | 
						|
    , report_label_ {new QLabel}
 | 
						|
  {
 | 
						|
    auto content_layout = new QVBoxLayout;
 | 
						|
    content_layout->setContentsMargins (QMargins {2, 2, 2, 2});
 | 
						|
 | 
						|
    // set up table
 | 
						|
    auto proxy_model = new DecodesFilterModel {id, this};
 | 
						|
    proxy_model->setSourceModel (decodes_model);
 | 
						|
    decodes_table_view_->setModel (proxy_model);
 | 
						|
    decodes_table_view_->verticalHeader ()->hide ();
 | 
						|
    decodes_table_view_->hideColumn (0);
 | 
						|
    content_layout->addWidget (decodes_table_view_);
 | 
						|
 | 
						|
    // set up controls
 | 
						|
    auto control_layout = new QHBoxLayout;
 | 
						|
    auto form_layout = new QFormLayout;
 | 
						|
    form_layout->addRow (tr ("Free text:"), message_line_edit_);
 | 
						|
    message_line_edit_->setValidator (new QRegExpValidator {message_alphabet, this});
 | 
						|
    connect (message_line_edit_, &QLineEdit::textEdited, [this] (QString const& text) {
 | 
						|
        Q_EMIT do_free_text (id_, text, false);
 | 
						|
      });
 | 
						|
    connect (message_line_edit_, &QLineEdit::editingFinished, [this] () {
 | 
						|
        Q_EMIT do_free_text (id_, message_line_edit_->text (), true);
 | 
						|
      });
 | 
						|
    control_layout->addLayout (form_layout);
 | 
						|
    control_layout->addWidget (auto_off_button_);
 | 
						|
    control_layout->addWidget (halt_tx_button_);
 | 
						|
    connect (auto_off_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
 | 
						|
        Q_EMIT do_halt_tx (id_, true);
 | 
						|
      });
 | 
						|
    connect (halt_tx_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
 | 
						|
        Q_EMIT do_halt_tx (id_, false);
 | 
						|
      });
 | 
						|
    content_layout->addLayout (control_layout);
 | 
						|
 | 
						|
    // set up status area
 | 
						|
    auto status_bar = new QStatusBar;
 | 
						|
    status_bar->addPermanentWidget (mode_label_);
 | 
						|
    status_bar->addPermanentWidget (dx_call_label_);
 | 
						|
    status_bar->addPermanentWidget (frequency_label_);
 | 
						|
    status_bar->addPermanentWidget (report_label_);
 | 
						|
    content_layout->addWidget (status_bar);
 | 
						|
    connect (this, &ClientWidget::topLevelChanged, status_bar, &QStatusBar::setSizeGripEnabled);
 | 
						|
 | 
						|
    // set up central widget
 | 
						|
    auto content_widget = new QFrame;
 | 
						|
    content_widget->setFrameStyle (QFrame::StyledPanel | QFrame::Sunken);
 | 
						|
    content_widget->setLayout (content_layout);
 | 
						|
    setWidget (content_widget);
 | 
						|
    // setMinimumSize (QSize {550, 0});
 | 
						|
    setFeatures (DockWidgetMovable | DockWidgetFloatable);
 | 
						|
    setAllowedAreas (Qt::BottomDockWidgetArea);
 | 
						|
 | 
						|
    // connect up table view signals
 | 
						|
    connect (decodes_table_view_, &QTableView::doubleClicked, this, [this, proxy_model] (QModelIndex const& index) {
 | 
						|
        Q_EMIT do_reply (proxy_model->mapToSource (index));
 | 
						|
      });
 | 
						|
  }
 | 
						|
 | 
						|
  Q_SLOT void update_status (QString const& id, Frequency f, QString const& mode, QString const& dx_call
 | 
						|
                             , QString const& report, QString const& tx_mode, bool tx_enabled, bool transmitting)
 | 
						|
  {
 | 
						|
    if (id == id_)
 | 
						|
      {
 | 
						|
        mode_label_->setText (QString {"Mode: %1%2"}.arg (mode).arg (tx_mode.isEmpty () ? tx_mode : '(' + tx_mode + ')'));
 | 
						|
        dx_call_label_->setText ("DX CALL: " + dx_call);
 | 
						|
        frequency_label_->setText ("QRG: " + Radio::pretty_frequency_MHz_string (f));
 | 
						|
        report_label_->setText ("SNR: " + report);
 | 
						|
        update_dynamic_property (frequency_label_, "transmitting", transmitting);
 | 
						|
        auto_off_button_->setEnabled (tx_enabled);
 | 
						|
        halt_tx_button_->setEnabled (transmitting);
 | 
						|
      }
 | 
						|
  }
 | 
						|
 | 
						|
  Q_SLOT void decode_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/
 | 
						|
                            , float /*delta_time*/, quint32 /*delta_frequency*/, QString const& /*mode*/
 | 
						|
                            , QString const& /*message*/)
 | 
						|
  {
 | 
						|
    if (client_id == id_)
 | 
						|
      {
 | 
						|
        decodes_table_view_->resizeColumnsToContents ();
 | 
						|
        decodes_table_view_->horizontalHeader ()->setStretchLastSection (true);
 | 
						|
        decodes_table_view_->scrollToBottom ();
 | 
						|
      }
 | 
						|
  }
 | 
						|
 | 
						|
  Q_SIGNAL void do_reply (QModelIndex const&);
 | 
						|
  Q_SIGNAL void do_halt_tx (QString const& id, bool auto_only);
 | 
						|
  Q_SIGNAL void do_free_text (QString const& id, QString const& text, bool);
 | 
						|
 | 
						|
private:
 | 
						|
  class DecodesFilterModel final
 | 
						|
    : public QSortFilterProxyModel
 | 
						|
  {
 | 
						|
  public:
 | 
						|
    DecodesFilterModel (QString const& id, QObject * parent = nullptr)
 | 
						|
      : QSortFilterProxyModel {parent}
 | 
						|
      , id_ {id}
 | 
						|
    {}
 | 
						|
 | 
						|
  protected:
 | 
						|
    bool filterAcceptsRow (int source_row, QModelIndex const& source_parent) const override
 | 
						|
    {
 | 
						|
      auto source_index_col0 = sourceModel ()->index (source_row, 0, source_parent);
 | 
						|
      return sourceModel ()->data (source_index_col0).toString () == id_;
 | 
						|
    }
 | 
						|
 | 
						|
  private:
 | 
						|
    QString id_;
 | 
						|
  };
 | 
						|
 | 
						|
  QString id_;
 | 
						|
  QTableView * decodes_table_view_;
 | 
						|
  QLineEdit * message_line_edit_;
 | 
						|
  QAbstractButton * auto_off_button_;
 | 
						|
  QAbstractButton * halt_tx_button_;
 | 
						|
  QLabel * mode_label_;
 | 
						|
  QLabel * dx_call_label_;
 | 
						|
  QLabel * frequency_label_;
 | 
						|
  QLabel * report_label_;
 | 
						|
};
 | 
						|
 | 
						|
class MainWindow
 | 
						|
  : public QMainWindow
 | 
						|
{
 | 
						|
  Q_OBJECT;
 | 
						|
 | 
						|
public:
 | 
						|
  MainWindow ()
 | 
						|
    : log_ {new QStandardItemModel {0, 10, this}}
 | 
						|
    , decodes_model_ {new DecodesModel {this}}
 | 
						|
    , server_ {new MessageServer {this}}
 | 
						|
    , multicast_group_line_edit_ {new QLineEdit}
 | 
						|
    , log_table_view_ {new QTableView}
 | 
						|
  {
 | 
						|
    // logbook
 | 
						|
    log_->setHeaderData (0, Qt::Horizontal, tr ("Date/Time"));
 | 
						|
    log_->setHeaderData (1, Qt::Horizontal, tr ("Callsign"));
 | 
						|
    log_->setHeaderData (2, Qt::Horizontal, tr ("Grid"));
 | 
						|
    log_->setHeaderData (3, Qt::Horizontal, tr ("Name"));
 | 
						|
    log_->setHeaderData (4, Qt::Horizontal, tr ("Frequency"));
 | 
						|
    log_->setHeaderData (5, Qt::Horizontal, tr ("Mode"));
 | 
						|
    log_->setHeaderData (6, Qt::Horizontal, tr ("Sent"));
 | 
						|
    log_->setHeaderData (7, Qt::Horizontal, tr ("Rec'd"));
 | 
						|
    log_->setHeaderData (8, Qt::Horizontal, tr ("Power"));
 | 
						|
    log_->setHeaderData (9, Qt::Horizontal, tr ("Comments"));
 | 
						|
    connect (server_, &MessageServer::qso_logged, this, &MainWindow::log_qso);
 | 
						|
 | 
						|
    // menu bar
 | 
						|
    auto file_menu = menuBar ()->addMenu (tr ("&File"));
 | 
						|
 | 
						|
    auto exit_action = new QAction {tr ("E&xit"), this};
 | 
						|
    exit_action->setShortcuts (QKeySequence::Quit);
 | 
						|
    exit_action->setToolTip (tr ("Exit the application"));
 | 
						|
    file_menu->addAction (exit_action);
 | 
						|
    connect (exit_action, &QAction::triggered, this, &MainWindow::close);
 | 
						|
 | 
						|
    view_menu_ = menuBar ()->addMenu (tr ("&View"));
 | 
						|
 | 
						|
    // central layout
 | 
						|
    auto central_layout = new QVBoxLayout;
 | 
						|
 | 
						|
    // server details
 | 
						|
    auto port_spin_box = new QSpinBox;
 | 
						|
    port_spin_box->setMinimum (1);
 | 
						|
    port_spin_box->setMaximum (std::numeric_limits<port_type>::max ());
 | 
						|
    auto group_box_layout = new QFormLayout;
 | 
						|
    group_box_layout->addRow (tr ("Port number:"), port_spin_box);
 | 
						|
    group_box_layout->addRow (tr ("Multicast Group (blank for unicast server):"), multicast_group_line_edit_);
 | 
						|
    auto group_box = new QGroupBox {tr ("Server Details")};
 | 
						|
    group_box->setLayout (group_box_layout);
 | 
						|
    central_layout->addWidget (group_box);
 | 
						|
 | 
						|
    log_table_view_->setModel (log_);
 | 
						|
    log_table_view_->verticalHeader ()->hide ();
 | 
						|
    central_layout->addWidget (log_table_view_);
 | 
						|
 | 
						|
    // central widget
 | 
						|
    auto central_widget = new QWidget;
 | 
						|
    central_widget->setLayout (central_layout);
 | 
						|
 | 
						|
    // main window setup
 | 
						|
    setCentralWidget (central_widget);
 | 
						|
    setDockOptions (AnimatedDocks | AllowNestedDocks | AllowTabbedDocks);
 | 
						|
    setTabPosition (Qt::BottomDockWidgetArea, QTabWidget::North);
 | 
						|
 | 
						|
    // connect up server
 | 
						|
    connect (server_, &MessageServer::error, [this] (QString const& message) {
 | 
						|
        QMessageBox::warning (this, tr ("Network Error"), message);
 | 
						|
      });
 | 
						|
    connect (server_, &MessageServer::client_opened, this, &MainWindow::add_client);
 | 
						|
    connect (server_, &MessageServer::client_closed, this, &MainWindow::remove_client);
 | 
						|
    connect (server_, &MessageServer::client_closed, decodes_model_, &DecodesModel::clear_decodes);
 | 
						|
    connect (server_, &MessageServer::decode, decodes_model_, &DecodesModel::add_decode);
 | 
						|
    connect (server_, &MessageServer::clear_decodes, decodes_model_, &DecodesModel::clear_decodes);
 | 
						|
    connect (decodes_model_, &DecodesModel::reply, server_, &MessageServer::reply);
 | 
						|
 | 
						|
    // UI behaviour
 | 
						|
    connect (port_spin_box, static_cast<void (QSpinBox::*)(int)> (&QSpinBox::valueChanged)
 | 
						|
             , [this] (port_type port) {server_->start (port);});
 | 
						|
    connect (multicast_group_line_edit_, &QLineEdit::editingFinished, [this, port_spin_box] () {
 | 
						|
        server_->start (port_spin_box->value (), QHostAddress {multicast_group_line_edit_->text ()});
 | 
						|
      });
 | 
						|
 | 
						|
    port_spin_box->setValue (2237); // start up in unicast mode
 | 
						|
    show ();
 | 
						|
  }
 | 
						|
 | 
						|
  Q_SLOT void log_qso (QString const& /*id*/, QDateTime time, 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)
 | 
						|
  {
 | 
						|
    QList<QStandardItem *> row;
 | 
						|
    row << new QStandardItem {time.toString ("dd-MMM-yyyy hh:mm")}
 | 
						|
        << new QStandardItem {dx_call}
 | 
						|
        << new QStandardItem {dx_grid}
 | 
						|
        << new QStandardItem {name}
 | 
						|
        << new QStandardItem {Radio::frequency_MHz_string (dial_frequency)}
 | 
						|
        << new QStandardItem {mode}
 | 
						|
        << new QStandardItem {report_sent}
 | 
						|
        << new QStandardItem {report_received}
 | 
						|
        << new QStandardItem {tx_power}
 | 
						|
        << new QStandardItem {comments};
 | 
						|
    log_->appendRow (row);
 | 
						|
    log_table_view_->resizeColumnsToContents ();
 | 
						|
    log_table_view_->horizontalHeader ()->setStretchLastSection (true);
 | 
						|
    log_table_view_->scrollToBottom ();
 | 
						|
  }
 | 
						|
 | 
						|
private:
 | 
						|
  void add_client (QString const& id)
 | 
						|
  {
 | 
						|
    auto dock = new ClientWidget {decodes_model_, id, this};
 | 
						|
    dock->setAttribute (Qt::WA_DeleteOnClose);
 | 
						|
    auto view_action = dock->toggleViewAction ();
 | 
						|
    view_action->setEnabled (true);
 | 
						|
    view_menu_->addAction (view_action);
 | 
						|
    addDockWidget (Qt::BottomDockWidgetArea, dock);
 | 
						|
    connect (server_, &MessageServer::status_update, dock, &ClientWidget::update_status);
 | 
						|
    connect (server_, &MessageServer::decode, dock, &ClientWidget::decode_added);
 | 
						|
    connect (dock, &ClientWidget::do_reply, decodes_model_, &DecodesModel::do_reply);
 | 
						|
    connect (dock, &ClientWidget::do_halt_tx, server_, &MessageServer::halt_tx);
 | 
						|
    connect (dock, &ClientWidget::do_free_text, server_, &MessageServer::free_text);
 | 
						|
    connect (view_action, &QAction::toggled, dock, &ClientWidget::setVisible);
 | 
						|
    dock_widgets_[id] = dock;
 | 
						|
    server_->replay (id);
 | 
						|
  }
 | 
						|
 | 
						|
  void remove_client (QString const& id)
 | 
						|
  {
 | 
						|
    auto iter = dock_widgets_.find (id);
 | 
						|
    if (iter != std::end (dock_widgets_))
 | 
						|
      {
 | 
						|
        (*iter)->close ();
 | 
						|
        dock_widgets_.erase (iter);
 | 
						|
      }
 | 
						|
  }
 | 
						|
 | 
						|
  QStandardItemModel * log_;
 | 
						|
  QMenu * view_menu_;
 | 
						|
  DecodesModel * decodes_model_;
 | 
						|
  MessageServer * server_;
 | 
						|
  QLineEdit * multicast_group_line_edit_;
 | 
						|
  QTableView * log_table_view_;
 | 
						|
 | 
						|
  // maps client id to widgets
 | 
						|
  QHash<QString, ClientWidget *> dock_widgets_;
 | 
						|
};
 | 
						|
 | 
						|
#include "MessageAggregator.moc"
 | 
						|
 | 
						|
int main (int argc, char * argv[])
 | 
						|
{
 | 
						|
  QApplication app {argc, argv};
 | 
						|
  try
 | 
						|
    {
 | 
						|
      QObject::connect (&app, SIGNAL (lastWindowClosed ()), &app, SLOT (quit ()));
 | 
						|
 | 
						|
      app.setApplicationName ("WSJT-X Reference UDP Message Aggregator Server");
 | 
						|
      app.setApplicationVersion ("1.0");
 | 
						|
 | 
						|
      {
 | 
						|
        QFile file {":/qss/default.qss"};
 | 
						|
        if (!file.open (QFile::ReadOnly))
 | 
						|
          {
 | 
						|
            throw_qstring ("failed to open \"" + file.fileName () + "\": " + file.errorString ());
 | 
						|
          }
 | 
						|
        app.setStyleSheet (file.readAll());
 | 
						|
      }
 | 
						|
 | 
						|
      MainWindow window;
 | 
						|
      return app.exec ();
 | 
						|
    }
 | 
						|
  catch (std::exception const & e)
 | 
						|
    {
 | 
						|
      QMessageBox::critical (nullptr, app.applicationName (), e.what ());
 | 
						|
      std:: cerr << "Error: " << e.what () << '\n';
 | 
						|
    }
 | 
						|
  catch (...)
 | 
						|
    {
 | 
						|
      QMessageBox::critical (nullptr, app.applicationName (), QObject::tr ("Unexpected error"));
 | 
						|
      std:: cerr << "Unexpected error\n";
 | 
						|
    }
 | 
						|
  return -1;
 | 
						|
}
 |