swift
textmessagecomponent.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2013 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QApplication>
7 #include <QLayout>
8 #include <QLineEdit>
9 #include <QPushButton>
10 #include <QStringBuilder>
11 #include <QTabWidget>
12 #include <QToolButton>
13 #include <QVBoxLayout>
14 #include <QWidget>
15 #include <Qt>
16 #include <QtGlobal>
17 
18 #include "ui_textmessagecomponent.h"
19 
20 #include "core/application.h"
24 #include "core/corefacade.h"
25 #include "gui/dockwidgetinfoarea.h"
26 #include "gui/guiapplication.h"
31 #include "misc/aviation/callsign.h"
33 #include "misc/iterator.h"
34 #include "misc/logmessage.h"
37 #include "misc/network/user.h"
38 #include "misc/pq/constants.h"
39 #include "misc/pq/frequency.h"
40 #include "misc/pq/units.h"
41 #include "misc/sequence.h"
42 #include "misc/verify.h"
43 
44 using namespace swift::core;
45 using namespace swift::core::context;
46 using namespace swift::misc;
47 using namespace swift::gui;
48 using namespace swift::gui::settings;
49 using namespace swift::gui::views;
50 using namespace swift::misc::network;
51 using namespace swift::misc::audio;
52 using namespace swift::misc::aviation;
53 using namespace swift::misc::physical_quantities;
54 using namespace swift::misc::simulation;
55 
56 namespace swift::gui::components
57 {
58  CTextMessageComponent::CTextMessageComponent(QWidget *parent) : QFrame(parent), ui(new Ui::CTextMessageComponent)
59  {
60  ui->setupUi(this);
61  ui->tw_TextMessages->setCurrentIndex(0);
62  ui->fr_TextMessage->setVisible(false);
63  ui->tvp_TextMessagesAll->setResizeMode(CTextMessageView::ResizingAuto);
64  ui->tvp_TextMessagesAll->setWordWrap(false);
65  ui->comp_AtcStations->setWithIcons(false);
66 
67  // lep_textMessages is the own line edit
68  bool c = connect(ui->lep_TextMessages, &CLineEditHistory::returnPressedUnemptyLine, this,
69  &CTextMessageComponent::textMessageEntered, Qt::QueuedConnection);
70  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
71  c = connect(ui->gb_Settings, &QGroupBox::toggled, this, &CTextMessageComponent::onSettingsChecked,
72  Qt::QueuedConnection);
73  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
74  c = connect(ui->gb_MessageTo, &QGroupBox::toggled, this, &CTextMessageComponent::onMessageToChecked,
75  Qt::QueuedConnection);
76  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
77 
78  c = connect(ui->comp_AtcStations, &CAtcButtonComponent::requestAtcStation, this,
79  &CTextMessageComponent::onAtcButtonClicked, Qt::QueuedConnection);
80  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
81 
82  c = connect(ui->cb_LatestFirst, &QCheckBox::toggled, this, &CTextMessageComponent::onLatestFirstChanged,
83  Qt::QueuedConnection);
84  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
85 
86  // style sheet
87  c = connect(sGui, &CGuiApplication::styleSheetsChanged, this, &CTextMessageComponent::onStyleSheetChanged,
88  Qt::QueuedConnection);
89  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
90  c = connect(ui->comp_SettingsStyle, &CSettingsTextMessageStyle::changed, this,
91  &CTextMessageComponent::updateSettings, Qt::QueuedConnection);
92  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
93 
95  {
97  &CCoreFacade::parseCommandLine, Qt::QueuedConnection);
98  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
99  c = connect(sGui->getIContextNetwork(), &IContextNetwork::textMessagesReceived, this,
100  &CTextMessageComponent::onTextMessageReceived, Qt::QueuedConnection);
101  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
102  c = connect(sGui->getIContextNetwork(), &IContextNetwork::textMessageSent, this,
103  &CTextMessageComponent::onTextMessageSent, Qt::QueuedConnection);
104  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
105  c = connect(sGui->getIContextOwnAircraft(), &IContextOwnAircraft::changedAircraftCockpit, this,
106  &CTextMessageComponent::onChangedAircraftCockpit, Qt::QueuedConnection);
107  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
108  }
109  Q_UNUSED(c)
110 
111  // init by settings
112  const QPointer<CTextMessageComponent> myself(this);
113  QTimer::singleShot(2000, this, [=] {
114  // init decoupled when sub components are fully init
115  if (!myself || !sGui || sGui->isShuttingDown()) { return; }
116  this->onSettingsChanged(); // init
117  this->showCurrentFrequenciesFromCockpit();
118  const bool latestFirst = m_messageSettings.get().isLatestFirst();
119  ui->tvp_TextMessagesAll->setSorting(CTextMessage::IndexUtcTimestamp,
120  latestFirst ? Qt::DescendingOrder : Qt::AscendingOrder);
121 
122  // hide for the beginning
123  ui->gb_Settings->setChecked(false);
124  ui->gb_MessageTo->setChecked(false);
125  });
126  }
127 
129 
131  {
132  bool c = CEnableForDockWidgetInfoArea::setParentDockWidgetInfoArea(parentDockableWidget);
133  c = c && connect(this->getDockWidgetInfoArea(), &CDockWidgetInfoArea::widgetTopLevelChanged, this,
134  &CTextMessageComponent::topLevelChanged, Qt::QueuedConnection);
135  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
136  return c;
137  }
138 
139  QWidget *CTextMessageComponent::getTabWidget(TextMessageTab tab) const
140  {
141  switch (tab)
142  {
143  case TextMessagesAll: return ui->tb_TextMessagesAll;
144  case TextMessagesCom1: return ui->tb_TextMessagesCOM1;
145  case TextMessagesCom2: return ui->tb_TextMessagesCOM2;
146  case TextMessagesUnicom: return ui->tb_TextMessagesUnicom;
147  default: Q_ASSERT_X(false, Q_FUNC_INFO, "Wrong index"); break;
148  }
149  return nullptr;
150  }
151 
152  CTextMessageTextEdit *CTextMessageComponent::getTextEdit(TextMessageTab tab) const
153  {
154  QWidget *w = this->getTabWidget(tab);
155  if (!w) { return nullptr; }
156  return this->findChild<CTextMessageTextEdit *>();
157  }
158 
159  void CTextMessageComponent::selectTabWidget(TextMessageTab tab)
160  {
161  QWidget *w = this->getTabWidget(tab);
162  if (w) { ui->tw_TextMessages->setCurrentWidget(w); }
163  }
164 
165  void CTextMessageComponent::selectTabWidget(const CCallsign &callsign, bool addIfNotExisting)
166  {
167  QWidget *tab = this->findTextMessageTabByCallsign(callsign);
168  if (!tab && addIfNotExisting) { tab = this->addNewTextMessageTab(callsign); }
169  if (!tab) { return; }
170  ui->tw_TextMessages->setCurrentWidget(tab);
171  }
172 
173  bool CTextMessageComponent::isCloseableTab(const QWidget *tabWidget) const
174  {
175  if (!tabWidget) { return false; }
176  return (tabWidget != ui->tb_TextMessagesAll && tabWidget != ui->tb_TextMessagesCOM1 &&
177  tabWidget != ui->tb_TextMessagesCOM2 && tabWidget != ui->tb_TextMessagesUnicom);
178  }
179 
180  void CTextMessageComponent::displayTextMessage(const CTextMessageList &messages)
181  {
182  using namespace std::chrono_literals;
183  if (messages.isEmpty()) { return; }
184  if (!sGui || sGui->isShuttingDown()) { return; }
185  const CSimulatedAircraft ownAircraft(this->getOwnAircraft());
186  const CTextMessageSettings msgSettings(m_messageSettings.getThreadLocal());
187  const bool playNotification = sGui && sGui->getIContextAudio();
188  const bool audioCsMentioned = playNotification && m_audioSettings.get().textCallsignMentioned();
189 
190  bool addedToAllMessages = false;
191  for (const CTextMessage &message : messages)
192  {
193  bool relevantForMe = false;
194  CNotificationSounds::NotificationFlag notification = CNotificationSounds::NoNotifications;
195 
196  // SELCAL
197  if (!m_usedAsOverlayWidget && message.isSelcalMessage() &&
198  ownAircraft.isSelcalSelected(message.getSelcalCode()))
199  {
200  // this is SELCAL for me
201  if (playNotification) { sGui->getCContextAudioBase()->playSelcalTone(message.getSelcalCode()); }
202 
203  if (msgSettings.popupSelcalMessages())
204  {
205  CStatusMessage msg = CLogMessage(this).info(u"SELCAL received");
206  this->emitDisplayInInfoWindow(CVariant::from(msg), 3s);
207  }
208  continue;
209  }
210 
211  // UNICOM
212  if (message.isSendToUnicom())
213  {
214  ui->tep_TextMessagesUnicom->insertTextMessage(message);
215 
216  // Message was received from others
217  if (!message.wasSent()) { notification = CNotificationSounds::NotificationTextMessageUnicom; }
218  relevantForMe = true;
219  }
220 
221  // check message, handle special cases first
222  if (message.isServerMessage())
223  {
224  // void
225  }
226  else if (message.isBroadcastMessage())
227  {
228  // FAKE private message
229  this->addPrivateChannelTextMessage(message);
230  relevantForMe = true;
231  }
232  else if (message.isRadioMessage())
233  {
234  // check for own COM frequencies
235  if (message.isSendToFrequency(ownAircraft.getCom1System().getFrequencyActive()))
236  {
237  ui->tep_TextMessagesCOM1->insertTextMessage(message);
238  if (!message.isSendToUnicom())
239  {
240  notification = CNotificationSounds::NotificationTextMessageFrequency;
241  }
242  relevantForMe = true;
243  }
244  if (message.isSendToFrequency(ownAircraft.getCom2System().getFrequencyActive()))
245  {
246  ui->tep_TextMessagesCOM2->insertTextMessage(message);
247  if (!message.isSendToUnicom())
248  {
249  notification = CNotificationSounds::NotificationTextMessageFrequency;
250  }
251  relevantForMe = true;
252  }
253 
254  // callsign mentioned notification
255  if (relevantForMe && audioCsMentioned && ownAircraft.hasCallsign() &&
256  message.mentionsCallsign(ownAircraft.getCallsign()))
257  {
258  notification = CNotificationSounds::NotificationTextCallsignMentioned;
259  // Flash taskbar icon
260  QApplication::alert(QWidget::topLevelWidget());
261  }
262  }
263  else if (message.isPrivateMessage())
264  {
265  // private message
266  this->addPrivateChannelTextMessage(message);
267  relevantForMe = true;
268  // Flash taskbar icon
269  QApplication::alert(QWidget::topLevelWidget());
270  }
271  else
272  {
273  SWIFT_AUDIT_X(false, Q_FUNC_INFO, "Wrong message type");
274  continue;
275  }
276 
277  // message for me? right frequency? otherwise quit
278  if (this->hasAllMessagesTab() && (relevantForMe || message.isServerMessage()))
279  {
280  ui->tvp_TextMessagesAll->push_back(message); // no sorting
281  ui->tvp_TextMessagesAll->resort();
282  addedToAllMessages = true;
283  }
284  if (!relevantForMe) { continue; }
285 
286  // Play notification
287  if (playNotification && notification != CNotificationSounds::NoNotifications)
288  {
289  sGui->getCContextAudioBase()->playNotification(notification, true);
290  }
291 
292  // overlay message if this channel is not selected
293  if (message.isServerMessage()) { continue; }
294  if (message.isBroadcastMessage()) { continue; }
295 
296  if (!message.wasSent() && !message.isSendToUnicom())
297  {
298  // if the channel is selected, do nothing
299  if (!this->isCorrespondingTextMessageTabSelected(message))
300  {
301  if (msgSettings.popup(message, ownAircraft))
302  {
303  this->emitDisplayInInfoWindow(CVariant::from(message), 15s);
304  }
305  }
306  } // message
307  } // for
308 
309  if (addedToAllMessages && ui->tvp_TextMessagesAll->isSortedByTimestampPropertyLatestLast())
310  {
311  ui->tvp_TextMessagesAll->scrollToBottom();
312  }
313  }
314 
315  void CTextMessageComponent::onChangedAircraftCockpit(const CSimulatedAircraft &aircraft,
316  const CIdentifier &originator)
317  {
318  // this is called for every overlay widget as well
319  Q_UNUSED(originator)
320  if (!this->isActivated()) { return; }
321  this->showCurrentFrequenciesFromCockpit(aircraft);
322  }
323 
324  void CTextMessageComponent::onSettingsChecked(bool checked)
325  {
326  ui->comp_SettingsOverlay->setVisible(checked);
327  ui->comp_SettingsStyle->setVisible(checked);
328  ui->cb_LatestFirst->setVisible(checked);
329  ui->gb_Settings->setFlat(!checked);
330  }
331 
332  void CTextMessageComponent::onMessageToChecked(bool checked)
333  {
334  ui->comp_AtcStations->setVisible(checked);
335  ui->gb_MessageTo->setFlat(!checked);
336  if (checked) { ui->comp_AtcStations->updateStations(); }
337  }
338 
339  void CTextMessageComponent::onSettingsChanged()
340  {
341  QList<CTextMessageTextEdit *> textEdits = this->findAllTextEdit();
342  const QString style = this->getStyleSheet();
343  const bool latestFirst = m_messageSettings.get().isLatestFirst();
344  for (CTextMessageTextEdit *textEdit : textEdits)
345  {
346  textEdit->setLatestFirst(latestFirst);
347  textEdit->setStyleSheetForContent(style);
348  }
349  ui->comp_SettingsStyle->setStyle(this->getStyleSheet());
350  if (latestFirst != ui->cb_LatestFirst->isChecked()) { ui->cb_LatestFirst->setChecked(latestFirst); }
351  this->update(); // refresh window
352  }
353 
354  void CTextMessageComponent::onLatestFirstChanged(bool checked)
355  {
356  CTextMessageSettings s = m_messageSettings.get();
357  if (s.isLatestFirst() == checked) { return; }
358  s.setLatestFirst(checked);
359  const CStatusMessage m = m_messageSettings.setAndSave(s);
360  CLogMessage::preformatted(m);
361  this->onSettingsChanged(); // latest first
362  }
363 
364  void CTextMessageComponent::onStyleSheetChanged()
365  {
366  this->onSettingsChanged(); // style sheet
367  }
368 
369  void CTextMessageComponent::onAtcButtonClicked(const CAtcStation &station)
370  {
371  if (station.getCallsign().isEmpty()) { return; }
372  this->addNewTextMessageTab(station.getCallsign());
373  }
374 
375  void CTextMessageComponent::updateSettings()
376  {
377  const QString style = ui->comp_SettingsStyle->getStyle();
378  CTextMessageSettings s = m_messageSettings.get();
379  s.setStyleSheet(style);
380  s.setLatestFirst(ui->cb_LatestFirst->isChecked());
381  const CStatusMessage m = m_messageSettings.setAndSave(s);
382  CLogMessage::preformatted(m);
383  this->onStyleSheetChanged();
384  }
385 
386  QList<CTextMessageTextEdit *> CTextMessageComponent::findAllTextEdit() const
387  {
388  return this->findChildren<CTextMessageTextEdit *>();
389  }
390 
391  QString CTextMessageComponent::getStyleSheet() const
392  {
393  const QString styleSheet = m_messageSettings.get().getStyleSheet();
394  return styleSheet.isEmpty() && sGui ?
396  styleSheet;
397  }
398 
399  bool CTextMessageComponent::isCorrespondingTextMessageTabSelected(const CTextMessage &textMessage) const
400  {
401  if (!this->isVisibleWidgetHack()) { return false; }
402  if (!textMessage.hasValidRecipient()) { return false; }
403  if (textMessage.isEmpty()) { return false; } // ignore empty message
404  if (textMessage.isPrivateMessage())
405  {
406  // private message
407  const CCallsign cs = textMessage.getSenderCallsign();
408  if (cs.isEmpty()) { return false; }
409  const QWidget *tab = this->findTextMessageTabByCallsign(cs, false);
410  if (!tab) { return false; }
411  return ui->tw_TextMessages->currentWidget() == tab;
412  }
413  else
414  {
415  // frequency message
416  const CSimulatedAircraft ownAircraft(this->getOwnAircraft());
417  if (ui->tw_TextMessages->currentWidget() == ui->tb_TextMessagesAll) { return true; }
418  if (textMessage.isSendToFrequency(ownAircraft.getCom1System().getFrequencyActive()))
419  {
420  return ui->tw_TextMessages->currentWidget() == ui->tb_TextMessagesCOM1;
421  }
422  if (textMessage.isSendToFrequency(ownAircraft.getCom2System().getFrequencyActive()))
423  {
424  return ui->tw_TextMessages->currentWidget() == ui->tb_TextMessagesCOM2;
425  }
426  return false;
427  }
428  }
429 
430  bool CTextMessageComponent::isNetworkConnected() const
431  {
433  }
434 
435  void CTextMessageComponent::showCurrentFrequenciesFromCockpit()
436  {
437  const CSimulatedAircraft ownAircraft = this->getOwnAircraft();
438  this->showCurrentFrequenciesFromCockpit(ownAircraft);
439  }
440 
441  void CTextMessageComponent::showCurrentFrequenciesFromCockpit(const CSimulatedAircraft &ownAircraft)
442  {
443  const CFrequency freq1 = ownAircraft.getCom1System().getFrequencyActive();
444  const CFrequency freq2 = ownAircraft.getCom2System().getFrequencyActive();
445 
446  CAtcStationList f1Stations;
447  CAtcStationList f2Stations;
448  if (sGui && sGui->getIContextNetwork())
449  {
450  f1Stations = sGui->getIContextNetwork()->getOnlineStationsForFrequency(freq1);
451  f2Stations = sGui->getIContextNetwork()->getOnlineStationsForFrequency(freq2);
452  }
453 
454  const QString f1n = QString::asprintf("%03.3f", freq1.valueRounded(CFrequencyUnit::MHz(), 3));
455  const QString f2n = QString::asprintf("%03.3f", freq2.valueRounded(CFrequencyUnit::MHz(), 3));
456  QString f1 = QStringLiteral("COM1: %1").arg(f1n);
457  QString f2 = QStringLiteral("COM2: %1").arg(f2n);
458  if (f1Stations.size() == 1) { f1 += u' ' % f1Stations.front().getCallsignAndControllerRealName(); }
459  if (f2Stations.size() == 1) { f2 += u' ' % f2Stations.front().getCallsignAndControllerRealName(); }
460 
461  ui->tb_TextMessagesCOM1->setToolTip(f1);
462  ui->tb_TextMessagesCOM1->setToolTip(f2);
463 
464  ui->tw_TextMessages->setTabText(ui->tw_TextMessages->indexOf(ui->tb_TextMessagesCOM1), f1);
465  ui->tw_TextMessages->setTabText(ui->tw_TextMessages->indexOf(ui->tb_TextMessagesCOM2), f2);
466 
467  // update tabs
468  this->updateAllTabs();
469  }
470 
471  QWidget *CTextMessageComponent::addNewTextMessageTab(const CCallsign &callsign)
472  {
473  if (callsign.isEmpty()) { return nullptr; }
474  QWidget *w = this->findTextMessageTabByCallsign(callsign, false);
475  if (w) { return w; }
476 
477  const QString tabName = callsign.asString();
478  const QString style = this->getStyleSheet();
479  const bool supervisor = callsign.isSupervisorCallsign();
480  QWidget *newTabWidget = new QWidget(this);
481  newTabWidget->setObjectName(u"Tab widget " % tabName);
482  newTabWidget->setProperty("callsign", callsign.asString());
483  QPushButton *closeButton = new QPushButton("Close", newTabWidget);
484  QVBoxLayout *layout = new QVBoxLayout(newTabWidget);
485  CTextMessageTextEdit *textEdit = new CTextMessageTextEdit(newTabWidget);
486  textEdit->setObjectName("tep_" + tabName);
487  int marginLeft, marginRight, marginTop, marginBottom;
488  ui->tb_TextMessagesAll->layout()->getContentsMargins(&marginLeft, &marginTop, &marginRight, &marginBottom);
489  newTabWidget->layout()->setContentsMargins(marginLeft, marginTop, marginRight, 2);
490  layout->addWidget(textEdit);
491  layout->addWidget(closeButton);
492  newTabWidget->setLayout(layout);
493  textEdit->setContextMenuPolicy(Qt::CustomContextMenu);
494  textEdit->setProperty("supervisormsg", supervisor);
495  textEdit->setStyleSheetForContent(style);
496 
497  const int index = ui->tw_TextMessages->addTab(newTabWidget, this->getCallsignAndRealName(callsign));
498  QToolButton *closeButtonInTab = new QToolButton(newTabWidget);
499  closeButtonInTab->setText("[X]");
500  closeButtonInTab->setProperty("supervisormsg", supervisor);
501  QTabBar *bar = ui->tw_TextMessages->tabBar();
502  bar->setTabButton(index, QTabBar::RightSide, closeButtonInTab); // changes parent
503  if (supervisor)
504  {
506  bar->setTabIcon(index, CIcon(callsign.toIcon()).toQIcon());
507  bar->setTabTextColor(index, QColor(Qt::yellow));
508  }
509  ui->tw_TextMessages->setCurrentIndex(index);
510  closeButtonInTab->setProperty("tabName", tabName);
511  closeButton->setProperty("tabName", tabName);
512 
513  connect(closeButton, &QPushButton::released, this, &CTextMessageComponent::closeTextMessageTab);
514  connect(closeButtonInTab, &QPushButton::released, this, &CTextMessageComponent::closeTextMessageTab);
515 
516  this->setTabWidgetDescription(callsign, index);
517  return newTabWidget;
518  }
519 
520  void CTextMessageComponent::setTabWidgetDescription(const CCallsign &callsign, int widgetIndex)
521  {
522  if (callsign.isEmpty()) { return; }
523 
524  QString realName;
526  {
527  realName = sGui->getIContextNetwork()->getUserForCallsign(callsign).getRealName();
528  if (!realName.isEmpty()) { ui->tw_TextMessages->setTabToolTip(widgetIndex, realName); }
529  }
530  const QString tt = realName.isEmpty() ? callsign.asString() : callsign.asString() % u": " % realName;
531  ui->tw_TextMessages->setTabText(widgetIndex, tt);
532  }
533 
534  QString CTextMessageComponent::getCallsignAndRealName(const CCallsign &callsign) const
535  {
536  if (callsign.isEmpty()) { return {}; }
537  if (m_showRealNames && sGui && !sGui->isShuttingDown() && sGui->getIContextNetwork())
538  {
539  const QString realName = sGui->getIContextNetwork()->getUserForCallsign(callsign).getRealName();
540  if (!realName.isEmpty()) { return callsign.asString() % u": " % realName; }
541  }
542  return callsign.asString();
543  }
544 
545  void CTextMessageComponent::updateAllTabs()
546  {
547  for (int index = ui->tw_TextMessages->count() - 1; index >= 0; index--)
548  {
549  QWidget *tab = ui->tw_TextMessages->widget(index);
550  if (!tab) { continue; }
551  if (!tab->toolTip().isEmpty()) { continue; }
552  const QString cs = tab->property("callsign").toString();
553  if (cs.isEmpty()) { continue; }
554  this->setTabWidgetDescription(CCallsign(cs), index);
555  }
556  }
557 
558  void CTextMessageComponent::addPrivateChannelTextMessage(const CTextMessage &textMessage)
559  {
560  if (!textMessage.isPrivateMessage()) { return; }
561  CCallsign cs = textMessage.wasSent() ? textMessage.getRecipientCallsign() : textMessage.getSenderCallsign();
562  if (cs.isEmpty()) { return; }
563 
564  const bool isBroadcast = textMessage.isBroadcastMessage();
565  if (isBroadcast) { cs.markAsBroadcastCallsign(); }
566 
567  const bool isWallopMessage = textMessage.isWallopMessage();
568  if (isWallopMessage) { cs.markAsWallopCallsign(); }
569 
570  const QWidget *tab = this->findTextMessageTabByCallsign(cs);
571  if (!tab) { tab = this->addNewTextMessageTab(cs); }
572  Q_ASSERT_X(tab, Q_FUNC_INFO, "Missing tab");
573  CTextMessageTextEdit *textEdit = tab->findChild<CTextMessageTextEdit *>();
574  SWIFT_VERIFY_X(textEdit, Q_FUNC_INFO, "Missing text edit");
575  if (!textEdit) { return; } // do not crash, though this situation should not happen
576  textEdit->insertTextMessage(textMessage);
577 
578  // sound
579  if (textMessage.isServerMessage()) { return; }
580  if (isBroadcast) { return; }
581 
582  const bool playSound = !textMessage.wasSent() && !m_usedAsOverlayWidget && sGui && !sGui->isShuttingDown() &&
584  if (sGui && sGui->getIContextAudio() && playSound)
585  {
586  const CSettings settings = m_audioSettings.get();
587  if (textMessage.isSupervisorMessage() && settings.textMessageSupervisor())
588  {
589  sGui->getCContextAudioBase()->playNotification(CNotificationSounds::NotificationTextMessageSupervisor,
590  true);
591  }
592  else if (textMessage.isPrivateMessage() && settings.textMessagePrivate())
593  {
594  sGui->getCContextAudioBase()->playNotification(CNotificationSounds::NotificationTextMessagePrivate,
595  true);
596  }
597  }
598  }
599 
600  CSimulatedAircraft CTextMessageComponent::getOwnAircraft() const
601  {
602  if (!sGui || !sGui->getIContextOwnAircraft()) { return CSimulatedAircraft(); }
604  }
605 
606  QWidget *CTextMessageComponent::findTextMessageTabByCallsign(const CCallsign &callsign,
607  bool callsignResolution) const
608  {
609  // search the private message tabs by property first
610  for (int index = ui->tw_TextMessages->count() - 1; index >= 0; index--)
611  {
612  QWidget *tab = ui->tw_TextMessages->widget(index);
613  if (tab && tab->property("callsign").toString() == callsign.asString()) { return tab; }
614  }
615 
616  QWidget *w = this->findTextMessageTabByName(callsign.asString());
617  if (w) { return w; }
618  if (!callsignResolution) { return nullptr; }
619 
620  // resolve callsign to COM tab
621  if (!sGui || !sGui->getIContextNetwork()) { return nullptr; }
622  const CAtcStation station(sGui->getIContextNetwork()->getOnlineStationForCallsign(callsign));
623  if (!station.getCallsign().isEmpty())
624  {
625  const CSimulatedAircraft ownAircraft(this->getOwnAircraft());
626  if (ownAircraft.getCom1System().isActiveFrequencySameFrequency(station.getFrequency()))
627  {
628  return this->getTabWidget(TextMessagesCom1);
629  }
630  else if (ownAircraft.getCom2System().isActiveFrequencySameFrequency(station.getFrequency()))
631  {
632  return this->getTabWidget(TextMessagesCom2);
633  }
634  }
635  return nullptr;
636  }
637 
638  QWidget *CTextMessageComponent::findTextMessageTabByName(const QString &name) const
639  {
640  if (name.isEmpty()) { return nullptr; }
641 
642  // search the private message tabs first
643  for (int index = ui->tw_TextMessages->count() - 1; index >= 0; index--)
644  {
645  const QString tabName = ui->tw_TextMessages->tabText(index);
646  if (!tabName.startsWith(name, Qt::CaseInsensitive)) { continue; }
647  QWidget *tab = ui->tw_TextMessages->widget(index);
648  return tab;
649  }
650  return nullptr;
651  }
652 
653  void CTextMessageComponent::closeTextMessageTab()
654  {
655  int index = -1;
656  const QObject *sender = QObject::sender(); // the button
657  const QString tabName = sender->property("tabName").toString();
658  QWidget *tw = this->findTextMessageTabByName(tabName);
659  if (!this->isCloseableTab(tw)) { return; }
660  if (tw) { index = ui->tw_TextMessages->indexOf(tw); }
661  if (index >= 0) { ui->tw_TextMessages->removeTab(index); }
662  }
663 
664  void CTextMessageComponent::topLevelChanged(QWidget *widget, bool topLevel)
665  {
666  // own input field if floating window
667  Q_UNUSED(widget);
668  ui->fr_TextMessage->setVisible(topLevel);
669  }
670 
671  void CTextMessageComponent::textMessageEntered()
672  {
673  if (!ui->fr_TextMessage->isVisible() || !ui->lep_TextMessages->isVisible()) { return; }
674  if (!this->isVisible()) { return; }
675 
676  const QString cl(ui->lep_TextMessages->getLastEnteredLineFormatted());
677  if (!cl.isEmpty()) { this->handleEnteredTextMessage(cl); }
678  }
679 
680  bool CTextMessageComponent::isVisibleWidgetHack() const
681  {
682  return m_usedAsOverlayWidget ? true : this->isVisibleWidget();
683  }
684 
685  CCallsign CTextMessageComponent::getCallsignPropertyForTab(int tabIndex, bool validated) const
686  {
687  if (tabIndex < 0 || tabIndex >= ui->tw_TextMessages->count()) { return {}; }
688  QWidget *tab = ui->tw_TextMessages->widget(tabIndex);
689  if (tab && !tab->property("callsign").toString().isEmpty())
690  {
691  const CCallsign cs(tab->property("callsign").toString());
692  if (!validated) { return cs; }
693  if (sGui && sGui->getIContextNetwork())
694  {
696  if (atc.hasCallsign())
697  {
698  return atc.getCallsign();
699  } // first hand callsign diretcly from network context
700 
702  if (aircraft.hasCallsign())
703  {
704  return aircraft.getCallsign();
705  } // first hand callsign diretcly from network context
706  }
707  return cs;
708  }
709  return {};
710  }
711 
712  void CTextMessageComponent::emitDisplayInInfoWindow(const CVariant &message,
713  std::chrono::milliseconds displayDuration)
714  {
715  if (m_usedAsOverlayWidget) { return; }
716  emit this->displayInInfoWindow(message, displayDuration);
717  }
718 
719  void CTextMessageComponent::handleEnteredTextMessage(const QString &textMessage)
720  {
721  if (!this->isVisibleWidgetHack()) { return; }
722 
723  QString cl(textMessage.trimmed().simplified());
724  if (cl.isEmpty()) { return; }
725 
726  // is this a command?
727  if (!cl.startsWith("."))
728  {
729  // build a command line -> e.g. ".msg 122.8 fooBar"
730  cl = this->textMessageToCommand(cl);
731  }
732 
733  // relay the command
734  if (cl.isEmpty()) { return; }
735  emit this->commandEntered(cl, this->componentIdentifier());
736  }
737 
738  QString CTextMessageComponent::textMessageToCommand(const QString &enteredLine)
739  {
740  // only if visible
741  if (enteredLine.isEmpty()) { return {}; }
742 
743  const int index = ui->tw_TextMessages->currentIndex();
744  QString cmd(".msg ");
745  if (index < 0 || index == ui->tw_TextMessages->indexOf(ui->tb_TextMessagesAll))
746  {
747  CLogMessage(this).validationError(u"Incorrect message channel");
748  return {};
749  }
750  else if (ui->tw_TextMessages->tabText(index) == "SUP")
751  {
753  u"Message cannot be send to SUP channel. To send another wallop message use .wallop instead");
754  return {};
755  }
756  else
757  {
758  if (index == ui->tw_TextMessages->indexOf(ui->tb_TextMessagesCOM1))
759  {
760  cmd.append(QString::number(this->getOwnAircraft().getCom1System().getFrequencyActive().valueRounded(
761  CFrequencyUnit::MHz(), 3)));
762  }
763  else if (index == ui->tw_TextMessages->indexOf(ui->tb_TextMessagesCOM2))
764  {
765  cmd.append(QString::number(this->getOwnAircraft().getCom2System().getFrequencyActive().valueRounded(
766  CFrequencyUnit::MHz(), 3)));
767  }
768  else if (index == ui->tw_TextMessages->indexOf(ui->tb_TextMessagesUnicom))
769  {
770  cmd.append(QString::number(
771  CPhysicalQuantitiesConstants::FrequencyUnicom().valueRounded(CFrequencyUnit::MHz(), 3)));
772  }
773  else
774  {
775  // not a standard channel
776  bool isNumber;
777  const QString selectedTabText = firstPartOfTabText(ui->tw_TextMessages->tabText(index).trimmed());
778  const double frequency = selectedTabText.toDouble(&isNumber);
779  if (isNumber)
780  {
781  const CFrequency radioFrequency = CFrequency(frequency, CFrequencyUnit::MHz());
782  if (CComSystem::isValidCivilAviationFrequency(radioFrequency))
783  {
784  cmd.append(QString::number(radioFrequency.valueRounded(CFrequencyUnit::MHz(), 3)));
785  }
786  else
787  {
788  // selectedTabText expected to be the callsign
789  // with T664 we resolve against the callsigns in the context if possible
790  const CCallsign cs = this->getCallsignPropertyForTab(index, true);
791  if (cs.isEmpty()) { cmd.append(selectedTabText); }
792  else { cmd.append(cs.isAtcCallsign() ? cs.getStringAsSet() : cs.asString()); }
793  }
794  }
795  else { cmd.append(selectedTabText); }
796  }
797  return cmd % u" " % enteredLine;
798  }
799  }
800 
801  void CTextMessageComponent::onTextMessageReceived(const CTextMessageList &messages)
802  {
803  if (!m_activeReceive) { return; }
804  this->displayTextMessage(messages);
805  }
806 
807  void CTextMessageComponent::onTextMessageSent(const CTextMessage &sentMessage)
808  {
809  if (!m_activeSend) { return; }
810  this->displayTextMessage(sentMessage);
811  }
812 
813  bool CTextMessageComponent::handleGlobalCommandLineText(const QString &commandLine, const CIdentifier &originator)
814  {
815  if (originator == this->componentIdentifier()) { return false; }
816  if (commandLine.isEmpty() || commandLine.startsWith(".")) { return false; }
817 
818  // no "dot" command input
819  if (!this->isVisibleWidgetHack()) { return false; } // invisible, do ignore
820  this->handleEnteredTextMessage(commandLine); // handle as it was entered by own command line
821 
822  return false; // we never handle the message directly, but forward it
823  }
824 
826  {
827  if (callsign.isEmpty())
828  {
829  CLogMessage(this).warning(u"No callsign to display text message");
830  return;
831  }
832  QWidget *w = this->findTextMessageTabByCallsign(callsign, true);
833  if (!w && sGui && sGui->getIContextNetwork())
834  {
835  if (!callsign.isAtcCallsign() && sGui->getIContextNetwork()->isAircraftInRange(callsign))
836  {
837  // we assume a private message from a pilot
838  w = this->addNewTextMessageTab(callsign);
839  }
840  else if (sGui->getIContextNetwork()->isOnlineStation(callsign))
841  {
842  // we assume a private message of ATC
843  w = this->addNewTextMessageTab(callsign);
844  }
845  }
846  if (!w) { return; }
847  ui->tw_TextMessages->setCurrentWidget(w);
848 
849  // force display
850  if (!m_usedAsOverlayWidget) { this->displayMyself(); }
851 
852  emit this->textMessageTabSelected();
853  }
854 
856  {
857  const CSimulatedAircraft ownAircraft = this->getOwnAircraft();
858  const CFrequency freq1 = ownAircraft.getCom1System().getFrequencyActive();
859  const CFrequency freq2 = ownAircraft.getCom2System().getFrequencyActive();
860  if (freq1 == frequency)
861  {
862  this->setTab(TextMessagesCom1);
863  return;
864  }
865  if (freq2 == frequency)
866  {
867  this->setTab(TextMessagesCom2);
868  return;
869  }
870  this->setTab(TextMessagesAll);
871  }
872 
873  void CTextMessageComponent::fontSizeMinus() { ui->comp_SettingsStyle->fontSizeMinus(); }
874 
875  void CTextMessageComponent::fontSizePlus() { ui->comp_SettingsStyle->fontSizePlus(); }
876 
878  {
879  // set via widget, as ALL can be removed
880  switch (tab)
881  {
882  case TextMessagesAll: ui->tw_TextMessages->setCurrentWidget(ui->tb_TextMessagesAll); break;
883  case TextMessagesCom1: ui->tw_TextMessages->setCurrentWidget(ui->tb_TextMessagesCOM1); break;
884  case TextMessagesCom2: ui->tw_TextMessages->setCurrentWidget(ui->tb_TextMessagesCOM2); break;
885  case TextMessagesUnicom: ui->tw_TextMessages->setCurrentWidget(ui->tb_TextMessagesUnicom); break;
886  default: break;
887  }
888 
889  emit this->textMessageTabSelected();
890  }
891 
892  void CTextMessageComponent::setAtcButtonsRowsColumns(int rows, int cols, bool setMaxElements)
893  {
894  ui->comp_AtcStations->setRowsColumns(rows, cols, setMaxElements);
895  }
896 
898  {
899  ui->comp_AtcStations->setBackgroundUpdates(backgroundUpdates);
900  }
901 
903  {
904  if (!ui->gb_MessageTo->isChecked()) { return; }
905  ui->comp_AtcStations->updateStations();
906  }
907 
909  {
910  return ui->tw_TextMessages->widget(0) == ui->tb_TextMessagesAll;
911  }
912 
913  void CTextMessageComponent::showSettings(bool show) { ui->gb_Settings->setVisible(show); }
914 
915  void CTextMessageComponent::showTextMessageEntry(bool show) { ui->fr_TextMessage->setVisible(show); }
916 
918  {
919  if (!ui->lep_TextMessages->isVisible()) { return; }
920  const CTextMessageSettings s = m_messageSettings.get();
921  if (m_usedAsOverlayWidget && !s.focusOverlayWindow()) { return; }
922  ui->lep_TextMessages->setFocus();
923  }
924 
925  void CTextMessageComponent::removeAllMessagesTab() { ui->tw_TextMessages->removeTab(0); }
926 
927  void CTextMessageComponent::activate(bool send, bool receive)
928  {
929  m_activeSend = send;
930  m_activeReceive = receive;
931  }
932 
933  QString CTextMessageComponent::firstPartOfTabText(const QString &tabText)
934  {
935  if (tabText.isEmpty()) { return {}; }
936  int index = tabText.indexOf(':');
937  if (index < 0) { index = tabText.indexOf(' '); }
938  if (index >= 0) { return tabText.left(index); }
939  return tabText;
940  }
941 } // namespace swift::gui::components
const context::IContextAudio * getIContextAudio() const
Direct access to contexts if a CCoreFacade has been initialized.
const context::IContextOwnAircraft * getIContextOwnAircraft() const
Direct access to contexts if a CCoreFacade has been initialized.
CCoreFacade * getCoreFacade()
Get the facade.
Definition: application.h:346
const context::IContextNetwork * getIContextNetwork() const
Direct access to contexts if a CCoreFacade has been initialized.
bool isShuttingDown() const
Is application shutting down?
const context::CContextAudioBase * getCContextAudioBase() const
Direct access to contexts if a CCoreFacade has been initialized.
void playSelcalTone(const swift::misc::aviation::CSelcal &selcal)
SELCAL.
void playNotification(swift::misc::audio::CNotificationSounds::NotificationFlag notification, bool considerSettings, int volume=-1)
Notification sounds.
virtual swift::misc::aviation::CAtcStationList getOnlineStationsForFrequency(const swift::misc::physical_quantities::CFrequency &frequency) const =0
Online stations for frequency.
virtual swift::misc::simulation::CSimulatedAircraft getAircraftInRangeForCallsign(const swift::misc::aviation::CCallsign &callsign) const =0
Aircraft for given callsign.
virtual swift::misc::network::CUser getUserForCallsign(const swift::misc::aviation::CCallsign &callsign) const =0
User for given callsign, e.g. for text messages.
virtual bool isOnlineStation(const swift::misc::aviation::CCallsign &callsign) const =0
Online station for callsign?
virtual swift::misc::aviation::CAtcStation getOnlineStationForCallsign(const swift::misc::aviation::CCallsign &callsign) const =0
Online station for callsign.
virtual bool isAircraftInRange(const swift::misc::aviation::CCallsign &callsign) const =0
Aircraft in range.
virtual bool isConnected() const =0
Network connected?
virtual swift::misc::simulation::CSimulatedAircraft getOwnAircraft() const =0
Get own aircraft.
void widgetTopLevelChanged(CDockWidget *, bool topLevel)
Top level has changed for given widget.
Specialized class for dock widgets serving as info area.
CDockWidgetInfoArea * getDockWidgetInfoArea() const
Corresponding dockable widget in info area.
virtual bool setParentDockWidgetInfoArea(CDockWidgetInfoArea *parentDockableWidget)
Corresponding dockable widget in info area.
const CStyleSheetUtility & getStyleSheetUtility() const
Style sheet handling.
void styleSheetsChanged()
Style sheet changed.
void returnPressedUnemptyLine()
Return has been pressed, line is NOT empty (spaces are trimmed)
static const QString & fileNameTextMessage()
File name textmessage.qss.
QString style(const QString &fileName) const
Style for given file name.
Specialized text edit for displaying text messages.
void insertTextMessage(const swift::misc::network::CTextMessage &textMessage, int maxMessages=-1)
Insert a message.
void setStyleSheetForContent(const QString &styleSheet)
Stylesheet for content.
void requestAtcStation(const swift::misc::aviation::CAtcStation &station)
ATC station clicked.
void changed()
Font or style changed from within the component.
void showTextMessageEntry(bool show)
Show an text entry field.
void removeAllMessagesTab()
Remove the all tab, the operation cannot be undone.
bool handleGlobalCommandLineText(const QString &commandLine, const swift::misc::CIdentifier &originator)
Used to allow direct input from global command line when visible.
void commandEntered(const QString &commandLine, const swift::misc::CIdentifier &originator)
Command line was entered.
void displayInInfoWindow(const swift::misc::CVariant &message, std::chrono::milliseconds displayDuration)
Message to be displayed in central info window.
void textMessageTabSelected()
Text message tab selected.
void setAtcButtonsRowsColumns(int rows, int cols, bool setMaxElements)
Rows/columns.
void showCorrespondingTab(const swift::misc::aviation::CCallsign &callsign)
Display the tab for given callsign.
void showSettings(bool show)
Show the settings.
virtual bool setParentDockWidgetInfoArea(CDockWidgetInfoArea *parentDockableWidget)
Corresponding dockable widget in info area.
void setAtcButtonsBackgroundUpdates(bool backgroundUpdates)
Background updates or explicitly called.
void focusTextEntry()
Focus the text entry field.
void showCorrespondingTabForFrequency(const swift::misc::physical_quantities::CFrequency &frequency)
Display the tab for given frequency.
void activate(bool send, bool receive)
Ignore incoming send/receive signals.
void setStyleSheet(const QString &styleSheet)
CSS style sheet.
bool focusOverlayWindow() const
Focus in the overlay window.
void setLatestFirst(bool latestFirst)
Latest messages 1st?
bool isLatestFirst() const
Latest messages 1st?
T get() const
Get a copy of the current value.
Definition: valuecache.h:408
Value object for icons. An icon is stored in the global icon repository and identified by its index....
Definition: icon.h:39
QIcon toQIcon() const
A QIcon.
Definition: icon.cpp:45
Value object encapsulating information identifying a component of a modular distributed swift process...
Definition: identifier.h:29
Class for emitting a log message.
Definition: logmessage.h:27
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
Derived & validationError(const char16_t(&format)[N])
Set the severity to error, providing a format string, and adding the validation category.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
size_type size() const
Returns number of elements in the sequence.
Definition: sequence.h:273
reference front()
Access the first element.
Definition: sequence.h:225
bool isEmpty() const
Synonym for empty.
Definition: sequence.h:285
Streamable status message, e.g.
Wrapper around QVariant which provides transparent access to CValueObject methods of the contained ob...
Definition: variant.h:66
Value object encapsulating information of audio related settings.
Definition: audiosettings.h:25
bool textMessageSupervisor() const
Simplified functions.
Definition: audiosettings.h:68
bool textMessagePrivate() const
Simplified functions.
Definition: audiosettings.h:64
Value object encapsulating information about an ATC station.
Definition: atcstation.h:38
const CCallsign & getCallsign() const
Get callsign.
Definition: atcstation.h:84
QString getCallsignAndControllerRealName() const
Callsign and controller's name if available.
Definition: atcstation.cpp:65
const physical_quantities::CFrequency & getFrequency() const
Get frequency.
Definition: atcstation.h:135
bool hasCallsign() const
Has callsign?
Definition: atcstation.h:87
Value object for a list of ATC stations.
Value object encapsulating information of a callsign.
Definition: callsign.h:30
const QString & asString() const
Get callsign (normalized)
Definition: callsign.h:96
CIcons::IconIndex toIcon() const
As icon, not implemented by all classes.
Definition: callsign.h:157
void markAsBroadcastCallsign()
Set a human readable name as "broadcast" callsign.
Definition: callsign.cpp:157
void markAsWallopCallsign()
Set a human readable name as "wallop-channel" callsign.
Definition: callsign.cpp:163
bool isSupervisorCallsign() const
Supervisor?
Definition: callsign.cpp:146
bool isEmpty() const
Is empty?
Definition: callsign.h:63
bool isAtcCallsign() const
ATC callsign.
Definition: callsign.cpp:139
const QString & getStringAsSet() const
Get callsign.
Definition: callsign.h:99
bool isActiveFrequencySameFrequency(const physical_quantities::CFrequency &comFrequency) const
Is active frequency the same frequency.
Definition: comsystem.cpp:53
swift::misc::physical_quantities::CFrequency getFrequencyActive() const
Active frequency.
Definition: modulator.cpp:30
Value object encapsulating information of a text message.
Definition: textmessage.h:31
bool isWallopMessage() const
Is this a message send via .wallop.
bool isPrivateMessage() const
Is private message?
Definition: textmessage.cpp:53
bool isBroadcastMessage() const
Is this a broadcast message.
bool isEmpty() const
Empty message.
Definition: textmessage.h:90
bool isServerMessage() const
Initial message of server?
bool hasValidRecipient() const
Valid receviver?
bool isSendToFrequency(const physical_quantities::CFrequency &frequency) const
Send to particular frequency?
bool wasSent() const
Was sent?
Definition: textmessage.h:146
const aviation::CCallsign & getSenderCallsign() const
Get callsign (from)
Definition: textmessage.h:54
const aviation::CCallsign & getRecipientCallsign() const
Get callsign (to)
Definition: textmessage.h:60
bool isSupervisorMessage() const
Supervisor message?
Definition: textmessage.cpp:58
Value object encapsulating a list of text messages.
const QString & getRealName() const
Get full name.
Definition: user.h:59
double valueRounded(MU unit, int digits=-1) const
Rounded value in given unit.
Comprehensive information of an aircraft.
const aviation::CComSystem & getCom2System() const
Get COM2 system.
bool hasCallsign() const
Callsign not empty, no further checks.
const aviation::CCallsign & getCallsign() const
Get callsign.
const aviation::CComSystem & getCom1System() const
Get COM1 system.
SWIFT_GUI_EXPORT swift::gui::CGuiApplication * sGui
Single instance of GUI application object.
Backend services of the swift project, like dealing with the network or the simulators.
Definition: actionbind.cpp:7
High level reusable GUI components.
Definition: aboutdialog.cpp:13
Views, mainly QTableView.
GUI related classes.
Free functions in swift::misc.
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30
#define SWIFT_AUDIT_X(COND, WHERE, WHAT)
A weaker kind of verify.
Definition: verify.h:38
#define SWIFT_VERIFY_X(COND, WHERE, WHAT)
A weaker kind of assert.
Definition: verify.h:26