From: James Turner Date: Fri, 26 Dec 2014 12:20:51 +0000 (+0300) Subject: In-app launcher for Mac, based on Qt5. X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=78e8f533124ad38c414d10470abcd2149b6d01e8;p=flightgear.git In-app launcher for Mac, based on Qt5. 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. --- 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 000000000..ddae8d36f Binary files /dev/null and b/src/GUI/history-icon.png differ diff --git a/src/GUI/large-search-icon.png b/src/GUI/large-search-icon.png new file mode 100644 index 000000000..8ab5253ab Binary files /dev/null and b/src/GUI/large-search-icon.png differ 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() } } -int fgInitAircraft(bool reinit) +void fgInitAircraftPaths(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); + if (!reinit) { + SGPath userAircraftDir = SGPath::documents(globals->get_fg_home()); + if (userAircraftDir != globals->get_fg_home()) { + userAircraftDir.append("FlightGear"); } + userAircraftDir.append("Aircraft"); - SGSharedPtr pkgRoot(globals->packageRoot()); - SGPropertyNode* aircraftProp = fgGetNode("/sim/aircraft", true); - aircraftProp->setAttribute(SGPropertyNode::PRESERVE, true); + 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) { 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::initAircraft() + +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); - } + 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() +{ 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()