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,
73  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
74  c = connect(ui->gb_MessageTo, &QGroupBox::toggled, this, &CTextMessageComponent::onMessageToChecked,
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,
84  Q_ASSERT_X(c, Q_FUNC_INFO, "Missing connect");
85 
86  // style sheet
87  c = connect(sGui, &CGuiApplication::styleSheetsChanged, this, &CTextMessageComponent::onStyleSheetChanged,
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);
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
261  }
262  }
263  else if (message.isPrivateMessage())
264  {
265  // private message
266  this->addPrivateChannelTextMessage(message);
267  relevantForMe = true;
268  // Flash taskbar icon
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  auto *newTabWidget = new QWidget(this);
481  newTabWidget->setObjectName(u"Tab widget " % tabName);
482  newTabWidget->setProperty("callsign", callsign.asString());
483  auto *closeButton = new QPushButton("Close", newTabWidget);
484  auto *layout = new QVBoxLayout(newTabWidget);
485  auto *textEdit = new CTextMessageTextEdit(newTabWidget);
486  textEdit->setObjectName("tep_" + tabName);
487  int marginLeft {};
488  int marginRight {};
489  int marginTop {};
490  int marginBottom {};
491  ui->tb_TextMessagesAll->layout()->getContentsMargins(&marginLeft, &marginTop, &marginRight, &marginBottom);
492  newTabWidget->layout()->setContentsMargins(marginLeft, marginTop, marginRight, 2);
493  layout->addWidget(textEdit);
494  layout->addWidget(closeButton);
495  newTabWidget->setLayout(layout);
496  textEdit->setContextMenuPolicy(Qt::CustomContextMenu);
497  textEdit->setProperty("supervisormsg", supervisor);
498  textEdit->setStyleSheetForContent(style);
499 
500  const int index = ui->tw_TextMessages->addTab(newTabWidget, this->getCallsignAndRealName(callsign));
501  auto *closeButtonInTab = new QToolButton(newTabWidget);
502  closeButtonInTab->setText("[X]");
503  closeButtonInTab->setProperty("supervisormsg", supervisor);
504  QTabBar *bar = ui->tw_TextMessages->tabBar();
505  bar->setTabButton(index, QTabBar::RightSide, closeButtonInTab); // changes parent
506  if (supervisor)
507  {
509  bar->setTabIcon(index, CIcon(callsign.toIcon()).toQIcon());
510  bar->setTabTextColor(index, QColor(Qt::yellow));
511  }
512  ui->tw_TextMessages->setCurrentIndex(index);
513  closeButtonInTab->setProperty("tabName", tabName);
514  closeButton->setProperty("tabName", tabName);
515 
516  connect(closeButton, &QPushButton::released, this, &CTextMessageComponent::closeTextMessageTab);
517  connect(closeButtonInTab, &QPushButton::released, this, &CTextMessageComponent::closeTextMessageTab);
518 
519  this->setTabWidgetDescription(callsign, index);
520  return newTabWidget;
521  }
522 
523  void CTextMessageComponent::setTabWidgetDescription(const CCallsign &callsign, int widgetIndex)
524  {
525  if (callsign.isEmpty()) { return; }
526 
527  QString realName;
529  {
530  realName = sGui->getIContextNetwork()->getUserForCallsign(callsign).getRealName();
531  if (!realName.isEmpty()) { ui->tw_TextMessages->setTabToolTip(widgetIndex, realName); }
532  }
533  const QString tt = realName.isEmpty() ? callsign.asString() : callsign.asString() % u": " % realName;
534  ui->tw_TextMessages->setTabText(widgetIndex, tt);
535  }
536 
537  QString CTextMessageComponent::getCallsignAndRealName(const CCallsign &callsign) const
538  {
539  if (callsign.isEmpty()) { return {}; }
540  if (m_showRealNames && sGui && !sGui->isShuttingDown() && sGui->getIContextNetwork())
541  {
542  const QString realName = sGui->getIContextNetwork()->getUserForCallsign(callsign).getRealName();
543  if (!realName.isEmpty()) { return callsign.asString() % u": " % realName; }
544  }
545  return callsign.asString();
546  }
547 
548  void CTextMessageComponent::updateAllTabs()
549  {
550  for (int index = ui->tw_TextMessages->count() - 1; index >= 0; index--)
551  {
552  QWidget *tab = ui->tw_TextMessages->widget(index);
553  if (!tab) { continue; }
554  if (!tab->toolTip().isEmpty()) { continue; }
555  const QString cs = tab->property("callsign").toString();
556  if (cs.isEmpty()) { continue; }
557  this->setTabWidgetDescription(CCallsign(cs), index);
558  }
559  }
560 
561  void CTextMessageComponent::addPrivateChannelTextMessage(const CTextMessage &textMessage)
562  {
563  if (!textMessage.isPrivateMessage()) { return; }
564  CCallsign cs = textMessage.wasSent() ? textMessage.getRecipientCallsign() : textMessage.getSenderCallsign();
565  if (cs.isEmpty()) { return; }
566 
567  const bool isBroadcast = textMessage.isBroadcastMessage();
568  if (isBroadcast) { cs.markAsBroadcastCallsign(); }
569 
570  const bool isWallopMessage = textMessage.isWallopMessage();
571  if (isWallopMessage) { cs.markAsWallopCallsign(); }
572 
573  const QWidget *tab = this->findTextMessageTabByCallsign(cs);
574  if (!tab) { tab = this->addNewTextMessageTab(cs); }
575  Q_ASSERT_X(tab, Q_FUNC_INFO, "Missing tab");
576  auto *textEdit = tab->findChild<CTextMessageTextEdit *>();
577  SWIFT_VERIFY_X(textEdit, Q_FUNC_INFO, "Missing text edit");
578  if (!textEdit) { return; } // do not crash, though this situation should not happen
579  textEdit->insertTextMessage(textMessage);
580 
581  // sound
582  if (textMessage.isServerMessage()) { return; }
583  if (isBroadcast) { return; }
584 
585  const bool playSound = !textMessage.wasSent() && !m_usedAsOverlayWidget && sGui && !sGui->isShuttingDown() &&
587  if (sGui && sGui->getIContextAudio() && playSound)
588  {
589  const CSettings settings = m_audioSettings.get();
590  if (textMessage.isSupervisorMessage() && settings.textMessageSupervisor())
591  {
592  sGui->getCContextAudioBase()->playNotification(CNotificationSounds::NotificationTextMessageSupervisor,
593  true);
594  }
595  else if (textMessage.isPrivateMessage() && settings.textMessagePrivate())
596  {
597  sGui->getCContextAudioBase()->playNotification(CNotificationSounds::NotificationTextMessagePrivate,
598  true);
599  }
600  }
601  }
602 
603  CSimulatedAircraft CTextMessageComponent::getOwnAircraft() const
604  {
605  if (!sGui || !sGui->getIContextOwnAircraft()) { return {}; }
607  }
608 
609  QWidget *CTextMessageComponent::findTextMessageTabByCallsign(const CCallsign &callsign,
610  bool callsignResolution) const
611  {
612  // search the private message tabs by property first
613  for (int index = ui->tw_TextMessages->count() - 1; index >= 0; index--)
614  {
615  QWidget *tab = ui->tw_TextMessages->widget(index);
616  if (tab && tab->property("callsign").toString() == callsign.asString()) { return tab; }
617  }
618 
619  QWidget *w = this->findTextMessageTabByName(callsign.asString());
620  if (w) { return w; }
621  if (!callsignResolution) { return nullptr; }
622 
623  // resolve callsign to COM tab
624  if (!sGui || !sGui->getIContextNetwork()) { return nullptr; }
625  const CAtcStation station(sGui->getIContextNetwork()->getOnlineStationForCallsign(callsign));
626  if (!station.getCallsign().isEmpty())
627  {
628  const CSimulatedAircraft ownAircraft(this->getOwnAircraft());
629  if (ownAircraft.getCom1System().isActiveFrequencySameFrequency(station.getFrequency()))
630  {
631  return this->getTabWidget(TextMessagesCom1);
632  }
633  else if (ownAircraft.getCom2System().isActiveFrequencySameFrequency(station.getFrequency()))
634  {
635  return this->getTabWidget(TextMessagesCom2);
636  }
637  }
638  return nullptr;
639  }
640 
641  QWidget *CTextMessageComponent::findTextMessageTabByName(const QString &name) const
642  {
643  if (name.isEmpty()) { return nullptr; }
644 
645  // search the private message tabs first
646  for (int index = ui->tw_TextMessages->count() - 1; index >= 0; index--)
647  {
648  const QString tabName = ui->tw_TextMessages->tabText(index);
649  if (!tabName.startsWith(name, Qt::CaseInsensitive)) { continue; }
650  QWidget *tab = ui->tw_TextMessages->widget(index);
651  return tab;
652  }
653  return nullptr;
654  }
655 
656  void CTextMessageComponent::closeTextMessageTab()
657  {
658  int index = -1;
659  const QObject *sender = QObject::sender(); // the button
660  const QString tabName = sender->property("tabName").toString();
661  QWidget *tw = this->findTextMessageTabByName(tabName);
662  if (!this->isCloseableTab(tw)) { return; }
663  if (tw) { index = ui->tw_TextMessages->indexOf(tw); }
664  if (index >= 0) { ui->tw_TextMessages->removeTab(index); }
665  }
666 
667  void CTextMessageComponent::topLevelChanged(QWidget *widget, bool topLevel)
668  {
669  // own input field if floating window
670  Q_UNUSED(widget);
671  ui->fr_TextMessage->setVisible(topLevel);
672  }
673 
674  void CTextMessageComponent::textMessageEntered()
675  {
676  if (!ui->fr_TextMessage->isVisible() || !ui->lep_TextMessages->isVisible()) { return; }
677  if (!this->isVisible()) { return; }
678 
679  const QString cl(ui->lep_TextMessages->getLastEnteredLineFormatted());
680  if (!cl.isEmpty()) { this->handleEnteredTextMessage(cl); }
681  }
682 
683  bool CTextMessageComponent::isVisibleWidgetHack() const
684  {
685  return m_usedAsOverlayWidget ? true : this->isVisibleWidget();
686  }
687 
688  CCallsign CTextMessageComponent::getCallsignPropertyForTab(int tabIndex, bool validated) const
689  {
690  if (tabIndex < 0 || tabIndex >= ui->tw_TextMessages->count()) { return {}; }
691  QWidget *tab = ui->tw_TextMessages->widget(tabIndex);
692  if (tab && !tab->property("callsign").toString().isEmpty())
693  {
694  const CCallsign cs(tab->property("callsign").toString());
695  if (!validated) { return cs; }
696  if (sGui && sGui->getIContextNetwork())
697  {
699  if (atc.hasCallsign())
700  {
701  return atc.getCallsign();
702  } // first hand callsign diretcly from network context
703 
705  if (aircraft.hasCallsign())
706  {
707  return aircraft.getCallsign();
708  } // first hand callsign diretcly from network context
709  }
710  return cs;
711  }
712  return {};
713  }
714 
715  void CTextMessageComponent::emitDisplayInInfoWindow(const CVariant &message,
716  std::chrono::milliseconds displayDuration)
717  {
718  if (m_usedAsOverlayWidget) { return; }
719  emit this->displayInInfoWindow(message, displayDuration);
720  }
721 
722  void CTextMessageComponent::handleEnteredTextMessage(const QString &textMessage)
723  {
724  if (!this->isVisibleWidgetHack()) { return; }
725 
726  QString cl(textMessage.trimmed().simplified());
727  if (cl.isEmpty()) { return; }
728 
729  // is this a command?
730  if (!cl.startsWith("."))
731  {
732  // build a command line -> e.g. ".msg 122.8 fooBar"
733  cl = this->textMessageToCommand(cl);
734  }
735 
736  // relay the command
737  if (cl.isEmpty()) { return; }
738  emit this->commandEntered(cl, this->componentIdentifier());
739  }
740 
741  QString CTextMessageComponent::textMessageToCommand(const QString &enteredLine)
742  {
743  // only if visible
744  if (enteredLine.isEmpty()) { return {}; }
745 
746  const int index = ui->tw_TextMessages->currentIndex();
747  QString cmd(".msg ");
748  if (index < 0 || index == ui->tw_TextMessages->indexOf(ui->tb_TextMessagesAll))
749  {
750  CLogMessage(this).validationError(u"Incorrect message channel");
751  return {};
752  }
753  else if (ui->tw_TextMessages->tabText(index) == "SUP")
754  {
756  u"Message cannot be send to SUP channel. To send another wallop message use .wallop instead");
757  return {};
758  }
759  else
760  {
761  if (index == ui->tw_TextMessages->indexOf(ui->tb_TextMessagesCOM1))
762  {
763  cmd.append(QString::number(this->getOwnAircraft().getCom1System().getFrequencyActive().valueRounded(
764  CFrequencyUnit::MHz(), 3)));
765  }
766  else if (index == ui->tw_TextMessages->indexOf(ui->tb_TextMessagesCOM2))
767  {
768  cmd.append(QString::number(this->getOwnAircraft().getCom2System().getFrequencyActive().valueRounded(
769  CFrequencyUnit::MHz(), 3)));
770  }
771  else if (index == ui->tw_TextMessages->indexOf(ui->tb_TextMessagesUnicom))
772  {
773  cmd.append(QString::number(
774  CPhysicalQuantitiesConstants::FrequencyUnicom().valueRounded(CFrequencyUnit::MHz(), 3)));
775  }
776  else
777  {
778  // not a standard channel
779  bool isNumber {};
780  const QString selectedTabText = firstPartOfTabText(ui->tw_TextMessages->tabText(index).trimmed());
781  const double frequency = selectedTabText.toDouble(&isNumber);
782  if (isNumber)
783  {
784  const CFrequency radioFrequency = CFrequency(frequency, CFrequencyUnit::MHz());
785  if (CComSystem::isValidCivilAviationFrequency(radioFrequency))
786  {
787  cmd.append(QString::number(radioFrequency.valueRounded(CFrequencyUnit::MHz(), 3)));
788  }
789  else
790  {
791  // selectedTabText expected to be the callsign
792  // with T664 we resolve against the callsigns in the context if possible
793  const CCallsign cs = this->getCallsignPropertyForTab(index, true);
794  if (cs.isEmpty()) { cmd.append(selectedTabText); }
795  else { cmd.append(cs.isAtcCallsign() ? cs.getStringAsSet() : cs.asString()); }
796  }
797  }
798  else { cmd.append(selectedTabText); }
799  }
800  return cmd % u" " % enteredLine;
801  }
802  }
803 
804  void CTextMessageComponent::onTextMessageReceived(const CTextMessageList &messages)
805  {
806  if (!m_activeReceive) { return; }
807  this->displayTextMessage(messages);
808  }
809 
810  void CTextMessageComponent::onTextMessageSent(const CTextMessage &sentMessage)
811  {
812  if (!m_activeSend) { return; }
813  this->displayTextMessage(sentMessage);
814  }
815 
816  bool CTextMessageComponent::handleGlobalCommandLineText(const QString &commandLine, const CIdentifier &originator)
817  {
818  if (originator == this->componentIdentifier()) { return false; }
819  if (commandLine.isEmpty() || commandLine.startsWith(".")) { return false; }
820 
821  // no "dot" command input
822  if (!this->isVisibleWidgetHack()) { return false; } // invisible, do ignore
823  this->handleEnteredTextMessage(commandLine); // handle as it was entered by own command line
824 
825  return false; // we never handle the message directly, but forward it
826  }
827 
829  {
830  if (callsign.isEmpty())
831  {
832  CLogMessage(this).warning(u"No callsign to display text message");
833  return;
834  }
835  QWidget *w = this->findTextMessageTabByCallsign(callsign, true);
836  if (!w && sGui && sGui->getIContextNetwork())
837  {
838  if (!callsign.isAtcCallsign() && sGui->getIContextNetwork()->isAircraftInRange(callsign))
839  {
840  // we assume a private message from a pilot
841  w = this->addNewTextMessageTab(callsign);
842  }
843  else if (sGui->getIContextNetwork()->isOnlineStation(callsign))
844  {
845  // we assume a private message of ATC
846  w = this->addNewTextMessageTab(callsign);
847  }
848  }
849  if (!w) { return; }
850  ui->tw_TextMessages->setCurrentWidget(w);
851 
852  // force display
853  if (!m_usedAsOverlayWidget) { this->displayMyself(); }
854 
855  emit this->textMessageTabSelected();
856  }
857 
859  {
860  const CSimulatedAircraft ownAircraft = this->getOwnAircraft();
861  const CFrequency freq1 = ownAircraft.getCom1System().getFrequencyActive();
862  const CFrequency freq2 = ownAircraft.getCom2System().getFrequencyActive();
863  if (freq1 == frequency)
864  {
865  this->setTab(TextMessagesCom1);
866  return;
867  }
868  if (freq2 == frequency)
869  {
870  this->setTab(TextMessagesCom2);
871  return;
872  }
873  this->setTab(TextMessagesAll);
874  }
875 
876  void CTextMessageComponent::fontSizeMinus() { ui->comp_SettingsStyle->fontSizeMinus(); }
877 
878  void CTextMessageComponent::fontSizePlus() { ui->comp_SettingsStyle->fontSizePlus(); }
879 
881  {
882  // set via widget, as ALL can be removed
883  switch (tab)
884  {
885  case TextMessagesAll: ui->tw_TextMessages->setCurrentWidget(ui->tb_TextMessagesAll); break;
886  case TextMessagesCom1: ui->tw_TextMessages->setCurrentWidget(ui->tb_TextMessagesCOM1); break;
887  case TextMessagesCom2: ui->tw_TextMessages->setCurrentWidget(ui->tb_TextMessagesCOM2); break;
888  case TextMessagesUnicom: ui->tw_TextMessages->setCurrentWidget(ui->tb_TextMessagesUnicom); break;
889  default: break;
890  }
891 
892  emit this->textMessageTabSelected();
893  }
894 
895  void CTextMessageComponent::setAtcButtonsRowsColumns(int rows, int cols, bool setMaxElements)
896  {
897  ui->comp_AtcStations->setRowsColumns(rows, cols, setMaxElements);
898  }
899 
901  {
902  ui->comp_AtcStations->setBackgroundUpdates(backgroundUpdates);
903  }
904 
906  {
907  if (!ui->gb_MessageTo->isChecked()) { return; }
908  ui->comp_AtcStations->updateStations();
909  }
910 
912  {
913  return ui->tw_TextMessages->widget(0) == ui->tb_TextMessagesAll;
914  }
915 
916  void CTextMessageComponent::showSettings(bool show) { ui->gb_Settings->setVisible(show); }
917 
918  void CTextMessageComponent::showTextMessageEntry(bool show) { ui->fr_TextMessage->setVisible(show); }
919 
921  {
922  if (!ui->lep_TextMessages->isVisible()) { return; }
923  const CTextMessageSettings s = m_messageSettings.get();
924  if (m_usedAsOverlayWidget && !s.focusOverlayWindow()) { return; }
925  ui->lep_TextMessages->setFocus();
926  }
927 
928  void CTextMessageComponent::removeAllMessagesTab() { ui->tw_TextMessages->removeTab(0); }
929 
930  void CTextMessageComponent::activate(bool send, bool receive)
931  {
932  m_activeSend = send;
933  m_activeReceive = receive;
934  }
935 
936  QString CTextMessageComponent::firstPartOfTabText(const QString &tabText)
937  {
938  if (tabText.isEmpty()) { return {}; }
939  int index = tabText.indexOf(':');
940  if (index < 0) { index = tabText.indexOf(' '); }
941  if (index >= 0) { return tabText.left(index); }
942  return tabText;
943  }
944 } // 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 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.
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:63
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:14
Views, mainly QTableView.
GUI related classes.
Free functions in swift::misc.
void toggled(bool checked)
void alert(QWidget *widget, int msec)
void toggled(bool on)
void addWidget(QWidget *w)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T findChild(QAnyStringView name, Qt::FindChildOptions options) const const
QVariant property(const char *name) const const
QObject * sender() const const
QString arg(Args &&... args) const const
QString asprintf(const char *cformat,...)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(qsizetype n) &&
QString number(double n, char format, int precision)
QString simplified() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
QString trimmed() const const
CaseInsensitive
QueuedConnection
CustomContextMenu
DescendingOrder
void setTabButton(int index, QTabBar::ButtonPosition position, QWidget *widget)
void setTabIcon(int index, const QIcon &icon)
void setTabTextColor(int index, const QColor &color)
QString toString() const const
QWidget * topLevelWidget() const const
QWidget(QWidget *parent, Qt::WindowFlags f)
QLayout * layout() const const
void show()
QStyle * style() const const
void update()
bool isVisible() const const
#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