diff --git a/src/blackgui/textmessagecomponent.cpp b/src/blackgui/textmessagecomponent.cpp new file mode 100644 index 000000000..363f2d90b --- /dev/null +++ b/src/blackgui/textmessagecomponent.cpp @@ -0,0 +1,441 @@ +#include "textmessagecomponent.h" +#include "blackmisc/nwuser.h" +#include "blackmisc/notificationsounds.h" +#include "ui_textmessagecomponent.h" + +#include +#include + +using namespace BlackCore; +using namespace BlackMisc; +using namespace BlackGui; +using namespace BlackMisc::Network; +using namespace BlackMisc::Aviation; +using namespace BlackMisc::PhysicalQuantities; +using namespace BlackMisc::Settings; + +namespace BlackGui +{ + + CTextMessageComponent::CTextMessageComponent(QWidget *parent) : + QTabWidget(parent), CRuntimeBasedComponent(nullptr, false), ui(new Ui::CTextMessageComponent), m_selcalCallback(nullptr), m_clearTextEditAction(nullptr), m_currentTextEdit(nullptr) + { + ui->setupUi(this); + this->m_clearTextEditAction = new QAction("Clear", this); + connect(this->m_clearTextEditAction, &QAction::triggered, this, &CTextMessageComponent::clearTextEdit); + + ui->te_TextMessagesAll->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->te_TextMessagesAll, &QTextEdit::customContextMenuRequested, this, &CTextMessageComponent::showContextMenuForTextEdit); + + ui->te_TextMessagesUnicom->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->te_TextMessagesUnicom, &QTextEdit::customContextMenuRequested, this, &CTextMessageComponent::showContextMenuForTextEdit); + + ui->te_TextMessagesCOM1->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->te_TextMessagesCOM1, &QTextEdit::customContextMenuRequested, this, &CTextMessageComponent::showContextMenuForTextEdit); + + ui->te_TextMessagesCOM2->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->te_TextMessagesCOM2, &QTextEdit::customContextMenuRequested, this, &CTextMessageComponent::showContextMenuForTextEdit); + } + + CTextMessageComponent::~CTextMessageComponent() + { + delete ui; + } + + void CTextMessageComponent::setToolTip(const QString &tooltipText, CTextMessageComponent::Tab tab) + { + this->getTab(tab)->setToolTip(tooltipText); + } + + QWidget *CTextMessageComponent::getTab(CTextMessageComponent::Tab tab) + { + switch (tab) + { + case TextMessagesAll: + return this->ui->tb_TextMessagesAll; + case TextMessagesCom1: + return this->ui->tb_TextMessagesCOM1; + case TextMessagesCom2: + return this->ui->tb_TextMessagesCOM2; + case TextMessagesUnicom: + return this->ui->tb_TextMessagesUnicom; + default: + qFatal("Wrong index"); + break; + } + return nullptr; + } + + /* + * Text messages received or send, append to GUI + */ + void CTextMessageComponent::appendTextMessagesToGui(const CTextMessageList &messages, bool sending) + { + if (messages.isEmpty()) return; + foreach(CTextMessage message, messages) + { + const QString currentSelcal = this->m_selcalCallback ? this->m_selcalCallback() : ""; + if (CSelcal::isValidCode(currentSelcal) && message.isSelcalMessageFor(currentSelcal)) + { + if (this->getOwnAircraft().isActiveFrequencyWithin25kHzChannel(message.getFrequency())) + { + // this is SELCAL for me + if (this->getIContextAudio()) + { + this->getIContextAudio()->playSelcalTone(currentSelcal); + } + else + { + emit this->displayOverlayInfo(CStatusMessage::getInfoMessage("SELCAL received", CStatusMessage::TypeGui)); + } + } + continue; // not displayed + } + + bool relevantForMe = false; + QString m = message.asString(true, false, "\t"); + + if (message.isSendToUnicom()) + { + this->ui->te_TextMessagesUnicom->append(m); + relevantForMe = true; + } + + // check message + if (message.isRadioMessage()) + { + // check for own COM frequencies + if (message.isSendToFrequency(this->getOwnAircraft().getCom1System().getFrequencyActive())) + { + this->ui->te_TextMessagesCOM1->append(m); + relevantForMe = true; + } + if (message.isSendToFrequency(this->getOwnAircraft().getCom2System().getFrequencyActive())) + { + this->ui->te_TextMessagesCOM2->append(m); + relevantForMe = true; + } + } + else if (message.isPrivateMessage() && !message.isServerMessage()) + { + // private message + this->addPrivateChannelTextMessage(message, sending); + relevantForMe = true; + } + + // message for me? right frequency? otherwise quit + if (relevantForMe || message.isServerMessage()) this->ui->te_TextMessagesAll->append(m); + if (!relevantForMe) return; + + // overlay message if this channel is not selected + if (!sending && !message.isSendToUnicom() && !message.isServerMessage()) + { + // if the channel is selected, do nothing + if (!this->isCorrespondingTextMessageTabSelected(message)) + emit this->displayOverlayInfo(message.asStatusMessage(true, true, "\t")); + } + } + } + + /* + * Is the tab of the message's receiver selected? + */ + bool CTextMessageComponent::isCorrespondingTextMessageTabSelected(CTextMessage textMessage) const + { + if (!this->isVisible()) return false; + if (!textMessage.hasValidRecipient()) return false; + if (textMessage.isEmpty()) return false; // ignore empty message + if (textMessage.isPrivateMessage()) + { + // private message + CCallsign cs = textMessage.getSenderCallsign(); + if (cs.isEmpty()) return false; + QWidget *tab = this->findTextMessageTabByName(cs.getStringAsSet()); + if (!tab) return false; + return this->currentWidget() == tab; + } + else + { + // frequency message + if (this->currentWidget() == this->ui->tb_TextMessagesAll) return true; + if (textMessage.isSendToFrequency(this->getOwnAircraft().getCom1System().getFrequencyActive())) + return this->currentWidget() == this->ui->tb_TextMessagesCOM1; + if (textMessage.isSendToFrequency(this->getOwnAircraft().getCom2System().getFrequencyActive())) + return this->currentWidget() == this->ui->tb_TextMessagesCOM2; + return false; + } + } + + /* + * Add new text message tab + */ + QWidget *CTextMessageComponent::addNewTextMessageTab(const QString &tabName) + { + QWidget *newTab = new QWidget(this); + QPushButton *closeButton = new QPushButton("Close", newTab); + QVBoxLayout *layout = new QVBoxLayout(newTab); + QTextEdit *textEdit = new QTextEdit(newTab); + int marginLeft, marginRight, marginTop, marginBottom; + this->ui->tb_TextMessagesAll->layout()->getContentsMargins(&marginLeft, &marginTop, &marginRight, &marginBottom); + newTab->layout()->setContentsMargins(marginLeft, marginTop, marginRight, 2); + textEdit->setReadOnly(true); + textEdit->setWordWrapMode(QTextOption::NoWrap); + layout->addWidget(textEdit); + layout->addWidget(closeButton); + newTab->setLayout(layout); + textEdit->setContextMenuPolicy(Qt::CustomContextMenu); + connect(textEdit, &QTextEdit::customContextMenuRequested, this, &CTextMessageComponent::showContextMenuForTextEdit); + int index = this->addTab(newTab, tabName); + this->connect(closeButton, &QPushButton::released, this, &CTextMessageComponent::closeTextMessageTab); + this->setCurrentIndex(index); + + if (this->getIContextNetwork()) + { + QString realName = this->getIContextNetwork()->getUserForCallsign(CCallsign(tabName)).getRealName(); + if (!realName.isEmpty()) this->setTabToolTip(index, realName); + } + return newTab; + } + + /* + * Add a private channel text message + */ + void CTextMessageComponent::addPrivateChannelTextMessage(const CTextMessage &textMessage, bool sending) + { + if (!textMessage.isPrivateMessage()) return; + CCallsign cs = sending ? textMessage.getRecipientCallsign() : textMessage.getSenderCallsign(); + if (cs.isEmpty()) return; + QWidget *tab = this->findTextMessageTabByName(cs.getStringAsSet()); + if (tab == nullptr) tab = this->findTextMessageTabByName(cs.asString()); + if (tab == nullptr) tab = this->addNewTextMessageTab(cs.getStringAsSet()); + Q_ASSERT(tab != nullptr); + QTextEdit *textEdit = tab->findChild(); + Q_ASSERT(textEdit != nullptr); + if (textEdit == nullptr) return; // do not crash, though this situation could not happen + textEdit->append(textMessage.asString(true, false, "\t")); + + // sound + if (this->getIContextAudio()) + this->getIContextAudio()->playNotification(BlackSound::CNotificationSounds::NotificationTextMessage); + } + + /* + * Message tab by name + */ + QWidget *CTextMessageComponent::findTextMessageTabByName(const QString &name) const + { + if (name.isEmpty()) return nullptr; + QString n = name.trimmed(); + for (int index = 0; index < this->count(); index++) + { + QString tabName = this->tabText(index); + if (tabName.indexOf(n, 0, Qt::CaseInsensitive) < 0) continue; + QWidget *tab = this->widget(index); + return tab; + } + return nullptr; + } + + /* + * Text message stub (sender/receiver) for current channel + */ + CTextMessage CTextMessageComponent::getTextMessageStubForChannel() + { + CTextMessage tm; + int index = this->currentIndex(); + if (index < 0) return tm; + if (index == this->indexOf(this->ui->tb_TextMessagesAll)) return tm; + + // from + tm.setSenderCallsign(this->getOwnAircraft().getCallsign()); + + // frequency text message? + if (index == this->indexOf(this->ui->tb_TextMessagesCOM1)) + { + tm.setFrequency(this->getOwnAircraft().getCom1System().getFrequencyActive()); + } + else if (index == this->indexOf(this->ui->tb_TextMessagesCOM2)) + { + tm.setFrequency(this->getOwnAircraft().getCom2System().getFrequencyActive()); + } + else if (index == this->indexOf(this->ui->tb_TextMessagesUnicom)) + { + tm.setFrequency(CPhysicalQuantitiesConstants::FrequencyUnicom()); + } + else + { + // not a standard channel + QString selectedTabText = this->tabText(index); + bool isNumber; + double frequency = selectedTabText.toDouble(&isNumber); + if (isNumber) + { + CFrequency radioFrequency = CFrequency(frequency, CFrequencyUnit::MHz()); + if (CComSystem::isValidCivilAviationFrequency(radioFrequency)) + { + tm.setFrequency(radioFrequency); + } + else + { + CCallsign toCallsign(selectedTabText); + tm.setRecipientCallsign(toCallsign); + } + } + else + { + CCallsign toCallsign(selectedTabText); + tm.setRecipientCallsign(toCallsign); + } + } + return tm; // now valid message stub with receiver + } + + /* + * Close message tab + */ + void CTextMessageComponent::closeTextMessageTab() + { + QObject *sender = QObject::sender(); + QWidget *parentWidget = qobject_cast(sender->parent()); + Q_ASSERT(parentWidget); + int index = -1; + + while (index < 0 && parentWidget) + { + index = this->indexOf(parentWidget); + parentWidget = parentWidget->parentWidget(); + } + if (index >= 0) this->removeTab(index); + } + + void CTextMessageComponent::showContextMenuForTextEdit(const QPoint &pt) + { + QObject *sender = QObject::sender(); + this->m_currentTextEdit = qobject_cast(sender); + Q_ASSERT(this->m_currentTextEdit); + + QMenu *menu = this->m_currentTextEdit->createStandardContextMenu(); + menu->setParent(this->m_currentTextEdit); + menu->setObjectName(this->m_currentTextEdit->objectName().append("_contextMenu")); + menu->addSeparator(); + menu->addAction(this->m_clearTextEditAction); + menu->exec(this->m_currentTextEdit->mapToGlobal(pt)); + delete menu; + } + + void CTextMessageComponent::clearTextEdit() + { + if (!this->m_currentTextEdit) return; + this->m_currentTextEdit->clear(); + this->m_currentTextEdit = nullptr; + } + + /* + * Command entered + */ + void CTextMessageComponent::commandEntered() + { + // TODO: just a first draft of the command line parser + // needs to be refactored, as soon as a first version works + + QLineEdit *lineEdit = qobject_cast(QObject::sender()); + Q_ASSERT(lineEdit); + + QString cmdLine = lineEdit->text().simplified(); + if (cmdLine.isEmpty()) return; + QList parts = cmdLine.toLower().split(' '); + if (parts.length() < 1) return; + QString cmd = parts[0].startsWith('.') ? parts[0].toLower() : ""; + if (cmd == ".m" || cmd == ".msg") + { + if (!this->getIContextNetwork() || !this->getIContextNetwork()->isConnected()) + { + this->sendStatusMessage(CStatusMessage(CStatusMessage::TypeTrafficNetwork, CStatusMessage::SeverityError, "network needs to be connected")); + return; + } + if (parts.length() < 3) + { + this->sendStatusMessage(CStatusMessage(CStatusMessage::TypeValidation, CStatusMessage::SeverityError, "incorrect message")); + return; + } + QString p = parts[1].trimmed(); // receiver + + // select current tab by command + this->setVisible(true); + if (p == "c1" || p == "com1") + { + this->setCurrentWidget(this->ui->tb_TextMessagesCOM1); + } + else if (p == "c2" || p == "com2") + { + this->setCurrentWidget(this->ui->tb_TextMessagesCOM2); + } + else if (p == "u" || p == "unicom" || p == "uni") + { + this->setCurrentWidget(this->ui->tb_TextMessagesUnicom); + } + else + { + QWidget *tab = this->findTextMessageTabByName(p.trimmed()); + if (tab == nullptr) tab = this->addNewTextMessageTab(p.trimmed().toUpper()); + this->setCurrentWidget(tab); + } + CTextMessage tm = this->getTextMessageStubForChannel(); + int index = cmdLine.indexOf(tm.getRecipientCallsign().getStringAsSet(), 0, Qt::CaseInsensitive); + if (index < 0) + { + this->sendStatusMessage( + CStatusMessage(CStatusMessage::TypeValidation, CStatusMessage::SeverityError, + "incomplete message") + ); + return; + } + QString msg(cmdLine.mid(index + tm.getRecipientCallsign().asString().length() + 1)); + tm.setMessage(msg); + if (tm.isEmpty()) return; + if (!this->isNetworkConnected()) return; + CTextMessageList tml(tm); + this->getIContextNetwork()->sendTextMessages(tml); + this->appendTextMessagesToGui(tml, true); + lineEdit->setText(""); + } + else if (cmd.startsWith(".")) + { + // dump CMDs + } + else + { + // single line, no command + // line is considered to be a message to the selected channel, send + if (!this->isNetworkConnected()) + { + this->sendStatusMessage(CStatusMessage(CStatusMessage::TypeTrafficNetwork, CStatusMessage::SeverityError, "network needs to be connected")); + return; + } + + if (!this->isVisible()) + { + this->sendStatusMessage(CStatusMessage(CStatusMessage::TypeTrafficNetwork, CStatusMessage::SeverityError, "text messages can only be sent from corresponding page")); + return; + } + + int index = this->currentIndex(); + if (index < 0 || index == this->indexOf(this->ui->tb_TextMessagesAll)) + { + this->sendStatusMessage(CStatusMessage(CStatusMessage::TypeValidation, CStatusMessage::SeverityError, "incorrect channel")); + } + else + { + CTextMessage tm = this->getTextMessageStubForChannel(); + tm.setMessage(cmdLine); + if (tm.isEmpty()) return; + if (!this->getIContextNetwork()) return; + CTextMessageList textMessageList(tm); + this->getIContextNetwork()->sendTextMessages(textMessageList); + this->appendTextMessagesToGui(textMessageList, true); + lineEdit->setText(""); + } + } + } +} + diff --git a/src/blackgui/textmessagecomponent.h b/src/blackgui/textmessagecomponent.h new file mode 100644 index 000000000..d80f35c8f --- /dev/null +++ b/src/blackgui/textmessagecomponent.h @@ -0,0 +1,110 @@ +#ifndef BLACKGUI_TEXTMESSAGECOMPONENT_H +#define BLACKGUI_TEXTMESSAGECOMPONENT_H + +#include "blackgui/runtimebasedcomponent.h" +#include "blackgui/timerbasedcomponent.h" +#include "blackmisc/nwtextmessage.h" +#include "blackmisc/avaircraft.h" + +#include +#include +#include + +namespace Ui { class CTextMessageComponent; } +namespace BlackGui +{ + //! Text message widget + class CTextMessageComponent : public QTabWidget, public CRuntimeBasedComponent + { + Q_OBJECT + + public: + //! Tabs + enum Tab + { + TextMessagesAll, + TextMessagesUnicom, + TextMessagesCom1, + TextMessagesCom2 + }; + + //! Constructor + explicit CTextMessageComponent(QWidget *parent = nullptr); + + //! Destructor + ~CTextMessageComponent(); + + //! Set tooltip + void setToolTip(const QString &tooltipText, Tab tab); + + //! SELCAL callback, SELCAL is obtained by that + void setSelcalCallback(const std::function &selcalCallback) { this->m_selcalCallback = selcalCallback; } + + signals: + //! Invisible text message + void displayOverlayInfo(const BlackMisc::CStatusMessage &message) const; + + public slots: + //! Command entered + void commandEntered(); + + /*! + * \brief Append text messages (received, to be sent) to GUI + * \param messages + * \param sending + */ + void appendTextMessagesToGui(const BlackMisc::Network::CTextMessageList &messages, bool sending = false); + + private: + Ui::CTextMessageComponent *ui; + QWidget *getTab(Tab tab); //!< enum to widget + std::function m_selcalCallback; //!< obtain SELCAL by that + QAction *m_clearTextEditAction; + QTextEdit *m_currentTextEdit; + + /*! + * \brief Add new text message tab + * \param tabName name of the new tab, usually the channel name + * \return + */ + QWidget *addNewTextMessageTab(const QString &tabName); + + //! Find text message tab by its name + QWidget *findTextMessageTabByName(const QString &name) const; + + /*! + * \brief Private channel text message + * \param textMessage + * \param sending sending or receiving + */ + void addPrivateChannelTextMessage(const BlackMisc::Network::CTextMessage &textMessage, bool sending = false); + + /*! + * Stub for sending a text message (eihter radio or private message). + * Sets sender / receiver depending on frequency / channel situation. + */ + BlackMisc::Network::CTextMessage getTextMessageStubForChannel(); + + //! own aircraft + const BlackMisc::Aviation::CAircraft getOwnAircraft() const { Q_ASSERT(this->getIContextNetwork()); return this->getIContextNetwork()->getOwnAircraft(); } + + //! For this text message's recepient, is the current tab selected? + bool isCorrespondingTextMessageTabSelected(BlackMisc::Network::CTextMessage textMessage) const; + + //! Network connected? + bool isNetworkConnected() const { return this->getIContextNetwork() && this->getIContextNetwork()->isConnected() ; } + + private slots: + //! Close text message tab + void closeTextMessageTab(); + + //! Context menu for text edit including clear + void showContextMenuForTextEdit(const QPoint &pt); + + //! Clear text edit + void clearTextEdit(); + + }; +} + +#endif // guard diff --git a/src/blackgui/textmessagecomponent.ui b/src/blackgui/textmessagecomponent.ui new file mode 100644 index 000000000..75282acf2 --- /dev/null +++ b/src/blackgui/textmessagecomponent.ui @@ -0,0 +1,162 @@ + + + CTextMessageComponent + + + + 0 + 0 + 580 + 375 + + + + TabWidget + + + 0 + + + + All + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + QTextEdit::NoWrap + + + true + + + 10 + + + + + + + + UNICOM + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + QTextEdit::NoWrap + + + true + + + 10 + + + + + + + + COM1 + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + QTextEdit::NoWrap + + + true + + + 10 + + + + + + + + COM2 + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + QTextEdit::NoWrap + + + true + + + 10 + + + + + + + + +