1 #include "ClientWidget.hpp"
2 
3 #include <limits>
4 #include <QRegExp>
5 #include <QColor>
6 #include <QtWidgets>
7 #include <QAction>
8 
9 #include "validators/MaidenheadLocatorValidator.hpp"
10 
11 namespace
12 {
13   //QRegExp message_alphabet {"[- A-Za-z0-9+./?]*"};
14   QRegExp message_alphabet {"[- @A-Za-z0-9+./?#<>]*"};
15   QRegularExpression cq_re {"(CQ|CQDX|QRZ)[^A-Z0-9/]+"};
16   QRegExpValidator message_validator {message_alphabet};
17   MaidenheadLocatorValidator locator_validator;
18   quint32 quint32_max {std::numeric_limits<quint32>::max ()};
19 
update_dynamic_property(QWidget * widget,char const * property,QVariant const & value)20   void update_dynamic_property (QWidget * widget, char const * property, QVariant const& value)
21   {
22     widget->setProperty (property, value);
23     widget->style ()->unpolish (widget);
24     widget->style ()->polish (widget);
25     widget->update ();
26   }
27 }
28 
IdFilterModel(ClientKey const & key,QObject * parent)29 ClientWidget::IdFilterModel::IdFilterModel (ClientKey const& key, QObject * parent)
30   : QSortFilterProxyModel {parent}
31   , key_ {key}
32   , rx_df_ (quint32_max)
33 {
34 }
35 
data(QModelIndex const & proxy_index,int role) const36 QVariant ClientWidget::IdFilterModel::data (QModelIndex const& proxy_index, int role) const
37 {
38   if (role == Qt::BackgroundRole)
39     {
40       switch (proxy_index.column ())
41         {
42         case 8:                 // message
43           {
44             auto message = QSortFilterProxyModel::data (proxy_index).toString ();
45             if (base_call_re_.pattern ().size ()
46                 && message.contains (base_call_re_))
47               {
48                 return QColor {255,200,200};
49               }
50             if (message.contains (cq_re))
51               {
52                 return QColor {200, 255, 200};
53               }
54           }
55           break;
56 
57         case 4:                 // DF
58           if (qAbs (QSortFilterProxyModel::data (proxy_index).toUInt () - rx_df_) <= 10)
59             {
60               return QColor {255, 200, 200};
61             }
62           break;
63 
64         default:
65           break;
66         }
67     }
68   return QSortFilterProxyModel::data (proxy_index, role);
69 }
70 
filterAcceptsRow(int source_row,QModelIndex const & source_parent) const71 bool ClientWidget::IdFilterModel::filterAcceptsRow (int source_row
72                                                     , QModelIndex const& source_parent) const
73 {
74   auto source_index_col0 = sourceModel ()->index (source_row, 0, source_parent);
75   return sourceModel ()->data (source_index_col0, Qt::UserRole + 1).value<ClientKey> () == key_;
76 }
77 
de_call(QString const & call)78 void ClientWidget::IdFilterModel::de_call (QString const& call)
79 {
80   if (call != call_)
81     {
82       beginResetModel ();
83       if (call.size ())
84         {
85           base_call_re_.setPattern ("[^A-Z0-9]*" + Radio::base_callsign (call) + "[^A-Z0-9]*");
86         }
87       else
88         {
89           base_call_re_.setPattern (QString {});
90         }
91       call_ = call;
92       endResetModel ();
93     }
94 }
95 
rx_df(quint32 df)96 void ClientWidget::IdFilterModel::rx_df (quint32 df)
97 {
98   if (df != rx_df_)
99     {
100       beginResetModel ();
101       rx_df_ = df;
102       endResetModel ();
103     }
104 }
105 
106 namespace
107 {
make_title(MessageServer::ClientKey const & key,QString const & version,QString const & revision)108   QString make_title (MessageServer::ClientKey const& key, QString const& version, QString const& revision)
109   {
110     QString title {QString {"%1(%2)"}.arg (key.second).arg (key.first.toString ())};
111     if (version.size ())
112       {
113         title += QString {" v%1"}.arg (version);
114       }
115     if (revision.size ())
116       {
117         title += QString {" (%1)"}.arg (revision);
118       }
119     return title;
120   }
121 }
122 
ClientWidget(QAbstractItemModel * decodes_model,QAbstractItemModel * beacons_model,ClientKey const & key,QString const & version,QString const & revision,QListWidget const * calls_of_interest,QWidget * parent)123 ClientWidget::ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model
124                             , ClientKey const& key, QString const& version, QString const& revision
125                             , QListWidget const * calls_of_interest, QWidget * parent)
126   : QDockWidget {make_title (key, version, revision), parent}
127   , key_ {key}
128   , done_ {false}
129   , calls_of_interest_ {calls_of_interest}
130   , decodes_proxy_model_ {key}
131   , beacons_proxy_model_ {key}
132   , erase_action_ {new QAction {tr ("&Erase Band Activity"), this}}
133   , erase_rx_frequency_action_ {new QAction {tr ("Erase &Rx Frequency"), this}}
134   , erase_both_action_ {new QAction {tr ("Erase &Both"), this}}
135   , decodes_table_view_ {new QTableView {this}}
136   , beacons_table_view_ {new QTableView {this}}
137   , message_line_edit_ {new QLineEdit {this}}
138   , grid_line_edit_ {new QLineEdit {this}}
139   , generate_messages_push_button_ {new QPushButton {tr ("&Gen Msgs"), this}}
140   , auto_off_button_ {nullptr}
141   , halt_tx_button_ {nullptr}
142   , de_label_ {new QLabel {this}}
143   , frequency_label_ {new QLabel {this}}
144   , tx_df_label_ {new QLabel {this}}
145   , report_label_ {new QLabel {this}}
146   , configuration_line_edit_ {new QLineEdit {this}}
147   , mode_line_edit_ {new QLineEdit {this}}
148   , frequency_tolerance_spin_box_ {new QSpinBox {this}}
149   , tx_mode_label_ {new QLabel {this}}
150   , tx_message_label_ {new QLabel {this}}
151   , submode_line_edit_ {new QLineEdit {this}}
152   , fast_mode_check_box_ {new QCheckBox {this}}
153   , tr_period_spin_box_ {new QSpinBox {this}}
154   , rx_df_spin_box_ {new QSpinBox {this}}
155   , dx_call_line_edit_ {new QLineEdit {this}}
156   , dx_grid_line_edit_ {new QLineEdit {this}}
157   , decodes_page_ {new QWidget {this}}
158   , beacons_page_ {new QWidget {this}}
159   , content_widget_ {new QFrame {this}}
160   , status_bar_ {new QStatusBar {this}}
161   , control_button_box_ {new QDialogButtonBox {this}}
162   , form_layout_ {new QFormLayout}
163   , horizontal_layout_ {new QHBoxLayout}
164   , subform1_layout_ {new QFormLayout}
165   , subform2_layout_ {new QFormLayout}
166   , subform3_layout_ {new QFormLayout}
167   , decodes_layout_ {new QVBoxLayout {decodes_page_}}
168   , beacons_layout_ {new QVBoxLayout {beacons_page_}}
169   , content_layout_ {new QVBoxLayout {content_widget_}}
170   , decodes_stack_ {new QStackedLayout}
171   , columns_resized_ {false}
172 {
173   // set up widgets
174   decodes_proxy_model_.setSourceModel (decodes_model);
175   decodes_table_view_->setModel (&decodes_proxy_model_);
176   decodes_table_view_->verticalHeader ()->hide ();
177   decodes_table_view_->hideColumn (0);
178   decodes_table_view_->horizontalHeader ()->setStretchLastSection (true);
179   decodes_table_view_->setContextMenuPolicy (Qt::ActionsContextMenu);
180   decodes_table_view_->insertAction (nullptr, erase_action_);
181   decodes_table_view_->insertAction (nullptr, erase_rx_frequency_action_);
182   decodes_table_view_->insertAction (nullptr, erase_both_action_);
183 
184   message_line_edit_->setValidator (&message_validator);
185   grid_line_edit_->setValidator (&locator_validator);
186   dx_grid_line_edit_->setValidator (&locator_validator);
187   tr_period_spin_box_->setRange (5, 1800);
188   tr_period_spin_box_->setSuffix (" s");
189   rx_df_spin_box_->setRange (200, 5000);
190   frequency_tolerance_spin_box_->setRange (1, 1000);
191   frequency_tolerance_spin_box_->setPrefix ("\u00b1");
192   frequency_tolerance_spin_box_->setSuffix (" Hz");
193 
194   form_layout_->addRow (tr ("Free text:"), message_line_edit_);
195   form_layout_->addRow (tr ("Temporary grid:"), grid_line_edit_);
196   form_layout_->addRow (tr ("Configuration name:"), configuration_line_edit_);
197   form_layout_->addRow (horizontal_layout_);
198   subform1_layout_->addRow (tr ("Mode:"), mode_line_edit_);
199   subform2_layout_->addRow (tr ("Submode:"), submode_line_edit_);
200   subform3_layout_->addRow (tr ("Fast mode:"), fast_mode_check_box_);
201   subform1_layout_->addRow (tr ("T/R period:"), tr_period_spin_box_);
202   subform2_layout_->addRow (tr ("Rx DF:"), rx_df_spin_box_);
203   subform3_layout_->addRow (tr ("Freq. Tol:"), frequency_tolerance_spin_box_);
204   subform1_layout_->addRow (tr ("DX call:"), dx_call_line_edit_);
205   subform2_layout_->addRow (tr ("DX grid:"), dx_grid_line_edit_);
206   subform3_layout_->addRow (generate_messages_push_button_);
207   horizontal_layout_->addLayout (subform1_layout_);
208   horizontal_layout_->addLayout (subform2_layout_);
209   horizontal_layout_->addLayout (subform3_layout_);
210 
__anon05ac2ec30302(QString const& text) 211   connect (message_line_edit_, &QLineEdit::textEdited, [this] (QString const& text) {
212       Q_EMIT do_free_text (key_, text, false);
213     });
__anon05ac2ec30402() 214   connect (message_line_edit_, &QLineEdit::editingFinished, [this] () {
215       Q_EMIT do_free_text (key_, message_line_edit_->text (), true);
216     });
__anon05ac2ec30502() 217   connect (grid_line_edit_, &QLineEdit::editingFinished, [this] () {
218       Q_EMIT location (key_, grid_line_edit_->text ());
219   });
__anon05ac2ec30602() 220   connect (configuration_line_edit_, &QLineEdit::editingFinished, [this] () {
221       Q_EMIT switch_configuration (key_, configuration_line_edit_->text ());
222   });
__anon05ac2ec30702() 223   connect (mode_line_edit_, &QLineEdit::editingFinished, [this] () {
224       QString empty;
225       Q_EMIT configure (key_, mode_line_edit_->text (), quint32_max, empty, fast_mode ()
226                         , quint32_max, quint32_max, empty, empty, false);
227   });
__anon05ac2ec30802(int i) 228   connect (frequency_tolerance_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) {
229       QString empty;
230       auto f = frequency_tolerance_spin_box_->specialValueText ().size () ? quint32_max : i;
231       Q_EMIT configure (key_, empty, f, empty, fast_mode ()
232                         , quint32_max, quint32_max, empty, empty, false);
233   });
__anon05ac2ec30902() 234   connect (submode_line_edit_, &QLineEdit::editingFinished, [this] () {
235       QString empty;
236       Q_EMIT configure (key_, empty, quint32_max, submode_line_edit_->text (), fast_mode ()
237                         , quint32_max, quint32_max, empty, empty, false);
238   });
__anon05ac2ec30a02(int state) 239   connect (fast_mode_check_box_, &QCheckBox::stateChanged, [this] (int state) {
240       QString empty;
241       Q_EMIT configure (key_, empty, quint32_max, empty, Qt::Checked == state
242                         , quint32_max, quint32_max, empty, empty, false);
243   });
__anon05ac2ec30b02(int i) 244   connect (tr_period_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) {
245       QString empty;
246       Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
247                         , i, quint32_max, empty, empty, false);
248   });
__anon05ac2ec30c02(int i) 249   connect (rx_df_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) {
250       QString empty;
251       Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
252                         , quint32_max, i, empty, empty, false);
253   });
__anon05ac2ec30d02() 254   connect (dx_call_line_edit_, &QLineEdit::editingFinished, [this] () {
255       QString empty;
256       Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
257                         , quint32_max, quint32_max, dx_call_line_edit_->text (), empty, false);
258   });
__anon05ac2ec30e02() 259   connect (dx_grid_line_edit_, &QLineEdit::editingFinished, [this] () {
260       QString empty;
261       Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
262                         , quint32_max, quint32_max, empty, dx_grid_line_edit_->text (), false);
263   });
264 
265   decodes_layout_->setContentsMargins (QMargins {2, 2, 2, 2});
266   decodes_layout_->addWidget (decodes_table_view_);
267   decodes_layout_->addLayout (form_layout_);
268 
269   beacons_proxy_model_.setSourceModel (beacons_model);
270   beacons_table_view_->setModel (&beacons_proxy_model_);
271   beacons_table_view_->verticalHeader ()->hide ();
272   beacons_table_view_->hideColumn (0);
273   beacons_table_view_->horizontalHeader ()->setStretchLastSection (true);
274   beacons_table_view_->setContextMenuPolicy (Qt::ActionsContextMenu);
275   beacons_table_view_->insertAction (nullptr, erase_action_);
276 
277   beacons_layout_->setContentsMargins (QMargins {2, 2, 2, 2});
278   beacons_layout_->addWidget (beacons_table_view_);
279 
280   decodes_stack_->addWidget (decodes_page_);
281   decodes_stack_->addWidget (beacons_page_);
282 
283   // stack alternative views
284   content_layout_->setContentsMargins (QMargins {2, 2, 2, 2});
285   content_layout_->addLayout (decodes_stack_);
286 
287   // set up controls
288   auto_off_button_ = control_button_box_->addButton (tr ("&Auto Off"), QDialogButtonBox::ActionRole);
289   halt_tx_button_ = control_button_box_->addButton (tr ("&Halt Tx"), QDialogButtonBox::ActionRole);
__anon05ac2ec30f02(bool ) 290   connect (generate_messages_push_button_, &QAbstractButton::clicked, [this] (bool /*checked*/) {
291       QString empty;
292       Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
293                         , quint32_max, quint32_max, empty, empty, true);
294   });
__anon05ac2ec31002(bool ) 295   connect (auto_off_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
296       Q_EMIT do_halt_tx (key_, true);
297     });
__anon05ac2ec31102(bool ) 298   connect (halt_tx_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
299       Q_EMIT do_halt_tx (key_, false);
300     });
301   content_layout_->addWidget (control_button_box_);
302 
303   // set up status area
304   status_bar_->addPermanentWidget (de_label_);
305   status_bar_->addPermanentWidget (tx_mode_label_);
306   status_bar_->addPermanentWidget (tx_message_label_);
307   status_bar_->addPermanentWidget (frequency_label_);
308   status_bar_->addPermanentWidget (tx_df_label_);
309   status_bar_->addPermanentWidget (report_label_);
310   content_layout_->addWidget (status_bar_);
311   connect (this, &ClientWidget::topLevelChanged, status_bar_, &QStatusBar::setSizeGripEnabled);
312 
313   // set up central widget
314   content_widget_->setFrameStyle (QFrame::StyledPanel | QFrame::Sunken);
315   setWidget (content_widget_);
316   // setMinimumSize (QSize {550, 0});
317   setAllowedAreas (Qt::BottomDockWidgetArea);
318   setFloating (true);
319 
320   // connect context menu actions
__anon05ac2ec31202(bool ) 321   connect (erase_action_, &QAction::triggered, [this] (bool /*checked*/) {
322       Q_EMIT do_clear_decodes (key_);
323     });
__anon05ac2ec31302(bool ) 324   connect (erase_rx_frequency_action_, &QAction::triggered, [this] (bool /*checked*/) {
325       Q_EMIT do_clear_decodes (key_, 1);
326     });
__anon05ac2ec31402(bool ) 327   connect (erase_both_action_, &QAction::triggered, [this] (bool /*checked*/) {
328       Q_EMIT do_clear_decodes (key_, 2);
329     });
330 
331   // connect up table view signals
__anon05ac2ec31502(QModelIndex const& index) 332   connect (decodes_table_view_, &QTableView::doubleClicked, [this] (QModelIndex const& index) {
333       Q_EMIT do_reply (decodes_proxy_model_.mapToSource (index), QApplication::keyboardModifiers () >> 24);
334     });
335 
336   // tell new client about calls of interest
337   for (int row = 0; row < calls_of_interest_->count (); ++row)
338     {
339       Q_EMIT highlight_callsign (key_, calls_of_interest_->item (row)->text (), QColor {Qt::blue}, QColor {Qt::yellow});
340     }
341 }
342 
dispose()343 void ClientWidget::dispose ()
344 {
345   done_ = true;
346   close ();
347 }
348 
closeEvent(QCloseEvent * e)349 void ClientWidget::closeEvent (QCloseEvent *e)
350 {
351   if (!done_)
352     {
353       Q_EMIT do_close (key_);
354       e->ignore ();      // defer closure until client actually closes
355     }
356   else
357     {
358       QDockWidget::closeEvent (e);
359     }
360 }
361 
~ClientWidget()362 ClientWidget::~ClientWidget ()
363 {
364   for (int row = 0; row < calls_of_interest_->count (); ++row)
365     {
366       // tell client to forget calls of interest
367       Q_EMIT highlight_callsign (key_, calls_of_interest_->item (row)->text ());
368     }
369 }
370 
fast_mode() const371 bool ClientWidget::fast_mode () const
372 {
373   return fast_mode_check_box_->isChecked ();
374 }
375 
376 namespace
377 {
update_line_edit(QLineEdit * le,QString const & value,bool allow_empty=true)378   void update_line_edit (QLineEdit * le, QString const& value, bool allow_empty = true)
379   {
380     le->setEnabled (value.size () || allow_empty);
381     if (!(le->hasFocus () && le->isModified ()))
382       {
383         le->setText (value);
384       }
385   }
386 
update_spin_box(QSpinBox * sb,int value,QString const & special_value=QString{})387   void update_spin_box (QSpinBox * sb, int value, QString const& special_value = QString {})
388   {
389     sb->setSpecialValueText (special_value);
390     bool enable {0 == special_value.size ()};
391     sb->setEnabled (enable);
392     if (!sb->hasFocus () && enable)
393       {
394         sb->setValue (value);
395       }
396   }
397 }
398 
update_status(ClientKey const & key,Frequency f,QString const & mode,QString const & dx_call,QString const & report,QString const & tx_mode,bool tx_enabled,bool transmitting,bool decoding,quint32 rx_df,quint32 tx_df,QString const & de_call,QString const & de_grid,QString const & dx_grid,bool watchdog_timeout,QString const & submode,bool fast_mode,quint8 special_op_mode,quint32 frequency_tolerance,quint32 tr_period,QString const & configuration_name,QString const & tx_message)399 void ClientWidget::update_status (ClientKey const& key, Frequency f, QString const& mode, QString const& dx_call
400                                   , QString const& report, QString const& tx_mode, bool tx_enabled
401                                   , bool transmitting, bool decoding, quint32 rx_df, quint32 tx_df
402                                   , QString const& de_call, QString const& de_grid, QString const& dx_grid
403                                   , bool watchdog_timeout, QString const& submode, bool fast_mode
404                                   , quint8 special_op_mode, quint32 frequency_tolerance, quint32 tr_period
405                                   , QString const& configuration_name, QString const& tx_message)
406 {
407     if (key == key_)
408     {
409       fast_mode_check_box_->setChecked (fast_mode);
410       decodes_proxy_model_.de_call (de_call);
411       decodes_proxy_model_.rx_df (rx_df);
412       QString special;
413       switch (special_op_mode)
414         {
415         case 1: special = "[NA VHF]"; break;
416         case 2: special = "[EU VHF]"; break;
417         case 3: special = "[FD]"; break;
418         case 4: special = "[RTTY RU]"; break;
419         case 5: special = "[WW DIGI]"; break;
420         case 6: special = "[Fox]"; break;
421         case 7: special = "[Hound]"; break;
422         default: break;
423         }
424       de_label_->setText (de_call.size () >= 0 ? QString {"DE: %1%2%3"}.arg (de_call)
425                           .arg (de_grid.size () ? '(' + de_grid + ')' : QString {})
426                           .arg (special)
427                           : QString {});
428       update_line_edit (mode_line_edit_, mode);
429       update_spin_box (frequency_tolerance_spin_box_, frequency_tolerance
430                        , quint32_max == frequency_tolerance ? QString {"n/a"} : QString {});
431       update_line_edit (submode_line_edit_, submode, false);
432       tx_mode_label_->setText (tx_mode.isEmpty () || tx_mode == mode ? "" : "Tx Mode: (" + tx_mode + ')');
433       tx_message_label_->setText (tx_message.isEmpty () ? "" : "Tx Msg: " + tx_message.trimmed ());
434       frequency_label_->setText ("QRG: " + Radio::pretty_frequency_MHz_string (f));
435       update_line_edit (dx_call_line_edit_, dx_call);
436       update_line_edit (dx_grid_line_edit_, dx_grid);
437       if (rx_df != quint32_max) update_spin_box (rx_df_spin_box_, rx_df);
438       update_spin_box (tr_period_spin_box_, tr_period
439                        , quint32_max == tr_period ? QString {"n/a"} : QString {});
440       tx_df_label_->setText (QString {"Tx: %1"}.arg (tx_df));
441       report_label_->setText ("SNR: " + report);
442       update_dynamic_property (frequency_label_, "transmitting", transmitting);
443       auto_off_button_->setEnabled (tx_enabled);
444       halt_tx_button_->setEnabled (transmitting);
445       update_line_edit (configuration_line_edit_, configuration_name);
446       update_dynamic_property (mode_line_edit_, "decoding", decoding);
447       update_dynamic_property (tx_df_label_, "watchdog_timeout", watchdog_timeout);
448     }
449 }
450 
decode_added(bool,ClientKey const & key,QTime,qint32,float,quint32,QString const &,QString const &,bool,bool)451 void ClientWidget::decode_added (bool /*is_new*/, ClientKey const& key, QTime /*time*/, qint32 /*snr*/
452                                  , float /*delta_time*/, quint32 /*delta_frequency*/, QString const& /*mode*/
453                                  , QString const& /*message*/, bool /*low_confidence*/, bool /*off_air*/)
454 {
455   if (key == key_ && !columns_resized_)
456     {
457       decodes_stack_->setCurrentIndex (0);
458       decodes_table_view_->resizeColumnsToContents ();
459       columns_resized_ = true;
460     }
461   decodes_table_view_->scrollToBottom ();
462 }
463 
beacon_spot_added(bool,ClientKey const & key,QTime,qint32,float,Frequency,qint32,QString const &,QString const &,qint32,bool)464 void ClientWidget::beacon_spot_added (bool /*is_new*/, ClientKey const& key, QTime /*time*/, qint32 /*snr*/
465                                       , float /*delta_time*/, Frequency /*delta_frequency*/, qint32 /*drift*/
466                                       , QString const& /*callsign*/, QString const& /*grid*/, qint32 /*power*/
467                                       , bool /*off_air*/)
468 {
469   if (key == key_ && !columns_resized_)
470     {
471       decodes_stack_->setCurrentIndex (1);
472       beacons_table_view_->resizeColumnsToContents ();
473       columns_resized_ = true;
474     }
475   beacons_table_view_->scrollToBottom ();
476 }
477 
decodes_cleared(ClientKey const & key)478 void ClientWidget::decodes_cleared (ClientKey const& key)
479 {
480   if (key == key_)
481     {
482       columns_resized_ = false;
483     }
484 }
485 
486 #include "moc_ClientWidget.cpp"
487