1
0
Fork 0

Pure QtQuick aircraft-UI

Moves previews, searching, rating handling and extended aircraft
information entirely into QQ. Many cosmetic tweaks and improvements
still to come.
This commit is contained in:
James Turner 2017-10-13 17:48:24 +02:00
parent 041b9527d3
commit d34edaa569
43 changed files with 2146 additions and 1532 deletions

View file

@ -0,0 +1,112 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
Item {
id: root
readonly property int margin: 8
signal select(var uri);
signal showDetails(var uri)
implicitHeight: Math.max(contentBox.childrenRect.height, thumbnailBox.height) + footer.height
implicitWidth: ListView.view.width
readonly property bool __isSelected: ListView.isCurrentItem
MouseArea {
anchors.fill: parent
onClicked: {
if (__isSelected) {
root.showDetails(model.uri)
} else {
root.select(model.uri)
}
}
}
Rectangle {
id: thumbnailBox
// thumbnail border
y: Math.max(0, Math.round((contentBox.childrenRect.height - height) * 0.5))
border.width: 1
border.color: "#7f7f7f"
width: thumbnail.width
height: thumbnail.height
ThumbnailImage {
id: thumbnail
aircraftUri: model.uri
maximumSize.width: 172
maximumSize.height: 128
}
}
Column {
id: contentBox
anchors {
margins: root.margin
left: thumbnailBox.right
right: parent.right
top: parent.top
}
spacing: root.margin
AircraftVariantChoice {
id: titleBox
width: parent.width
aircraft: model.uri;
currentIndex: model.activeVariant
onSelected: {
model.activeVariant = index
root.select(model.uri)
}
}
Text {
id: description
width: parent.width
text: model.description
maximumLineCount: 3
wrapMode: Text.WordWrap
elide: Text.ElideRight
height: implicitHeight
visible: model.description != ""
}
AircraftDownloadPanel
{
id: downloadPanel
visible: (model.package != undefined)
packageSize: model.packageSizeBytes
installStatus: model.packageStatus
downloadedBytes: model.downloadedBytes
uri: model.uri
width: parent.width
compact: true
}
} // of content column
Item {
id: footer
height: 12
width: parent.width
anchors.bottom: parent.bottom
Rectangle {
color: "#68A6E1"
height: 1
width: parent.width - 60
anchors.centerIn: parent
}
}
} // of root item

View file

