diff --git a/src/plugins/simulator/xplane/simulator_xplane.cpp b/src/plugins/simulator/xplane/simulator_xplane.cpp index f9b0db72d..3087ce169 100644 --- a/src/plugins/simulator/xplane/simulator_xplane.cpp +++ b/src/plugins/simulator/xplane/simulator_xplane.cpp @@ -266,19 +266,38 @@ namespace BlackSimPlugin void CSimulatorXPlane::displayStatusMessage(const BlackMisc::CStatusMessage &message) const { + // avoid infinite recursion in case this function is called due to a message caused by this very function + static bool isInFunction = false; + if (isInFunction) { return; } + isInFunction = true; + /* We do not assert here as status message may come because of network problems */ if (!isConnected()) { return; } - //! \todo XP driver, display text message: XPLMSpeakString()? http://www.xsquawkbox.net/xpsdk/mediawiki/XPLMSpeakString - Q_UNUSED(message); + QColor color; + switch (message.getSeverity()) + { + case CStatusMessage::SeverityDebug: color = "teal"; break; + case CStatusMessage::SeverityInfo: color = "cyan"; break; + case CStatusMessage::SeverityWarning: color = "orange"; break; + case CStatusMessage::SeverityError: color = "red"; break; + } + + m_service->addTextMessage("swift: " + message.getMessage(), color.redF(), color.greenF(), color.blueF()); + isInFunction = false; } void CSimulatorXPlane::displayTextMessage(const BlackMisc::Network::CTextMessage &message) const { if (!isConnected()) { return; } - //! \todo XP driver, display text message: XPLMSpeakString()? http://www.xsquawkbox.net/xpsdk/mediawiki/XPLMSpeakString - Q_UNUSED(message); + QColor color; + if (message.isServerMessage()) { color = "orchid"; } + else if (message.isSupervisorMessage()) { color = "yellow"; } + else if (message.isPrivateMessage()) { color = "magenta"; } + else { color = "lime"; } + + m_service->addTextMessage(message.getSenderCallsign().toQString() + ": " + message.getMessage(), color.redF(), color.greenF(), color.blueF()); } CAircraftModelList CSimulatorXPlane::getInstalledModels() const diff --git a/src/plugins/simulator/xplane/xbus_service_proxy.cpp b/src/plugins/simulator/xplane/xbus_service_proxy.cpp index 17c4dcd01..3f2674bec 100644 --- a/src/plugins/simulator/xplane/xbus_service_proxy.cpp +++ b/src/plugins/simulator/xplane/xbus_service_proxy.cpp @@ -20,6 +20,11 @@ namespace BlackSimPlugin if (! dummy) { m_dbusInterface->relayParentSignals(); } } + void CXBusServiceProxy::addTextMessage(const QString &text, double red, double green, double blue) + { + m_dbusInterface->callDBus(QLatin1String("addTextMessage"), text, red, green, blue); + } + void CXBusServiceProxy::updateAirportsInRange() { m_dbusInterface->callDBus(QLatin1String("updateAirportsInRange")); diff --git a/src/plugins/simulator/xplane/xbus_service_proxy.h b/src/plugins/simulator/xplane/xbus_service_proxy.h index 6d3742326..f775c8568 100644 --- a/src/plugins/simulator/xplane/xbus_service_proxy.h +++ b/src/plugins/simulator/xplane/xbus_service_proxy.h @@ -79,6 +79,9 @@ namespace BlackSimPlugin void airportsInRangeUpdated(const QStringList &icaoCodes, const QStringList &names, const QList &lats, const QList &lons, const QList &alts); public slots: + //! \copydoc XBus::CService::addTextMessage + void addTextMessage(const QString &text, double red, double green, double blue); + //! \copydoc XBus::CService::updateAirportsInRange void updateAirportsInRange(); diff --git a/src/xbus/messages.cpp b/src/xbus/messages.cpp new file mode 100644 index 000000000..0d6fdc673 --- /dev/null +++ b/src/xbus/messages.cpp @@ -0,0 +1,141 @@ +/* Copyright (C) 2015 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include "messages.h" +#include +#include + +namespace XBus +{ + + const int c_screenWidth = 1024; + const int c_screenHeight = 768; + const int c_boxLeft = 128; + const int c_boxTop = c_screenHeight - 16; + const int c_boxRight = c_screenWidth - 128; + + void CMessageBox::draw() + { + static int lineHeight = 0; + if (! lineHeight) + { + XPLMGetFontDimensions(xplmFont_Basic, nullptr, &lineHeight, nullptr); + } + static const int lineSpace = lineHeight / 3; + const int boxBottom = c_boxTop - lineSpace * 2 - (lineHeight + lineSpace) * m_messages.size(); + XPLMDrawTranslucentDarkBox(c_boxLeft, c_boxTop, c_boxRight, boxBottom); + + static int arrowWidth = 0, arrowHeight = 0; + if (! arrowHeight) + { + XPGetElementDefaultDimensions(xpElement_LittleUpArrow, &arrowWidth, &arrowHeight, nullptr); + } + + static const int x = c_boxLeft + lineSpace; + if (m_upArrow) + { + const int y = c_boxTop - lineSpace - arrowHeight; + XPDrawElement(x, y, x + arrowWidth, y + arrowHeight, xpElement_LittleUpArrow, 0); + } + if (m_downArrow) + { + const int y = c_boxTop - (lineHeight + lineSpace) * m_messages.size(); + XPDrawElement(x, y, x + arrowWidth, y + arrowHeight, xpElement_LittleDownArrow, 0); + } + for (size_t i = 0; i < m_messages.size(); ++i) + { + const int y = c_boxTop - (lineHeight + lineSpace) * (i + 1); + XPLMDrawString(m_messages[i].m_rgb.data(), x + arrowWidth + arrowWidth / 2, y, const_cast(m_messages[i].m_text.c_str()), nullptr, xplmFont_Basic); + } + } + + int CMessageBox::maxLineLength() const + { + static int len = 0; + if (! len) + { + int charWidth; + XPLMGetFontDimensions(xplmFont_Basic, &charWidth, nullptr, nullptr); + len = (c_boxRight - c_boxLeft - 20) / charWidth; + } + return len; + } + + CMessageBoxControl::CMessageBoxControl() : + m_showCommand("org/swift-project/xbus/show_messages", "Show XBus text messages", [this] { show(); }), + m_hideCommand("org/swift-project/xbus/hide_messages", "Hide XBus text messages", [this] { hide(); }), + m_toggleCommand("org/swift-project/xbus/toggle_messages", "Toggle XBus text messages", [this] { toggle(); }), + m_scrollUpCommand("org/swift-project/xbus/scroll_up", "Scroll up XBus text messages", [this] { scrollUp(); }), + m_scrollDownCommand("org/swift-project/xbus/scroll_down", "Scroll down XBus text messages", [this] { scrollDown(); }), + m_scrollToTopCommand("org/swift-project/xbus/scroll_top", "Scroll to top of XBus text messages", [this] { scrollToTop(); }), + m_scrollToBottomCommand("org/swift-project/xbus/scroll_bottom", "Scroll to bottom of XBus text messages", [this] { scrollToBottom(); }), + m_debugCommand("org/swift-project/xbus/debug", "", [this] { static int c = 0; this->addMessage({ "hello " + std::to_string(c++), 0, .75, 0 }); }) + {} + + void CMessageBoxControl::addMessage(const CMessage &message) + { + if (m_messages.size() >= c_maxTotalLines) { m_messages.erase(m_messages.begin()); } + m_messages.push_back(message); + if (m_position + 1 >= m_messages.size() || ! m_visible) + { + show(); + scrollToBottom(); + } + } + + void CMessageBoxControl::scrollUp() + { + if (! m_visible) { return; } + + if (m_position - 1 >= std::min(m_messages.size(), c_maxVisibleLines)) + { + m_position--; + } + updateVisibleLines(); + } + + void CMessageBoxControl::scrollDown() + { + if (! m_visible) { return; } + + if (m_position + 1 <= m_messages.size()) + { + m_position++; + } + updateVisibleLines(); + } + + void CMessageBoxControl::scrollToTop() + { + if (! m_visible) { return; } + + m_position = std::min(m_messages.size(), c_maxVisibleLines); + updateVisibleLines(); + } + + void CMessageBoxControl::scrollToBottom() + { + if (! m_visible) { return; } + + m_position = m_messages.size(); + updateVisibleLines(); + } + + void CMessageBoxControl::updateVisibleLines() + { + const size_t lines = std::min(m_messages.size(), c_maxVisibleLines); + const auto end = m_messages.cbegin() + m_position; + m_messageBox.setMessages(end - lines, end); + m_messageBox.enableArrows(m_position > lines, m_position < m_messages.size()); + } + +} diff --git a/src/xbus/messages.h b/src/xbus/messages.h new file mode 100644 index 000000000..258f46182 --- /dev/null +++ b/src/xbus/messages.h @@ -0,0 +1,124 @@ +/* Copyright (C) 2015 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#ifndef BLACKSIM_XBUS_MESSAGES_H +#define BLACKSIM_XBUS_MESSAGES_H + +//! \file + +#include "drawable.h" +#include "command.h" +#include +#include +#include +#include + +namespace XBus +{ + + /*! + * Class representing a single line of text to be drawn in a message box. + */ + struct CMessage + { + //! Constructor. + CMessage(const std::string &text, float r, float g, float b) : m_text(text) + { + // MSVC initializer list bug + std::tie(m_rgb[0], m_rgb[1], m_rgb[2]) = std::tie(r, g, b); + } + + //! Text. + std::string m_text; + + //! Color. + std::array m_rgb; + }; + + /*! + * Class for drawing a gray box with text messages. + */ + class CMessageBox : public CDrawable + { + public: + //! Constructor. + CMessageBox() : CDrawable(xplm_Phase_Window, true) {} + + //! Set messages to draw in message box, from a pair of iterators. + template + void setMessages(Iterator begin, Iterator end) + { + m_messages.clear(); + std::copy(begin, end, std::back_inserter(m_messages)); + } + + //! Set whether to draw a small arrow at the bottom of the box. + void enableArrows(bool up, bool down) + { + m_upArrow = up; + m_downArrow = down; + } + + //! Returns the maximum number of characters per line. + int maxLineLength() const; + + protected: + virtual void draw() override; + + private: + std::vector m_messages; + bool m_upArrow = false; + bool m_downArrow = false; + }; + + /*! + * Class which builds upon CMessageBox with a scrollback buffer and commands for user control. + */ + class CMessageBoxControl + { + public: + //! Constructor. + CMessageBoxControl(); + + //! Add a new message to the bottom of the list. + void addMessage(const CMessage &message); + + //! \copydoc XBus::CMessageBox::maxLineLength + int maxLineLength() const { return m_messageBox.maxLineLength(); } + + private: + void show() { m_messageBox.show(); m_visible = true; } + void hide() { m_messageBox.hide(); m_visible = false; } + void toggle() { if (m_visible) { hide(); } else { show(); } } + void scrollUp(); + void scrollDown(); + void scrollToTop(); + void scrollToBottom(); + void updateVisibleLines(); + + bool m_visible = false; + std::vector m_messages; + size_t m_position = 0; + const size_t c_maxVisibleLines = 8; + const size_t c_maxTotalLines = 1024; + CMessageBox m_messageBox; + + CCommand m_showCommand; + CCommand m_hideCommand; + CCommand m_toggleCommand; + CCommand m_scrollUpCommand; + CCommand m_scrollDownCommand; + CCommand m_scrollToTopCommand; + CCommand m_scrollToBottomCommand; + CCommand m_debugCommand; + }; + +} + +#endif diff --git a/src/xbus/service.cpp b/src/xbus/service.cpp index fb14c10ec..6c0c18dac 100644 --- a/src/xbus/service.cpp +++ b/src/xbus/service.cpp @@ -28,6 +28,30 @@ namespace XBus emit aircraftModelChanged(path, filename, getAircraftLivery(), getAircraftIcaoCode()); } + void CService::addTextMessage(const QString &text, double red, double green, double blue) + { + if (text.isEmpty()) { return; } + int lineLength = m_messages.maxLineLength() - 1; + QStringList wrappedLines; + for (int i = 0; i < text.size(); i += lineLength) + { + static const QChar ellipsis = 0x2026; + wrappedLines.push_back(text.mid(i, lineLength) + ellipsis); + } + wrappedLines.back().chop(1); + if (wrappedLines.back().isEmpty()) { wrappedLines.pop_back(); } + else if (wrappedLines.back().size() == 1 && wrappedLines.size() > 1) + { + (wrappedLines.end() - 2)->chop(1); + (wrappedLines.end() - 2)->append(wrappedLines.back()); + wrappedLines.pop_back(); + } + for (const auto &line : wrappedLines) + { + m_messages.addMessage({ line.toStdString(), static_cast(red), static_cast(green), static_cast(blue) }); + } + } + QString CService::getAircraftModelPath() const { char filename[256]; diff --git a/src/xbus/service.h b/src/xbus/service.h index 053172834..16114dc7b 100644 --- a/src/xbus/service.h +++ b/src/xbus/service.h @@ -12,6 +12,7 @@ #define NOMINMAX #endif #include "datarefs.h" +#include "messages.h" #include "blackmisc/geo/geodesicgrid.h" #include #include @@ -71,6 +72,9 @@ namespace XBus void airportsInRangeUpdated(const QStringList &icaoCodes, const QStringList &names, const QDoubleList &lats, const QDoubleList &lons, const QDoubleList &alts); public slots: + //! Add a text message to the on-screen display, with RGB components in the range [0,1] + void addTextMessage(const QString &text, double red, double green, double blue); + //! Called by newly connected client to cause airportsInRangeUpdated to be emitted. void updateAirportsInRange(); @@ -217,6 +221,7 @@ namespace XBus double getSpeedBrakeRatio() const { return m_speedBrakeRatio.get(); } private: + CMessageBoxControl m_messages; BlackMisc::Geo::CGeodesicGrid<128, XPLMNavRef> m_airports; QTimer *m_airportUpdater = nullptr; void readAirportsDatabase(); diff --git a/src/xbus/xbus.pro b/src/xbus/xbus.pro index 64f6c79b5..1b870cac8 100644 --- a/src/xbus/xbus.pro +++ b/src/xbus/xbus.pro @@ -9,11 +9,12 @@ CONFIG += shared plugin CONFIG += blackmisc blackcore win32 { - equals(WORD_SIZE,64): LIBS += -lXPLM_64 - equals(WORD_SIZE,32): LIBS += -lXPLM + equals(WORD_SIZE,64): LIBS += -lXPLM_64 -lXPWidgets_64 + equals(WORD_SIZE,32): LIBS += -lXPLM -lXPWidgets } else:macx { - LIBS += -framework XPLM -framework Cocoa -framework CoreFoundation + LIBS += -framework XPLM -framework XPWidgets \ + -framework Cocoa -framework CoreFoundation DEFINES += XUTILS_EXCLUDE_MAC_CRAP=1 } else:unix {