From 78e8f533124ad38c414d10470abcd2149b6d01e8 Mon Sep 17 00:00:00 2001 From: James Turner Date: Fri, 26 Dec 2014 15:20:51 +0300 Subject: [PATCH] In-app launcher for Mac, based on Qt5. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old Mac launcher doesn’t work on Yosemite, add a tiny Qt-based launcher inside the main process (no need to fork / exec) which runs before the OSG window is created. Will be merged for 3.4, hopefully with no impact on other platforms. --- CMakeLists.txt | 10 + src/GUI/AirportDiagram.cxx | 264 +++++++ src/GUI/AirportDiagram.hxx | 55 ++ src/GUI/CMakeLists.txt | 34 +- src/GUI/EditRatingsFilterDialog.cxx | 39 + src/GUI/EditRatingsFilterDialog.hxx | 29 + src/GUI/EditRatingsFilterDialog.ui | 245 ++++++ src/GUI/Launcher.ui | 678 ++++++++++++++++ src/GUI/QtLauncher.cxx | 1138 +++++++++++++++++++++++++++ src/GUI/QtLauncher.hxx | 77 ++ src/GUI/history-icon.png | Bin 0 -> 3862 bytes src/GUI/large-search-icon.png | Bin 0 -> 8899 bytes src/GUI/resources.qrc | 6 + src/Include/config_cmake.h.in | 2 + src/Main/CMakeLists.txt | 4 + src/Main/fg_init.cxx | 48 +- src/Main/fg_init.hxx | 2 + src/Main/main.cxx | 36 +- src/Main/options.cxx | 25 +- src/Main/options.hxx | 7 +- src/Navaids/NavDataCache.cxx | 97 ++- src/Navaids/NavDataCache.hxx | 18 +- src/Viewer/WindowBuilder.cxx | 13 +- 23 files changed, 2784 insertions(+), 43 deletions(-) create mode 100644 src/GUI/AirportDiagram.cxx create mode 100644 src/GUI/AirportDiagram.hxx create mode 100644 src/GUI/EditRatingsFilterDialog.cxx create mode 100644 src/GUI/EditRatingsFilterDialog.hxx create mode 100644 src/GUI/EditRatingsFilterDialog.ui create mode 100644 src/GUI/Launcher.ui create mode 100644 src/GUI/QtLauncher.cxx create mode 100644 src/GUI/QtLauncher.hxx create mode 100644 src/GUI/history-icon.png create mode 100644 src/GUI/large-search-icon.png create mode 100644 src/GUI/resources.qrc diff --git a/CMakeLists.txt b/CMakeLists.txt index bab846818..f2ab94ff0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -277,6 +277,16 @@ endif (USE_DBUS) # Sqlite always depends on the threading lib list(APPEND SQLITE3_LIBRARY ${CMAKE_THREAD_LIBS_INIT}) +############################################################################## +## Qt5 setup setup + +find_package(Qt5 5.1 COMPONENTS Widgets) +if (Qt5Widgets_FOUND) + message(STATUS "Will enable Qt launcher GUI") + set(HAVE_QT 1) + set(CMAKE_AUTOMOC ON) +endif() + ############################################################################## find_package(PLIB REQUIRED puaux pu js fnt) diff --git a/src/GUI/AirportDiagram.cxx b/src/GUI/AirportDiagram.cxx new file mode 100644 index 000000000..68acba52f --- /dev/null +++ b/src/GUI/AirportDiagram.cxx @@ -0,0 +1,264 @@ +#include "AirportDiagram.hxx" + +#include +#include + +#include +#include +#include +#include + +/* equatorial and polar earth radius */ +const float rec = 6378137; // earth radius, equator (?) +const float rpol = 6356752.314f; // earth radius, polar (?) + +//Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis) +static float earth_radius_lat( float lat ) +{ + double a = cos(lat)/rec; + double b = sin(lat)/rpol; + return 1.0f / sqrt( a * a + b * b ); +} + + +AirportDiagram::AirportDiagram(QWidget* pr) : +QWidget(pr) +{ + setSizePolicy(QSizePolicy::MinimumExpanding, + QSizePolicy::MinimumExpanding); + setMinimumSize(100, 100); +} + +void AirportDiagram::setAirport(FGAirportRef apt) +{ + m_airport = apt; + m_projectionCenter = apt ? apt->geod() : SGGeod(); + m_scale = 1.0; + m_bounds = QRectF(); // clear + m_runways.clear(); + + if (apt) { + buildTaxiways(); + buildPavements(); + } + + update(); +} + +void AirportDiagram::addRunway(FGRunwayRef rwy) +{ + Q_FOREACH(RunwayData rd, m_runways) { + if (rd.runway == rwy->reciprocalRunway()) { + return; // only add one end of reciprocal runways + } + } + + QPointF p1 = project(rwy->geod()), + p2 = project(rwy->end()); + extendBounds(p1); + extendBounds(p2); + + RunwayData r; + r.p1 = p1; + r.p2 = p2; + r.widthM = qRound(rwy->widthM()); + r.runway = rwy; + m_runways.append(r); + update(); +} + +void AirportDiagram::addParking(FGParking* park) +{ + QPointF p = project(park->geod()); + extendBounds(p); + update(); +} + +void AirportDiagram::paintEvent(QPaintEvent* pe) +{ + QPainter p(this); + p.fillRect(rect(), QColor(0x3f, 0x3f, 0x3f)); + + // fit bounds within our available space, allowing for a margin + const int MARGIN = 32; // pixels + double ratioInX = (width() - MARGIN * 2) / m_bounds.width(); + double ratioInY = (height() - MARGIN * 2) / m_bounds.height(); + double scale = std::min(ratioInX, ratioInY); + + QTransform t; + t.translate(width() / 2, height() / 2); // center projection origin in the widget + t.scale(scale, scale); + // center the bounding box (may not be at the origin) + t.translate(-m_bounds.center().x(), -m_bounds.center().y()); + p.setTransform(t); + +// pavements + QBrush brush(QColor(0x9f, 0x9f, 0x9f)); + Q_FOREACH(const QPainterPath& path, m_pavements) { + p.drawPath(path); + } + +// taxiways + Q_FOREACH(const TaxiwayData& t, m_taxiways) { + QPen pen(QColor(0x9f, 0x9f, 0x9f)); + pen.setWidth(t.widthM); + p.setPen(pen); + p.drawLine(t.p1, t.p2); + } + +// runways + QPen pen(Qt::magenta); + QFont f; + f.setPixelSize(14); + p.setFont(f); + + Q_FOREACH(const RunwayData& r, m_runways) { + p.setTransform(t); + + pen.setWidth(r.widthM); + p.setPen(pen); + p.drawLine(r.p1, r.p2); + + // draw idents + QString ident = QString::fromStdString(r.runway->ident()); + + p.translate(r.p1); + p.rotate(r.runway->headingDeg()); + // invert scaling factor so we can use screen pixel sizes here + p.scale(1.0 / scale, 1.0/ scale); + + p.drawText(QRect(-100, 5, 200, 200), ident, Qt::AlignHCenter | Qt::AlignTop); + + FGRunway* recip = r.runway->reciprocalRunway(); + QString recipIdent = QString::fromStdString(recip->ident()); + + p.setTransform(t); + p.translate(r.p2); + p.rotate(recip->headingDeg()); + p.scale(1.0 / scale, 1.0/ scale); + + p.drawText(QRect(-100, 5, 200, 200), recipIdent, Qt::AlignHCenter | Qt::AlignTop); + } +} + + +void AirportDiagram::extendBounds(const QPointF& p) +{ + if (p.x() < m_bounds.left()) { + m_bounds.setLeft(p.x()); + } else if (p.x() > m_bounds.right()) { + m_bounds.setRight(p.x()); + } + + if (p.y() < m_bounds.top()) { + m_bounds.setTop(p.y()); + } else if (p.y() > m_bounds.bottom()) { + m_bounds.setBottom(p.y()); + } +} + +QPointF AirportDiagram::project(const SGGeod& geod) const +{ + double r = earth_radius_lat(geod.getLatitudeRad()); + double ref_lat = m_projectionCenter.getLatitudeRad(), + ref_lon = m_projectionCenter.getLongitudeRad(), + lat = geod.getLatitudeRad(), + lon = geod.getLongitudeRad(), + lonDiff = lon - ref_lon; + + double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) ); + if (c == 0.0) { + // angular distance from center is 0 + return QPointF(0.0, 0.0); + } + + double k = c / sin(c); + double x, y; + if (ref_lat == (90 * SG_DEGREES_TO_RADIANS)) + { + x = (SGD_PI / 2 - lat) * sin(lonDiff); + y = -(SGD_PI / 2 - lat) * cos(lonDiff); + } + else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS)) + { + x = (SGD_PI / 2 + lat) * sin(lonDiff); + y = (SGD_PI / 2 + lat) * cos(lonDiff); + } + else + { + x = k * cos(lat) * sin(lonDiff); + y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) ); + } + + return QPointF(x, -y) * r * m_scale; +} + +void AirportDiagram::buildTaxiways() +{ + m_taxiways.clear(); + for (unsigned int tIndex=0; tIndex < m_airport->numTaxiways(); ++tIndex) { + FGTaxiwayRef tx = m_airport->getTaxiwayByIndex(tIndex); + + TaxiwayData td; + td.p1 = project(tx->geod()); + td.p2 = project(tx->pointOnCenterline(tx->lengthM())); + extendBounds(td.p1); + extendBounds(td.p2); + td.widthM = tx->widthM(); + m_taxiways.append(td); + } +} + +void AirportDiagram::buildPavements() +{ + m_pavements.clear(); + for (unsigned int pIndex=0; pIndex < m_airport->numPavements(); ++pIndex) { + FGPavementRef pave = m_airport->getPavementByIndex(pIndex); + if (pave->getNodeList().empty()) { + continue; + } + + QPainterPath pp; + QPointF startPoint; + bool closed = true; + QPointF p0 = project(pave->getNodeList().front()->mPos); + + FGPavement::NodeList::const_iterator it; + for (it = pave->getNodeList().begin(); it != pave->getNodeList().end(); ) { + const FGPavement::BezierNode *bn = dynamic_cast(it->get()); + bool close = (*it)->mClose; + + // increment iterator so we can look at the next point + ++it; + QPointF nextPoint = (it == pave->getNodeList().end()) ? startPoint : project((*it)->mPos); + + if (bn) { + QPointF control = project(bn->mControl); + QPointF endPoint = close ? startPoint : nextPoint; + pp.quadTo(control, endPoint); + } else { + // straight line segment + if (closed) { + pp.moveTo(p0); + closed = false; + startPoint = p0; + } else + pp.lineTo(p0); + } + + if (close) { + closed = true; + pp.closeSubpath(); + startPoint = QPointF(); + } + + p0 = nextPoint; + } // of nodes iteration + + if (!closed) { + pp.closeSubpath(); + } + + m_pavements.append(pp); + } // of pavements iteration +} diff --git a/src/GUI/AirportDiagram.hxx b/src/GUI/AirportDiagram.hxx new file mode 100644 index 000000000..d6802e555 --- /dev/null +++ b/src/GUI/AirportDiagram.hxx @@ -0,0 +1,55 @@ +#include +#include + +#include +#include + +class AirportDiagram : public QWidget +{ +public: + AirportDiagram(QWidget* pr); + + void setAirport(FGAirportRef apt); + + void addRunway(FGRunwayRef rwy); + void addParking(FGParking* park); +protected: + virtual void paintEvent(QPaintEvent* pe); + // wheel event for zoom + + // mouse drag for pan + + +private: + void extendBounds(const QPointF& p); + QPointF project(const SGGeod& geod) const; + + void buildTaxiways(); + void buildPavements(); + + FGAirportRef m_airport; + SGGeod m_projectionCenter; + double m_scale; + QRectF m_bounds; + + struct RunwayData { + QPointF p1, p2; + int widthM; + FGRunwayRef runway; + }; + + QList m_runways; + + struct TaxiwayData { + QPointF p1, p2; + int widthM; + + bool operator<(const TaxiwayData& other) const + { + return widthM < other.widthM; + } + }; + + QList m_taxiways; + QList m_pavements; +}; diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt index e51a32679..d31e4f192 100644 --- a/src/GUI/CMakeLists.txt +++ b/src/GUI/CMakeLists.txt @@ -53,17 +53,41 @@ if(WIN32) FGWindowsMenuBar.cxx WindowsFileDialog.cxx) endif() - + if (APPLE) - list(APPEND HEADERS FGCocoaMenuBar.hxx - CocoaFileDialog.hxx + list(APPEND HEADERS FGCocoaMenuBar.hxx + CocoaFileDialog.hxx CocoaMouseCursor.hxx CocoaHelpers.h CocoaHelpers_private.h) - list(APPEND SOURCES FGCocoaMenuBar.mm + list(APPEND SOURCES FGCocoaMenuBar.mm CocoaFileDialog.mm CocoaMouseCursor.mm CocoaHelpers.mm) endif() - + + + + +if (HAVE_QT) + qt5_wrap_ui(uic_sources Launcher.ui EditRatingsFilterDialog.ui) + qt5_add_resources(qrc_sources resources.qrc) + + include_directories(${PROJECT_BINARY_DIR}/src/GUI) + + add_library(fglauncher QtLauncher.cxx + QtLauncher.hxx + AirportDiagram.cxx + AirportDiagram.hxx + EditRatingsFilterDialog.cxx + EditRatingsFilterDialog.hxx + ${uic_sources} + ${qrc_sources}) + + target_link_libraries(fglauncher Qt5::Core Qt5::Widgets ) + + +endif() + + flightgear_component(GUI "${SOURCES}" "${HEADERS}") diff --git a/src/GUI/EditRatingsFilterDialog.cxx b/src/GUI/EditRatingsFilterDialog.cxx new file mode 100644 index 000000000..f6b5bd144 --- /dev/null +++ b/src/GUI/EditRatingsFilterDialog.cxx @@ -0,0 +1,39 @@ +#include "EditRatingsFilterDialog.hxx" +#include "ui_EditRatingsFilterDialog.h" + +EditRatingsFilterDialog::EditRatingsFilterDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::EditRatingsFilterDialog) +{ + ui->setupUi(this); +} + +EditRatingsFilterDialog::~EditRatingsFilterDialog() +{ + delete ui; +} + +void EditRatingsFilterDialog::setRatings(int *ratings) +{ + for (int i=0; i<4; ++i) { + QAbstractSlider* s = sliderForIndex(i); + s->setValue(ratings[i]); + } +} + +int EditRatingsFilterDialog::getRating(int index) const +{ + return sliderForIndex(index)->value(); +} + +QAbstractSlider* EditRatingsFilterDialog::sliderForIndex(int index) const +{ + switch (index) { + case 0: return ui->fdmSlider; + case 1: return ui->systemsSlider; + case 2: return ui->cockpitSlider; + case 3: return ui->modelSlider; + default: + return 0; + } +} \ No newline at end of file diff --git a/src/GUI/EditRatingsFilterDialog.hxx b/src/GUI/EditRatingsFilterDialog.hxx new file mode 100644 index 000000000..0e0faf467 --- /dev/null +++ b/src/GUI/EditRatingsFilterDialog.hxx @@ -0,0 +1,29 @@ +#ifndef EDITRATINGSFILTERDIALOG_HXX +#define EDITRATINGSFILTERDIALOG_HXX + +#include + +namespace Ui { +class EditRatingsFilterDialog; +} + +class QAbstractSlider; + +class EditRatingsFilterDialog : public QDialog +{ + Q_OBJECT + +public: + explicit EditRatingsFilterDialog(QWidget *parent = 0); + ~EditRatingsFilterDialog(); + + void setRatings(int* ratings); + + int getRating(int index) const; +private: + Ui::EditRatingsFilterDialog *ui; + + QAbstractSlider* sliderForIndex(int index) const; +}; + +#endif // EDITRATINGSFILTERDIALOG_HXX diff --git a/src/GUI/EditRatingsFilterDialog.ui b/src/GUI/EditRatingsFilterDialog.ui new file mode 100644 index 000000000..ed6e698ad --- /dev/null +++ b/src/GUI/EditRatingsFilterDialog.ui @@ -0,0 +1,245 @@ + + + EditRatingsFilterDialog + + + Qt::WindowModal + + + + 0 + 0 + 623 + 594 + + + + Dialog + + + true + + + + + + Specify the minimum required completeness of aircraft in each area for it to be shown in the aircraft list. + + + true + + + + + + + Cockpit: + + + + + + + 5 + + + 3 + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksBelow + + + 1 + + + + + + + 3D panel, cockpit and instrumentation status + + + + + + + Flight model: + + + + + + + 5 + + + 3 + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksBelow + + + 1 + + + + + + + Accuracy of the flight (aerodynamic) model compared to available data and testing/ + + + true + + + + + + + Exterior model: + + + + + + + 5 + + + 3 + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksBelow + + + 1 + + + + + + + Quality and detail of exterior model, including animated moving parts such as control surfaces and under-carriage + + + true + + + + + + + Systems: + + + + + + + 5 + + + 3 + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksBelow + + + 1 + + + + + + + Completeness of systems modellings, including fuel handling, autopilot operation, startup & failure procedures. + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + EditRatingsFilterDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditRatingsFilterDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/GUI/Launcher.ui b/src/GUI/Launcher.ui new file mode 100644 index 000000000..46060e728 --- /dev/null +++ b/src/GUI/Launcher.ui @@ -0,0 +1,678 @@ + + + Launcher + + + + 0 + 0 + 696 + 711 + + + + Start FlightGear + + + + 6 + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + true + + + + + + + + + + true + + + + + + + + 0 + 0 + + + + + 171 + 128 + + + + TextLabel + + + + + + + Location: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + Aircraft: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + Settings: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + + + true + + + + + + + + + 0 + + + + Aircraft + + + + 4 + + + 4 + + + 8 + + + 4 + + + 4 + + + + + + + + 4 + + + 4 + + + + + Search: + + + + + + + Search aircraft + + + true + + + + + + + false + + + + + + + + + + + + 0 + 0 + + + + Hide aircraft based on completeness (rating) + + + true + + + + + + + Edit... + + + false + + + + + + + + + + Location + + + + 4 + + + 4 + + + 4 + + + + + + + Search: + + + + + + + Enter an ICAO code or search by name + + + + + + + + 0 + 0 + + + + false + + + false + + + + + + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + Runway: + + + + + + + + + + Parking: + + + + + + + On 10-mile final + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + TextLabel + + + Qt::AlignBottom|Qt::AlignHCenter + + + + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + + + + + + + + Settings + + + + + + + + Time of day: + + + + + + + + Current time + + + + + Dawn + + + + + Morning + + + + + Noon + + + + + Afternoon + + + + + Dusk + + + + + Evening + + + + + Night + + + + + + + + + + Enable Multi-sample anti-aliasing + + + + + + + Enable deferred rendering (Rembrandt) + + + + + + + Enable automatic scenery downloading (TerraSync) + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + <html><head/><body><p>If scenery download is disabled, you may need to download additional files from <a href="http://www.flightgear.org/download/scenery/"><span style=" text-decoration: underline; color:#0000ff;">this page</span></a> and install them in a scenery location; otherwise some objects may be missing from the world.</p></body></html> + + + true + + + true + + + + + + + + + Fetch real weather online + + + + + + + Start full-screen + + + + + + + Start paused + + + + + + + + + Custom aircraft directory: + + + + + + + Open in Finder + + + false + + + + + + + + + Additional scenery locations + + + + 8 + + + 8 + + + 8 + + + 8 + + + 0 + + + + + + + + Qt::Horizontal + + + + 567 + 20 + + + + + + + + + 0 + 0 + + + + + 20 + 20 + + + + - + + + + + + + + 0 + 0 + + + + + 20 + 20 + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Quit + + + false + + + + + + + Qt::Horizontal + + + + 412 + 20 + + + + + + + + Run + + + false + + + false + + + + + + + + + + AirportDiagram + QWidget +
GUI/AirportDiagram.hxx
+ 1 +
+
+ + +
diff --git a/src/GUI/QtLauncher.cxx b/src/GUI/QtLauncher.cxx new file mode 100644 index 000000000..e177b8925 --- /dev/null +++ b/src/GUI/QtLauncher.cxx @@ -0,0 +1,1138 @@ +#include "QtLauncher.hxx" + +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Simgear +#include +#include +#include +#include + +#include "ui_Launcher.h" +#include "EditRatingsFilterDialog.hxx" + +#include
+#include +#include +#include // for parking +#include
+ +using namespace flightgear; + +const int MAX_RECENT_AIRPORTS = 32; +const int MAX_RECENT_AIRCRAFT = 20; + +namespace { // anonymous namespace + +const int AircraftPathRole = Qt::UserRole + 1; +const int AircraftAuthorsRole = Qt::UserRole + 2; +const int AircraftRatingRole = Qt::UserRole + 100; + +void initNavCache() +{ + NavDataCache* cache = NavDataCache::instance(); + if (cache->isRebuildRequired()) { + QProgressDialog rebuildProgress("Initialising navigation data, this may take several minutes", + QString() /* cancel text */, + 0, 0); + rebuildProgress.setWindowModality(Qt::WindowModal); + rebuildProgress.show(); + + while (!cache->rebuild()) { + // sleep to give the rebuild thread more time + SGTimeStamp::sleepForMSec(50); + rebuildProgress.setValue(0); + QCoreApplication::processEvents(); + } + } +} + +struct AircraftItem +{ + AircraftItem() { + // oh for C++11 initialisers + for (int i=0; i<4; ++i) ratings[i] = 0; + } + + AircraftItem(QDir dir, QString filePath) + { + for (int i=0; i<4; ++i) ratings[i] = 0; + + SGPropertyNode root; + readProperties(filePath.toStdString(), &root); + + if (!root.hasChild("sim")) { + throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString()); + } + + SGPropertyNode_ptr sim = root.getNode("sim"); + + path = filePath; + description = sim->getStringValue("description"); + authors = sim->getStringValue("author"); + + if (sim->hasChild("rating")) { + parseRatings(sim->getNode("rating")); + } + + if (dir.exists("thumbnail.jpg")) { + thumbnail.load(dir.filePath("thumbnail.jpg")); + // resize to the standard size + if (thumbnail.height() > 128) { + thumbnail = thumbnail.scaledToHeight(128); + } + } + + } + + QString path; + QPixmap thumbnail; + QString description; + QString authors; + int ratings[4]; + +private: + void parseRatings(SGPropertyNode_ptr ratingsNode) + { + ratings[0] = ratingsNode->getIntValue("FDM"); + ratings[1] = ratingsNode->getIntValue("systems"); + ratings[2] = ratingsNode->getIntValue("cockpit"); + ratings[3] = ratingsNode->getIntValue("model"); + } +}; + +class AircraftScanThread : public QThread +{ + Q_OBJECT +public: + AircraftScanThread(QStringList dirsToScan) : + m_dirs(dirsToScan), + m_done(false) + { + + } + + /** thread-safe access to items already scanned */ + QList items() + { + QList result; + QMutexLocker g(&m_lock); + result.swap(m_items); + g.unlock(); + return result; + } + + void setDone() + { + m_done = true; + } +Q_SIGNALS: + void addedItems(); + +protected: + virtual void run() + { + Q_FOREACH(QString d, m_dirs) { + scanAircraftDir(QDir(d)); + if (m_done) { + return; + } + } + } + +private: + void scanAircraftDir(QDir path) + { + QStringList filters; + filters << "*-set.xml"; + Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) { + QDir childDir(child.absoluteFilePath()); + Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) { + try { + AircraftItem item(childDir, xmlChild.absoluteFilePath()); + // lock mutex whil we modify the items array + { + QMutexLocker g(&m_lock); + m_items.append(item); + } + } catch (sg_exception& e) { + continue; + } + + if (m_done) { + return; + } + } // of set.xml iteration + + emit addedItems(); + } // of subdir iteration + } + + QMutex m_lock; + QStringList m_dirs; + QList m_items; + bool m_done; +}; + +class AircraftItemModel : public QAbstractListModel +{ + Q_OBJECT +public: + AircraftItemModel(QObject* pr) : + QAbstractListModel(pr) + { + QStringList dirs; + Q_FOREACH(std::string ap, globals->get_aircraft_paths()) { + dirs << QString::fromStdString(ap); + } + + SGPath rootAircraft(globals->get_fg_root()); + rootAircraft.append("Aircraft"); + dirs << QString::fromStdString(rootAircraft.str()); + + m_scanThread = new AircraftScanThread(dirs); + connect(m_scanThread, &AircraftScanThread::finished, this, + &AircraftItemModel::onScanFinished); + connect(m_scanThread, &AircraftScanThread::addedItems, + this, &AircraftItemModel::onScanResults); + m_scanThread->start(); + } + + ~AircraftItemModel() + { + if (m_scanThread) { + m_scanThread->setDone(); + m_scanThread->wait(1000); + delete m_scanThread; + } + } + + virtual int rowCount(const QModelIndex& parent) const + { + return m_items.size(); + } + + virtual QVariant data(const QModelIndex& index, int role) const + { + const AircraftItem& item(m_items.at(index.row())); + if (role == Qt::DisplayRole) { + return item.description; + } else if (role == Qt::DecorationRole) { + return item.thumbnail; + } else if (role == AircraftPathRole) { + return item.path; + } else if (role == AircraftAuthorsRole) { + return item.authors; + } else if (role >= AircraftRatingRole) { + return item.ratings[role - AircraftRatingRole]; + } else if (role == Qt::ToolTipRole) { + return item.path; + } + + return QVariant(); + } + + QModelIndex indexOfAircraftPath(QString path) const + { + for (int row=0; row newItems = m_scanThread->items(); + if (newItems.isEmpty()) + return; + + int firstRow = m_items.count(); + int lastRow = firstRow + newItems.count() - 1; + beginInsertRows(QModelIndex(), firstRow, lastRow); + m_items.append(newItems); + endInsertRows(); + } + + void onScanFinished() + { + delete m_scanThread; + m_scanThread = NULL; + } + +private: + AircraftScanThread* m_scanThread; + QList m_items; +}; + +class AircraftItemDelegate : public QStyledItemDelegate +{ +public: + const int MARGIN = 4; + + virtual void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const + { + // selection feedback rendering + if (option.state & QStyle::State_Selected) { + QLinearGradient grad(option.rect.topLeft(), option.rect.bottomLeft()); + grad.setColorAt(0.0, QColor(152, 163, 180)); + grad.setColorAt(1.0, QColor(90, 107, 131)); + + QBrush backgroundBrush(grad); + painter->fillRect(option.rect, backgroundBrush); + + painter->setPen(QColor(90, 107, 131)); + painter->drawLine(option.rect.topLeft(), option.rect.topRight()); + + } + + QRect contentRect = option.rect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN); + + QPixmap thumbnail = index.data(Qt::DecorationRole).value(); + painter->drawPixmap(contentRect.topLeft(), thumbnail); + + // draw 1px frame + painter->setPen(QColor(0x7f, 0x7f, 0x7f)); + painter->setBrush(Qt::NoBrush); + painter->drawRect(contentRect.left(), contentRect.top(), thumbnail.width(), thumbnail.height()); + + QString description = index.data(Qt::DisplayRole).toString(); + contentRect.setLeft(contentRect.left() + MARGIN + thumbnail.width()); + + painter->setPen(Qt::black); + QFont f; + f.setPointSize(18); + painter->setFont(f); + + QRect actualBounds; + painter->drawText(contentRect, Qt::TextWordWrap, description, &actualBounds); + + QString authors = index.data(AircraftAuthorsRole).toString(); + + f.setPointSize(12); + painter->setFont(f); + + QRect authorsRect = contentRect; + authorsRect.moveTop(actualBounds.bottom() + MARGIN); + painter->drawText(authorsRect, Qt::TextWordWrap, + QString("by: %1").arg(authors), + &actualBounds); + + QRect r = contentRect; + r.setWidth(contentRect.width() / 2); + r.moveTop(actualBounds.bottom() + MARGIN); + r.setHeight(24); + + drawRating(painter, "Flight model:", r, index.data(AircraftRatingRole).toInt()); + r.moveTop(r.bottom()); + drawRating(painter, "Systems:", r, index.data(AircraftRatingRole + 1).toInt()); + + r.moveTop(actualBounds.bottom() + MARGIN); + r.moveLeft(r.right()); + drawRating(painter, "Cockpit:", r, index.data(AircraftRatingRole + 2).toInt()); + r.moveTop(r.bottom()); + drawRating(painter, "Exterior model:", r, index.data(AircraftRatingRole + 3).toInt()); + + + } + + virtual QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const + { + return QSize(500, 128 + (MARGIN * 2)); + } + +private: + void drawRating(QPainter* painter, QString label, const QRect& box, int value) const + { + const int DOT_SIZE = 10; + const int DOT_MARGIN = 4; + + QRect dotBox = box; + dotBox.setLeft(box.right() - (DOT_MARGIN * 6 + DOT_SIZE * 5)); + + painter->setPen(Qt::black); + QRect textBox = box; + textBox.setRight(dotBox.left() - DOT_MARGIN); + painter->drawText(textBox, Qt::AlignVCenter | Qt::AlignRight, label); + + painter->setPen(Qt::NoPen); + QRect dot(dotBox.left() + DOT_MARGIN, + dotBox.center().y() - (DOT_SIZE / 2), + DOT_SIZE, + DOT_SIZE); + for (int i=0; i<5; ++i) { + painter->setBrush((i < value) ? QColor(0x3f, 0x3f, 0x3f) : QColor(0xaf, 0xaf, 0xaf)); + painter->drawEllipse(dot); + dot.moveLeft(dot.right() + DOT_MARGIN); + } + } +}; + +} // of anonymous namespace + +class AirportSearchModel : public QAbstractListModel +{ + Q_OBJECT +public: + AirportSearchModel() : + m_searchActive(false) + { + } + + void setSearch(QString t) + { + beginResetModel(); + + m_airports.clear(); + m_ids.clear(); + + std::string term(t.toUpper().toStdString()); + // try ICAO lookup first + FGAirportRef ref = FGAirport::findByIdent(term); + if (ref) { + m_ids.push_back(ref->guid()); + m_airports.push_back(ref); + } else { + m_search.reset(new NavDataCache::ThreadedAirportSearch(term)); + QTimer::singleShot(100, this, SLOT(onSearchResultsPoll())); + m_searchActive = true; + } + + endResetModel(); + } + + bool isSearchActive() const + { + return m_searchActive; + } + + virtual int rowCount(const QModelIndex&) const + { + // if empty, return 1 for special 'no matches'? + return m_ids.size(); + } + + virtual QVariant data(const QModelIndex& index, int role) const + { + if (!index.isValid()) + return QVariant(); + + FGAirportRef apt = m_airports[index.row()]; + if (!apt.valid()) { + apt = FGPositioned::loadById(m_ids[index.row()]); + m_airports[index.row()] = apt; + } + + if (role == Qt::DisplayRole) { + QString name = QString::fromStdString(apt->name()); + return QString("%1: %2").arg(QString::fromStdString(apt->ident())).arg(name); + } + + if (role == Qt::EditRole) { + return QString::fromStdString(apt->ident()); + } + + if (role == Qt::UserRole) { + return m_ids[index.row()]; + } + + return QVariant(); + } + + QString firstIdent() const + { + if (m_ids.empty()) + return QString(); + + if (!m_airports.front().valid()) { + m_airports[0] = FGPositioned::loadById(m_ids.front()); + } + + return QString::fromStdString(m_airports.front()->ident()); + } + +Q_SIGNALS: + void searchComplete(); + +private slots: + void onSearchResultsPoll() + { + PositionedIDVec newIds = m_search->results(); + + beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1); + for (int i=m_ids.size(); i < newIds.size(); ++i) { + m_ids.push_back(newIds[i]); + m_airports.push_back(FGAirportRef()); // null ref + } + endInsertRows(); + + if (m_search->isComplete()) { + m_searchActive = false; + m_search.reset(); + emit searchComplete(); + } else { + QTimer::singleShot(100, this, SLOT(onSearchResultsPoll())); + } + } + +private: + PositionedIDVec m_ids; + mutable std::vector m_airports; + bool m_searchActive; + QScopedPointer m_search; +}; + +class AircraftProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + AircraftProxyModel(QObject* pr) : + QSortFilterProxyModel(pr), + m_ratingsFilter(true) + { + for (int i=0; i<4; ++i) { + m_ratings[i] = 3; + } + } + + void setRatings(int* ratings) + { + ::memcpy(m_ratings, ratings, sizeof(int) * 4); + invalidate(); + } + +public slots: + void setRatingFilterEnabled(bool e) + { + if (e == m_ratingsFilter) { + return; + } + + m_ratingsFilter = e; + invalidate(); + } + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const + { + if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) { + return false; + } + + if (m_ratingsFilter) { + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + for (int i=0; i<4; ++i) { + if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) { + return false; + } + } + } + + return true; + } + +private: + bool m_ratingsFilter; + int m_ratings[4]; +}; + +QtLauncher::QtLauncher() : + QDialog(), + m_ui(NULL) +{ + m_ui.reset(new Ui::Launcher); + m_ui->setupUi(this); + + for (int i=0; i<4; ++i) { + m_ratingFilters[i] = 3; + } + + m_airportsModel = new AirportSearchModel; + m_ui->searchList->setModel(m_airportsModel); + connect(m_ui->searchList, &QListView::clicked, + this, &QtLauncher::onAirportChoiceSelected); + connect(m_airportsModel, &AirportSearchModel::searchComplete, + this, &QtLauncher::onAirportSearchComplete); + + SGPath p = SGPath::documents(); + p.append("FlightGear"); + p.append("Aircraft"); + m_customAircraftDir = QString::fromStdString(p.str()); + m_ui->customAircraftDirLabel->setText(QString("Custom aircraft folder: %1").arg(m_customAircraftDir)); + + globals->append_aircraft_path(m_customAircraftDir.toStdString()); + + // create and configure the proxy model + m_aircraftProxy = new AircraftProxyModel(this); + m_aircraftProxy->setSourceModel(new AircraftItemModel(this)); + + m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive); + m_aircraftProxy->setSortRole(Qt::DisplayRole); + m_aircraftProxy->setDynamicSortFilter(true); + + m_ui->aircraftList->setModel(m_aircraftProxy); + m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_ui->aircraftList->setItemDelegate(new AircraftItemDelegate); + m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection); + connect(m_ui->aircraftList, &QListView::clicked, + this, &QtLauncher::onAircraftSelected); + + connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)), + this, SLOT(updateAirportDescription())); + connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)), + this, SLOT(updateAirportDescription())); + connect(m_ui->runwayRadio, SIGNAL(toggled(bool)), + this, SLOT(updateAirportDescription())); + connect(m_ui->parkingRadio, SIGNAL(toggled(bool)), + this, SLOT(updateAirportDescription())); + connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)), + this, SLOT(updateAirportDescription())); + + + connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun())); + connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit())); + connect(m_ui->airportEdit, SIGNAL(returnPressed()), + this, SLOT(onSearchAirports())); + + connect(m_ui->aircraftFilter, &QLineEdit::textChanged, + m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString); + + connect(m_ui->airportHistory, &QPushButton::clicked, + this, &QtLauncher::onPopupAirportHistory); + connect(m_ui->aircraftHistory, &QPushButton::clicked, + this, &QtLauncher::onPopupAircraftHistory); + + restoreSettings(); + + connect(m_ui->openAircraftDirButton, &QPushButton::clicked, + this, &QtLauncher::onOpenCustomAircraftDir); + + QAction* qa = new QAction(this); + qa->setShortcut(QKeySequence("Ctrl+Q")); + connect(qa, &QAction::triggered, this, &QtLauncher::onQuit); + addAction(qa); + + connect(m_ui->editRatingFilter, &QPushButton::clicked, + this, &QtLauncher::onEditRatingsFilter); + connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled, + m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled); + + QIcon historyIcon(":/history-icon"); + m_ui->aircraftHistory->setIcon(historyIcon); + m_ui->airportHistory->setIcon(historyIcon); + + m_ui->searchIcon->setPixmap(QPixmap(":/search-icon")); + + connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)), + this, SLOT(updateSettingsSummary())); + connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)), + this, SLOT(updateSettingsSummary())); + connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)), + this, SLOT(updateSettingsSummary())); + connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)), + this, SLOT(updateSettingsSummary())); + connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)), + this, SLOT(updateSettingsSummary())); + + updateSettingsSummary(); + + connect(m_ui->addSceneryPath, &QToolButton::clicked, + this, &QtLauncher::onAddSceneryPath); + connect(m_ui->removeSceneryPath, &QToolButton::clicked, + this, &QtLauncher::onRemoveSceneryPath); +} + +QtLauncher::~QtLauncher() +{ + +} + +bool QtLauncher::runLauncherDialog() +{ + Q_INIT_RESOURCE(resources); + + // startup the nav-cache now. This pre-empts normal startup of + // the cache, but no harm done. (Providing scenery paths are consistent) + + initNavCache(); + + // setup scenery paths now, especially TerraSync path for airport + // parking locations (after they're downloaded) + + QtLauncher dlg; + dlg.exec(); + if (dlg.result() != QDialog::Accepted) { + return false; + } + + return true; +} + +void QtLauncher::restoreSettings() +{ + QSettings settings; + m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool()); + m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool()); + m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool()); + m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool()); + m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool()); + m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool()); + m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt()); + + // full paths to -set.xml files + m_recentAircraft = settings.value("recent-aircraft").toStringList(); + + if (!m_recentAircraft.empty()) { + m_selectedAircraft = m_recentAircraft.front(); + } else { + // select the default C172p + } + + updateSelectedAircraft(); + + // ICAO identifiers + m_recentAirports = settings.value("recent-airports").toStringList(); + if (!m_recentAirports.empty()) { + setAirport(FGAirport::findByIdent(m_recentAirports.front().toStdString())); + } + updateAirportDescription(); + + // rating filters + m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool()); + int index = 0; + Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) { + m_ratingFilters[index++] = v.toInt(); + } + + m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked()); + m_aircraftProxy->setRatings(m_ratingFilters); + + QStringList sceneryPaths = settings.value("scenery-paths").toStringList(); + m_ui->sceneryPathsList->addItems(sceneryPaths); +} + +void QtLauncher::saveSettings() +{ + QSettings settings; + settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked()); + settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked()); + settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked()); + settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked()); + settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked()); + settings.setValue("start-paused", m_ui->startPausedCheck->isChecked()); + settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked()); + settings.setValue("recent-aircraft", m_recentAircraft); + settings.setValue("recent-airports", m_recentAirports); + settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex()); + + QStringList paths; + for (int i=0; isceneryPathsList->count(); ++i) { + paths.append(m_ui->sceneryPathsList->item(i)->text()); + } + + settings.setValue("scenery-paths", paths); +} + +void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const +{ + flightgear::Options* opt = flightgear::Options::sharedInstance(); + std::string stdName(name.toStdString()); + if (cbox->isChecked()) { + opt->addOption("enable-" + stdName, ""); + } else { + opt->addOption("disable-" + stdName, ""); + } +} + +void QtLauncher::onRun() +{ + accept(); + + flightgear::Options* opt = flightgear::Options::sharedInstance(); + setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync"); + setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch"); + setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt"); + setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen"); + setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze"); + + // aircraft + if (!m_selectedAircraft.isEmpty()) { + QFileInfo setFileInfo(m_selectedAircraft); + opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString()); + QString setFile = setFileInfo.fileName(); + Q_ASSERT(setFile.endsWith("-set.xml")); + setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion + opt->addOption("aircraft", setFile.toStdString()); + + // manage aircraft history + if (m_recentAircraft.contains(m_selectedAircraft)) + m_recentAircraft.removeOne(m_selectedAircraft); + m_recentAircraft.prepend(m_selectedAircraft); + if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT) + m_recentAircraft.pop_back(); + + qDebug() << Q_FUNC_INFO << "recent aircraft is now" << m_recentAircraft; + } + + // airport / location + if (m_selectedAirport) { + opt->addOption("airport", m_selectedAirport->ident()); + } + + if (m_ui->runwayRadio->isChecked()) { + int index = m_ui->runwayCombo->currentData().toInt(); + if (index >= 0) { + // explicit runway choice + opt->addOption("runway", m_selectedAirport->getRunwayByIndex(index)->ident()); + } + + if (m_ui->onFinalCheckbox->isChecked()) { + opt->addOption("glideslope", "3.0"); + opt->addOption("offset-distance", "10.0"); // in nautical miles + } + } else if (m_ui->parkingRadio->isChecked()) { + // parking selection + + } + + // time of day + if (m_ui->timeOfDayCombo->currentIndex() != 0) { + QString dayval = m_ui->timeOfDayCombo->currentText().toLower(); + opt->addOption("timeofday", dayval.toStdString()); + } + + // scenery paths + for (int i=0; isceneryPathsList->count(); ++i) { + QString path = m_ui->sceneryPathsList->item(i)->text(); + opt->addOption("fg-scenery", path.toStdString()); + } + + saveSettings(); +} + +void QtLauncher::onQuit() +{ + reject(); +} + +void QtLauncher::onSearchAirports() +{ + QString search = m_ui->airportEdit->text(); + m_airportsModel->setSearch(search); + + if (m_airportsModel->isSearchActive()) { + m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search)); + m_ui->locationStack->setCurrentIndex(2); + } else if (m_airportsModel->rowCount(QModelIndex()) == 1) { + QString ident = m_airportsModel->firstIdent(); + setAirport(FGAirport::findByIdent(ident.toStdString())); + m_ui->locationStack->setCurrentIndex(0); + } +} + +void QtLauncher::onAirportSearchComplete() +{ + int numResults = m_airportsModel->rowCount(QModelIndex()); + if (numResults == 0) { + m_ui->searchStatusText->setText(QString("No matching airports for '%1'").arg(m_ui->airportEdit->text())); + } else if (numResults == 1) { + QString ident = m_airportsModel->firstIdent(); + setAirport(FGAirport::findByIdent(ident.toStdString())); + m_ui->locationStack->setCurrentIndex(0); + } else { + m_ui->locationStack->setCurrentIndex(1); + } +} + +void QtLauncher::onAirportChanged() +{ + m_ui->runwayCombo->setEnabled(m_selectedAirport); + m_ui->parkingCombo->setEnabled(m_selectedAirport); + m_ui->airportDiagram->setAirport(m_selectedAirport); + + m_ui->runwayRadio->setChecked(true); // default back to runway mode + // unelss multiplayer is enabled ? + + if (!m_selectedAirport) { + m_ui->airportDescription->setText(QString()); + m_ui->airportDiagram->setEnabled(false); + return; + } + + m_ui->airportDiagram->setEnabled(true); + + m_ui->runwayCombo->clear(); + m_ui->runwayCombo->addItem("Automatic", -1); + for (unsigned int r=0; rnumRunways(); ++r) { + FGRunwayRef rwy = m_selectedAirport->getRunwayByIndex(r); + // add runway with index as data role + m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r); + + m_ui->airportDiagram->addRunway(rwy); + } + + m_ui->parkingCombo->clear(); + FGAirportDynamics* dynamics = m_selectedAirport->getDynamics(); + PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType( + m_selectedAirport->guid(), + FGPositioned::PARKING); + if (parkings.empty()) { + m_ui->parkingCombo->setEnabled(false); + m_ui->parkingRadio->setEnabled(false); + } else { + m_ui->parkingCombo->setEnabled(true); + m_ui->parkingRadio->setEnabled(true); + Q_FOREACH(PositionedID parking, parkings) { + FGParking* park = dynamics->getParking(parking); + m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()), parking); + + m_ui->airportDiagram->addParking(park); + } + } +} + +void QtLauncher::updateAirportDescription() +{ + if (!m_selectedAirport) { + m_ui->airportDescription->setText(QString("No airport selected")); + return; + } + + QString ident = QString::fromStdString(m_selectedAirport->ident()), + name = QString::fromStdString(m_selectedAirport->name()); + QString locationOnAirport; + if (m_ui->runwayRadio->isChecked()) { + bool onFinal = m_ui->onFinalCheckbox->isChecked(); + QString runwayName = (m_ui->runwayCombo->currentIndex() == 0) ? + "active runway" : + QString("runway %1").arg(m_ui->runwayCombo->currentText()); + + if (onFinal) { + locationOnAirport = QString("on 10-mile final to %1").arg(runwayName); + } else { + locationOnAirport = QString("on %1").arg(runwayName); + } + } else if (m_ui->parkingRadio->isChecked()) { + locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText()); + } + + m_ui->airportDescription->setText(QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport)); +} + +void QtLauncher::onAirportChoiceSelected(const QModelIndex& index) +{ + m_ui->locationStack->setCurrentIndex(0); + setAirport(FGPositioned::loadById(index.data(Qt::UserRole).toULongLong())); +} + +void QtLauncher::onAircraftSelected(const QModelIndex& index) +{ + m_selectedAircraft = index.data(AircraftPathRole).toString(); + updateSelectedAircraft(); +} + +void QtLauncher::updateSelectedAircraft() +{ + try { + QFileInfo info(m_selectedAircraft); + AircraftItem item(info.dir(), m_selectedAircraft); + m_ui->thumbnail->setPixmap(item.thumbnail); + m_ui->aircraftDescription->setText(item.description); + } catch (sg_exception& e) { + m_ui->thumbnail->setPixmap(QPixmap()); + m_ui->aircraftDescription->setText(""); + } +} + +void QtLauncher::onPopupAirportHistory() +{ + if (m_recentAirports.isEmpty()) { + return; + } + + QMenu m; + Q_FOREACH(QString aptCode, m_recentAirports) { + FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString()); + QString name = QString::fromStdString(apt->name()); + QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name)); + act->setData(aptCode); + } + + QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft()); + QAction* triggered = m.exec(popupPos); + if (triggered) { + FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString()); + setAirport(apt); + m_ui->airportEdit->clear(); + m_ui->locationStack->setCurrentIndex(0); + } +} + +QModelIndex QtLauncher::proxyIndexForAircraftPath(QString path) const +{ + return m_aircraftProxy->mapFromSource(sourceIndexForAircraftPath(path)); +} + +QModelIndex QtLauncher::sourceIndexForAircraftPath(QString path) const +{ + AircraftItemModel* sourceModel = qobject_cast(m_aircraftProxy->sourceModel()); + Q_ASSERT(sourceModel); + return sourceModel->indexOfAircraftPath(path); +} + +void QtLauncher::onPopupAircraftHistory() +{ + if (m_recentAircraft.isEmpty()) { + return; + } + + QMenu m; + Q_FOREACH(QString path, m_recentAircraft) { + QModelIndex index = sourceIndexForAircraftPath(path); + if (!index.isValid()) { + // not scanned yet + continue; + } + QAction* act = m.addAction(index.data(Qt::DisplayRole).toString()); + act->setData(path); + } + + QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft()); + QAction* triggered = m.exec(popupPos); + if (triggered) { + m_selectedAircraft = triggered->data().toString(); + QModelIndex index = proxyIndexForAircraftPath(m_selectedAircraft); + m_ui->aircraftList->selectionModel()->setCurrentIndex(index, + QItemSelectionModel::ClearAndSelect); + m_ui->aircraftFilter->clear(); + updateSelectedAircraft(); + } +} + +void QtLauncher::setAirport(FGAirportRef ref) +{ + if (m_selectedAirport == ref) + return; + + m_selectedAirport = ref; + onAirportChanged(); + + if (ref.valid()) { + // maintain the recent airport list + QString icao = QString::fromStdString(ref->ident()); + if (m_recentAirports.contains(icao)) { + // move to front + m_recentAirports.removeOne(icao); + m_recentAirports.push_front(icao); + } else { + // insert and trim list if necessary + m_recentAirports.push_front(icao); + if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) { + m_recentAirports.pop_back(); + } + } + } + + updateAirportDescription(); +} + +void QtLauncher::onOpenCustomAircraftDir() +{ + QUrl u = QUrl::fromLocalFile(m_customAircraftDir); + QDesktopServices::openUrl(u); +} + +void QtLauncher::onEditRatingsFilter() +{ + EditRatingsFilterDialog dialog(this); + dialog.setRatings(m_ratingFilters); + + dialog.exec(); + if (dialog.result() == QDialog::Accepted) { + QVariantList vl; + for (int i=0; i<4; ++i) { + m_ratingFilters[i] = dialog.getRating(i); + vl.append(m_ratingFilters[i]); + } + m_aircraftProxy->setRatings(m_ratingFilters); + + QSettings settings; + settings.setValue("min-ratings", vl); + } +} + +void QtLauncher::updateSettingsSummary() +{ + QStringList summary; + if (m_ui->timeOfDayCombo->currentIndex() > 0) { + summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower())); + } + + if (m_ui->rembrandtCheckbox->isChecked()) { + summary.append("Rembrandt enabled"); + } + + if (m_ui->fetchRealWxrCheckbox->isChecked()) { + summary.append("live weather"); + } + + if (m_ui->terrasyncCheck->isChecked()) { + summary.append("automatic scenery downloads"); + } + + if (m_ui->startPausedCheck->isChecked()) { + summary.append("paused"); + } + + QString s = summary.join(", "); + s[0] = s[0].toUpper(); + m_ui->settingsDescription->setText(s); +} + +void QtLauncher::onAddSceneryPath() +{ + QString path = QFileDialog::getExistingDirectory(this, tr("Choose scenery folder")); + if (!path.isEmpty()) { + m_ui->sceneryPathsList->addItem(path); + saveSettings(); + } +} + +void QtLauncher::onRemoveSceneryPath() +{ + if (m_ui->sceneryPathsList->currentItem()) { + delete m_ui->sceneryPathsList->currentItem(); + saveSettings(); + } +} + +#include "QtLauncher.moc" + diff --git a/src/GUI/QtLauncher.hxx b/src/GUI/QtLauncher.hxx new file mode 100644 index 000000000..62b630aaa --- /dev/null +++ b/src/GUI/QtLauncher.hxx @@ -0,0 +1,77 @@ + +#include +#include +#include +#include + +#include + +namespace Ui +{ + class Launcher; +} + +class AirportSearchModel; +class QModelIndex; +class AircraftProxyModel; +class QCheckBox; + +class QtLauncher : public QDialog +{ + Q_OBJECT +public: + QtLauncher(); + virtual ~QtLauncher(); + + static bool runLauncherDialog(); + +private slots: + void onRun(); + void onQuit(); + + void onSearchAirports(); + + void onAirportChanged(); + + void onAirportChoiceSelected(const QModelIndex& index); + void onAircraftSelected(const QModelIndex& index); + + void onPopupAirportHistory(); + void onPopupAircraftHistory(); + + void onOpenCustomAircraftDir(); + + void onEditRatingsFilter(); + + void updateAirportDescription(); + void updateSettingsSummary(); + + void onAirportSearchComplete(); + + void onAddSceneryPath(); + void onRemoveSceneryPath(); +private: + void setAirport(FGAirportRef ref); + void updateSelectedAircraft(); + + void restoreSettings(); + void saveSettings(); + + QModelIndex proxyIndexForAircraftPath(QString path) const; + QModelIndex sourceIndexForAircraftPath(QString path) const; + + void setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const; + + QScopedPointer m_ui; + AirportSearchModel* m_airportsModel; + AircraftProxyModel* m_aircraftProxy; + + FGAirportRef m_selectedAirport; + + QString m_selectedAircraft; + QStringList m_recentAircraft, + m_recentAirports; + QString m_customAircraftDir; + + int m_ratingFilters[4]; +}; diff --git a/src/GUI/history-icon.png b/src/GUI/history-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ddae8d36f6ec483c72f9169454b7eb1a18bcdbad GIT binary patch literal 3862 zcmZuz2UHVV*BwHnE1+~xB2ok)p(98SEs7=xNJ8(uNeiI#CQZ5l6fiU?QWTWlyYL8z zG!+mjkzS?e@xy!X`@X-;T6gBonZ3_G``lS~-DuPU&5JY~GynixL};l!JbOaU2NlKH z-P7NH7XWBf9N=&i0uG0uJlt#@oNWL=>tfamte);9OBA6=rfKLh4}@=*!=D=R`HmJd zSSK10i~>c5^ZTe>&|qQ|d}OWHN~RgbW2MeS8y0FgPQA^5>Tc6fQ(u^hbE4iTliHci zB=ulsCW$rUlXe>c*ID7b_h#`{Lti?Me;&_)8Dwvi%#Le7O=t4W=EZ^W#_4=N?DC#c z+BE4QGJ%Etn`1W=`R$~JmjianII&jx%8?dD^^ z(Fn^z#_@EAg{in~7iA5uaC@On#)omquuobm<62w8 zj4j$9woJ}k3)Cek^!gv`Z}G1ZvQ^@;F*DAqZ(&U=z#gyR%d1t*Y5G953x)E+>|5jp z1lYa+R8PO!xfbbcn%w!5PJlLcb=BFEEXN6Y>zB-NPN>_bmPpzCjQ;iGM|TQYl|pVH zm?dn~7Yg4L-cuVGpu2=4Sd;b{PUl)>9scO+5Pp@@%DNTtOJC<#Ds_0YeM5wN2s#mJ zO%on8Ivd0$EkMa0nB~M|?`;MIM989Q5tO}d3IO~DIj^gdKPX_uh(U~|x4F^2EUCgX zLEQgO;t@Bkag6J%whE8#;vQXswFh6WFniuixN{rmaw4VIYZ}QAs3=I-{32Jdggf~( z1PBk-VjzD(4%GrVD1+atC{CRHf?^Y7K4FgTz?TsJA@GAxX?F@^utDoZcUrn&O=X(M zwtJ#v(;*I2p_>fMxhf@zEcx^qs$&eChE#rV71V7e@-BFEB15wB;Shfjn^0H@9*$Dj zz2U`CPh}Ez^qO)AB1u6a3kYXS2aQ-d|Doo~{ECvb_1o;-S{B7i zHSMgkoST4fgjstEm2xi?`LG6>igQa%H4)8A#(3Q+i6&3KKVL`N(3-@~-G;x>fokZPrC;TLy`c!_WfD(RYPtfwxLN)Y6PE<3&0j znJ1VVRpQ=}Pgtzp;kwk*%KnqfJ+PL|Slm^r99%;`9@h}O(s6N?f6KQ~Xqsswl)lY# z)AH%{W||?|AQ1JL?kckLtep*>JUAIBaaEc0)Nx>ls-9YO%FDVvMI8hHwR+IN1v>O zO1;U?guKh8t~&WZ_HLcBpRymk;Nit0*DvUj^!hAAWRqIJ-2j+oiUe*4H`iB|QkvwH zwt{=YhfE#EVR2^A909ZIL3jTwJgEigv?;gghU8T~!fVV{w<|;-)H8F_ET36sT1H!j z{gmp`;cH9|Nv^q!ELL8s{LEKyokMk`!0&Ty_5EtOuQp#ruWHKVACIIgb?tKhYTBi? z;j=MXxVgZ!z_C#E=^;hCsQH1!L-vBWl(>etgdNGnI>H>`n{1m^2+M$rE(9<-Y zG}|LS8`6FN8!i|UKbVT9WvmbA+45Dz z%Hqq)*WQQ>E9Os?DwJ6jRiW+j9#jopF-e9R$*9yA*XUwZk@sL|m|2C+23}Dorz|JG zMYF{y=u_(uzUURstI&Xi}w7IrBV$&xs~5Bn70~ZuVp*rd!Od{y*#`{s}PHh&0?8i zF%w=B))iS1=EewOE}&himn)AtzIJ9hnOFt%L^{q{RSgzSEq*8~7MbyBSqr$6X;L}G z((s^xuy<*X=a~Okj8Y-WH>&B<{)Cs$&4%!RQO1>Dv$ca;adVUcPXykLAl}SmVFm>U zFAnC>(y?x`ei32w7=Aok%jJ;-wT8x-`MTr3=zmL_Gh8_uxIZ3G^yzfzoSIOWB-I|@ zqQ6IjmuXv@|r@1r$c3?pQk$jG( zcE&e}N8`sx@NMv0>R&YZU=8piN<~_6>dP0r!5wX6Z4IH^u+L_^#$Q-KswZ%t2zhEL zIuqt2Pd2Eq z$3oXQxv$PJoJL=1FJ90t-!-elE`Y~V8lsMqiQTt|@mnSbHYZ77T=@I&VKrQ*NwgKB z9Knp310hvD{P2EnwjDBGoSK-LCn!-oT>OVac=bW`h$^2&Oyw;T2?c>BSchbuD7^lY z!|1yHm0acFB<-7?7~6*Q=YMMd){5hO$rpy9t#W)nZKr9I^GRobadCG(bKbT8YTr)Z zaiEl~6xQ3W+2_y5lQyY!r%PoKW97!*>KaZhGeWbVr#-=Y^v;)YnIf4~>Gt^K?ss#Kd)!Dr ze#4(!DP2NI znWf;7tgC}pMU-BiKHZJm%gC(yWOt8{G|2qwrECs1a9g!IVkFFOR>Is{&QA& zW%+b}2(H3(tInhNThsB0KCaZRT-M}HKvqE0S|llc>{>(i3$xWghoui2E=^1BRiqbl zWzU;01Q3r*j$zY#^#KddPqR45V=0c-q0LWDO}{HF1x%BMDU0b%l?ME8lk7=Wt8=*m zG)STB5~VWI=)t&cantVV>v3~J)ib+<$XYn;H0`hg*>QnTOt_^{t+A|Grb&o9jGOX* zx7)SZABP*6i)a!2^W%QnaA{5;_PEK;vafT~X?P_=>F#to(s|X*Zc_9VWaR( z%yen9(q0G&e{8keleDyoJV2)9#4BN+QlD)5^1IkB&g_Tp9E;0g0?*Er)b3gsPXM63 zaz4m_%vWpxK=#!^5AB7%uPtZg<^r{_cC)mB`ntHEp#ebNSMF@;V&i22@pW-_^_25f z;Q0$7cQ!u{!+0QnLA;z4c+mGz5V)I%4MYkm2^Hm0q=7&n@*dWcmxZs0tLKfsll&`>nvJKGhl9J9 zgPSYlJgDVY|ATe@ zwbx3F?>J&(x4 z{x{)2h0C8S$RRx(Y|gBo8&njNhy9<}KX`fAx%>atjlWy{SM;pciZo|R|5`Ui8tI$b z#Q*?GL#V0fp{ctJ2zJIgOoOq@AHz|qf+jZ1ssF zNZ5t^v2BhdK4&xhN*GBc$H`1U;r0oIYBWj_jdy63Ri3wUFBJYT*i()k z1Z`eT%(~Z`ZuPEF!&>kwSrNrfpKjlh2=_J`9iBt&I`HnSfcNtv*ov7o{Si}rAn!MD z=q{Lza`1~s8TCpRQCy_;PVR#_yt>(fPn%Kn{$#W#G@VXoy)(Sk@EPf9e*0yK_0P>~ zWkSE#*mnuW49Eqdw-^z9NGcfK<`9hRR5$L}Z4^a{U06qAXu9C2!^dv=EXoZr>2fWS z8tL)A_+gqmIZZQFB%noFE;uXVb^uf+byj#g4G z=&3B?jqAo;7}w*Bnh7ry4;;QtyjJ^DbW!)K^A0CoIu-6EmDuAhCMmPL^*i8=fmq&7 z$Lt2sGL(?>JkX|nqm^L@zUex&Yz028lYs){S)i>WRVhBFp#uTp#8{4%`Y7W_N+pSG z6=)21&lKGExZVdR;!qFpuv}_l`j0;5p}lNI%qOFAZka-*t2XLGcECht7-eoFJ9RXVTV_grAnkN}BvfXHPF3$Vha9Me9=?DLKrq{B}5CFdX)GIMRR_b-EB(0iWTK)&{&}lLUe+VBx zRX*OgQl{0O3cK^74bGOqv{cr$d^+7R+E&4ws1}XI43_1BC~&@h8VlO8zz4^??SoPV zVEuyq&!5XIRRAwVy3xocYrb7LzdQx)LHIEi05_>eq}7=y@=C-i2jEd?xfTHqSArLk z&ZJqdZw**nAJu4G!TC6oy5B8_d8m$`))tQPpn5uy*)MiM#_Km!z{@IyLPc!7%u zYurE#3FSBGUvX65iPL4KNuKE8ddf;`2vdN1Wy9l05~QwBti?1hzW&OW)et{@{vTa4 zuEE#qJRB6GAmk3{8$_Ch^~Z#87Em8BS1Ag^Rp=T<%Xv^{;ZP&zmz5i-B#0WisF$B$ z055}$x)O1v25>>Y-|FB#Ig*o!(_sdZKC_Ky`i}KeJ|m9k`;(?<#dxPR5F|9;B#nlu zk9IXwH)TxLD;XnnCraTrM4>4Jg#JZs1I zhxyYl#WM7Du*Ko&fo%hcpQSJ|+cAG2p~Kz8y$d4k64XQ#tN+IoLgLw@59g?gnU!u; zg0ZS4QYeKmQ@>~CNUueFP_kh%U=?R=dzLu(#!SJPMpw#Ey_`9h(TIgOK~f`$os~je z8740?rzWD@B;_gPDf{C+VX?!c&abrQY(4chIbUU89)(1K)RWZJ!Lr1Xc-!RNRBsmb zq?uIrR3q+OHlt_$%J+r0jaZtUQk`0eg33?%ZMO4?$vm0bJo2h zzLQ9oLDx=4OZSm3Rj^NpHp3xfFk?MqQ?S3*N{^rpthcVWS!c+boPIF!!GfhGQY62u ze7}h6x5W2_67e$A;u;;x0=1eka)Si!&#$E$^c%F?Wzfhl?zlY zYAxgEMCVL5tT%i(;-Ma)ZA2Hd|GBZZ1o=*p?rtqNjvYm<;;?zMRsB&aTFTZP;}|0x zD`y96+@u>PFbqqr(2m!xXh^t#AKDxiNxYE=mxz`iZ+B}i@#VQRxx>11yIwe% z!E9d7+^B2>9{HuwB93oxskPKn9)(BWC?cDlC~>S+`&@JRzt7 z<+Te9_e;T2);-V0j~;#=?qDvsyqs)Li(>4+#DGF%G?faK``bZed-YYxLt2TulX z{Ddro++8i(-R?tfJB1ExiOPbf%k}4*)Z7g^o0repcVq1t?G@)R!*7S9&7UkeL@Z8f z=4n>88(Y;JZuH4C$!IROagBW@vg@^@wA){>s426VPh~RveFh(1iC9r*`IoS}r0c7F zwJ>rXwBPpQ{Kt(u+6~wJ-@bnT{oU|3AQJP%n-IJq4`Qf8avHwoKZ- z*W}-FY4`Z_;H0`g&+N0v9431lQXZ2UnjllicaXOT6OCGa)&;X z^(HUb)(|LYx&#Y96FFMxzhpSffr`y}M)>~PX1mKhuSe-c=;crUn+~u4{>s{0)q80> z;7|6`v8P;;nGZgvQRnFyHA9vv1QZFmKHzTmelYwezU{ks_Z#OYv7zLMm++m{o$21{H#U6r z7dgKq%kHKwXDoiUo$lq$7?0P4S;mGm%JMuUUsb4gV~_tF=X_iHc1NL1VLa+8YQd}O zv==iRmHKBjxP#-q{v!EzNv@y!O`9crum@xNdpBKDd9k=%-Qaw30TGHJI@AlY()!t%b=_2UlhX0MO-(ydS=cuUb0ss(@ z|8D>R8Cf&{01dm6ob(3|;JyJ~Eli&B4y=ev!pj=KDn%yc78pVI4$RC>rtyKfk-V3Z z*g@f_F*4$&oiWm5L|QoF+skUah_FCL{0J!kLJl`biZN^te7eZXl|6VX`A1;t&hX=c z``Fzd|H77?wwoO_ckPcerF_f;y+{cA<29{y^oBMKSC!oaR&3#qUL)rKMz^w8%W zh?W5JA2XjT+emaKyJS6*!x0Yxm3O&UaL#wp+0(KlTJgEhDa?4^2Z{p;=<{WB`PG& zdPf<@0@}-$z>W*Mb3{r+@PJH#=`>qIfX#T9Ar?_Mvso?-qijw5m@CBG7++#}JxdOT zgVWEXO_xhwFS2>lxl7VK*5?$~cx&!0ete$glUc^ZBY->_ArEi{{dUv$ahw=aKxhWM z)Pl^4dVv^^3OhSJ2Ut?o6U_UNyQCF4R?4zH>pZLS1H0Vmb?bKB1_bf`1njQ?_C>w@ zUk7+{pQW;l01jivv6&aegfHow%UDXtC{8WH!Z}ry2Rxk|`JKy^Sn=?#b;H(Q;fc)i z@N@GvYRc@3%xk#GuX#x^Rn#~Yl~maNCVDmc2^$Z5Pu7!1Z;RJNav@V2%0*^1GZ&03 z%6@M+R&SN(Rd#M(N3#!~-_+5407H!R(YrYDivFXU-%Fj?o2ZxjO_IwE$t;ac2Wk z_%nV!6eozF^0_*eKCA9D2;>@>E=!b(mFR_G`C)J%#XFN+Niq)$xgmBm+D)DZA3pWm z?K~Tq#%9GL2XMa^#!j2=it)&Xu9;CY>BRU{=E_e8Bi{6i5xG8)qRBNlo{Yn&0h=Iy zAp1)580)I$tE(&^rc{VL?^3Itob=xlEh3b+osZQDEKe)eeDBc7=k_I97Fh$_wRCk4 zA!Y!2aMoajWiP4S`%^g$M$ueCcJ`B!be6nfN0^Zt3u9ko$S9%)Scja%+qN2NHUZ5YsPqVj8zIBC_L zKM-!*uY%wAIcEmsj}0G|ZRxmmo+`#wES{fTb`?kE1f<<>w(*Llysw+X-IHM~%&NV) zj{K0g%29qr`Z_^tSyZ&fxvGt1%Ur5O1Ag_8-NZb^q~@m9GeI)scdQ&pD5|hpxhBTg z=WP?iZkK2}u@jR{E_(nOVmfLctt*zmbF6-Q(6@M10jeUjts*ww|CdPp=Ke$@&;2On zPL%wr)XwDEz|@rJSfgsFtLdyX5ze!4+)|U9L!&$%gLNl z0??kZ2QDT}E+<}u{M|GZB%m2RQ?z<%HkC1oBKg9#6uL_vJmPI;p!a&I+@fFzC%ZoU zYev!Y9J(UpROnOI81e(pIkp-TG~mOoQ~fLZ^5*haLxwq; z_rqqHJ&Hk1D5Z|ClJEY)L*xhEGy8D<+Y8~z{dXwsZniVK2BC;?Vbw`Idx;&R&yY)Y zj!i{x@>7ZW+RIN#7fcq{@n^i0qG<=NNpZ2=)Vlgm7_oaB?)6GzF~RPB zCX6MpZKj|*q-cW9*qD5Bc{`}`BZx2a6Q51mUt-vXZ)5|p{11I0IVx(9%JKnejQ;s# z;gf=Oj^e>#F}phoB~$B)A54atp4-q>{;s*XOwy`Q@26_$nKb9y(b#Nt`oHA90|(l? zV~#TIA>UT0`Jba}dS^iN;5)oGNs(n=r9_}>h)de0xE$%MldRgGrCxPLJ3*&QW+A*9A_PKQy~)QO0TzTM`XUm>P@79P>^Ji;CN5$69eOR!eZ6 zQWb7}~=Mw>@*Vzc2JUPe!_d7AVPWG+JO?`LnARvGJg%OyR|C{8BP zk4~70ozShY0oulq*@vHfXE+r(@EoevNgM3r#c<+^;bS=R8bMe7!`@e(PH>feY@~sa zi93vOI1(5!*GxWgEk`A|N^w#stx{^3=Bl9R8tW>D3lUz8C7{yDaGfWn6YUk_vkIRg zNK7knNf{y~hz3s9rLlpzs=Q3dTwKTAi*)IYOxsIZ(Tj&&_2rTNtW2Y+^n<;( z+(4EEcTBJiCnC)IOxWBl=q***MPGF z>1FeNs1M2`(%T0vF{(F`VlU~D<6%)>+y9bNdtn2Lp=Nz)PdGdzJM)h3a}I6dwFNXg3p!Loue6+O z!f(N4G5zIkKTDPWIaCf#lqMK4c-<~PybDpaGLzShoNM04d6$aA$0kI;X5}`)Gom-5 z6#ZReWE!Y6vK{Hxh+Ed{bLbe;dnwWDlUlW^RCaDWVs1Aj**m-ZJ%<3pxG@?{{%dES zW_GB2Cs}C8THhbF!(p*|Ri(tWKqe@?>#*9v4Iwps41X5P%F4w&mia)G3T{SbssSESGwT62_LVqZ}_{I z(nr?BG)E!>Q*_#R@kN@9)=Jc5-4t{pJw<5L9ju1B8_4|`^irrn0#V&uW*+&Rx{QiTg2%Bp}H#7d2h5{I2NaWU-M@38huO;8 zlzM9Yn)?m2yP~TfiaBSUfZWd;5wE80BUguEn`2$Gd`KLgQYC+wgpMRrs(jZI{Nt}g ze<|iqs|b5{3p0#Fp523AM-9YF)1dKIULmi+o!2#)=ECE6N4J_HTpVCBrUv(Je`1*p zIGFxm?#a8&km6`lCwekR=Ysf>Y82Gb>wpO<`}x{r3k~Nu{ErRK`1vds)x_GS4HOpR zE{2;fvDcL0p5EFS{=_iBQN+iZL1vhlP&2fNrFApQpLpMj?{=V`(1{%pneC|v_C?eD zc*zYJ3u!U@J5K7PNxe8<@Ra3i2j2H&#jef9ax(FePVRO^;SrItaw$^|cWPlhl9lC; z^`!suO1}#4Ph)cI(RX8$tH~?qaIg+<)?~jRPq))XZFM$ZCaKJLSv?6RcJF3b>=;&t+6CG3tOm<}yS%PXi2Yk@ zZY=SOXS1*SeAABO=<|&xQQ{e{PsV;~wVl{aZFhZVdvf@kSspwu)6}21M6T9!oz!& z<r5l))BUT{`cZEy zHRHHob95QI12&p#DOggwt3jwcB+Jj*_LlZC?St^;;9HK8K4i-5 zT}42DR{hN>`>l^_9j4Yj9jJDq52+>JJ@S7wR5mn#V_SkdCfUFJ*ezG9Y+O`AAi z7>i_IzUb7JnBaJl12pV};X&DG4i8mO-JA7A>V8jkkH-T#GS4V<4Mq239NXOO&X3KR zq=``uY^yrsrsLUkfTG(r9F#^M3dM?E?w4ZAf&0b=E;7VDmQ{Ep-w`Zxz=Im0FQKcK#*LxOL@V8MVJ71eA{e*Xzm8!9`v%0& zE~SxvXRP>xd}*1Fq5zzptjp@dy@mB}j(idfEd;msuPQ0n`;&I-!O zdj~%OJX)Tj%Tup{O&NgiY2d#K?i5kJwfc*>*?5!jB)j|!R=s^WXfB}2lgJ?>s1NXx z9PPADyz5gpqb^+o;uSF+w=n1vm}LmCoc@SNbpC>1psqs-wN+pp;Oy|`cs5{LV8Pyd zOXPWOGUk7)(%KV3a!|&)w0UR4EzpSpD!^A=`camF!N|aA&W10LlsY8dD`G5t#&{aS zsNH}gk3(8OZwn!^X?+)1%?o8_+iLDr{aXa0#(HwhkWS;+BlSLzehN^PWiEtMygN~Y zAo(Ew4eB_1z>H4j=$Q#h8zdLCZ1kh>4mO7)$w3sS;{jXP3rT6|ttY@3>63}KtPnB2 zxTw5LR#&4B14Mb}&p>xPYNhjl(6Vphvaf#wr?-dUuNK9gY8=|Jxt->)fLfi!NQ9>* z!u@vY$pn1Yb+&Z_yqX3>M*bIEA1$b^=6dpD(0jwph`aAe3ovqC67^V7!SYrZxgItWobhDx#AU@~fen`)!8RtVc7fNW4X*N_bmDScF1 znB?QzN1wNKIt``^AarblK18}2W<~T8pEDO$A`xf2kvqhqll|mGY!ZVn_$xu@n+1(W|tgGwq+xtP8plsDZdO9y%Hu$NbE2I&B>o)-BBGYc|=S=U? z^2)@UdC-DtU`k#~UA%IxdiSCv;J-rpJr#Rw;afc3r^RrxTCl;7w$s`}*4bU$0Nam6 ze&Sd+D=uKW@F_NxF?Hf5!y80;E z%Qa!N1fvk&IXUqS^(;Z#YmNSseX>&&svZ65({s?RQrg!5fdH83NTc1KeB@iHg}2fV zSX-OJtih(jL|oD{3qyMAC)EjB@=ZuIND7~@SR6vcc}IH5h$QMNeD?@H)4@g7RP#BH zh=_ylQxtbZ+Px9}gF3(iyPD6~{&5fXe<5u7ZW19%P7l~<$7pTo2ZDAd_H7bl1!Q+% zw4*8IumK9jUACSuZ(6NA*xKzsE1;I|xK{^Y(-L$#47d42ci8DM<>*R~x|(bx=#Irl zgY8RJ=?@*7Mn6C4ckLa6>elzbVn)w2Cnu>2my=a+T{5YuhG-}Wjml4^5(-ZgC3FJ znc1Q~okB61(^>uba~y>N|DBHN04bn~D6;>4Q=Mi2K>y=03w&Hm(t}inJaVr0UNH=a zrx&PJEW%0gC{yss;8Kqqk=u;n2M6A>pj{<=7W|xMBZ%++@15~~k;n*jrNue}z+BQQ z1bBRm(kduCA%HG_FQ}x7l}jgy(Tn8(IdqK#aLR3?7OfT&&$DggrGZ@ zZsASZ&9z3*N3$)O6E}n{(I6M7tpT7ch_ro*tZN#^FPP z7KD9E3s^xeDRWiV!|~v(0tK5h;z;L;zF;?hp$fe;3+PxCq81)4@|JFW!=uRYLJlg| zCH2d4D>?Bk(;(s>1*nroQ4`e$WdBL_XD2~$FmIfsIGxcD?I7$MRs1M5-N zpP^C60L#bub7nP$aD*DvgQ7O*ABUEgI`kA7!1kyRXI5Vk7bQMF@K>4 znFpQZu)_Dum3}&T1eb!Ij!8XzH1ysy@{T~{L4zJM!vuvMIr(78_&=we)?Hja4FV7> zRDynMv#Nhx*ujm<@TUeJBjnlwZ47IIZhf4zG+E;Cohbx80x3>LII-XPB3Y35c?PrY z=(a25%RH*j1u5@87Sjx5BF;$eKOsE=hSA&*2Z-4tfH2@=sGa~C643wu0-c3;08+pc WcCOs$s~$fR07~*|a^*6{f&UME*7%zM literal 0 HcmV?d00001 diff --git a/src/GUI/resources.qrc b/src/GUI/resources.qrc new file mode 100644 index 000000000..dc348f85f --- /dev/null +++ b/src/GUI/resources.qrc @@ -0,0 +1,6 @@ + + + history-icon.png + large-search-icon.png + + \ No newline at end of file diff --git a/src/Include/config_cmake.h.in b/src/Include/config_cmake.h.in index 0f1516464..20a2ca777 100644 --- a/src/Include/config_cmake.h.in +++ b/src/Include/config_cmake.h.in @@ -46,3 +46,5 @@ #cmakedefine HAVE_CRASHRPT #cmakedefine ENABLE_FLITE + +#cmakedefine HAVE_QT diff --git a/src/Main/CMakeLists.txt b/src/Main/CMakeLists.txt index f799ae608..949b379dd 100644 --- a/src/Main/CMakeLists.txt +++ b/src/Main/CMakeLists.txt @@ -152,6 +152,10 @@ if(ENABLE_FLITE) endif() endif() +if (Qt5Core_FOUND) + target_link_libraries(fgfs Qt5::Widgets fglauncher) +endif() + if (APPLE) install(TARGETS fgfs BUNDLE DESTINATION .) else() diff --git a/src/Main/fg_init.cxx b/src/Main/fg_init.cxx index 1e544277b..6d16c8c94 100644 --- a/src/Main/fg_init.cxx +++ b/src/Main/fg_init.cxx @@ -501,25 +501,37 @@ static void initAircraftDirsNasalSecurity() } } +void fgInitAircraftPaths(bool reinit) +{ + if (!reinit) { + SGPath userAircraftDir = SGPath::documents(globals->get_fg_home()); + if (userAircraftDir != globals->get_fg_home()) { + userAircraftDir.append("FlightGear"); + } + userAircraftDir.append("Aircraft"); + + SGSharedPtr pkgRoot(new Root(userAircraftDir, FLIGHTGEAR_VERSION)); + // set the http client later (too early in startup right now) + globals->setPackageRoot(pkgRoot); + } + + SGSharedPtr pkgRoot(globals->packageRoot()); + SGPropertyNode* aircraftProp = fgGetNode("/sim/aircraft", true); + aircraftProp->setAttribute(SGPropertyNode::PRESERVE, true); + + if (!reinit) { + flightgear::Options::sharedInstance()->initPaths(); + } +} + int fgInitAircraft(bool reinit) { - if (!reinit) { - // FIXME - use Documents/FlightGear/Aircraft - SGPath userAircraftDir = globals->get_fg_home(); - userAircraftDir.append("Aircraft"); - - SGSharedPtr pkgRoot(new Root(userAircraftDir, FLIGHTGEAR_VERSION)); - // set the http client later (too early in startup right now) - globals->setPackageRoot(pkgRoot); - } - - SGSharedPtr pkgRoot(globals->packageRoot()); - SGPropertyNode* aircraftProp = fgGetNode("/sim/aircraft", true); - aircraftProp->setAttribute(SGPropertyNode::PRESERVE, true); - if (!reinit) { flightgear::Options::sharedInstance()->initAircraft(); } + + SGSharedPtr pkgRoot(globals->packageRoot()); + SGPropertyNode* aircraftProp = fgGetNode("/sim/aircraft", true); string aircraftId(aircraftProp->getStringValue()); PackageRef acftPackage = pkgRoot->getPackageById(aircraftId); @@ -588,7 +600,12 @@ fgInitNav () return false; } } - + + // depend on when the NavCache was initialised, scenery paths may not + // have been setup. This is a safe place to consistently check the value, + // and drop the ground-nets if something has changed + cache->dropGroundnetsIfRequired(); + FGTACANList *channellist = new FGTACANList; globals->set_channellist( channellist ); @@ -1056,6 +1073,7 @@ void fgStartNewReset() } fgGetNode("/sim")->removeChild("aircraft-dir"); + fgInitAircraftPaths(true); fgInitAircraft(true); render = new FGRenderer; diff --git a/src/Main/fg_init.hxx b/src/Main/fg_init.hxx index 927ab9fdc..ef8a6bd82 100644 --- a/src/Main/fg_init.hxx +++ b/src/Main/fg_init.hxx @@ -39,6 +39,8 @@ bool fgInitHome(); // Read in configuration (file and command line) int fgInitConfig ( int argc, char **argv, bool reinit ); +void fgInitAircraftPaths(bool reinit); + int fgInitAircraft(bool reinit); // log various settings / configuration state diff --git a/src/Main/main.cxx b/src/Main/main.cxx index 42dcf6ba4..581bc7aa3 100644 --- a/src/Main/main.cxx +++ b/src/Main/main.cxx @@ -80,6 +80,10 @@ extern bool global_crashRptEnabled; #include "subsystemFactory.hxx" #include "options.hxx" +#if defined(HAVE_QT) +#include +#include +#endif using namespace flightgear; @@ -394,7 +398,14 @@ static void logToFile() } // Main top level initialization -int fgMainInit( int argc, char **argv ) { +int fgMainInit( int argc, char **argv ) +{ +#if defined(HAVE_QT) + QApplication app(argc, argv); + app.setOrganizationName("FlightGear"); + app.setApplicationName("FlightGear"); + app.setOrganizationDomain("flightgear.org"); +#endif // set default log levels sglog().setLogLevels( SG_ALL, SG_ALERT ); @@ -421,11 +432,6 @@ int fgMainInit( int argc, char **argv ) { SG_LOG( SG_GENERAL, SG_INFO, "Jenkins number/ID " << HUDSON_BUILD_NUMBER << ":" << HUDSON_BUILD_ID); - // Allocate global data structures. This needs to happen before - // we parse command line options - - - // seed the random number generator sg_srandom_time(); @@ -447,7 +453,23 @@ int fgMainInit( int argc, char **argv ) { } else if (configResult == flightgear::FG_OPTIONS_EXIT) { return EXIT_SUCCESS; } - + + // launcher needs to know the aircraft paths in use + fgInitAircraftPaths(false); + +#if defined(HAVE_QT) + bool showLauncher = flightgear::Options::checkForArg(argc, argv, "launcher"); + // an Info.plist bundle can't define command line arguments, but it can set + // environment variables. This avoids needed a wrapper shell-script on OS-X. + showLauncher |= (::getenv("FG_LAUNCHER") != 0); + + if (showLauncher) { + if (!QtLauncher::runLauncherDialog()) { + return EXIT_SUCCESS; + } + } +#endif + configResult = fgInitAircraft(false); if (configResult == flightgear::FG_OPTIONS_ERROR) { return EXIT_FAILURE; diff --git a/src/Main/options.cxx b/src/Main/options.cxx index 7ba86d827..65419fb21 100644 --- a/src/Main/options.cxx +++ b/src/Main/options.cxx @@ -1484,6 +1484,7 @@ struct OptionDesc { {"language", true, OPTION_IGNORE, "", false, "", 0 }, {"console", false, OPTION_IGNORE, "", false, "", 0 }, + {"launcher", false, OPTION_IGNORE, "", false, "", 0 }, {"disable-rembrandt", false, OPTION_BOOL, "/sim/rendering/rembrandt/enabled", false, "", 0 }, {"enable-rembrandt", false, OPTION_BOOL, "/sim/rendering/rembrandt/enabled", true, "", 0 }, {"renderer", true, OPTION_STRING, "/sim/rendering/rembrandt/renderer", false, "", 0 }, @@ -1953,18 +1954,22 @@ void Options::init(int argc, char **argv, const SGPath& appDataPath) config.append( "system.fgfsrc" ); readConfig(config); } - + +void Options::initPaths() +{ + BOOST_FOREACH(const string& paths, valuesForOption("fg-aircraft")) { + globals->append_aircraft_paths(paths); + } + + const char* envp = ::getenv("FG_AIRCRAFT"); + if (envp) { + globals->append_aircraft_paths(envp); + } + +} + void Options::initAircraft() { - BOOST_FOREACH(const string& paths, valuesForOption("fg-aircraft")) { - globals->append_aircraft_paths(paths); - } - - const char* envp = ::getenv("FG_AIRCRAFT"); - if (envp) { - globals->append_aircraft_paths(envp); - } - string aircraft; if (isOptionSet("aircraft")) { aircraft = valueForOption("aircraft"); diff --git a/src/Main/options.hxx b/src/Main/options.hxx index 97d71415a..40005363f 100644 --- a/src/Main/options.hxx +++ b/src/Main/options.hxx @@ -98,7 +98,12 @@ public: * (set properties, etc). */ OptionResult processOptions(); - + + /** + * process command line options relating to scenery / aircraft / data paths + */ + void initPaths(); + /** * init the aircraft options */ diff --git a/src/Navaids/NavDataCache.cxx b/src/Navaids/NavDataCache.cxx index f6756613b..18666d4d5 100644 --- a/src/Navaids/NavDataCache.cxx +++ b/src/Navaids/NavDataCache.cxx @@ -1072,7 +1072,10 @@ NavDataCache::NavDataCache() } homePath.append(os.str()); - + + // permit additional DB connections from the same process + sqlite3_config(SQLITE_CONFIG_MULTITHREAD); + for (int t=0; t < MAX_TRIES; ++t) { try { d.reset(new NavDataCachePrivate(homePath, this)); @@ -1162,8 +1165,6 @@ bool NavDataCache::isRebuildRequired() return true; } - dropGroundnetsIfRequired(); - SG_LOG(SG_NAVCACHE, SG_INFO, "NavCache: no main cache rebuild required"); return false; } @@ -2190,6 +2191,11 @@ bool NavDataCache::isReadOnly() const return d->readOnly; } +SGPath NavDataCache::path() const +{ + return d->path; +} + ///////////////////////////////////////////////////////////////////////////////////////// // Transaction RAII object @@ -2215,6 +2221,91 @@ void NavDataCache::Transaction::commit() _committed = true; _instance->commitTransaction(); } + +///////////////////////////////////////////////////////////////////////////// + +class NavDataCache::ThreadedAirportSearch::ThreadedAirportSearchPrivate : public SGThread +{ +public: + ThreadedAirportSearchPrivate() : + db(NULL), + isComplete(false), + quit(false) + {} + + virtual void run() + { + while (!quit) { + int err = sqlite3_step(query); + if (err == SQLITE_DONE) { + break; + } else if (err == SQLITE_ROW) { + PositionedID r = sqlite3_column_int64(query, 0); + SGGuard g(lock); + results.push_back(r); + } else if (err == SQLITE_BUSY) { + // sleep a tiny amount + SGTimeStamp::sleepForMSec(1); + } else { + std::string errMsg = sqlite3_errmsg(db); + SG_LOG(SG_NAVCACHE, SG_ALERT, "Sqlite error:" << errMsg << " running threaded airport query"); + } + } + + SGGuard g(lock); + isComplete = true; + } + + SGMutex lock; + sqlite3* db; + sqlite3_stmt_ptr query; + PositionedIDVec results; + bool isComplete; + bool quit; +}; + +NavDataCache::ThreadedAirportSearch::ThreadedAirportSearch(const std::string& term) : + d(new ThreadedAirportSearchPrivate) +{ + SGPath p = NavDataCache::instance()->path(); + int openFlags = SQLITE_OPEN_READONLY; + std::string pathUtf8 = simgear::strutils::convertWindowsLocal8BitToUtf8(p.str()); + sqlite3_open_v2(pathUtf8.c_str(), &d->db, openFlags, NULL); + + std::string sql = "SELECT rowid FROM positioned WHERE name LIKE '%" + term + + "%' AND type >= 1 AND type <= 3"; + sqlite3_prepare_v2(d->db, sql.c_str(), sql.length(), &d->query, NULL); + + d->start(); +} + +NavDataCache::ThreadedAirportSearch::~ThreadedAirportSearch() +{ + { + SGGuard g(d->lock); + d->quit = true; + } + + d->join(); + sqlite3_finalize(d->query); + sqlite3_close_v2(d->db); +} + +PositionedIDVec NavDataCache::ThreadedAirportSearch::results() const +{ + PositionedIDVec r; + { + SGGuard g(d->lock); + r = d->results; + } + return r; +} + +bool NavDataCache::ThreadedAirportSearch::isComplete() const +{ + SGGuard g(d->lock); + return d->isComplete; +} } // of namespace flightgear diff --git a/src/Navaids/NavDataCache.hxx b/src/Navaids/NavDataCache.hxx index e5d9e7c06..bf87c2423 100644 --- a/src/Navaids/NavDataCache.hxx +++ b/src/Navaids/NavDataCache.hxx @@ -57,7 +57,9 @@ public: // singleton accessor static NavDataCache* instance(); - + + SGPath path() const; + /** * predicate - check if the cache needs to be rebuilt. * This can happen is the cache file is missing or damaged, or one of the @@ -277,6 +279,20 @@ public: }; bool isReadOnly() const; + + class ThreadedAirportSearch + { + public: + ThreadedAirportSearch(const std::string& term); + ~ThreadedAirportSearch(); + + PositionedIDVec results() const; + + bool isComplete() const; + private: + class ThreadedAirportSearchPrivate; + std::auto_ptr d; + }; private: NavDataCache(); diff --git a/src/Viewer/WindowBuilder.cxx b/src/Viewer/WindowBuilder.cxx index 586cdbce0..901b72e14 100644 --- a/src/Viewer/WindowBuilder.cxx +++ b/src/Viewer/WindowBuilder.cxx @@ -25,6 +25,10 @@ #include +#if defined(HAVE_QT) && defined(SG_MAC) + #include +#endif + using namespace std; using namespace osg; @@ -244,7 +248,14 @@ GraphicsWindow* WindowBuilder::getDefaultWindow() GraphicsContext::Traits* traits = new GraphicsContext::Traits(*defaultTraits); traits->windowName = "FlightGear"; - + +#if defined(HAVE_QT) && defined(SG_MAC) + // avoid both QApplication and OSG::CocoaViewer doing single-application + // init (Apple menu, making front process, etc) + int flags = osgViewer::GraphicsWindowCocoa::WindowData::CheckForEvents; + traits->inheritedWindowData = new osgViewer::GraphicsWindowCocoa::WindowData(flags); +#endif + GraphicsContext* gc = GraphicsContext::createGraphicsContext(traits); if (gc) { defaultWindow = WindowSystemAdapter::getWSA()