@ -1,10 +1,17 @@
import QtQuick 2.0
import FGLauncher 1.0
import FlightGear.Launcher 1.0
Item {
Rectangle {
id: root
property alias aircraftURI: aircraft.uri
property alias aurcradftURI: aircraft.uri
color: "white"
readonly property int margin: 16
MouseArea {
// consume all mouse-clicks on the detail view
anchors.fill: parent
}
AircraftInfo
{
@ -12,40 +19,151 @@ Item {
}
Column {
Text {
id: aircraftName
width: root.width - (margin * 2)
spacing: root.margin
anchors.horizontalCenter: parent.horizontalCenter
AircraftVariantChoice {
id: headingBox
fontPixelSize: 30
popupFontPixelSize: 18
anchors {
margins: 100 // space for back button
left: parent.left
right: parent.right
}
aircraft: aircraftURI
currentIndex: aircraft.variant
onSelected: {
aircraft.variant = index
_launcher.selectedAircraft = aircraft.uri;
}
}
Image {
id: preview
// selector overlay
// left / right arrows
// this element normally hides itself unless needed
AircraftWarningPanel {
id: warningBox
aircraftStatus: aircraft.status
requiredFGVersion: aircraft.minimumFGVersion
width: parent.width
}
Timer {
id: previewCycleTimer
// thumbnails + description + authors container
Item {
width: parent.width
height: childrenRect.height
Rectangle {
id: thumbnailBox
// thumbnail border
border.width: 1
border.color: "#7f7f7f"
width: thumbnail.width
height: thumbnail.height
ThumbnailImage {
id: thumbnail
aircraftUri: root.aircraftURI
maximumSize.width: 172
maximumSize.height: 128
}
}
Column {
anchors.left: thumbnailBox.right
anchors.leftMargin: root.margin
anchors.right: parent.right
spacing: root.margin
Text {
id: aircraftDescription
text: aircraft.description
width: parent.width
wrapMode: Text.WordWrap
visible: aircraft.description != ""
font.pixelSize: 14
}
Text {
id: aircraftAuthors
text: qsTr("by %1").arg(aircraft.authors)
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
visible: (aircraft.authors != undefined)
}
}
}
AircraftDownloadPanel {
width: parent.width
uri: aircraft.uri
installStatus: aircraft.installStatus
packageSize: aircraft.packageSize
downloadedBytes: aircraft.downloadedBytes
}
Text {
id: aircraftDescription
AircraftPreviewPanel {
id: previews
width: parent.width
previews: aircraft.previews
visible: aircraft.previews.length > 0
}
Grid {
id: ratingGrid
anchors.left: parent.left
visible: aircraft.ratings != undefined
rows: 2
columns: 3
rowSpacing: root.margin
columnSpacing: root.margin
Text {
id: ratingsLabel
text: qsTr("Ratings:")
}
AircraftRating {
title: qsTr("Flight model")
value: aircraft.ratings[0]
}
AircraftRating {
title: qsTr("Systems")
value: aircraft.ratings[1]
}
Item {
width: ratingsLabel.width
height: 1
} // placeholder
AircraftRating {
title: qsTr("Cockpit")
value: aircraft.ratings[2]
}
AircraftRating {
title: qsTr("Exterior")
value: aircraft.ratings[3]
}
}
Text {
id: aircraftAuthors
text: qsTr("Local file location: %1").arg(aircraft.pathOnDisk);
visible: aircraft.pathOnDisk != undefined
}
// info button
// version warning!
// ratings box
// package size
// install / download / update button
} // main layout column
}

View file

@ -5,28 +5,34 @@ Item {
id: root
property url uri
property int installStatus: AircraftModel.PackageNotInstalled
property int installStatus: LocalAircraftCache.PackageNotInstalled
property int packageSize: 0
property int downloadedBytes: 0
readonly property bool active: (installStatus == AircraftModel.PackageQueued) ||
(installStatus == AircraftModel.PackageDownloading)
readonly property bool active: (installStatus == LocalAircraftCache.PackageQueued) ||
(installStatus == LocalAircraftCache.PackageDownloading)
readonly property int compactWidth: button.width + sizeText.width
property bool compact: false
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
state: "not-installed"
onInstallStatusChanged: {
if (installStatus == AircraftModel.PackageInstalled) {
if (installStatus == LocalAircraftCache.PackageInstalled) {
state = "installed";
} else if (installStatus == AircraftModel.PackageNotInstalled) {
} else if (installStatus == LocalAircraftCache.PackageNotInstalled) {
state = "not-installed"
} else if (installStatus == AircraftModel.PackageUpdateAvailable) {
} else if (installStatus == LocalAircraftCache.PackageUpdateAvailable) {
state = "has-update"
} else if (installStatus == AircraftModel.PackageQueued) {
} else if (installStatus == LocalAircraftCache.PackageQueued) {
state = "queued"
} else if (installStatus == AircraftModel.PackageDownloading) {
} else if (installStatus == LocalAircraftCache.PackageDownloading) {
state = "downloading"
}
}
@ -177,7 +183,7 @@ Item {
Text {
id: statusText
visible: false
text: "Downloaded " + (root.downloadedBytes / 0x100000).toFixed(1) +
text: (compact ? "" : "Downloaded ") + (root.downloadedBytes / 0x100000).toFixed(1) +
"MB of " + (root.packageSize / 0x100000).toFixed(1) + "MB";
}
} // item container for progress bar and text

View file

@ -0,0 +1,225 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
Item {
id: root
readonly property int margin: 8
// background
height: Math.max(contentBox.childrenRect.height, thumbnailBox.height) + footer.height
MouseArea {
anchors.fill: parent
onClicked: {
root.ListView.view.currentIndex = model.index
_launcher.selectAircraft(model.uri);
}
}
Rectangle {
id: thumbnailBox
// thumbnail border
y: Math.max(0, Math.round((contentBox.childrenRect.height - height) * 0.5))
border.width: 1
border.color: "#7f7f7f"
width: thumbnail.width
height: thumbnail.height
ThumbnailImage {
id: thumbnail
aircraftUri: model.uri
maximumSize.width: 300
maximumSize.height: 200
}
Image {
id: previewIcon
visible: model.hasPreviews
anchors.left: parent.left
anchors.bottom: parent.bottom
source: "qrc:///preview-icon"
opacity: showPreviewsMouse.containsMouse ? 1.0 : 0.5
Behavior on opacity {
NumberAnimation { duration: 100 }
}
}
MouseArea {
id: showPreviewsMouse
hoverEnabled: true
anchors.fill: parent
visible: model.hasPreviews
onClicked: {
_launcher.showPreviewsFor(model.uri);
}
}
}
Column {
id: contentBox
anchors.leftMargin: root.margin
anchors.left: thumbnailBox.right
anchors.right: parent.right
anchors.rightMargin: root.margin
spacing: root.margin
Item
{
// box to contain the aircraft title and the variant
// selection button arrows
id: headingBox
width: parent.width
height: title.height
ArrowButton {
id: previousVariantButton
visible: (model.variantCount > 0) && (model.activeVariant > 0)
arrow: "qrc:///left-arrow-icon"
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
onClicked: {
model.activeVariant = (model.activeVariant - 1)
}
}
Text {
id: title
anchors.verticalCenter: parent.verticalCenter
anchors.left: previousVariantButton.right
anchors.right: nextVariantButton.left
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 24
text: model.title
elide: Text.ElideRight
}
ArrowButton {
id: nextVariantButton
arrow: "qrc:///right-arrow-icon"
visible: (model.variantCount > 0) && (model.activeVariant < model.variantCount - 1)
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
onClicked: {
model.activeVariant = (model.activeVariant + 1)
}
}
} // top header box (title + variant arrows)
// this element normally hides itself unless needed
AircraftWarningPanel {
id: warningBox
aircraftStatus: model.aircraftStatus
requiredFGVersion: model.requiredFGVersion
width: parent.width
}
Text {
id: authors
visible: (model.authors != undefined)
width: parent.width
text: "by: " + model.authors
maximumLineCount: 3
wrapMode: Text.WordWrap
elide: Text.ElideRight
}
Text {
id: description
width: parent.width
text: model.description
maximumLineCount: 10
wrapMode: Text.WordWrap
elide: Text.ElideRight
}
Item {
// this area holds the install/update/remove button,
// and the ratings grid. when a download / update is
// happening, the content is re-arranged to give more room
// for the progress bar and feedback
id: bottomContent
readonly property int minimumWidthForBottomContent: ratingGrid.width + downloadPanel.compactWidth + root.margin
width: Math.max(parent.width, minimumWidthForBottomContent)
height: ratingGrid.height
AircraftDownloadPanel
{
id: downloadPanel
visible: (model.package != undefined)
packageSize: model.packageSizeBytes
installStatus: model.packageStatus
downloadedBytes: model.downloadedBytes
uri: model.uri
width: parent.width // full width, grid sits on top
}
Grid {
id: ratingGrid
anchors.right: parent.right
// hide ratings when the panel is doing something, to
// make more room for the progress bar and text
visible: model.hasRatings && !downloadPanel.active
rows: 2
columns: 2
rowSpacing: root.margin
columnSpacing: root.margin
AircraftRating {
title: "Flight model"
value: model.ratingFDM;
}
AircraftRating {
title: "Systems"
value: model.ratingSystems;
}
AircraftRating {
title: "Cockpit"
value: model.ratingCockpit;
}
AircraftRating {
title: "Exterior"
value: model.ratingExterior;
}
}
}
} // of content column
Item {
id: footer
height: 12
width: parent.width
anchors.bottom: parent.bottom
Rectangle {
color: "#68A6E1"
height: 2
width: parent.width - 60
anchors.centerIn: parent
}
}
} // of root item

View file

@ -1,434 +0,0 @@
// AircraftItemDelegate.cxx - part of GUI launcher using Qt5
//
// Written by James Turner, started March 2015.
//
// Copyright (C) 2014 James Turner <zakalawe@mac.com>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "AircraftItemDelegate.hxx"
#include <QDebug>
#include <QPainter>
#include <QLinearGradient>
#include <QListView>
#include <QMouseEvent>
#include <QFontMetrics>
#include "AircraftModel.hxx"
const int DOT_SIZE = 11;
const int DOT_MARGIN = 2;
int dotBoxWidth()
{
return (DOT_MARGIN * 6 + DOT_SIZE * 5);
}
AircraftItemDelegate::AircraftItemDelegate(QListView* view) :
m_view(view)
{
view->viewport()->installEventFilter(this);
view->viewport()->setMouseTracking(true);
m_leftArrowIcon.load(":/left-arrow-icon");
m_rightArrowIcon.load(":/right-arrow-icon");
m_openPreviewsIcon.load(":/preview-icon");
m_openPreviewsIcon = m_openPreviewsIcon.scaled(32, 32, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
void AircraftItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option,
const QModelIndex & index) const
{
QRect contentRect = option.rect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
QVariant v = index.data(AircraftPackageStatusRole);
const AircraftItemStatus status = static_cast<AircraftItemStatus>(v.toInt());
if (status == MessageWidget) {
painter->setPen(QColor(0x7f, 0x7f, 0x7f));
painter->setBrush(Qt::NoBrush);
// draw bottom dividing line
painter->drawLine(contentRect.left(), contentRect.bottom() + MARGIN,
contentRect.right(), contentRect.bottom() + MARGIN);
return;
}
// 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());
}
// thumbnail
QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
quint32 yPos = contentRect.center().y() - (thumbnail.height() / 2);
painter->drawPixmap(contentRect.left(), yPos, thumbnail);
// draw 1px frame
QRect thumbFrame(contentRect.left(), yPos, thumbnail.width(), thumbnail.height());
painter->setPen(QColor(0x7f, 0x7f, 0x7f));
painter->setBrush(Qt::NoBrush);
painter->drawRect(thumbFrame);
if (!index.data(AircraftPreviewsRole).toList().empty()) {
QRect previewIconRect = m_openPreviewsIcon.rect();
previewIconRect.moveBottomLeft(thumbFrame.bottomLeft());
painter->drawPixmap(previewIconRect, m_openPreviewsIcon);
}
// draw bottom dividing line
painter->drawLine(contentRect.left(), contentRect.bottom() + MARGIN,
contentRect.right(), contentRect.bottom() + MARGIN);
int variantCount = index.data(AircraftVariantCountRole).toInt();
int currentVariant = index.data(AircraftVariantRole).toInt();
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 descriptionRect = contentRect.adjusted(ARROW_SIZE, 0, -ARROW_SIZE, 0),
actualBounds;
if (variantCount > 0) {
bool canLeft = (currentVariant > 0);
bool canRight = (currentVariant < variantCount );
QRect leftArrowRect = leftCycleArrowRect(option.rect, index);
if (canLeft) {
painter->drawPixmap(leftArrowRect.topLeft() + QPoint(2, 2), m_leftArrowIcon);
}
QRect rightArrowRect = rightCycleArrowRect(option.rect, index);
if (canRight) {
painter->drawPixmap(rightArrowRect.topLeft() + QPoint(2, 2), m_rightArrowIcon);
}
}
painter->drawText(descriptionRect, Qt::TextWordWrap, description, &actualBounds);
QString authors = index.data(AircraftAuthorsRole).toString();
f.setPointSize(12);
QFontMetrics smallMetrics(f);
painter->setFont(f);
if (!authors.isEmpty()) {
// ellide this beyond some maximum size, with a click to expand?
QRect authorsRect = descriptionRect;
authorsRect.moveTop(actualBounds.bottom() + MARGIN);
painter->drawText(authorsRect, Qt::TextWordWrap,
tr("by: %1").arg(authors),
&actualBounds);
}
QString longDescription = index.data(AircraftLongDescriptionRole).toString();
if (!longDescription.isEmpty()) {
QRect longDescriptionRect = descriptionRect;
longDescriptionRect.moveTop(actualBounds.bottom() + MARGIN);
painter->drawText(longDescriptionRect, Qt::TextWordWrap,
longDescription, &actualBounds);
}
painter->setRenderHint(QPainter::Antialiasing, true);
if (index.data(AircraftHasRatingsRole).toBool()) {
int ratingsWidth = smallMetrics.width("Flight model:") + dotBoxWidth();
QRect r = contentRect;
r.setWidth(ratingsWidth);
r.moveLeft(contentRect.right() - (ratingsWidth * 2));
r.moveTop(actualBounds.bottom() + MARGIN);
r.setHeight(qMax(24, smallMetrics.height() + MARGIN));
drawRating(painter, tr("Flight model:"), r, index.data(AircraftRatingRole).toInt());
r.moveTop(r.bottom());
drawRating(painter, tr("Systems:"), r, index.data(AircraftRatingRole + 1).toInt());
r.moveTop(actualBounds.bottom() + MARGIN);
r.moveLeft(r.right());
drawRating(painter, tr("Cockpit:"), r, index.data(AircraftRatingRole + 2).toInt());
r.moveTop(r.bottom());
drawRating(painter, tr("Exterior:"), r, index.data(AircraftRatingRole + 3).toInt());
}
double downloadFraction = 0.0;
QString buttonText, infoText;
QColor buttonColor(27, 122, 211);
double sizeInMBytes = index.data(AircraftPackageSizeRole).toInt();
sizeInMBytes /= 0x100000;
if (status == PackageDownloading) {
buttonText = tr("Cancel");
double downloadedMB = index.data(AircraftInstallDownloadedSizeRole).toInt();
downloadedMB /= 0x100000;
infoText = tr("%1 MB of %2 MB").arg(downloadedMB, 0, 'f', 1).arg(sizeInMBytes, 0, 'f', 1);
buttonColor = QColor(0xcf, 0xcf, 0xcf);
downloadFraction = downloadedMB / sizeInMBytes;
} else if (status == PackageQueued) {
buttonText = tr("Cancel");
infoText = tr("Waiting to download %1 MB").arg(sizeInMBytes, 0, 'f', 1);
buttonColor = QColor(0xcf, 0xcf, 0xcf);
} else {
infoText = QStringLiteral("%1MB").arg(sizeInMBytes, 0, 'f', 1);
if (status == PackageNotInstalled) {
buttonText = tr("Install");
} else if (status == PackageUpdateAvailable) {
buttonText = tr("Update");
} else if (status == PackageInstalled) {
bool canUninstall = index.data(AircraftPackageIdRole).isValid(); // local aircraft have no package ID
if (canUninstall) {
buttonText = tr("Uninstall");
} else {
infoText.clear();
}
}
}
QRect buttonRect = packageButtonRect(option.rect, index);
if (!buttonText.isEmpty()) {
painter->setBrush(Qt::NoBrush);
painter->setPen(Qt::NoPen);
painter->setBrush(buttonColor);
painter->drawRoundedRect(buttonRect, 5, 5);
painter->setPen(Qt::white);
painter->drawText(buttonRect, Qt::AlignCenter, buttonText);
}
QRect infoTextRect = buttonRect;
infoTextRect.setLeft(buttonRect.right() + MARGIN);
infoTextRect.setWidth(200);
if (status == PackageDownloading) {
QRect progressRect = infoTextRect;
progressRect.setHeight(6);
painter->setPen(QPen(QColor(0xcf, 0xcf, 0xcf), 0));
painter->setBrush(Qt::NoBrush);
painter->drawRoundedRect(progressRect, 3, 3);
infoTextRect.setTop(progressRect.bottom() + 1);
QRect progressBarRect = progressRect.marginsRemoved(QMargins(2, 2, 2, 2));
progressBarRect.setWidth(static_cast<int>(progressBarRect.width() * downloadFraction));
painter->setBrush(QColor(27, 122, 211));
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(progressBarRect, 2, 2);
}
if (!infoText.isEmpty()) {
painter->setPen(Qt::black);
painter->drawText(infoTextRect, Qt::AlignLeft | Qt::AlignVCenter, infoText);
}
painter->setRenderHint(QPainter::Antialiasing, false);
}
QSize AircraftItemDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const
{
QVariant v = index.data(AircraftPackageStatusRole);
const AircraftItemStatus status = static_cast<AircraftItemStatus>(v.toInt());
if (status == MessageWidget) {
QSize r = option.rect.size();
r.setHeight(100);
return r;
}
QRect contentRect = option.rect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
QSize thumbnailSize = index.data(AircraftThumbnailSizeRole).toSize();
contentRect.setLeft(contentRect.left() + MARGIN + thumbnailSize.width());
contentRect.setBottom(9999); // large value to avoid clipping
contentRect.adjust(ARROW_SIZE, 0, -ARROW_SIZE, 0);
QFont f;
f.setPointSize(18);
QFontMetrics metrics(f);
int textHeight = metrics.boundingRect(contentRect, Qt::TextWordWrap,
index.data().toString()).height();
f.setPointSize(12);
QFontMetrics smallMetrics(f);
QString authors = tr("by: %1").arg(index.data(AircraftAuthorsRole).toString());
if (!authors.isEmpty()) {
textHeight += MARGIN;
textHeight += smallMetrics.boundingRect(contentRect, Qt::TextWordWrap, authors).height();
}
QString desc = index.data(AircraftLongDescriptionRole).toString();
if (!desc.isEmpty()) {
textHeight += MARGIN;
textHeight += smallMetrics.boundingRect(contentRect, Qt::TextWordWrap, desc).height();
}
if (index.data(AircraftHasRatingsRole).toBool()) {
// ratings
int ratingHeight = qMax(24, smallMetrics.height() + MARGIN);
textHeight += ratingHeight * 2;
}
textHeight += BUTTON_HEIGHT;
textHeight = qMax(textHeight, thumbnailSize.height());
return QSize(option.rect.width(), textHeight + (MARGIN * 2));
}
bool AircraftItemDelegate::eventFilter( QObject*, QEvent* event )
{
if ( event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease )
{
QMouseEvent* me = static_cast< QMouseEvent* >( event );
QModelIndex index = m_view->indexAt( me->pos() );
int variantCount = index.data(AircraftVariantCountRole).toInt();
int variantIndex = index.data(AircraftVariantRole).toInt();
QRect vr = m_view->visualRect(index);
if ( (event->type() == QEvent::MouseButtonRelease) && (variantCount > 0) )
{
QRect leftCycleRect = leftCycleArrowRect(vr, index),
rightCycleRect = rightCycleArrowRect(vr, index);
if ((variantIndex > 0) && leftCycleRect.contains(me->pos())) {
m_view->model()->setData(index, variantIndex - 1, AircraftVariantRole);
emit variantChanged(index);
return true;
} else if ((variantIndex < variantCount) && rightCycleRect.contains(me->pos())) {
m_view->model()->setData(index, variantIndex + 1, AircraftVariantRole);
emit variantChanged(index);
return true;
}
}
if ((event->type() == QEvent::MouseButtonRelease) &&
packageButtonRect(vr, index).contains(me->pos()))
{
QVariant v = index.data(AircraftPackageStatusRole);
const AircraftItemStatus status = static_cast<AircraftItemStatus>(v.toInt());
if (status == PackageNotInstalled) {
emit requestInstall(index);
} else if ((status == PackageDownloading) || (status == PackageQueued)) {
emit cancelDownload(index);
} else if (status == PackageUpdateAvailable) {
emit requestInstall(index);
} else if (status == PackageInstalled) {
emit requestUninstall(index);
}
return true;
}
if ((event->type() == QEvent::MouseButtonRelease) &&
!index.data(AircraftPreviewsRole).toList().empty() &&
showPreviewsRect(vr, index).contains(me->pos()))
{
emit showPreviews(index);
}
} else if ( event->type() == QEvent::MouseMove ) {
}
return false;
}
QRect AircraftItemDelegate::leftCycleArrowRect(const QRect& visualRect, const QModelIndex& index) const
{
QRect contentRect = visualRect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
contentRect.setLeft(contentRect.left() + MARGIN + thumbnail.width());
QRect r = contentRect;
r.setRight(r.left() + ARROW_SIZE);
r.setBottom(r.top() + ARROW_SIZE);
return r;
}
QRect AircraftItemDelegate::rightCycleArrowRect(const QRect& visualRect, const QModelIndex& index) const
{
QRect contentRect = visualRect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
contentRect.setLeft(contentRect.left() + MARGIN + thumbnail.width());
QRect r = contentRect;
r.setLeft(r.right() - ARROW_SIZE);
r.setBottom(r.top() + ARROW_SIZE);
return r;
}
QRect AircraftItemDelegate::packageButtonRect(const QRect& visualRect, const QModelIndex& index) const
{
QRect contentRect = visualRect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
contentRect.setLeft(contentRect.left() + MARGIN + thumbnail.width());
return QRect(contentRect.left() + ARROW_SIZE, contentRect.bottom() - BUTTON_HEIGHT,
BUTTON_WIDTH, BUTTON_HEIGHT);
}
QRect AircraftItemDelegate::showPreviewsRect(const QRect& visualRect, const QModelIndex& index) const
{
QRect contentRect = visualRect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
const quint32 yPos = contentRect.center().y() - (thumbnail.height() / 2);
QRect thumbFrame(contentRect.left(), yPos, thumbnail.width(), thumbnail.height());
QRect previewIconRect = m_openPreviewsIcon.rect();
previewIconRect.moveBottomLeft(thumbFrame.bottomLeft());
return previewIconRect;
}
void AircraftItemDelegate::drawRating(QPainter* painter, QString label, const QRect& box, int value) const
{
QRect dotBox = box;
dotBox.setLeft(box.right() - dotBoxWidth());
painter->setPen(Qt::black);
QRect textBox = box;
textBox.setRight(dotBox.left());
painter->drawText(textBox, Qt::AlignVCenter | Qt::AlignRight, label);
painter->setPen(Qt::NoPen);
// magic +1 offset in to account for fonts having more empty ascent
// space than descent space
QRect dot(dotBox.left() + DOT_MARGIN,
dotBox.center().y() - (DOT_SIZE / 2) + 1,
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);
}
}

View file

@ -1,72 +0,0 @@
// AircraftItemDelegate.hxx - part of GUI launcher using Qt5
//
// Written by James Turner, started March 2015.
//
// Copyright (C) 2014 James Turner <zakalawe@mac.com>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FG_GUI_AIRCRAFT_ITEM_DELEGATE
#define FG_GUI_AIRCRAFT_ITEM_DELEGATE
#include <QStyledItemDelegate>
class QListView;
class AircraftItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
static const int MARGIN = 4;
static const int ARROW_SIZE = 20;
static const int BUTTON_HEIGHT = 24;
static const int BUTTON_WIDTH = 80;
AircraftItemDelegate(QListView* view);
virtual void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const Q_DECL_OVERRIDE;
virtual QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const Q_DECL_OVERRIDE;
virtual bool eventFilter( QObject*, QEvent* event ) Q_DECL_OVERRIDE;
Q_SIGNALS:
void variantChanged(const QModelIndex& index);
void requestInstall(const QModelIndex& index);
void requestUninstall(const QModelIndex& index);
void cancelDownload(const QModelIndex& index);
void showPreviews(const QModelIndex& index);
private:
QRect leftCycleArrowRect(const QRect& visualRect, const QModelIndex& index) const;
QRect rightCycleArrowRect(const QRect& visualRect, const QModelIndex& index) const;
QRect packageButtonRect(const QRect& visualRect, const QModelIndex& index) const;
QRect showPreviewsRect(const QRect& visualRect, const QModelIndex& index) const;
void drawRating(QPainter* painter, QString label, const QRect& box, int value) const;
QListView* m_view;
QPixmap m_leftArrowIcon,
m_rightArrowIcon,
m_openPreviewsIcon;
};
#endif

View file

@ -1,5 +1,5 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
import QtQuick 2.2
import FlightGear.Launcher 1.0 as FG
Item
{
@ -7,230 +7,62 @@ Item
readonly property int margin: 8
Component
Rectangle
{
id: aircraftDelegate
id: tabBar
height: installedAircraftButton.height + margin
width: parent.width
Item {
// background
Row {
anchors.centerIn: parent
spacing: root.margin
height: Math.max(contentBox.childrenRect.height, thumbnailBox.height) + footer.height
width: root.width
MouseArea {
anchors.fill: parent
TabButton {
id: installedAircraftButton
text: qsTr("Installed Aircraft")
onClicked: {
list.currentIndex = model.index
_launcher.selectAircraft(model.uri);
root.state = "installed"
}
active: root.state == "installed"
}
Rectangle {
id: thumbnailBox
// thumbnail border
y: Math.max(0, Math.round((contentBox.childrenRect.height - height) * 0.5))
border.width: 1
border.color: "#7f7f7f"
width: thumbnail.width
height: thumbnail.height
ThumbnailImage {
id: thumbnail
aircraftUri: model.uri
maximumSize.width: 300
maximumSize.height: 200
TabButton {
id: browseButton
text: qsTr("Browse")
onClicked: {
root.state = "browse"
}
active: root.state == "browse"
}
} // of header row
Image {
id: previewIcon
visible: model.hasPreviews
anchors.left: parent.left
anchors.bottom: parent.bottom
source: "qrc:///preview-icon"
opacity: showPreviewsMouse.containsMouse ? 1.0 : 0.5
Behavior on opacity {
NumberAnimation { duration: 100 }
}
}
SearchButton {
id: searchButton
MouseArea {
id: showPreviewsMouse
hoverEnabled: true
anchors.fill: parent
visible: model.hasPreviews
width: 180
height: installedAircraftButton.height
onClicked: {
_launcher.showPreviewsFor(model.uri);
}
}
anchors.right: parent.right
anchors.rightMargin: margin
anchors.verticalCenter: parent.verticalCenter
onSearch: {
_launcher.searchAircraftModel.setAircraftFilterString(term)
root.state = "search"
}
Column {
id: contentBox
active: root.state == "search"
}
anchors.leftMargin: root.margin
anchors.left: thumbnailBox.right
anchors.right: parent.right
anchors.rightMargin: root.margin
Rectangle {
color: "#68A6E1"
height: 1
width: parent.width - 20
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
}
}
spacing: root.margin
Item
{
// box to contain the aircraft title and the variant
// selection button arrows
id: headingBox
width: parent.width
height: title.height
ArrowButton {
id: previousVariantButton
visible: (model.variantCount > 0) && (model.activeVariant > 0)
arrow: "qrc:///left-arrow-icon"
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
onClicked: {
model.activeVariant = (model.activeVariant - 1)
}
}
Text {
id: title
anchors.verticalCenter: parent.verticalCenter
anchors.left: previousVariantButton.right
anchors.right: nextVariantButton.left
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 24
text: model.title
elide: Text.ElideRight
}
ArrowButton {
id: nextVariantButton
arrow: "qrc:///right-arrow-icon"
visible: (model.variantCount > 0) && (model.activeVariant < model.variantCount - 1)
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
onClicked: {
model.activeVariant = (model.activeVariant + 1)
}
}
} // top header box (title + variant arrows)
// this element normally hides itself unless needed
AircraftWarningPanel {
id: warningBox
aircraftStatus: model.aircraftStatus
requiredFGVersion: model.requiredFGVersion
width: parent.width
}
Text {
id: authors
visible: (model.authors != undefined)
width: parent.width
text: "by: " + model.authors
maximumLineCount: 3
wrapMode: Text.WordWrap
elide: Text.ElideRight
}
Text {
id: description
width: parent.width
text: model.description
maximumLineCount: 10
wrapMode: Text.WordWrap
elide: Text.ElideRight
}
Item {
// this area holds the install/update/remove button,
// and the ratings grid. when a download / update is
// happening, the content is re-arranged to give more room
// for the progress bar and feedback
id: bottomContent
readonly property int minimumWidthForBottomContent: ratingGrid.width + downloadPanel.compactWidth + root.margin
width: Math.max(parent.width, minimumWidthForBottomContent)
height: ratingGrid.height
AircraftDownloadPanel
{
id: downloadPanel
visible: (model.package != undefined)
packageSize: model.packageSizeBytes
installStatus: model.packageStatus
downloadedBytes: model.downloadedBytes
uri: model.uri
width: parent.width // full width, grid sits on top
}
Grid {
id: ratingGrid
anchors.right: parent.right
// hide ratings when the panel is doing something, to
// make more room for the progress bar and text
visible: model.hasRatings && !downloadPanel.active
rows: 2
columns: 2
rowSpacing: root.margin
columnSpacing: root.margin
AircraftRating {
title: "Flight model"
value: model.ratingFDM;
}
AircraftRating {
title: "Systems"
value: model.ratingSystems;
}
AircraftRating {
title: "Cockpit"
value: model.ratingCockpit;
}
AircraftRating {
title: "Exterior"
value: model.ratingExterior;
}
}
}
} // of content column
Item {
id: footer
height: 12
width: parent.width
anchors.bottom: parent.bottom
Rectangle {
color: "#68A6E1"
height: 2
width: parent.width - 60
anchors.centerIn: parent
}
}
} // of Component root item
} // of Component
Component {
id: highlight
@ -242,11 +74,51 @@ Item
}
}
Component {
id: ratingsHeader
AircraftRatingsPanel {
width: aircraftList.width - 80
x: (aircraftList.width - width) / 2
}
}
Component {
id: noDefaultCatalogHeader
NoDefaultCatalogPanel {
width: aircraftList.width - 80
x: (aircraftList.width - width) / 2
}
}
Component {
id: updateAllHeader
UpdateAllPanel {
width: aircraftList.width - 80
x: (aircraftList.width - width) / 2
}
}
ListView {
id: list
model: _filteredModel
anchors.fill: parent
delegate: aircraftDelegate
id: aircraftList
anchors {
left: parent.left
top: tabBar.bottom
bottom: parent.bottom
right: scrollbar.left
}
delegate: AircraftCompactDelegate {
onSelect: {
aircraftList.currentIndex = model.index;
_launcher.selectedAircraft = uri;
}
onShowDetails: root.showDetails(uri)
}
clip: true
highlight: highlight
highlightMoveDuration: 100
@ -259,102 +131,100 @@ Item
}
Scrollbar {
anchors.right: parent.right
height: parent.height
function updateSelectionFromLauncher()
{
model.selectVariantForAircraftURI(_launcher.selectedAircraft);
var row = model.indexForURI(_launcher.selectedAircraft);
if (row >= 0) {
currentIndex = row;
} else {
// clear selection in view, so we don't show something
// erroneous such as the previous value
currentIndex = -1;
}
}
}
Scrollbar {
id: scrollbar
anchors.right: parent.right
anchors.top: tabBar.bottom
height: aircraftList.height
flickable: aircraftList
}
Connections
{
target: _launcher
onSelectAircraftIndex: {
console.warn("Selecting aircraft:" + index);
list.currentIndex = index;
aircraftList.currentIndex = index;
aircraftList.model.select
}
}
Rectangle {
id: updateAllBox
state: "installed"
visible: _aircraftModel.showUpdateAll
width: parent.width
height: updateAllRow.childrenRect.height + root.margin * 2
Row {
y: root.margin
id: updateAllRow
spacing: root.margin
Text {
text: _aircraftModel.aircraftNeedingUpdated + " aircraft have updates available - download and install them now?"
wrapMode: Text.WordWrap
anchors.verticalCenter: parent.verticalCenter
states: [
State {
name: "installed"
PropertyChanges {
target: aircraftList
model: _launcher.installedAircraftModel
header: _launcher.baseAircraftModel.showUpdateAll ? updateAllHeader : null
}
},
Button {
text: "Update all"
anchors.verticalCenter: parent.verticalCenter
onClicked: {
_launcher.requestUpdateAllAircraft();
_launcher.showUpdateAll = false
}
State {
name: "search"
PropertyChanges {
target: aircraftList
model: _launcher.searchAircraftModel
header: null
}
},
Button {
text: "Not now"
anchors.verticalCenter: parent.verticalCenter
onClicked: {
_launcher.showUpdateAll = false
}
State {
name: "browse"
PropertyChanges {
target: aircraftList
model: _launcher.browseAircraftModel
header: _launcher.showNoOfficialHanger ? noDefaultCatalogHeader : ratingsHeader
}
}
} // of update-all prompt
]
Rectangle {
id: noDefaultCatalogBox
visible: _launcher.showNoOfficialHanger
width: parent.width
height: noDefaultCatalogRow.childrenRect.height + root.margin * 2
function showDetails(uri)
{
// set URI, start animation
// change state
detailsView.aircraftURI = uri;
detailsView.visible = true
}
Row {
y: root.margin
id: noDefaultCatalogRow
spacing: root.margin
function goBack()
{
detailsView.visible = false;
}
Text {
text: "The official FlightGear aircraft hangar is not added, so many standard "
+ "aircraft will not be available. You can add the hangar now, or hide "
+ "this message. The offical hangar can always be restored from the 'Add-Ons' page."
wrapMode: Text.WordWrap
anchors.verticalCenter: parent.verticalCenter
width: noDefaultCatalogBox.width - (addDefaultButton.width + hideButton.width + root.margin * 3)
}
AircraftDetailsView {
id: detailsView
anchors.fill: parent
visible: false
Button {
id: addDefaultButton
text: qsTr("Add default hangar")
anchors.verticalCenter: parent.verticalCenter
Button {
anchors { left: parent.left; top: parent.top; margins: root.margin }
width: 60
onClicked: {
_launcher.officialCatalogAction("add-official");
}
}
Button {
id: hideButton
text: qsTr("Hide")
anchors.verticalCenter: parent.verticalCenter
onClicked: {
_launcher.officialCatalogAction("hide");
}
id: backButton
text: "< Back"
onClicked: {
// ensure that if the variant was changed inside the detailsView,
// that we update our selection correctly
aircraftList.updateSelectionFromLauncher();
root.goBack();
}
}
}
// no default catalog pop-over
}

View file

@ -298,14 +298,6 @@ QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, const DelegateSta
}
}
if (role == AircraftThumbnailSizeRole) {
QPixmap pm = item->thumbnail(false);
if (pm.isNull()) {
return QSize(STANDARD_THUMBNAIL_WIDTH, STANDARD_THUMBNAIL_HEIGHT);
}
return pm.size();
}
if (role == Qt::DisplayRole) {
if (item->description.isEmpty()) {
return tr("Missing description for: %1").arg(item->baseName());
@ -320,20 +312,12 @@ QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, const DelegateSta
return item->authors;
} else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
return item->ratings[role - AircraftRatingRole];
} else if (role == AircraftPreviewsRole) {
QVariantList result;
Q_FOREACH(QUrl u, item->previews) {
result.append(u);
}
return result;
} else if (role == AircraftHasPreviewsRole) {
return !item->previews.empty();
} else if (role == AircraftThumbnailRole) {
return item->thumbnail();
} else if (role == AircraftPackageIdRole) {
// can we fake an ID? otherwise fall through to a null variant
} else if (role == AircraftPackageStatusRole) {
return PackageInstalled; // always the case
return LocalAircraftCache::PackageInstalled; // always the case
} else if (role == Qt::ToolTipRole) {
return item->path;
} else if (role == AircraftURIRole) {
@ -395,34 +379,25 @@ QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, const Delega
InstallRef i = item->existingInstall();
if (i.valid()) {
if (i->isDownloading()) {
return PackageDownloading;
return LocalAircraftCache::PackageDownloading;
}
if (i->isQueued()) {
return PackageQueued;
return LocalAircraftCache::PackageQueued;
}
if (i->hasUpdate()) {
return PackageUpdateAvailable;
return LocalAircraftCache::PackageUpdateAvailable;
}
return PackageInstalled;
return LocalAircraftCache::PackageInstalled;
} else {
return PackageNotInstalled;
return LocalAircraftCache::PackageNotInstalled;
}
} else if (role == AircraftVariantCountRole) {
// this value wants the number of aditional variants, i.e not
// including the primary. Hence the -1 term.
return static_cast<quint32>(item->variants().size() - 1);
} else if (role == AircraftThumbnailSizeRole) {
QPixmap pm = packageThumbnail(item, state, false).value<QPixmap>();
if (pm.isNull())
return QSize(STANDARD_THUMBNAIL_WIDTH, STANDARD_THUMBNAIL_HEIGHT);
return pm.size();
} else if (role == AircraftThumbnailRole) {
return packageThumbnail(item, state);
} else if (role == AircraftPreviewsRole) {
return packagePreviews(item, state);
} else if (role == AircraftHasPreviewsRole) {
return !item->previewsForVariant(state.variant).empty();
} else if (role == AircraftAuthorsRole) {
std::string authors = item->getLocalisedProp("author", state.variant);
if (!authors.empty()) {
@ -492,35 +467,6 @@ QVariant AircraftItemModel::packageThumbnail(PackageRef p, const DelegateState&
return QVariant();
}
QVariant AircraftItemModel::packagePreviews(PackageRef p, const DelegateState& ds) const
{
const Package::PreviewVec& previews = p->previewsForVariant(ds.variant);
if (previews.empty()) {
return QVariant();
}
QVariantList result;
// if we have an install, return file URLs, not remote (http) ones
InstallRef ex = p->existingInstall();
if (ex.valid()) {
for (auto p : previews) {
SGPath localPreviewPath = ex->path() / p.path;
if (!localPreviewPath.exists()) {
qWarning() << "missing local preview" << QString::fromStdString(localPreviewPath.utf8Str());
continue;
}
result.append(QUrl::fromLocalFile(QString::fromStdString(localPreviewPath.utf8Str())));
}
}
// return remote urls
for (auto p : previews) {
result.append(QUrl(QString::fromStdString(p.url)));
}
return result;
}
bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
int row = index.row();
@ -553,7 +499,6 @@ QHash<int, QByteArray> AircraftItemModel::roleNames() const
result[AircraftPackageSizeRole] = "packageSizeBytes";
result[AircraftPackageStatusRole] = "packageStatus";
result[AircraftHasPreviewsRole] = "hasPreviews";
result[AircraftInstallDownloadedSizeRole] = "downloadedBytes";
result[AircraftVariantRole] = "activeVariant";
@ -655,6 +600,10 @@ QString AircraftItemModel::nameForAircraftURI(QUrl uri) const
{
if (uri.isLocalFile()) {
AircraftItemPtr item = LocalAircraftCache::instance()->findItemWithUri(uri);
if (!item) {
return {};
}
const QString path = uri.toLocalFile();
if (item->path == path) {
return item->description;
@ -675,10 +624,9 @@ QString AircraftItemModel::nameForAircraftURI(QUrl uri) const
}
} else {
qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme();
return QString();
}
return QString();
return {};
}
void AircraftItemModel::onScanAddedItems(int count)

View file

@ -47,18 +47,14 @@ const int AircraftInstallPercentRole = Qt::UserRole + 11;
const int AircraftPackageSizeRole = Qt::UserRole + 12;
const int AircraftInstallDownloadedSizeRole = Qt::UserRole + 13;
const int AircraftURIRole = Qt::UserRole + 14;
const int AircraftThumbnailSizeRole = Qt::UserRole + 15;
const int AircraftIsHelicopterRole = Qt::UserRole + 16;
const int AircraftIsSeaplaneRole = Qt::UserRole + 17;
const int AircraftPackageRefRole = Qt::UserRole + 19;
const int AircraftThumbnailRole = Qt::UserRole + 20;
const int AircraftPreviewsRole = Qt::UserRole + 21;
const int AircraftStatusRole = Qt::UserRole + 22;
const int AircraftMinVersionRole = Qt::UserRole + 23;
const int AircraftHasPreviewsRole = Qt::UserRole + 24;
const int AircraftRatingRole = Qt::UserRole + 100;
const int AircraftVariantDescriptionRole = Qt::UserRole + 200;
@ -73,16 +69,7 @@ class AircraftItemModel : public QAbstractListModel
Q_PROPERTY(int aircraftNeedingUpdated READ aircraftNeedingUpdated NOTIFY aircraftNeedingUpdatedChanged)
Q_PROPERTY(bool showUpdateAll READ showUpdateAll WRITE setShowUpdateAll NOTIFY aircraftNeedingUpdatedChanged)
Q_ENUMS(AircraftItemStatus)
Q_ENUMS(AircraftStatus)
public:
enum AircraftItemStatus {
PackageNotInstalled = 0,
PackageInstalled,
PackageUpdateAvailable,
PackageQueued,
PackageDownloading
};
AircraftItemModel(QObject* pr);

View file

@ -0,0 +1,87 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
Rectangle {
id: root
property var previews: []
property int activePreview: 0
readonly property bool __havePreviews: (previews.length > 0)
onPreviewsChanged: {
activePreview = 0
}
height: width / (16/9)
color: "#7f7f7f"
border.width: 1
border.color: "#3f3f3f"
Timer {
id: previewCycleTimer
}
PreviewImage {
width: parent.width
height: parent.height
imageUrl: __havePreviews ? root.previews[root.activePreview] : ""
}
Row {
visible: (previews.length > 1)
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
spacing: 8
Repeater
{
model: root.previews
Rectangle {
height: 8
width: 8
radius: 4
color: (model.index == root.activePreview) ? "white" : "#cfcfcf"
}
}
}
Image {
id: leftArrow
source: "qrc:///preview/left-arrow-icon"
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
opacity: leftMouseArea.containsMouse ? 1.0 : 0.3
visible: root.activePreview > 0
MouseArea {
id: leftMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.activePreview = Math.max(root.activePreview - 1, 0)
}
}
}
Image {
id: rightArrow
source: "qrc:///preview/right-arrow-icon"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
opacity: rightMouseArea.containsMouse ? 1.0 : 0.3
visible: root.activePreview < (root.previews.length - 1)
MouseArea {
id: rightMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.activePreview = Math.min(root.activePreview + 1, root.previews.length - 1)
}
}
}
}

View file

@ -0,0 +1,97 @@
import QtQuick 2.2
import FlightGear.Launcher 1.0 as FG
ListHeaderBox
{
contents: [
ToggleSwitch {
id: doFilterCheck
checked: _launcher.browseAircraftModel.ratingsFilterEnabled
onCheckedChanged: {
_launcher.browseAircraftModel.ratingsFilterEnabled = checked
}
label: qsTr("Filter aircraft based on rating")
anchors.verticalCenter: parent.verticalCenter
},
Text {
anchors.right: parent.right
anchors.rightMargin: root.margin
text: qsTr("Adjust minimum ratings")
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
onClicked: {
editRatingsPanel.visible = true
}
}
},
// mouse are behind panel to consume clicks
MouseArea {
width: 10000 // deliberately huge values here
height: 10000
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
visible: editRatingsPanel.visible
onClicked: {
editRatingsPanel.visible = false
}
},
Rectangle {
id: editRatingsPanel
visible: false
width: parent.width
y: parent.height - 1
height: childrenRect.height + 24
border.width: 1
border.color: "#9f9f9f"
Column {
y: 12
spacing: 24
Text {
text: qsTr("Aircraft are rated by the community based on four critiera, on a scale from " +
"one to five. The ratings are designed to help make an informed guess how "+
"complete and functional an aircraft is.")
width: editRatingsPanel.width - 100
wrapMode: Text.WordWrap
anchors.horizontalCenter: parent.horizontalCenter
}
RatingSlider {
label: qsTr("Minimum flight-model (FDM) rating:")
ratings: _launcher.browseAircraftModel.ratings
ratingIndex: 0
}
RatingSlider {
label: qsTr("Minimum visual model rating")
ratings: _launcher.browseAircraftModel.ratings
ratingIndex: 1
}
RatingSlider {
label: qsTr("Minimum systems rating")
ratings: _launcher.browseAircraftModel.ratings
ratingIndex: 2
}
RatingSlider {
label: qsTr("Minimum FDM rating")
ratings: _launcher.browseAircraftModel.ratings
ratingIndex: 3
}
}
}
]
}

View file

@ -3,15 +3,24 @@
#include "AircraftModel.hxx"
#include <simgear/package/Package.hxx>
AircraftProxyModel::AircraftProxyModel(QObject *pr) :
AircraftProxyModel::AircraftProxyModel(QObject *pr, QAbstractItemModel * source) :
QSortFilterProxyModel(pr)
{
m_ratings = {3, 3, 3, 3};
setSourceModel(source);
setSortCaseSensitivity(Qt::CaseInsensitive);
setFilterCaseSensitivity(Qt::CaseInsensitive);
setSortRole(Qt::DisplayRole);
setDynamicSortFilter(true);
}
void AircraftProxyModel::setRatings(int *ratings)
void AircraftProxyModel::setRatings(QList<int> ratings)
{
::memcpy(m_ratings, ratings, sizeof(int) * 4);
if (ratings == m_ratings)
return;
m_ratings = ratings;
invalidate();
emit ratingsChanged();
}
void AircraftProxyModel::setAircraftFilterString(QString s)
@ -27,6 +36,22 @@ void AircraftProxyModel::setAircraftFilterString(QString s)
invalidate();
}
int AircraftProxyModel::indexForURI(QUrl uri) const
{
auto sourceIndex = qobject_cast<AircraftItemModel*>(sourceModel())->indexOfAircraftURI(uri);
auto ourIndex = mapFromSource(sourceIndex);
if (!sourceIndex.isValid() || !ourIndex.isValid()) {
return -1;
}
return ourIndex.row();
}
void AircraftProxyModel::selectVariantForAircraftURI(QUrl uri)
{
qobject_cast<AircraftItemModel*>(sourceModel())->selectVariantForAircraftURI(uri);
}
void AircraftProxyModel::setRatingFilterEnabled(bool e)
{
if (e == m_ratingsFilter) {
@ -35,6 +60,7 @@ void AircraftProxyModel::setRatingFilterEnabled(bool e)
m_ratingsFilter = e;
invalidate();
emit ratingsFilterEnabledChanged();
}
void AircraftProxyModel::setInstalledFilterEnabled(bool e)
@ -58,8 +84,8 @@ bool AircraftProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
if (m_onlyShowInstalled) {
QVariant v = index.data(AircraftPackageStatusRole);
const auto status = static_cast<AircraftItemModel::AircraftItemStatus>(v.toInt());
if (status == AircraftItemModel::PackageNotInstalled) {
const auto status = static_cast<LocalAircraftCache::PackageStatus>(v.toInt());
if (status == LocalAircraftCache::PackageNotInstalled) {
return false;
}
}
@ -67,8 +93,8 @@ bool AircraftProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
// if there is no search active, i.e we are browsing, we might apply the
// ratings filter.
if (m_filterString.isEmpty() && !m_onlyShowInstalled && m_ratingsFilter) {
for (int i=0; i<4; ++i) {
if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
for (int i=0; i<m_ratings.size(); ++i) {
if (m_ratings.at(i) > index.data(AircraftRatingRole + i).toInt()) {
return false;
}
}

View file

@ -9,14 +9,39 @@ class AircraftProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
AircraftProxyModel(QObject* pr);
AircraftProxyModel(QObject* pr, QAbstractItemModel * source);
void setRatings(int* ratings);
Q_PROPERTY(QList<int> ratings READ ratings WRITE setRatings NOTIFY ratingsChanged)
void setAircraftFilterString(QString s);
Q_PROPERTY(bool ratingsFilterEnabled READ ratingsFilterEnabled WRITE setRatingFilterEnabled NOTIFY ratingsFilterEnabledChanged)
Q_INVOKABLE void setAircraftFilterString(QString s);
/**
* Compute the row (index in QML / ListView speak) based on an aircraft URI.
* Return -1 if the UIR is not present in the (filtered) model
**/
Q_INVOKABLE int indexForURI(QUrl uri) const;
Q_INVOKABLE void selectVariantForAircraftURI(QUrl uri);
QList<int> ratings() const
{
return m_ratings;
}
bool ratingsFilterEnabled() const
{
return m_ratingsFilter;
}
void setRatings(QList<int> ratings);
void setRatingFilterEnabled(bool e);
signals:
void ratingsChanged();
void ratingsFilterEnabledChanged();
public slots:
void setRatingFilterEnabled(bool e);
void setInstalledFilterEnabled(bool e);
@ -28,7 +53,7 @@ private:
bool m_ratingsFilter = true;
bool m_onlyShowInstalled = false;
int m_ratings[4] = {3, 3, 3, 3};
QList<int> m_ratings;
QString m_filterString;
SGPropertyNode_ptr m_filterProps;
};

View file

@ -0,0 +1,131 @@
import QtQuick 2.0
import QtQuick.Window 2.0
import FlightGear.Launcher 1.0
Rectangle {
id: root
implicitHeight: title.implicitHeight
radius: 4
border.color: "#68A6E1"
border.width: headingMouseArea.containsMouse ? 1 : 0
color: headingMouseArea.containsMouse ? "#7fffffff" : "transparent"
readonly property bool __enabled: aircraftInfo.numVariants > 1
property alias aircraft: aircraftInfo.uri
property alias currentIndex: aircraftInfo.variant
property alias fontPixelSize: title.font.pixelSize
property int popupFontPixelSize: 0
signal selected(var index)
Text {
id: title
anchors.verticalCenter: parent.verticalCenter
anchors.right: upDownIcon.left
anchors.left: parent.left
anchors.leftMargin: 4
anchors.rightMargin: 4
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 24
text: aircraftInfo.name
elide: Text.ElideRight
maximumLineCount: 1
color: "black"
}
Image {
id: upDownIcon
source: "qrc:///up-down-arrow"
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
visible: __enabled
}
MouseArea {
id: headingMouseArea
enabled: __enabled
anchors.fill: parent
hoverEnabled: true
onClicked: {
var screenPos = _launcher.mapToGlobal(title, Qt.point(0, -title.height * currentIndex))
if (screenPos.y < 0) {
// if the popup would appear off the screen, use the first item
// position instead
screenPos = _launcher.mapToGlobal(title, Qt.point(0, 0))
}
popupFrame.x = screenPos.x;
popupFrame.y = screenPos.y;
popupFrame.visible = true
}
}
AircraftInfo
{
id: aircraftInfo
}
Window {
id: popupFrame
modality: Qt.WindowModal
width: root.width
flags: Qt.Popup
height: choicesColumn.childrenRect.height
visible: false
color: "white"
Rectangle {
border.width: 1
border.color: "#afafaf"
anchors.fill: parent
}
Column {
id: choicesColumn
Repeater {
model: popupFrame.visible ? aircraftInfo.variantNames : 0
delegate: Item {
width: popupFrame.width
height: choiceText.implicitHeight
Text {
id: choiceText
text: modelData
// allow override the size in case the title size is enormous
font.pixelSize: (popupFontPixelSize > 0) ? popupFontPixelSize : title.font.pixelSize
color: choiceArea.containsMouse ? "#68A6E1" : "black"
anchors {
left: parent.left
right: parent.right
margins: 4
}
}
MouseArea {
id: choiceArea
hoverEnabled: true
anchors.fill: parent
onClicked: {
root.selected(model.index)
popupFrame.visible = false
}
}
} // of delegate Item
} // of Repeater
}
} // of popup frame
}

View file

@ -6,7 +6,7 @@ Rectangle {
property int aircraftStatus
property var requiredFGVersion
visible: (model.aircraftStatus != AircraftModel.AircraftOk)
visible: (aircraftStatus != LocalAircraftCache.AircraftOk)
implicitHeight: warningText.height + 8
@ -23,7 +23,7 @@ Rectangle {
State {
name: "sim-version-too-low"
when: aircraftStatus == AircraftModel.AircraftNeedsNewerSimulator
when: aircraftStatus == LocalAircraftCache.AircraftNeedsNewerSimulator
PropertyChanges {
target: warningText
@ -33,7 +33,7 @@ Rectangle {
State {
name: "unmaintained"
when: aircraftStatus == AircraftModel.AircraftUnmaintained
when: aircraftStatus == LocalAircraftCache.AircraftUnmaintained
PropertyChanges {
target: warningText

View file

@ -19,7 +19,6 @@ Item {
Image {
id: img
visible: (model.variantCount > 0)
anchors.centerIn: parent
}

View file

@ -9,7 +9,7 @@ Rectangle {
signal clicked
width: 120
height: 30
height: buttonText.implicitHeight + (radius * 2)
radius: 6
color: mouse.containsMouse ? "#064989" : "#1b7ad3"

View file

@ -68,12 +68,10 @@ endif()
if (HAVE_QT)
qt5_wrap_ui(uic_sources Launcher.ui
EditRatingsFilterDialog.ui
SetupRootDialog.ui
AddCatalogDialog.ui
PathsDialog.ui
LocationWidget.ui
NoOfficialHangar.ui
InstallSceneryDialog.ui
)
qt5_add_resources(qrc_sources resources.qrc)
@ -100,8 +98,6 @@ if (HAVE_QT)
AirportDiagram.hxx
NavaidDiagram.cxx
NavaidDiagram.hxx
EditRatingsFilterDialog.cxx
EditRatingsFilterDialog.hxx
ExtraSettingsSection.cxx
ExtraSettingsSection.hxx
SetupRootDialog.cxx
@ -122,8 +118,6 @@ if (HAVE_QT)
QtFileDialog.hxx
InstallSceneryDialog.hxx
InstallSceneryDialog.cxx
PreviewWindow.cxx
PreviewWindow.hxx
SettingsSection.cxx
SettingsSection.hxx
SettingsSectionQML.cxx
@ -146,18 +140,13 @@ if (HAVE_QT)
ViewCommandLinePage.hxx
MPServersModel.cpp
MPServersModel.h
ThumbnailImageItem.cxx
ThumbnailImageItem.hxx
FlickableExtentQuery.cxx
FlickableExtentQuery.hxx
${uic_sources}
${qrc_sources}
${qml_sources})
set_property(TARGET fglauncher PROPERTY AUTOMOC ON)
target_link_libraries(fglauncher Qt5::Core Qt5::Widgets Qt5::Network Qt5::Qml Qt5::Quick Qt5::QuickWidgets SimGearCore)
target_include_directories(fglauncher PRIVATE ${PROJECT_BINARY_DIR}/src/GUI
${Qt5Quick_PRIVATE_INCLUDE_DIRS})
target_include_directories(fglauncher PRIVATE ${PROJECT_BINARY_DIR}/src/GUI)
add_library(fgqmlui QQuickDrawable.cxx
QQuickDrawable.hxx
@ -173,12 +162,17 @@ if (HAVE_QT)
QmlAircraftInfo.hxx
LocalAircraftCache.cxx
LocalAircraftCache.hxx
PreviewImageItem.cxx
PreviewImageItem.hxx
ThumbnailImageItem.cxx
ThumbnailImageItem.hxx
FlickableExtentQuery.cxx
FlickableExtentQuery.hxx
)
set_property(TARGET fgqmlui PROPERTY AUTOMOC ON)
target_link_libraries(fgqmlui Qt5::Quick Qt5::Network Qt5::Qml SimGearCore)
target_include_directories(fgqmlui PRIVATE ${PROJECT_BINARY_DIR}/src/GUI)
target_include_directories(fgqmlui PRIVATE ${PROJECT_BINARY_DIR}/src/GUI ${Qt5Quick_PRIVATE_INCLUDE_DIRS})
endif()

45
src/GUI/Checkbox.qml Normal file
View file

@ -0,0 +1,45 @@
import QtQuick 2.0
Item {
property bool checked: false
property alias label: label.text
implicitWidth: checkBox.width + label.width + 16
implicitHeight: label.height
Rectangle {
id: checkBox
width: 18
height: 18
border.color: mouseArea.containsMouse ? "#68A6E1" : "#9f9f9f"
border.width: 1
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 12
height: 12
anchors.centerIn: parent
id: checkMark
color: "#9f9f9f"
visible: checked
}
}
Text {
id: label
anchors.left: checkBox.right
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
anchors.fill: parent
id: mouseArea
hoverEnabled: true
onClicked: {
checked = !checked
}
}
}

View file

@ -1,59 +0,0 @@
// EditEatingsFilterDialog.cxx - part of GUI launcher using Qt5
//
// Written by James Turner, started December 2014.
//
// Copyright (C) 2014 James Turner <zakalawe@mac.com>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#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;
}
}

View file

@ -1,49 +0,0 @@
// EditEatingsFilterDialog.hxx - part of GUI launcher using Qt5
//
// Written by James Turner, started December 2014.
//
// Copyright (C) 2014 James Turner <zakalawe@mac.com>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef EDITRATINGSFILTERDIALOG_HXX
#define EDITRATINGSFILTERDIALOG_HXX
#include <QDialog>
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

View file

@ -1,245 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditRatingsFilterDialog</class>
<widget class="QDialog" name="EditRatingsFilterDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>623</width>
<height>594</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="label">
<property name="text">
<string>Specify the minimum required completeness of aircraft in each area for it to be shown in the aircraft list.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Cockpit:</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSlider" name="cockpitSlider">
<property name="maximum">
<number>5</number>
</property>
<property name="value">
<number>3</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="invertedControls">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>3D panel, cockpit and instrumentation status</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Flight model:</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QSlider" name="fdmSlider">
<property name="maximum">
<number>5</number>
</property>
<property name="value">
<number>3</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="invertedControls">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Accuracy of the flight (aerodynamic) model compared to available data and testing/</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Exterior model:</string>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QSlider" name="modelSlider">
<property name="maximum">
<number>5</number>
</property>
<property name="value">
<number>3</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="invertedControls">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Quality and detail of exterior model, including animated moving parts such as control surfaces and under-carriage</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Systems:</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QSlider" name="systemsSlider">
<property name="maximum">
<number>5</number>
</property>
<property name="value">
<number>3</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="invertedControls">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Completeness of systems modellings, including fuel handling, autopilot operation, startup &amp; failure procedures.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="9" column="1" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>EditRatingsFilterDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>EditRatingsFilterDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -446,7 +446,7 @@
</layout>
</widget>
<widget class="QWidget" name="aircraftPage">
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="0,0,1">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>4</number>
</property>
@ -462,80 +462,6 @@
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Search:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="aircraftFilter">
<property name="placeholderText">
<string>Search aircraft (press Enter to search)</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QCheckBox" name="onlyShowInstalledCheck">
<property name="text">
<string>Only show installed aircraft</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="ratingsFilterCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Hide aircraft based on completeness (rating)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="editRatingFilter">
<property name="text">
<string>Edit...</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QQuickWidget" name="aircraftList"/>
</item>

View file

@ -7,7 +7,10 @@
#include <QMenu>
#include <QMenuBar>
#include <QMenu>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QQuickItem>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QQmlContext>
@ -28,21 +31,21 @@
// launcher headers
#include "QtLauncher.hxx"
#include "EditRatingsFilterDialog.hxx"
#include "AircraftModel.hxx"
#include "PathsDialog.hxx"
#include "AircraftSearchFilterModel.hxx"
#include "DefaultAircraftLocator.hxx"
#include "SettingsWidgets.hxx"
#include "PreviewWindow.hxx"
#include "LaunchConfig.hxx"
#include "SettingsSectionQML.hxx"
#include "ExtraSettingsSection.hxx"
#include "ViewCommandLinePage.hxx"
#include "MPServersModel.h"
#include "ThumbnailImageItem.hxx"
#include "PreviewImageItem.hxx"
#include "FlickableExtentQuery.hxx"
#include "LocalAircraftCache.hxx"
#include "QmlAircraftInfo.hxx"
#include "ui_Launcher.h"
@ -100,7 +103,6 @@ LauncherMainWindow::LauncherMainWindow() :
connect(viewCommandLineAction, &QAction::triggered,
this, &LauncherMainWindow::onViewCommandLine);
m_ui->aircraftFilter->setClearButtonEnabled(true);
m_serversModel = new MPServersModel(this);
m_serversModel->refresh();
@ -112,18 +114,6 @@ LauncherMainWindow::LauncherMainWindow() :
this, &LauncherMainWindow::onSubsytemIdleTimeout);
m_subsystemIdleTimer->start();
// create and configure the proxy model
m_aircraftProxy = new AircraftProxyModel(this);
connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
this, &LauncherMainWindow::maybeRestoreAircraftSelection);
connect(m_ui->onlyShowInstalledCheck, &QAbstractButton::toggled,
m_aircraftProxy, &AircraftProxyModel::setInstalledFilterEnabled);
connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
m_aircraftProxy, &AircraftProxyModel::setAircraftFilterString);
connect(m_ui->flyButton, SIGNAL(clicked()), this, SLOT(onRun()));
connect(m_ui->summaryButton, &QAbstractButton::clicked, this, &LauncherMainWindow::onClickToolboxButton);
connect(m_ui->aircraftButton, &QAbstractButton::clicked, this, &LauncherMainWindow::onClickToolboxButton);
@ -145,26 +135,22 @@ LauncherMainWindow::LauncherMainWindow() :
connect(qa, &QAction::triggered, this, &LauncherMainWindow::onQuit);
addAction(qa);
connect(m_ui->editRatingFilter, &QPushButton::clicked,
this, &LauncherMainWindow::onEditRatingsFilter);
connect(m_ui->onlyShowInstalledCheck, &QCheckBox::toggled,
this, &LauncherMainWindow::onShowInstalledAircraftToggled);
QIcon historyIcon(":/history-icon");
m_ui->aircraftHistory->setIcon(historyIcon);
m_ui->locationHistory->setIcon(historyIcon);
m_aircraftModel = new AircraftItemModel(this);
m_aircraftProxy->setSourceModel(m_aircraftModel);
m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
m_aircraftProxy->setSortRole(Qt::DisplayRole);
m_aircraftProxy->setDynamicSortFilter(true);
m_installedAircraftModel = new AircraftProxyModel(this, m_aircraftModel);
m_installedAircraftModel->setInstalledFilterEnabled(true);
m_browseAircraftModel = new AircraftProxyModel(this, m_aircraftModel);
m_browseAircraftModel->setRatingFilterEnabled(true);
m_aircraftSearchModel = new AircraftProxyModel(this, m_aircraftModel);
m_ui->aircraftList->setResizeMode(QQuickWidget::SizeRootObjectToView);
m_ui->aircraftList->engine()->rootContext()->setContextProperty("_filteredModel", m_aircraftProxy);
m_ui->aircraftList->engine()->rootContext()->setContextProperty("_aircraftModel", m_aircraftModel);
m_ui->aircraftList->engine()->rootContext()->setContextProperty("_launcher", this);
m_ui->aircraftList->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership);
@ -187,13 +173,6 @@ LauncherMainWindow::LauncherMainWindow() :
this, &LauncherMainWindow::onAircraftPathsChanged);
m_ui->stack->addWidget(addOnsPage);
// after any kind of reset, try to restore selection and scroll
// to match the m_selectedAircraft. This needs to be delayed
// fractionally otherwise the scrollTo seems to be ignored,
// unfortunately.
connect(m_aircraftProxy, &AircraftProxyModel::modelReset,
this, &LauncherMainWindow::delayedAircraftModelReset);
QSettings settings;
LocalAircraftCache::instance()->setPaths(settings.value("aircraft-paths").toStringList());
LocalAircraftCache::instance()->scanDirs();
@ -224,12 +203,15 @@ void LauncherMainWindow::initQML()
qmlRegisterType<SettingsDateTime>("FlightGear.Launcher", 1, 0, "DateTime");
qmlRegisterType<SettingsPath>("FlightGear.Launcher", 1, 0, "PathChooser");
qmlRegisterUncreatableType<QAbstractItemModel>("FlightGear.Launcher", 1, 0, "QAIM", "no");
qmlRegisterUncreatableType<AircraftProxyModel>("FlightGear.Launcher", 1, 0, "AircraftProxyModel", "no");
qmlRegisterUncreatableType<SettingsControl>("FlightGear.Launcher", 1, 0, "Control", "Base class");
qmlRegisterUncreatableType<LaunchConfig>("FlightGear.Launcher", 1, 0, "LaunchConfig", "Singleton API");
qmlRegisterType<FlickableExtentQuery>("FlightGear.Launcher", 1, 0, "FlickableExtentQuery");
qmlRegisterType<QmlAircraftInfo>("FlightGear.Launcher", 1, 0, "AircraftInfo");
m_config = new LaunchConfig(this);
connect(m_config, &LaunchConfig::collect, this, &LauncherMainWindow::collectAircraftArgs);
m_ui->location->setLaunchConfig(m_config);
@ -251,8 +233,19 @@ void LauncherMainWindow::initQML()
flightgear::WeatherScenariosModel* weatherScenariosModel = new flightgear::WeatherScenariosModel(this);
m_qmlEngine->rootContext()->setContextProperty("_weatherScenarios", weatherScenariosModel);
qmlRegisterUncreatableType<LocalAircraftCache>("FlightGear.Launcher", 1, 0, "LocalAircraftCache", "Aircraft cache");
qmlRegisterUncreatableType<AircraftItemModel>("FlightGear.Launcher", 1, 0, "AircraftModel", "Built-in model");
qmlRegisterType<ThumbnailImageItem>("FlightGear.Launcher", 1, 0, "ThumbnailImage");
qmlRegisterType<PreviewImageItem>("FlightGear.Launcher", 1, 0, "PreviewImage");
QNetworkDiskCache* diskCache = new QNetworkDiskCache(this);
SGPath cachePath = globals->get_fg_home() / "PreviewsCache";
diskCache->setCacheDirectory(QString::fromStdString(cachePath.utf8Str()));
QNetworkAccessManager* netAccess = new QNetworkAccessManager(this);
netAccess->setCache(diskCache);
PreviewImageItem::setGlobalNetworkAccess(netAccess);
}
void LauncherMainWindow::buildSettingsSections()
@ -389,23 +382,7 @@ void LauncherMainWindow::restoreSettings()
m_ui->location->restoreLocation(currentLocation);
// rating filters
m_ui->onlyShowInstalledCheck->setChecked(settings.value("only-show-installed", false).toBool());
if (m_ui->onlyShowInstalledCheck->isChecked()) {
m_ui->ratingsFilterCheck->setEnabled(false);
}
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);
updateSelectedAircraft();
maybeRestoreAircraftSelection();
Q_FOREACH(SettingsSection* ss, findChildren<SettingsSection*>()) {
ss->restoreState(settings);
@ -414,31 +391,11 @@ void LauncherMainWindow::restoreSettings()
m_serversModel->requestRestore();
}
void LauncherMainWindow::delayedAircraftModelReset()
{
QTimer::singleShot(1, this, SLOT(maybeRestoreAircraftSelection()));
}
void LauncherMainWindow::maybeRestoreAircraftSelection()
{
QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
if (proxyIndex.isValid()) {
emit selectAircraftIndex(proxyIndex.row());
// and also select the correct variant on the model
m_aircraftModel->selectVariantForAircraftURI(m_selectedAircraft);
}
}
void LauncherMainWindow::saveSettings()
{
QSettings settings;
settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
settings.setValue("only-show-installed", m_ui->onlyShowInstalledCheck->isChecked());
settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
settings.setValue("recent-location-sets", m_recentLocations);
settings.setValue("window-geometry", saveGeometry());
Q_FOREACH(SettingsSection* ss, findChildren<SettingsSection*>()) {
@ -605,15 +562,6 @@ void LauncherMainWindow::onAircraftInstalledCompleted(QModelIndex index)
maybeUpdateSelectedAircraft(index);
}
void LauncherMainWindow::onRatingsFilterToggled()
{
QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
if (proxyIndex.isValid()) {
emit selectAircraftIndex(proxyIndex.row());
}
}
void LauncherMainWindow::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
{
qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
@ -628,12 +576,6 @@ void LauncherMainWindow::onAircraftInstallFailed(QModelIndex index, QString erro
maybeUpdateSelectedAircraft(index);
}
void LauncherMainWindow::selectAircraft(QUrl aircraftUri)
{
m_selectedAircraft = aircraftUri;
updateSelectedAircraft();
}
void LauncherMainWindow::onRestoreDefaults()
{
QMessageBox mbox(this);
@ -694,7 +636,7 @@ void LauncherMainWindow::updateSelectedAircraft()
m_ui->aircraftDescription->setText(longDesc.toString());
int status = index.data(AircraftPackageStatusRole).toInt();
bool canRun = (status == AircraftItemModel::PackageInstalled);
bool canRun = (status == LocalAircraftCache::PackageInstalled);
m_ui->flyButton->setEnabled(canRun);
LauncherAircraftType aircraftType = Airplane;
@ -728,18 +670,6 @@ void LauncherMainWindow::setSceneryPaths()
flightgear::launcherSetSceneryPaths();
}
QModelIndex LauncherMainWindow::proxyIndexForAircraftURI(QUrl uri) const
{
return m_aircraftProxy->mapFromSource(sourceIndexForAircraftURI(uri));
}
QModelIndex LauncherMainWindow::sourceIndexForAircraftURI(QUrl uri) const
{
AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
Q_ASSERT(sourceModel);
return sourceModel->indexOfAircraftURI(uri);
}
void LauncherMainWindow::onPopupAircraftHistory()
{
if (m_recentAircraft.isEmpty()) {
@ -760,13 +690,7 @@ void LauncherMainWindow::onPopupAircraftHistory()
QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
QAction* triggered = m.exec(popupPos);
if (triggered) {
const QUrl uri = triggered->data().toUrl();
m_selectedAircraft = uri;
QModelIndex index = proxyIndexForAircraftURI(m_selectedAircraft);
emit selectAircraftIndex(index.row());
m_ui->aircraftFilter->clear();
m_aircraftModel->selectVariantForAircraftURI(uri);
updateSelectedAircraft();
setSelectedAircraft(triggered->data().toUrl());
}
}
@ -790,25 +714,6 @@ void LauncherMainWindow::onPopupLocationHistory()
}
}
void LauncherMainWindow::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 LauncherMainWindow::updateSettingsSummary()
{
QStringList summary;
@ -826,12 +731,6 @@ void LauncherMainWindow::updateSettingsSummary()
m_ui->settingsDescription->setText(s);
}
void LauncherMainWindow::onShowInstalledAircraftToggled(bool b)
{
m_ui->ratingsFilterCheck->setEnabled(!b);
maybeRestoreAircraftSelection();
}
void LauncherMainWindow::onSubsytemIdleTimeout()
{
globals->get_subsystem_mgr()->update(0.0);
@ -901,6 +800,27 @@ void LauncherMainWindow::officialCatalogAction(QString s)
emit showNoOfficialHangarChanged();
}
QUrl LauncherMainWindow::selectedAircraft() const
{
return m_selectedAircraft;
}
QPointF LauncherMainWindow::mapToGlobal(QQuickItem *item, const QPointF &pos) const
{
QPointF scenePos = item->mapToScene(pos);
return m_ui->aircraftList->mapToGlobal(scenePos.toPoint());
}
void LauncherMainWindow::setSelectedAircraft(QUrl selectedAircraft)
{
if (m_selectedAircraft == selectedAircraft)
return;
m_selectedAircraft = selectedAircraft;
updateSelectedAircraft();
emit selectedAircraftChanged(m_selectedAircraft);
}
simgear::pkg::PackageRef LauncherMainWindow::packageForAircraftURI(QUrl uri) const
{
if (uri.scheme() != "package") {
@ -979,18 +899,6 @@ bool LauncherMainWindow::validateMetarString(QString metar)
return true;
}
void LauncherMainWindow::showPreviewsFor(QUrl aircraftUri) const
{
PreviewWindow* previewWindow = new PreviewWindow;
QModelIndex index = m_aircraftModel->indexOfAircraftURI(aircraftUri);
if (!index.isValid()) {
return;
}
QVariant urls = index.data(AircraftPreviewsRole);
previewWindow->setUrls(urls.toList());
}
void LauncherMainWindow::requestInstallUpdate(QUrl aircraftUri)
{
// also select, otherwise UI is confusing

View file

@ -46,6 +46,7 @@ class LaunchConfig;
class ExtraSettingsSection;
class ViewCommandLinePage;
class MPServersModel;
class QQuickItem;
class LauncherMainWindow : public QMainWindow
{
@ -53,6 +54,13 @@ class LauncherMainWindow : public QMainWindow
Q_PROPERTY(bool showNoOfficialHanger READ showNoOfficialHanger NOTIFY showNoOfficialHangarChanged)
Q_PROPERTY(AircraftProxyModel* installedAircraftModel MEMBER m_installedAircraftModel CONSTANT)
Q_PROPERTY(AircraftProxyModel* browseAircraftModel MEMBER m_browseAircraftModel CONSTANT)
Q_PROPERTY(AircraftProxyModel* searchAircraftModel MEMBER m_aircraftSearchModel CONSTANT)
Q_PROPERTY(AircraftItemModel* baseAircraftModel MEMBER m_aircraftModel CONSTANT)
Q_PROPERTY(QUrl selectedAircraft READ selectedAircraft WRITE setSelectedAircraft NOTIFY selectedAircraftChanged)
public:
LauncherMainWindow();
virtual ~LauncherMainWindow();
@ -62,7 +70,6 @@ public:
bool wasRejected();
Q_INVOKABLE bool validateMetarString(QString metar);
Q_INVOKABLE void showPreviewsFor(QUrl aircraftUri) const;
Q_INVOKABLE void requestInstallUpdate(QUrl aircraftUri);
@ -78,11 +85,19 @@ public:
Q_INVOKABLE void officialCatalogAction(QString s);
Q_INVOKABLE void selectAircraft(QUrl aircraftUri);
QUrl selectedAircraft() const;
// work around the fact, that this is not available on QQuickItem until 5.7
Q_INVOKABLE QPointF mapToGlobal(QQuickItem* item, const QPointF& pos) const;
public slots:
void setSelectedAircraft(QUrl selectedAircraft);
signals:
void showNoOfficialHangarChanged();
void selectAircraftIndex(int index);
void selectedAircraftChanged(QUrl selectedAircraft);
protected:
virtual void closeEvent(QCloseEvent *event) override;
@ -100,8 +115,6 @@ private slots:
void onPopupAircraftHistory();
void onPopupLocationHistory();
void onEditRatingsFilter();
void updateSettingsSummary();
void onSubsytemIdleTimeout();
@ -109,10 +122,6 @@ private slots:
void onAircraftInstalledCompleted(QModelIndex index);
void onAircraftInstallFailed(QModelIndex index, QString errorMessage);
void onShowInstalledAircraftToggled(bool b);
void maybeRestoreAircraftSelection();
void onRestoreDefaults();
void onViewCommandLine();
@ -134,15 +143,11 @@ private:
void restoreSettings();
void saveSettings();
QModelIndex proxyIndexForAircraftURI(QUrl uri) const;
QModelIndex sourceIndexForAircraftURI(QUrl uri) const;
simgear::pkg::PackageRef packageForAircraftURI(QUrl uri) const;
// need to wait after a model reset before restoring selection and
// scrolling, to give the view time it seems.
void delayedAircraftModelReset();
void onRatingsFilterToggled();
void updateLocationHistory();
bool shouldShowOfficialCatalogMessage() const;
@ -153,8 +158,10 @@ private:
void initQML();
QScopedPointer<Ui::Launcher> m_ui;
AircraftProxyModel* m_aircraftProxy;
AircraftProxyModel* m_installedAircraftModel;
AircraftItemModel* m_aircraftModel;
AircraftProxyModel* m_aircraftSearchModel;
AircraftProxyModel* m_browseAircraftModel;
MPServersModel* m_serversModel = nullptr;
QUrl m_selectedAircraft;

26
src/GUI/ListHeaderBox.qml Normal file
View file

@ -0,0 +1,26 @@
import QtQuick 2.2
import FlightGear.Launcher 1.0 as FG
Item {
id: root
readonly property int margin: 8
height: visible ? 48 : 0
z: 100
property alias contents: contentBox.children
Rectangle {
id: contentBox
width: parent.width
height: 40
y: 4
color: "white"
border.width: 1
border.color: "#9f9f9f"
}
}

View file

@ -181,6 +181,18 @@ QPixmap AircraftItem::thumbnail(bool loadIfRequired) const
return m_thumbnail;
}
int AircraftItem::indexOfVariant(QUrl uri) const
{
const QString path = uri.toLocalFile();
for (int i=0; i< variants.size(); ++i) {
if (variants.at(i)->path == path) {
return i;
}
}
return -1;
}
QVariant AircraftItem::status(int variant)
{
if (needsMaintenance) {
@ -443,6 +455,21 @@ int LocalAircraftCache::findIndexWithUri(QUrl aircraftUri) const
return -1;
}
AircraftItemPtr LocalAircraftCache::primaryItemFor(AircraftItemPtr item) const
{
if (!item || item->variantOf.isEmpty())
return item;
for (int row=0; row < m_items.size(); ++row) {
const AircraftItemPtr p(m_items.at(row));
if (p->baseName() == item->variantOf) {
return p;
}
}
return {};
}
QVector<AircraftItemPtr> LocalAircraftCache::newestItems(int count)
{
QVector<AircraftItemPtr> r;

View file

@ -49,6 +49,8 @@ struct AircraftItem
QPixmap thumbnail(bool loadIfRequired = true) const;
int indexOfVariant(QUrl uri) const;
bool excluded = false;
QString path;
QString description;
@ -99,6 +101,8 @@ public:
AircraftItemPtr findItemWithUri(QUrl aircraftUri) const;
int findIndexWithUri(QUrl aircraftUri) const;
AircraftItemPtr primaryItemFor(AircraftItemPtr item) const;
QVector<AircraftItemPtr> newestItems(int count);
QVariant aircraftStatus(AircraftItemPtr item) const;
@ -111,6 +115,16 @@ public:
AircraftNeedsOlderSimulator // won't ever occur for the moment
};
enum PackageStatus {
PackageNotInstalled = 0,
PackageInstalled,
PackageUpdateAvailable,
PackageQueued,
PackageDownloading
};
Q_ENUMS(PackageStatus)
Q_ENUMS(AircraftStatus)
signals:
void scanStarted();

View file

@ -0,0 +1,48 @@
import QtQuick 2.2
import FlightGear.Launcher 1.0 as FG
Rectangle {
id: root
property int margin: 6
height: noDefaultCatalogRow.childrenRect.height + margin * 2;
z: 100
color: "white"
border.width: 1
border.color: "#9f9f9f"
Row {
y: root.margin
id: noDefaultCatalogRow
spacing: root.margin
Text {
text: "The official FlightGear aircraft hangar is not added, so many standard "
+ "aircraft will not be available. You can add the hangar now, or hide "
+ "this message. The offical hangar can always be restored from the 'Add-Ons' page."
wrapMode: Text.WordWrap
anchors.verticalCenter: parent.verticalCenter
width: root.width - (addDefaultButton.width + hideButton.width + root.margin * 3)
}
Button {
id: addDefaultButton
text: qsTr("Add default hangar")
anchors.verticalCenter: parent.verticalCenter
onClicked: {
_launcher.officialCatalogAction("add-official");
}
}
Button {
id: hideButton
text: qsTr("Hide")
anchors.verticalCenter: parent.verticalCenter
onClicked: {
_launcher.officialCatalogAction("hide");
}
}
}
}

95
src/GUI/PopupChoice.qml Normal file
View file

@ -0,0 +1,95 @@
import QtQuick 2.0
Item {
id: root
property alias label: label.text
property var choices: []
property string displayRole: ""
property int currentIndex: 0
Text {
id: label
anchors.left: root.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
color: mouseArea.containsMouse ? "#68A6E1" : "black"
}
Rectangle {
id: currentChoiceFrame
radius: 4
border.color: mouseArea.containsMouse ? "#68A6E1" : "#9f9f9f"
border.width: 1
height: root.height
width: parent.width / 2
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
Text {
id: currentChoiceText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 8
text: choices[currentIndex][displayRole]
color: mouseArea.containsMouse ? "#68A6E1" : "#7F7F7F"
}
Image {
id: upDownIcon
source: "qrc:///up-down-arrow"
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
id: mouseArea
hoverEnabled: true
onClicked: {
popupFrame.visible = true
}
}
Rectangle {
id: popupFrame
width: currentChoiceFrame.width
anchors.left: currentChoiceFrame.left
// todo - position so current item lies on top
anchors.top: currentChoiceFrame.bottom
height: choicesColumn.childrenRect.height
visible: false
border.color: "#9f9f9f"
border.width: 1
Column {
id: choicesColumn
Repeater {
model: choices
delegate: Text {
text: choices[model.index][root.displayRole]
width: popupFrame.width
height: 40
MouseArea {
anchors.fill: parent
onClicked: {
root.currentIndex = model.index
popupFrame.visible = false
}
}
}
}
}
}
}

View file

@ -0,0 +1,127 @@
#include "PreviewImageItem.hxx"
#include <QSGSimpleTextureNode>
#include <QQuickWindow>
#include <QFileInfo>
#include <QDir>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <Main/globals.hxx>
namespace {
QNetworkAccessManager* global_previewNetAccess = nullptr;
}
PreviewImageItem::PreviewImageItem(QQuickItem* parent) :
QQuickItem(parent)
{
setFlag(ItemHasContents);
// setImplicitWidth(STANDARD_THUMBNAIL_WIDTH);
// setImplicitHeight(STANDARD_THUMBNAIL_HEIGHT);
Q_ASSERT(global_previewNetAccess);
}
PreviewImageItem::~PreviewImageItem()
{
}
QSGNode *PreviewImageItem::updatePaintNode(QSGNode* oldNode, QQuickItem::UpdatePaintNodeData *)
{
if (m_image.isNull()) {
delete oldNode;
return nullptr;
}
QSGSimpleTextureNode* textureNode = static_cast<QSGSimpleTextureNode*>(oldNode);
if (m_imageDirty || !textureNode) {
if (!textureNode) {
textureNode = new QSGSimpleTextureNode;
textureNode->setOwnsTexture(true);
}
QSGTexture* tex = window()->createTextureFromImage(m_image);
textureNode->setTexture(tex);
textureNode->markDirty(QSGBasicGeometryNode::DirtyMaterial);
m_imageDirty = false;
}
textureNode->setRect(QRectF(0, 0, width(), height()));
return textureNode;
}
QUrl PreviewImageItem::imageUrl() const
{
return m_imageUrl;
}
QSize PreviewImageItem::sourceSize() const
{
return m_image.size();
}
void PreviewImageItem::setGlobalNetworkAccess(QNetworkAccessManager *netAccess)
{
global_previewNetAccess = netAccess;
}
void PreviewImageItem::setImageUrl( QUrl url)
{
if (m_imageUrl == url)
return;
m_imageUrl = url;
m_downloadRetryCount = 0;
startDownload();
emit imageUrlChanged();
}
void PreviewImageItem::startDownload()
{
if (m_imageUrl.isEmpty())
return;
QNetworkRequest request(m_imageUrl);
QNetworkReply* reply = global_previewNetAccess->get(request);
connect(reply, &QNetworkReply::finished, this, &PreviewImageItem::onFinished);
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
this, SLOT(onDownloadError(QNetworkReply::NetworkError)));
}
void PreviewImageItem::setImage(QImage image)
{
m_image = image;
m_imageDirty = true;
setImplicitSize(m_image.width(), m_image.height());
emit sourceSizeChanged();
update();
}
void PreviewImageItem::onFinished()
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
QImage img;
if (!img.load(reply, nullptr)) {
qWarning() << Q_FUNC_INFO << "failed to read image data from" << reply->url();
return;
}
setImage(img);
}
void PreviewImageItem::onDownloadError(QNetworkReply::NetworkError errorCode)
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (errorCode == 403) {
if (m_downloadRetryCount++ < 4) {
startDownload(); // retry
return;
}
}
qWarning() << "failed to download:" << reply->url();
qWarning() << reply->errorString();
}

View file

@ -0,0 +1,58 @@
#ifndef PREVIEW_IMAGEITEM_HXX
#define PREVIEW_IMAGEITEM_HXX
#include <memory>
#include <QQuickItem>
#include <QUrl>
#include <QImage>
#include <QNetworkReply>
class QNetworkAccessManager;
class PreviewImageItem : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QUrl imageUrl READ imageUrl WRITE setImageUrl NOTIFY imageUrlChanged)
Q_PROPERTY(QSize sourceSize READ sourceSize NOTIFY sourceSizeChanged)
// Q_PROPERTY(QSize maximumSize READ maximumSize WRITE setMaximumSize NOTIFY maximumSizeChanged)
public:
PreviewImageItem(QQuickItem* parent = nullptr);
~PreviewImageItem();
QSGNode* updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;
QUrl imageUrl() const;
QSize sourceSize() const;
static void setGlobalNetworkAccess(QNetworkAccessManager* netAccess);
signals:
void imageUrlChanged();
void sourceSizeChanged();
public slots:
void setImageUrl(QUrl url);
private slots:
void onDownloadError(QNetworkReply::NetworkError errorCode);
void onFinished();
private:
void setImage(QImage image);
void startDownload();
QUrl m_imageUrl;
bool m_imageDirty = false;
QImage m_image;
unsigned int m_downloadRetryCount = 0;
};
#endif // PREVIEW_IMAGEITEM_HXX

View file

@ -4,14 +4,81 @@
#include <QDebug>
#include <simgear/package/Install.hxx>
#include <simgear/package/Root.hxx>
#include <simgear/structure/exception.hxx>
#include <Include/version.h>
#include <Main/globals.hxx>
#include "LocalAircraftCache.hxx"
QmlAircraftInfo::QmlAircraftInfo(QObject *parent) : QObject(parent)
{
using namespace simgear::pkg;
class QmlAircraftInfo::Delegate : public simgear::pkg::Delegate
{
public:
Delegate(QmlAircraftInfo* info) :
p(info)
{
globals->packageRoot()->addDelegate(this);
}
~Delegate()
{
globals->packageRoot()->removeDelegate(this);
}
protected:
void catalogRefreshed(CatalogRef, StatusCode) override
{
}
void startInstall(InstallRef aInstall) override
{
if (aInstall->package() == p->packageRef()) {
p->setDownloadBytes(0);
}
}
void installProgress(InstallRef aInstall, unsigned int bytes, unsigned int total) override
{
if (aInstall->package() == p->packageRef()) {
p->setDownloadBytes(bytes);
}
}
void finishInstall(InstallRef aInstall, StatusCode aReason) override
{
Q_UNUSED(aReason);
if (aInstall->package() == p->packageRef()) {
p->infoChanged();
}
}
void installStatusChanged(InstallRef aInstall, StatusCode aReason) override
{
Q_UNUSED(aReason);
if (aInstall->package() == p->packageRef()) {
p->downloadChanged();
}
}
void finishUninstall(const PackageRef& pkg) override
{
if (pkg == p->packageRef()) {
p->downloadChanged();
}
}
private:
QmlAircraftInfo* p;
};
QmlAircraftInfo::QmlAircraftInfo(QObject *parent)
: QObject(parent)
, _delegate(new Delegate(this))
{
}
QmlAircraftInfo::~QmlAircraftInfo()
@ -19,9 +86,15 @@ QmlAircraftInfo::~QmlAircraftInfo()
}
int QmlAircraftInfo::numPreviews() const
QUrl QmlAircraftInfo::uri() const
{
return 0;
if (_item) {
QUrl::fromLocalFile(resolveItem()->path);
} else if (_package) {
return QUrl("package:" + QString::fromStdString(_package->qualifiedVariantId(_variant)));
}
return {};
}
int QmlAircraftInfo::numVariants() const
@ -94,6 +167,47 @@ QVariantList QmlAircraftInfo::ratings() const
return {};
}
QVariantList QmlAircraftInfo::previews() const
{
if (_item) {
QVariantList result;
Q_FOREACH(QUrl u, _item->previews) {
result.append(u);
}
return result;
}
if (_package) {
const auto& previews = _package->previewsForVariant(_variant);
if (previews.empty()) {
return {};
}
QVariantList result;
// if we have an install, return file URLs, not remote (http) ones
auto ex = _package->existingInstall();
if (ex.valid()) {
for (auto p : previews) {
SGPath localPreviewPath = ex->path() / p.path;
if (!localPreviewPath.exists()) {
qWarning() << "missing local preview" << QString::fromStdString(localPreviewPath.utf8Str());
continue;
}
result.append(QUrl::fromLocalFile(QString::fromStdString(localPreviewPath.utf8Str())));
}
}
// return remote urls
for (auto p : previews) {
result.append(QUrl(QString::fromStdString(p.url)));
}
return result;
}
return {};
}
QUrl QmlAircraftInfo::thumbnail() const
{
if (_item) {
@ -143,7 +257,7 @@ int QmlAircraftInfo::packageSize() const
int QmlAircraftInfo::downloadedBytes() const
{
return 0;
return _downloadBytes;
}
QVariant QmlAircraftInfo::status() const
@ -180,16 +294,45 @@ AircraftItemPtr QmlAircraftInfo::resolveItem() const
return _item;
}
void QmlAircraftInfo::setUri(QUrl uri)
void QmlAircraftInfo::setUri(QUrl u)
{
if (_uri == uri)
if (uri() == u)
return;
_uri = uri;
_item.clear();
_package.clear();
if (u.isLocalFile()) {
_item = LocalAircraftCache::instance()->findItemWithUri(u);
if (_item->variantOf.isEmpty()) {
_variant = 0;
} else {
_item = LocalAircraftCache::instance()->primaryItemFor(_item);
_variant = _item->indexOfVariant(u);
}
} else if (u.scheme() == "package") {
auto ident = u.path().toStdString();
try {
_package = globals->packageRoot()->getPackageById(ident);
_variant = _package->indexOfVariant(ident);
} catch (sg_exception&) {
qWarning() << "couldn't find package/variant for " << u;
}
}
emit uriChanged();
emit infoChanged();
emit downloadChanged();
}
void QmlAircraftInfo::setVariant(int variant)
{
if (_variant == variant)
return;
_variant = variant;
emit infoChanged();
emit variantChanged(_variant);
}
void QmlAircraftInfo::requestInstallUpdate()
@ -222,3 +365,63 @@ QVariant QmlAircraftInfo::packageAircraftStatus(simgear::pkg::PackageRef p)
return (c < 0) ? LocalAircraftCache::AircraftNeedsNewerSimulator :
LocalAircraftCache::AircraftOk;
}
QVariant QmlAircraftInfo::installStatus() const
{
if (_item) {
return LocalAircraftCache::PackageInstalled;
}
if (_package) {
auto i = _package->existingInstall();
if (i.valid()) {
if (i->isDownloading()) {
return LocalAircraftCache::PackageDownloading;
}
if (i->isQueued()) {
return LocalAircraftCache::PackageQueued;
}
if (i->hasUpdate()) {
return LocalAircraftCache::PackageUpdateAvailable;
}
return LocalAircraftCache::PackageInstalled;
} else {
return LocalAircraftCache::PackageNotInstalled;
}
}
return {};
}
PackageRef QmlAircraftInfo::packageRef() const
{
return _package;
}
void QmlAircraftInfo::setDownloadBytes(int bytes)
{
_downloadBytes = bytes;
emit downloadChanged();;
}
QStringList QmlAircraftInfo::variantNames() const
{
QStringList result;
if (_item) {
Q_FOREACH(auto v, _item->variants) {
if (v->description.isEmpty()) {
qWarning() << Q_FUNC_INFO << "missing description for " << v->path;
}
result.append(v->description);
}
} else if (_package) {
for (int vindex = 0; vindex < _package->variants().size(); ++vindex) {
if (_package->nameForVariant(vindex).empty()) {
qWarning() << Q_FUNC_INFO << "missing description for variant" << vindex;
}
result.append(QString::fromStdString(_package->nameForVariant(vindex)));
}
}
return result;
}

View file

@ -1,6 +1,8 @@
#ifndef QMLAIRCRAFTINFO_HXX
#define QMLAIRCRAFTINFO_HXX
#include <memory>
#include <QObject>
#include <QUrl>
#include <QSharedPointer>
@ -16,8 +18,9 @@ class QmlAircraftInfo : public QObject
Q_OBJECT
Q_PROPERTY(QUrl uri READ uri WRITE setUri NOTIFY uriChanged)
Q_PROPERTY(int variant READ variant WRITE setVariant NOTIFY variantChanged)
Q_PROPERTY(int numPreviews READ numPreviews NOTIFY infoChanged)
Q_PROPERTY(QVariantList previews READ previews NOTIFY infoChanged)
Q_PROPERTY(int numVariants READ numVariants NOTIFY infoChanged)
Q_PROPERTY(QString name READ name NOTIFY infoChanged)
@ -35,6 +38,7 @@ class QmlAircraftInfo : public QObject
Q_PROPERTY(int downloadedBytes READ downloadedBytes NOTIFY downloadChanged)
Q_PROPERTY(QVariant status READ status NOTIFY infoChanged)
Q_PROPERTY(QVariant installStatus READ installStatus NOTIFY downloadChanged)
Q_PROPERTY(QString minimumFGVersion READ minimumFGVersion NOTIFY infoChanged)
@ -46,16 +50,15 @@ class QmlAircraftInfo : public QObject
Q_PROPERTY(QVariantList ratings READ ratings NOTIFY infoChanged)
Q_PROPERTY(QStringList variantNames READ variantNames NOTIFY infoChanged)
public:
explicit QmlAircraftInfo(QObject *parent = nullptr);
virtual ~QmlAircraftInfo();
QUrl uri() const
{
return _uri;
}
QUrl uri() const;
int numPreviews() const;
int numVariants() const;
QString name() const;
@ -63,6 +66,8 @@ public:
QString authors() const;
QVariantList ratings() const;
QVariantList previews() const;
QUrl thumbnail() const;
QString pathOnDisk() const;
@ -74,21 +79,42 @@ public:
QString minimumFGVersion() const;
static QVariant packageAircraftStatus(simgear::pkg::PackageRef p);
int variant() const
{
return _variant;
}
QVariant installStatus() const;
simgear::pkg::PackageRef packageRef() const;
void setDownloadBytes(int bytes);
QStringList variantNames() const;
signals:
void uriChanged();
void infoChanged();
void downloadChanged();
void variantChanged(int variant);
public slots:
void setUri(QUrl uri);
void setVariant(int variant);
private:
QUrl _uri;
class Delegate;
std::unique_ptr<Delegate> _delegate;
simgear::pkg::PackageRef _package;
AircraftItemPtr _item;
int _variant = 0;
int _downloadBytes = 0;
AircraftItemPtr resolveItem() const;
int m_variant;
};
#endif // QMLAIRCRAFTINFO_HXX

18
src/GUI/RatingSlider.qml Normal file
View file

@ -0,0 +1,18 @@
import QtQuick 2.0
Slider {
property var ratings: []
property int ratingIndex: 0
min: 0
max: 5
value: ratings[ratingIndex]
width: editRatingsPanel.width - 30
anchors.horizontalCenter: parent.horizontalCenter
sliderWidth: width / 2
onValueChanged: {
ratings[ratingIndex] = value
}
}

75
src/GUI/SearchButton.qml Normal file
View file

@ -0,0 +1,75 @@
import QtQuick 2.2
Rectangle {
id: root
// property string text
property bool active: false
signal search(string term)
radius: 6
border.width: 1
border.color: (mouse.containsMouse | active) ? "#1b7ad3" : "#cfcfcf"
clip: true
TextInput {
id: buttonText
anchors.left: parent.left
anchors.right: searchIcon.left
anchors.margins: 4
anchors.verticalCenter: parent.verticalCenter
color: "#3f3f3f"
onTextChanged: {
searchTimer.restart();
}
onEditingFinished: {
root.search(text);
focus = false;
}
text: "Search"
}
Image {
id: searchIcon
source: "qrc:///search-icon-small"
anchors.right: parent.right
anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
onClicked: {
buttonText.forceActiveFocus();
buttonText.text = "";
}
}
onActiveChanged: {
if (!active) {
// rest search text when we deactive
buttonText.text = "Search"
searchTimer.stop();
}
}
Timer {
id: searchTimer
interval: 800
onTriggered: {
if (buttonText.text.length > 2) {
root.search(buttonText.text)
}
}
}
}

90
src/GUI/Slider.qml Normal file
View file

@ -0,0 +1,90 @@
import QtQuick 2.0
Item {
property alias label: labelText.text
property int min: 0
property int max: 100
property int value: 50
property int sliderWidth: 0
readonly property real __percentFull: value / (max - min)
implicitHeight: labelText.height
implicitWidth: labelText.width * 2
Text {
id: labelText
width: parent.width - (emptyTrack.width + 8)
horizontalAlignment: Text.AlignRight
}
Rectangle {
id: emptyTrack
width: sliderWidth > 0 ? sliderWidth : parent.width - (labelText.implicitWidth + 8)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 2
color: "#9f9f9f"
Rectangle {
id: fullTrack
height: parent.height
color:"#68A6E1"
width: parent.width * __percentFull
}
// invisble item that moves directly with the mouse
Item {
id: dragThumb
height: 2
width: 2
anchors.verticalCenter: parent.verticalCenter
Drag.active: thumbMouse.drag.active
onXChanged: {
if (thumbMouse.drag.active) {
var frac = x / emptyTrack.width;
value = min + (max - min) * frac;
}
}
}
Rectangle {
id: thumb
width: 12
height: 12
radius: 6
color: "#68A6E1"
anchors.verticalCenter: parent.verticalCenter
x: parent.width * __percentFull
MouseArea {
id: thumbMouse
hoverEnabled: true
anchors.fill: parent
drag.axis: Drag.XAxis
drag.minimumX: 0
drag.maximumX: emptyTrack.width
drag.target: dragThumb
onPressed: {
dragThumb.x = emptyTrack.width * __percentFull
}
}
}
} // of base track rectangle
}

33
src/GUI/TabButton.qml Normal file
View file

@ -0,0 +1,33 @@
import QtQuick 2.0
Rectangle {
id: root
property alias text: buttonText.text
property bool active: false
signal clicked
width: buttonText.width + (radius * 2)
height: buttonText.height + (radius * 2)
radius: 6
color: mouse.containsMouse ? "#064989" : (active ? "#1b7ad3" : "white")
Text {
id: buttonText
anchors.centerIn: parent
color: (active | mouse.containsMouse) ? "white" : "#3f3f3f"
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.clicked();
}
}
}

57
src/GUI/ToggleSwitch.qml Normal file
View file

@ -0,0 +1,57 @@
import QtQuick 2.0
Item {
property bool checked: false
property alias label: label.text
implicitWidth: track.width + label.width + 16
implicitHeight: label.height
Rectangle {
id: track
width: height * 2
height: 12
radius: 6
color: checked ? "#68A6E1" :"#9f9f9f"
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: thumb
width: 18
height: 18
radius: 9
anchors.verticalCenter: parent.verticalCenter
color: checked ? "#68A6E1" : "white"
border.width: 1
border.color: "#9f9f9f"
x: checked ? parent.width - (6 + radius) : -3
Behavior on x {
NumberAnimation {
duration: 250
}
}
}
}
Text {
id: label
anchors.left: track.right
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
anchors.fill: parent
id: mouseArea
hoverEnabled: true
onClicked: {
checked = !checked
}
}
}

View file

@ -0,0 +1,48 @@
import QtQuick 2.2
import FlightGear.Launcher 1.0 as FG
ListHeaderBox {
id: root
property int margin: 6
contents: [
Text {
id: updateAllText
anchors {
left: parent.left
right: updateAllButton.left
margins: root.margin
verticalCenter: parent.verticalCenter
}
text: qsTr("%1 aircraft have updates available - download and install them now?").
arg( _launcher.baseAircraftModel.aircraftNeedingUpdated);
wrapMode: Text.WordWrap
},
Button {
id: updateAllButton
text: qsTr("Update all")
anchors.verticalCenter: parent.verticalCenter
anchors.right: notNowButton.left
anchors.rightMargin: root.margin
onClicked: {
_launcher.requestUpdateAllAircraft();
_launcher.baseAircraftModel.showUpdateAll = false
}
},
Button {
id: notNowButton
text: qsTr("Not now")
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: root.margin
onClicked: {
_launcher.baseAircraftModel.showUpdateAll = false
}
}
]
}

View file

@ -36,6 +36,23 @@
<file>AircraftWarningPanel.qml</file>
<file>Scrollbar.qml</file>
<file>AircraftDetailsView.qml</file>
<file>AircraftFullDelegate.qml</file>
<file>AircraftCompactDelegate.qml</file>
<file>SearchButton.qml</file>
<file>AircraftPreviewPanel.qml</file>
<file>TabButton.qml</file>
<file>Checkbox.qml</file>
<file>Slider.qml</file>
<file>ToggleSwitch.qml</file>
<file>RatingSlider.qml</file>
<file>PopupChoice.qml</file>
<file alias="up-down-arrow">up-down-arrow.png</file>
<file>AircraftVariantChoice.qml</file>
<file alias="search-icon-small">search-icon-small.png</file>
<file>AircraftRatingsPanel.qml</file>
<file>NoDefaultCatalogPanel.qml</file>
<file>ListHeaderBox.qml</file>
<file>UpdateAllPanel.qml</file>
</qresource>
<qresource prefix="/preview">
<file alias="close-icon">preview-close.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

BIN
src/GUI/up-down-arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B