1
0
Fork 0

Stubbing out Add-ons controller

Also CatalogListModel stubs
This commit is contained in:
James Turner 2018-03-16 22:01:21 +00:00
parent 3b79530b06
commit 205fadbff3
27 changed files with 1560 additions and 158 deletions

View file

@ -0,0 +1,216 @@
#include "AddOnsController.hxx"
#include <QSettings>
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>
#include <QQmlComponent>
#include <QDesktopServices>
#include <QValidator>
#include <simgear/package/Root.hxx>
#include <simgear/package/Catalog.hxx>
#include <Main/globals.hxx>
#include <Network/HTTPClient.hxx>
#include "LocalAircraftCache.hxx"
#include "LauncherMainWindow.hxx"
#include "CatalogListModel.hxx"
#include "InstallSceneryDialog.hxx"
#include "QtLauncher.hxx"
//////////////////////////////////////////////////////////////////////////
AddOnsController::AddOnsController(LauncherMainWindow *parent) :
QObject(parent),
m_launcher(parent)
{
m_catalogs = new CatalogListModel(this,
simgear::pkg::RootRef(globals->packageRoot()));
connect(m_catalogs, &CatalogListModel::catalogsChanged, this, &AddOnsController::onCatalogsChanged);
QSettings settings;
m_sceneryPaths = settings.value("scenery-paths").toStringList();
m_aircraftPaths = settings.value("aircraft-paths").toStringList();
qmlRegisterUncreatableType<AddOnsController>("FlightGear.Launcher", 1, 0, "AddOnsControllers", "no");
qmlRegisterUncreatableType<CatalogListModel>("FlightGear.Launcher", 1, 0, "CatalogListModel", "no");
}
QStringList AddOnsController::aircraftPaths() const
{
return m_aircraftPaths;
}
QStringList AddOnsController::sceneryPaths() const
{
return m_sceneryPaths;
}
QString AddOnsController::addAircraftPath() const
{
QString path = QFileDialog::getExistingDirectory(m_launcher, tr("Choose aircraft folder"));
if (path.isEmpty()) {
return {};
}
// the user might add a directory containing an 'Aircraft' subdir. Let's attempt
// to check for that case and handle it gracefully.
bool pathOk = false;
if (LocalAircraftCache::isCandidateAircraftPath(path)) {
pathOk = true;
} else {
// no aircraft in speciied path, look for Aircraft/ subdir
QDir d(path);
if (d.exists("Aircraft")) {
QString p2 = d.filePath("Aircraft");
if (LocalAircraftCache::isCandidateAircraftPath(p2)) {
pathOk = true;
}
}
}
if (!pathOk) {
QMessageBox mb;
mb.setText(tr("No aircraft found in the folder '%1' - add anyway?").arg(path));
mb.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
mb.setDefaultButton(QMessageBox::No);
mb.exec();
if (mb.result() == QMessageBox::No) {
return {};
}
}
return path;
}
QString AddOnsController::addSceneryPath() const
{
QString path = QFileDialog::getExistingDirectory(m_launcher, tr("Choose scenery folder"));
if (path.isEmpty()) {
return {};
}
// validation
SGPath p(path.toStdString());
bool isValid = false;
for (const auto& dir: {"Objects", "Terrain", "Buildings", "Roads", "Pylons", "NavData"}) {
if ((p / dir).exists()) {
isValid = true;
break;
}
}
if (!isValid) {
QMessageBox mb;
mb.setText(tr("The folder '%1' doesn't appear to contain scenery - add anyway?").arg(path));
mb.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
mb.setDefaultButton(QMessageBox::No);
mb.setInformativeText(tr("Added scenery should contain at least one of the following "
"folders: Objects, Terrain, Buildings, Roads, Pylons, NavData."));
mb.exec();
if (mb.result() == QMessageBox::No) {
return {};
}
}
return path;
}
QString AddOnsController::installCustomScenery()
{
QSettings settings;
QString downloadDir = settings.value("download-dir").toString();
InstallSceneryDialog dlg(m_launcher, downloadDir);
if (dlg.exec() == QDialog::Accepted) {
return dlg.sceneryPath();
}
return {};
}
void AddOnsController::openDirectory(QString path)
{
QUrl u = QUrl::fromLocalFile(path);
QDesktopServices::openUrl(u);
}
void AddOnsController::setAircraftPaths(QStringList aircraftPaths)
{
if (m_aircraftPaths == aircraftPaths)
return;
qInfo() << Q_FUNC_INFO << aircraftPaths << ", was " << m_aircraftPaths;
m_aircraftPaths = aircraftPaths;
emit aircraftPathsChanged(m_aircraftPaths);
QSettings settings;
settings.setValue("aircraft-paths", m_aircraftPaths);
auto aircraftCache = LocalAircraftCache::instance();
aircraftCache->setPaths(m_aircraftPaths);
aircraftCache->scanDirs();
}
void AddOnsController::setSceneryPaths(QStringList sceneryPaths)
{
if (m_sceneryPaths == sceneryPaths)
return;
m_sceneryPaths = sceneryPaths;
QSettings settings;
settings.setValue("scenery-paths", m_sceneryPaths);
flightgear::launcherSetSceneryPaths();
emit sceneryPathsChanged(m_sceneryPaths);
}
void AddOnsController::officialCatalogAction(QString s)
{
if (s == "hide") {
QSettings settings;
settings.setValue("hide-official-catalog-message", true);
} else if (s == "add-official") {
// TEMPROARY, FIXME
// AddOnsPage::addDefaultCatalog(nullptr, false /* not silent */);
}
emit showNoOfficialHangarChanged();
}
bool AddOnsController::shouldShowOfficialCatalogMessage() const
{
QSettings settings;
bool showOfficialCatalogMesssage = !globals->get_subsystem<FGHTTPClient>()->isDefaultCatalogInstalled();
if (settings.value("hide-official-catalog-message").toBool()) {
showOfficialCatalogMesssage = false;
}
return showOfficialCatalogMesssage;
}
bool AddOnsController::isOfficialHangarRegistered()
{
return globals->get_subsystem<FGHTTPClient>()->isDefaultCatalogInstalled();
}
bool AddOnsController::showNoOfficialHangar() const
{
return shouldShowOfficialCatalogMessage();
}
void AddOnsController::onCatalogsChanged()
{
emit showNoOfficialHangarChanged();
emit isOfficialHangarRegisteredChanged();
}

View file

@ -0,0 +1,64 @@
#ifndef ADDONSCONTROLLER_HXX
#define ADDONSCONTROLLER_HXX
#include <QObject>
class CatalogListModel;
class LauncherMainWindow;
class AddOnsController : public QObject
{
Q_OBJECT
Q_PROPERTY(QStringList aircraftPaths READ aircraftPaths WRITE setAircraftPaths NOTIFY aircraftPathsChanged)
Q_PROPERTY(QStringList sceneryPaths READ sceneryPaths WRITE setSceneryPaths NOTIFY sceneryPathsChanged)
Q_PROPERTY(CatalogListModel* catalogs READ catalogs CONSTANT)
Q_PROPERTY(bool isOfficialHangarRegistered READ isOfficialHangarRegistered NOTIFY isOfficialHangarRegisteredChanged)
Q_PROPERTY(bool showNoOfficialHangar READ showNoOfficialHangar NOTIFY showNoOfficialHangarChanged)
public:
explicit AddOnsController(LauncherMainWindow *parent = nullptr);
QStringList aircraftPaths() const;
QStringList sceneryPaths() const;
CatalogListModel* catalogs() const
{ return m_catalogs; }
Q_INVOKABLE QString addAircraftPath() const;
Q_INVOKABLE QString addSceneryPath() const;
// we would ideally do this in-page, but needs some extra work
Q_INVOKABLE QString installCustomScenery();
Q_INVOKABLE void openDirectory(QString path);
bool isOfficialHangarRegistered();
bool showNoOfficialHangar() const;
Q_INVOKABLE void officialCatalogAction(QString s);
signals:
void aircraftPathsChanged(QStringList aircraftPaths);
void sceneryPathsChanged(QStringList sceneryPaths);
void isOfficialHangarRegisteredChanged();
void showNoOfficialHangarChanged();
public slots:
void setAircraftPaths(QStringList aircraftPaths);
void setSceneryPaths(QStringList sceneryPaths);
private:
bool shouldShowOfficialCatalogMessage() const;
void onCatalogsChanged();
LauncherMainWindow* m_launcher;
CatalogListModel* m_catalogs = nullptr;
QStringList m_aircraftPaths;
QStringList m_sceneryPaths;
};
#endif // ADDONSCONTROLLER_HXX

View file

@ -92,10 +92,6 @@ if (HAVE_QT)
AircraftModel.cxx
CatalogListModel.cxx
CatalogListModel.hxx
AddCatalogDialog.cxx
AddCatalogDialog.hxx
PathsDialog.cxx
PathsDialog.hxx
LocationWidget.cxx
LocationWidget.hxx
QtMessageBox.cxx
@ -126,6 +122,8 @@ if (HAVE_QT)
RecentLocationsModel.hxx
LauncherController.cxx
LauncherController.hxx
AddOnsController.cxx
AddOnsController.hxx
${uic_sources}
${qrc_sources}
${qml_sources})

View file

@ -22,6 +22,7 @@
#include <QDebug>
#include <QUrl>
#include <QTimer>
// Simgear
#include <simgear/props/props_io.hxx>
@ -32,25 +33,61 @@
// FlightGear
#include <Main/globals.hxx>
#include <Network/HTTPClient.hxx>
using namespace simgear::pkg;
CatalogListModel::CatalogListModel(QObject* pr, simgear::pkg::RootRef& rootRef) :
class CatalogDelegate : public simgear::pkg::Delegate
{
public:
CatalogDelegate(CatalogListModel* outer) : p(outer) {}
void catalogRefreshed(CatalogRef catalog, StatusCode) override
{
p->onCatalogStatusChanged(catalog);
}
void startInstall(InstallRef) override {}
void installProgress(InstallRef, unsigned int, unsigned int) override {}
void finishInstall(InstallRef, StatusCode ) override {}
private:
CatalogListModel* p = nullptr;
};
CatalogListModel::CatalogListModel(QObject* pr, const
simgear::pkg::RootRef& rootRef) :
QAbstractListModel(pr),
m_packageRoot(rootRef)
{
refresh();
m_delegate = new CatalogDelegate(this);
m_packageRoot->addDelegate(m_delegate);
resetData();
}
CatalogListModel::~CatalogListModel()
{
m_packageRoot->removeDelegate(m_delegate);
}
void CatalogListModel::refresh()
void CatalogListModel::resetData()
{
CatalogList updatedCatalogs = m_packageRoot->allCatalogs();
std::sort(updatedCatalogs.begin(), updatedCatalogs.end(),
[](const CatalogRef& catA, const CatalogRef& catB)
{ // lexicographic ordering
return catA->name() < catB->name();
});
if (updatedCatalogs == m_catalogs)
return;
beginResetModel();
m_catalogs = m_packageRoot->allCatalogs();
m_catalogs = updatedCatalogs;
endResetModel();
emit catalogsChanged();
}
int CatalogListModel::rowCount(const QModelIndex& parent) const
@ -83,6 +120,10 @@ QVariant CatalogListModel::data(const QModelIndex& index, int role) const
}
}
return tr("%1 - %2").arg(name).arg(desc);
} else if (role == CatalogDescriptionRole) {
return QString::fromStdString(cat->description());
} else if (role == CatalogNameRole) {
return QString::fromStdString(cat->name());
} else if (role == Qt::ToolTipRole) {
return QString::fromStdString(cat->url());
} else if (role == CatalogUrlRole) {
@ -93,6 +134,10 @@ QVariant CatalogListModel::data(const QModelIndex& index, int role) const
return static_cast<quint32>(cat->packages().size());
} else if (role == CatalogInstallCountRole) {
return static_cast<quint32>(cat->installedPackages().size());
} else if (role == CatalogStatusRole) {
return translateStatusForCatalog(cat);
} else if (role == CatalogIsNewlyAdded) {
return (cat == m_newlyAddedCatalog);
}
return QVariant();
@ -112,3 +157,171 @@ Qt::ItemFlags CatalogListModel::flags(const QModelIndex &index) const
}
return r;
}
QHash<int, QByteArray> CatalogListModel::roleNames() const
{
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
result[CatalogUrlRole] = "url";
result[CatalogIdRole] = "id";
result[CatalogDescriptionRole] = "description";
result[CatalogNameRole] = "name";
result[CatalogStatusRole] = "status";
result[CatalogIsNewlyAdded] = "isNewlyAdded";
return result;
}
void CatalogListModel::removeCatalog(int index)
{
if ((index < 0) || (index >= m_catalogs.size())) {
return;
}
const std::string removeId = m_catalogs.at(index)->id();
m_packageRoot->removeCatalogById(removeId);
resetData();
}
void CatalogListModel::refreshCatalog(int index)
{
if ((index < 0) || (index >= m_catalogs.size())) {
return;
}
m_catalogs.at(index)->refresh();
}
void CatalogListModel::installDefaultCatalog()
{
FGHTTPClient* http = globals->get_subsystem<FGHTTPClient>();
m_newlyAddedCatalog = Catalog::createFromUrl(m_packageRoot, http->getDefaultCatalogUrl());
emit isAddingCatalogChanged();
emit statusOfAddingCatalogChanged();
resetData();
}
void CatalogListModel::addCatalogByUrl(QUrl url)
{
if (m_newlyAddedCatalog) {
qWarning() << Q_FUNC_INFO << "already adding a catalog";
return;
}
m_newlyAddedCatalog = Catalog::createFromUrl(m_packageRoot, url.toString().toStdString());
resetData();
emit isAddingCatalogChanged();
}
int CatalogListModel::indexOf(QUrl url)
{
std::string urlString = url.toString().toStdString();
auto it = std::find_if(m_catalogs.begin(), m_catalogs.end(),
[urlString](simgear::pkg::CatalogRef cat) { return cat->url() == urlString;});
if (it == m_catalogs.end())
return -1;
return std::distance(m_catalogs.begin(), it);
}
void CatalogListModel::finalizeAddCatalog()
{
if (!m_newlyAddedCatalog) {
qWarning() << Q_FUNC_INFO << "no catalog add in progress";
return;
}
auto it = std::find(m_catalogs.begin(), m_catalogs.end(), m_newlyAddedCatalog);
if (it == m_catalogs.end()) {
qWarning() << Q_FUNC_INFO << "couldn't find new catalog in m_catalogs" << QString::fromStdString(m_newlyAddedCatalog->url());
return;
}
const int row = std::distance(m_catalogs.begin(), it);
m_newlyAddedCatalog.clear();
emit isAddingCatalogChanged();
emit statusOfAddingCatalogChanged();
emit dataChanged(index(row), index(row));
}
void CatalogListModel::abandonAddCatalog()
{
if (!m_newlyAddedCatalog)
return;
m_packageRoot->removeCatalog(m_newlyAddedCatalog);
m_newlyAddedCatalog.clear();
emit isAddingCatalogChanged();
emit statusOfAddingCatalogChanged();
resetData();
}
bool CatalogListModel::isAddingCatalog() const
{
return m_newlyAddedCatalog.get() != nullptr;
}
void CatalogListModel::onCatalogStatusChanged(Catalog* cat)
{
if (cat == nullptr) {
resetData();
return;
}
//qInfo() << Q_FUNC_INFO << "for" << QString::fromStdString(cat->url()) << translateStatusForCatalog(cat);
// download the official catalog often fails with a 404 due to how we
// compute the version-specific URL. This is the logic which bounces the UI
// to the fallback URL.
if (cat->status() == Delegate::FAIL_NOT_FOUND) {
FGHTTPClient* http = globals->get_subsystem<FGHTTPClient>();
if (cat->url() == http->getDefaultCatalogUrl()) {
cat->setUrl(http->getDefaultCatalogFallbackUrl());
cat->refresh(); // and trigger another refresh
return;
}
}
if (cat == m_newlyAddedCatalog) {
// defer this signal slightly so that QML calling finalizeAdd or
// abandonAdd in response, doesn't re-enter the package code
QTimer::singleShot(0, this, &CatalogListModel::statusOfAddingCatalogChanged);
return;
}
auto it = std::find(m_catalogs.begin(), m_catalogs.end(), cat);
if (it == m_catalogs.end())
return;
int row = std::distance(m_catalogs.begin(), it);
emit dataChanged(index(row), index(row));
}
CatalogListModel::CatalogStatus CatalogListModel::translateStatusForCatalog(CatalogRef cat) const
{
switch (cat->status()) {
case Delegate::STATUS_SUCCESS:
case Delegate::STATUS_REFRESHED:
return Ok;
case Delegate::FAIL_DOWNLOAD: return NetworkError;
case Delegate::STATUS_IN_PROGRESS: return Refreshing;
case Delegate::FAIL_NOT_FOUND: return NotFoundOnServer;
case Delegate::FAIL_VERSION: return IncomaptbleVersion;
case Delegate::FAIL_HTTP_FORBIDDEN: return HTTPForbidden;
case Delegate::FAIL_VALIDATION: return InvalidData;
default:
return UnknownError;
}
}
CatalogListModel::CatalogStatus CatalogListModel::statusOfAddingCatalog() const
{
if (!m_newlyAddedCatalog.get()) {
return NoAddInProgress;
}
return translateStatusForCatalog(m_newlyAddedCatalog);
}

View file

@ -34,17 +34,24 @@ const int CatalogUrlRole = Qt::UserRole + 1;
const int CatalogIdRole = Qt::UserRole + 2;
const int CatalogPackageCountRole = Qt::UserRole + 3;
const int CatalogInstallCountRole = Qt::UserRole + 4;
const int CatalogStatusRole = Qt::UserRole + 5;
const int CatalogDescriptionRole = Qt::UserRole + 6;
const int CatalogNameRole = Qt::UserRole + 7;
const int CatalogIsNewlyAdded = Qt::UserRole + 8;
class CatalogDelegate;
class CatalogListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool isAddingCatalog READ isAddingCatalog NOTIFY isAddingCatalogChanged)
Q_PROPERTY(CatalogStatus statusOfAddingCatalog READ statusOfAddingCatalog NOTIFY statusOfAddingCatalogChanged)
public:
CatalogListModel(QObject* pr, simgear::pkg::RootRef& root);
CatalogListModel(QObject* pr, const simgear::pkg::RootRef& root);
~CatalogListModel();
void refresh();
int rowCount(const QModelIndex& parent) const override;
QVariant data(const QModelIndex& index, int role) const override;
@ -53,12 +60,64 @@ public:
Qt::ItemFlags flags(const QModelIndex &index) const override;
private slots:
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void removeCatalog(int index);
Q_INVOKABLE void refreshCatalog(int index);
Q_INVOKABLE void installDefaultCatalog();
// returns the index of the new catalog
Q_INVOKABLE void addCatalogByUrl(QUrl url);
Q_INVOKABLE void finalizeAddCatalog();
Q_INVOKABLE void abandonAddCatalog();
bool isAddingCatalog() const;
enum CatalogStatus
{
Ok = 0,
Refreshing,
NetworkError,
NotFoundOnServer,
IncomaptbleVersion,
HTTPForbidden,
InvalidData,
UnknownError,
NoAddInProgress
};
Q_ENUM(CatalogStatus)
void onCatalogStatusChanged(simgear::pkg::Catalog *cat);
CatalogStatus statusOfAddingCatalog() const;
signals:
void isAddingCatalogChanged();
void statusOfAddingCatalogChanged();
void catalogsChanged();
public slots:
void resetData();
private slots:
private:
simgear::pkg::RootRef m_packageRoot;
simgear::pkg::CatalogList m_catalogs;
simgear::pkg::CatalogRef m_newlyAddedCatalog;
int indexOf(QUrl url);
CatalogDelegate* m_delegate;
CatalogStatus translateStatusForCatalog(simgear::pkg::CatalogRef cat) const;
};
#endif // of FG_GUI_CATALOG_LIST_MODEL

View file

@ -297,7 +297,10 @@
</item>
</layout>
</widget>
<widget class="LocationWidget" name="location"/>
<widget class="QQuickWidget" name="environmentPage">
</widget>
<widget class="QWidget" name="newSettingsPage">
@ -319,6 +322,33 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="addOnsPage">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QQuickWidget" name="addOns"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>

View file

@ -42,7 +42,6 @@
// remove me once location widget is ported to Quick
#include "LocationWidget.hxx"
#include "PathsDialog.hxx"
using namespace simgear::pkg;
@ -152,7 +151,6 @@ void LauncherController::restoreSettings()
m_serversModel->requestRestore();
emit summaryChanged();
emit showNoOfficialHangarChanged();
}
void LauncherController::saveSettings()
@ -366,20 +364,8 @@ void LauncherController::downloadDirChanged(QString path)
aircraftCache->setPaths(settings.value("aircraft-paths").toStringList());
aircraftCache->scanDirs();
emit showNoOfficialHangarChanged();
// re-set scenery dirs
setSceneryPaths();
}
bool LauncherController::shouldShowOfficialCatalogMessage() const
{
QSettings settings;
bool showOfficialCatalogMesssage = !globals->get_subsystem<FGHTTPClient>()->isDefaultCatalogInstalled();
if (settings.value("hide-official-catalog-message").toBool()) {
showOfficialCatalogMesssage = false;
}
return showOfficialCatalogMesssage;
flightgear::launcherSetSceneryPaths();
}
QmlAircraftInfo *LauncherController::selectedAircraftInfo() const
@ -483,14 +469,6 @@ simgear::pkg::PackageRef LauncherController::packageForAircraftURI(QUrl uri) con
return globals->packageRoot()->getPackageById(ident.toStdString());
}
void LauncherController::onAircraftPathsChanged()
{
QSettings settings;
auto aircraftCache = LocalAircraftCache::instance();
aircraftCache->setPaths(settings.value("aircraft-paths").toStringList());
aircraftCache->scanDirs();
}
bool LauncherController::validateMetarString(QString metar)
{
if (metar.isEmpty()) {
@ -561,11 +539,6 @@ void LauncherController::queryMPServers()
m_serversModel->refresh();
}
bool LauncherController::showNoOfficialHanger() const
{
return shouldShowOfficialCatalogMessage();
}
QString LauncherController::versionString() const
{
return FLIGHTGEAR_VERSION;
@ -618,25 +591,6 @@ void LauncherController::onAircraftInstallFailed(QModelIndex index, QString erro
}
void LauncherController::setSceneryPaths()
{
flightgear::launcherSetSceneryPaths();
}
void LauncherController::officialCatalogAction(QString s)
{
if (s == "hide") {
QSettings settings;
settings.setValue("hide-official-catalog-message", true);
} else if (s == "add-official") {
// TEMPROARY, FIXME
AddOnsPage::addDefaultCatalog(nullptr, false /* not silent */);
}
emit showNoOfficialHangarChanged();
}
QPointF LauncherController::mapToGlobal(QQuickItem *item, const QPointF &pos) const
{
QPointF scenePos = item->mapToScene(pos);

View file

@ -43,8 +43,6 @@ class LauncherController : public QObject
{
Q_OBJECT
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)
@ -92,10 +90,6 @@ public:
Q_INVOKABLE void queryMPServers();
bool showNoOfficialHanger() const;
Q_INVOKABLE void officialCatalogAction(QString s);
QUrl selectedAircraft() const;
// work around the fact, that this is not available on QQuickItem until 5.7
@ -145,8 +139,6 @@ public:
bool canFly() const;
void setSceneryPaths();
AircraftItemModel* baseAircraftModel() const
{ return m_aircraftModel; }
@ -154,7 +146,6 @@ public:
void saveSettings();
signals:
void showNoOfficialHangarChanged();
void selectedAircraftChanged(QUrl selectedAircraft);
void searchChanged();
@ -176,8 +167,6 @@ public slots:
void setEnvironmentSummary(QStringList environmentSummary);
void onAircraftPathsChanged();
private slots:
void onAircraftInstalledCompleted(QModelIndex index);
@ -198,8 +187,6 @@ private:
// scrolling, to give the view time it seems.
void delayedAircraftModelReset();
bool shouldShowOfficialCatalogMessage() const;
void collectAircraftArgs();
private:

View file

@ -9,7 +9,6 @@
#include <QMenu>
#include <QPushButton>
#include <QStandardItemModel>
#include <QDesktopServices>
#include <QQuickItem>
@ -34,13 +33,17 @@
// launcher headers
#include "QtLauncher.hxx"
#include "PathsDialog.hxx"
#include "AircraftModel.hxx"
#include "AircraftSearchFilterModel.hxx"
#include "DefaultAircraftLocator.hxx"
#include "LaunchConfig.hxx"
#include "ViewCommandLinePage.hxx"
#include "AircraftModel.hxx"
#include "LocalAircraftCache.hxx"
#include "LauncherController.hxx"
#include "DefaultAircraftLocator.hxx"
#include "AddOnsController.hxx"
#include "CatalogListModel.hxx"
#include "ui_Launcher.h"
@ -107,18 +110,6 @@ LauncherMainWindow::LauncherMainWindow() :
connect(qa, &QAction::triggered, this, &LauncherMainWindow::onQuit);
addAction(qa);
AddOnsPage* addOnsPage = new AddOnsPage(NULL, globals->packageRoot());
connect(addOnsPage, &AddOnsPage::sceneryPathsChanged,
m_controller, &LauncherController::setSceneryPaths);
connect(addOnsPage, &AddOnsPage::aircraftPathsChanged,
m_controller, &LauncherController::onAircraftPathsChanged);
m_ui->stack->addWidget(addOnsPage);
connect(m_controller->baseAircraftModel(), &AircraftItemModel::catalogsRefreshed,
addOnsPage, &AddOnsPage::onCatalogsRefreshed);
m_viewCommandLinePage = new ViewCommandLinePage;
m_viewCommandLinePage->setLaunchConfig(m_controller->config());
m_ui->stack->addWidget(m_viewCommandLinePage);
@ -127,7 +118,9 @@ LauncherMainWindow::LauncherMainWindow() :
restoreGeometry(settings.value("window-geometry").toByteArray());
m_controller->restoreSettings();
restoreSettings();
flightgear::launcherSetSceneryPaths();
auto addOnsCtl = new AddOnsController(this);
////////////
#if defined(Q_OS_WIN)
@ -144,6 +137,7 @@ LauncherMainWindow::LauncherMainWindow() :
m_ui->aircraftList->engine()->addImportPath("qrc:///");
m_ui->aircraftList->engine()->rootContext()->setContextProperty("_launcher", m_controller);
m_ui->aircraftList->engine()->rootContext()->setContextProperty("_addOns", addOnsCtl);
connect( m_ui->aircraftList, &QQuickWidget::statusChanged,
this, &LauncherMainWindow::onQuickStatusChanged);
@ -184,17 +178,18 @@ LauncherMainWindow::LauncherMainWindow() :
connect( m_ui->summary, &QQuickWidget::statusChanged,
this, &LauncherMainWindow::onQuickStatusChanged);
m_ui->summary->setSource(QUrl("qrc:///qml/Summary.qml"));
// addOns
m_ui->addOns->engine()->addImportPath("qrc:///");
m_ui->addOns->engine()->rootContext()->setContextProperty("_launcher", m_controller);
m_ui->addOns->engine()->rootContext()->setContextProperty("_addOns", addOnsCtl);
m_ui->addOns->setResizeMode(QQuickWidget::SizeRootObjectToView);
m_ui->addOns->setSource(QUrl("qrc:///qml/AddOns.qml"));
//////////////////////////
}
void LauncherMainWindow::restoreSettings()
{
if (!m_inAppMode) {
m_controller->setSceneryPaths();
}
}
void LauncherMainWindow::saveSettings()
{
QSettings settings;
@ -363,5 +358,3 @@ void LauncherMainWindow::onChangeDataDir()
flightgear::restartTheApp();
}

44
src/GUI/qml/AddButton.qml Normal file
View file

@ -0,0 +1,44 @@
import QtQuick 2.4
import "."
Rectangle {
id: root
property bool enabled: true
signal clicked
width: label.implicitWidth + height + Style.margin
height: icon.implicitHeight + 1
radius: Style.roundRadius
color: enabled ? (mouse.containsMouse ? Style.activeColor : Style.themeColor) : Style.disabledThemeColor
Text {
id: label
text: qsTr("Add")
color: "white"
anchors {
left: parent.left
leftMargin: Style.margin
verticalCenter: parent.verticalCenter
}
}
Image {
id: icon
anchors.right: parent.right
source: "qrc:///add-icon"
}
MouseArea {
id: mouse
anchors.fill: parent
enabled: root.enabled
hoverEnabled: true
onClicked: {
root.clicked();
}
}
}

View file

@ -0,0 +1,165 @@
import QtQuick 2.4
import FlightGear.Launcher 1.0 as FG
import "."
Rectangle {
id: addCatalogPanel
state: "start"
height: column.height
readonly property bool isActive: (state != "start")
Behavior on height {
NumberAnimation { duration: 200; }
}
Column {
id: column
width: parent.width
spacing: Style.margin
ClickableText {
id: addCatalogPromptText
width: parent.width
wrapMode: Text.WordWrap
onClicked: {
addCatalogPanel.state = "enter-url";
}
}
LineEdit {
id: addCatalogUrlEntry
width: parent.width
useFullWidth: true
label: qsTr("Hangar URL:");
placeholder: "http://www.flightgear.org/hangars/some-catalog.xml"
}
Item {
id: buttonBox
width: parent.width
height: childrenRect.height
Button {
id: addCatalogCancelButton
anchors.right: addCatalogProceedButton.left
anchors.rightMargin: Style.margin
text: qsTr("Cancel")
onClicked: {
_addOns.catalogs.abandonAddCatalog();
addCatalogPanel.state = "start";
}
}
Button {
id: addCatalogProceedButton
anchors.right: parent.right
text: qsTr("Add hangar")
// todo check for a well-formed URL
enabled: addCatalogUrlEntry.text !== ""
onClicked: {
// this occurs in the retry case; abandon the current
// add before starting the new one
if (_addOns.catalogs.isAddingCatalog) {
_addOns.catalogs.abandonAddCatalog();
}
_addOns.catalogs.addCatalogByUrl(addCatalogUrlEntry.text)
addCatalogPanel.state = "add-in-progress"
}
}
} // of button box
} // of column layout
Connections {
target: _addOns.catalogs
onStatusOfAddingCatalogChanged: {
var status = _addOns.catalogs.statusOfAddingCatalog;
if (status == FG.CatalogListModel.Refreshing) {
// this can happen when adding the default catalog,
// ensure our state matches
addCatalogPanel.state = "add-in-progress"
return;
}
if (status == FG.CatalogListModel.NoAddInProgress) {
addCatalogPanel.state = "start";
return;
}
if (status == FG.CatalogListModel.Ok) {
// success, we are done
_addOns.catalogs.finalizeAddCatalog();
addCatalogPanel.state = "start";
return;
}
// if we reach this point, we're in an error situation, so
// handle that
addCatalogPanel.state = "add-failed-retry";
}
}
function addErrorMessage()
{
switch (_addOns.catalogs.statusOfAddingCatalog) {
case FG.CatalogListModel.NotFoundOnServer:
return qsTr("Failed to find a hangar description at the URL: '%1'. " +
"Check you entered the URL correctly.");
case FG.CatalogListModel.HTTPForbidden:
return qsTr("Access to the hangar data was forbidden by the server. " +
"Please check the URL you entered, or contact the hangar authors.");
case FG.CatalogListModel.NetworkError:
return qsTr("Failed to download from the server due to a network problem. " +
"Check your Internet connection is working, and that you entered the correct URL.");
case FG.CatalogListModel.IncomaptbleVersion:
return qsTr("The hangar you requested is for a different version of FlightGear. "
+ "(This is version %1)").arg(_launcher.versionString);
case FG.CatalogListModel.InvalidData:
return qsTr("The requested hangar seems to be invalid (damaged or incomplete). "
+ "Please contact the hangar authors, or try again later");
default:
return qsTr("Unknown error: " + _addOns.catalogs.statusOfAddingCatalog);
}
}
states: [
State {
name: "start"
PropertyChanges { target: addCatalogPromptText; text: qsTr("Click here to add a new aircraft hangar. (Note this requires an Internet connection)"); clickable: true }
PropertyChanges { target: addCatalogUrlEntry; visible: false; text: "" } // reset URL on cancel / success
PropertyChanges { target: buttonBox; visible: false }
},
State {
name: "enter-url"
PropertyChanges { target: addCatalogPromptText; text: qsTr("Enter a hangar location (URL) to add."); clickable: false }
PropertyChanges { target: addCatalogUrlEntry; visible: true }
PropertyChanges { target: buttonBox; visible: true }
},
State {
name: "add-in-progress"
PropertyChanges { target: addCatalogPromptText; text: qsTr("Retrieving hangar information..."); clickable: false }
PropertyChanges { target: addCatalogUrlEntry; visible: false }
PropertyChanges { target: addCatalogProceedButton; enabled: false }
},
State {
name: "add-failed-retry"
PropertyChanges { target: addCatalogPromptText; text: qsTr("There was a problem adding the hangar: %1.").arg(addErrorMessage()); clickable: false }
PropertyChanges { target: addCatalogUrlEntry; visible: true }
PropertyChanges { target: addCatalogProceedButton; enabled: true }
}
]
}

409
src/GUI/qml/AddOns.qml Normal file
View file

@ -0,0 +1,409 @@
import QtQuick 2.4
import FlightGear.Launcher 1.0
import "."
Flickable {
flickableDirection: Flickable.VerticalFlick
contentHeight: contents.childrenRect.height
interactive: false
Component {
id: catalogDelegate
Rectangle {
id: delegateRoot
// don't show the delegate for newly added catalogs, until the
// adding process is complete
visible: !model.isNewlyAdded
height: catalogTextColumn.childrenRect.height
border.width: 1
border.color: Style.minorFrameColor
width: catalogsColumn.width
Column {
id: catalogTextColumn
anchors.left: parent.left
anchors.right: catalogDeleteButton.left
anchors.rightMargin: Style.margin
spacing: Style.margin
Text {
font.pixelSize: Style.subHeadingFontPixelSize
font.bold: true
width: parent.width
text: model.name
}
Text {
visible: model.status === CatalogListModel.Ok
width: parent.width
text: model.description
wrapMode: Text.WordWrap
}
ClickableText {
visible: model.status !== CatalogListModel.Ok
width: parent.width
wrapMode: Text.WordWrap
text: qsTr("This hangar is currently disabled due to a problem. " +
"Click here to try updating the hangar information from the server. "
+ "(An Internet connection is required for this)");
onClicked: {
_addOns.catalogs.refreshCatalog(model.index)
}
}
Text {
width: parent.width
text: model.url
}
}
DeleteButton {
id: catalogDeleteButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: delegateHover.containsMouse
onClicked: {
confirmDeleteHangar.visible = true;
}
}
MouseArea {
id: delegateHover
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
YesNoPanel {
id: confirmDeleteHangar
visible: false
anchors.fill: parent
yesText: qsTr("Remove")
noText: qsTr("Cancel")
yesIsDestructive: true
promptText: qsTr("Remove this hangar? (Downloaded aircraft will be deleted from your computer)");
onAccepted: {
_addOns.catalogs.removeCatalog(model.index);
}
onRejected: {
confirmDeleteHangar.visible = false
}
}
}
}
Column {
id: contents
width: parent.width - (Style.margin * 2)
x: Style.margin
spacing: Style.margin
//////////////////////////////////////////////////////////////////
// catalogs //////////////////////////////////////////////////////
Item {
id: catalogHeaderItem
width: parent.width
height: catalogHeadingText.height + catalogDescriptionText.height + Style.margin
Text {
id: catalogHeadingText
text: qsTr("Aircraft hangars")
font.pixelSize: Style.headingFontPixelSize
width: parent.width
}
Text {
id: catalogDescriptionText
text: qsTr("Aircraft hangars are managed collections of aircraft, which can be " +
"downloaded, installed and updated inside FlightGear.")
anchors {
top: catalogHeadingText.bottom
topMargin: Style.margin
}
width: parent.width
wrapMode: Text.WordWrap
}
} // of catalogs header item
Rectangle {
width: parent.width
height: catalogsColumn.childrenRect.height + Style.margin * 2
border.width: 1
border.color: Style.frameColor
clip: true
Column {
id: catalogsColumn
width: parent.width - Style.margin * 2
x: Style.margin
y: Style.margin
spacing: Style.margin
Repeater {
id: catalogsRepeater
model: _addOns.catalogs
delegate: catalogDelegate
}
ClickableText {
visible: !_addOns.isOfficialHangarRegistered && !addCatalogPanel.isActive
width: parent.width
text : qsTr("The official FlightGear aircraft hangar is not set up. To add it, click here.");
onClicked: {
_addOns.catalogs.installDefaultCatalog()
}
}
AddCatalogPanel {
id: addCatalogPanel
width: parent.width
}
}
}
//////////////////////////////////////////////////////////////////
Item {
// spacing item
width: parent.width
height: Style.margin * 2
}
Item {
id: aircraftHeaderItem
width: parent.width
height: aircraftHeading.height + aircraftDescriptionText.height + Style.margin
Text {
id: aircraftHeading
text: qsTr("Additional aircraft folders")
font.pixelSize: Style.headingFontPixelSize
anchors {
left: parent.left
right: addAircraftPathButton.left
rightMargin: Style.margin
}
}
Text {
id: aircraftDescriptionText
text: qsTr("To use aircraft you download yourself, FlightGear needs to " +
"know the folder(s) containing the aircraft data.")
anchors {
top: aircraftHeading.bottom
topMargin: Style.margin
left: parent.left
right: addAircraftPathButton.left
rightMargin: Style.margin
}
wrapMode: Text.WordWrap
}
AddButton {
id: addAircraftPathButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
onClicked: {
var newPath =_addOns.addAircraftPath();
if (newPath != "") {
_addOns.aircraftPaths.push(newPath)
}
}
}
} // of aircraft header item
Rectangle {
width: parent.width
height: aircraftPathsColumn.childrenRect.height + 1
border.width: 1
border.color: Style.frameColor
clip: true
Column {
id: aircraftPathsColumn
width: parent.width - Style.margin * 2
x: Style.margin
Repeater {
id: aircraftPathsRepeater
model: _addOns.aircraftPaths
delegate: PathListDelegate {
width: aircraftPathsColumn.width
deletePromptText: qsTr("Remove the aircraft folder: '%1' from the list? (The folder contents will not be changed)").arg(modelData);
onPerformDelete: {
var modifiedPaths = _addOns.aircraftPaths.slice()
modifiedPaths.splice(model.index, 1);
_addOns.aircraftPaths = modifiedPaths;
}
onPerformMove: {
var modifiedPaths = _addOns.aircraftPaths.slice()
modifiedPaths.splice(model.index, 1);
modifiedPaths.splice(newIndex, 0, modelData)
_addOns.aircraftPaths = modifiedPaths;
}
}
}
Text {
visible: (aircraftPathsRepeater.count == 0)
width: parent.width
text : qsTr("No custom aircraft paths are configured.");
}
}
}
//////////////////////////////////////////////////////////////////
Item {
// spacing item
width: parent.width
height: Style.margin * 2
}
Item {
id: sceneryHeaderItem
width: parent.width
height: sceneryHeading.height + sceneryDescriptionText.height + Style.margin
Text {
id: sceneryHeading
text: qsTr("Additional scenery folders")
font.pixelSize: Style.headingFontPixelSize
anchors {
left: parent.left
right: addSceneryPathButton.left
rightMargin: Style.margin
}
}
Text {
id: sceneryDescriptionText
text: qsTr("To use scenery you download yourself, FlightGear needs " +
"to know the folders containing the scenery data. " +
"Adjust the order of the list to control which scenery is used in a region.");
anchors {
top: sceneryHeading.bottom
topMargin: Style.margin
left: parent.left
right: addSceneryPathButton.left
rightMargin: Style.margin
}
wrapMode: Text.WordWrap
}
AddButton {
id: addSceneryPathButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
onClicked: {
var newPath =_addOns.addSceneryPath();
if (newPath !== "") {
_addOns.sceneryPaths.push(newPath)
}
}
}
} // of aircraft header item
Rectangle {
width: parent.width
height: sceneryPathsColumn.childrenRect.height + 1
border.width: 1
border.color: Style.frameColor
clip: true
Column {
id: sceneryPathsColumn
width: parent.width - Style.margin * 2
x: Style.margin
Repeater {
id: sceneryPathsRepeater
model: _addOns.sceneryPaths
delegate: PathListDelegate {
width: sceneryPathsColumn.width
deletePromptText: qsTr("Remove the scenery folder: '%1' from the list? (The folder contents will not be changed)").arg(modelData);
onPerformDelete: {
var modifiedPaths = _addOns.sceneryPaths.slice()
modifiedPaths.splice(model.index, 1);
_addOns.sceneryPaths = modifiedPaths;
}
onPerformMove: {
}
}
}
Text {
visible: (sceneryPathsRepeater.count == 0)
width: parent.width
text : qsTr("No custom scenery paths are configured.");
}
}
}
Item {
width: parent.width
height: Math.max(installTarballText.implicitHeight, installTarballButton.height)
Button {
id: installTarballButton
text: "Install add-on scenery"
onClicked: {
var path = _addOns.installCustomScenery();
if (path != "") {
// insert into scenery paths if not already present
for (var p in _addOns.sceneryPaths) {
if (p === path)
return; // found, we are are done
}
// not found, add it
_addOns.sceneryPaths.push(path);
}
}
}
Text {
id: installTarballText
anchors {
left: installTarballButton.right
right: parent.right
leftMargin: Style.margin
}
wrapMode: Text.WordWrap
text: qsTr("If you have downloaded scenery manually from the official FlightGear website, " +
"you can use this button to extract and install it into a suitable folder. " +
"(Scenery downloaded this way should have a file name such as 'w40n020.tar.gz')"
)
}
} // of install-tarbal item
} // of column
}

View file

@ -184,7 +184,7 @@ Item
PropertyChanges {
target: aircraftList
model: _launcher.browseAircraftModel
header: _launcher.showNoOfficialHanger ? noDefaultCatalogHeader : ratingsHeader
header: _addOns.showNoOfficialHangar ? noDefaultCatalogHeader : ratingsHeader
}
}
]

View file

@ -7,14 +7,16 @@ Rectangle {
property string text
property string hoverText: ""
property bool enabled: true
property bool destructiveAction: false
readonly property string __baseColor: destructiveAction ? Style.destructiveActionColor : Style.themeColor
signal clicked
width: Style.strutSize * 2
width: Math.max(Style.strutSize * 2, buttonText.implicitWidth + radius * 2)
height: buttonText.implicitHeight + (radius * 2)
radius: Style.roundRadius
color: enabled ? (mouse.containsMouse ? Style.activeColor : Style.themeColor) : Style.disabledThemeColor
color: enabled ? (mouse.containsMouse ? Style.activeColor : __baseColor) : Style.disabledThemeColor
Text {
id: buttonText

View file

@ -2,18 +2,22 @@ import QtQuick 2.4
import "."
Text {
id: root
signal clicked();
property bool clickable: true
property color baseTextColor: Style.baseTextColor
color: mouse.containsMouse ? Style.themeColor : baseTextColor
MouseArea {
id: mouse
hoverEnabled: true
enabled: root.clickable
hoverEnabled: root.clickable
anchors.fill: parent
onClicked: parent.clicked();
cursorShape: Qt.PointingHandCursor
cursorShape: root.clickable ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}

View file

@ -0,0 +1,30 @@
import QtQuick 2.4
import "."
Item {
id: root
property bool enabled: true
signal clicked
width: height
height: icon.implicitHeight + 1
Image {
id: icon
x: 0
y: 0
source: "qrc:///cancel-icon"
}
MouseArea {
id: mouse
anchors.fill: parent
enabled: root.enabled
onClicked: {
root.clicked();
}
}
}

View file

@ -0,0 +1,37 @@
import QtQuick 2.4
Item {
width: height
height: icon.implicitHeight + 1
signal moveUp();
signal moveDown();
Image {
id: icon
x: 0
y: 0
source: "qrc:///reorder-list-icon"
}
MouseArea {
id: moveUpArea
height: parent.height / 2
width: parent.width
onClicked: {
parent.moveUp();
}
}
MouseArea {
id: moveDownArea
height: parent.height / 2
width: parent.width
anchors.bottom: parent.bottom
onClicked: {
parent.moveDown();
}
}
}

67
src/GUI/qml/LineEdit.qml Normal file
View file

@ -0,0 +1,67 @@
import QtQuick 2.4
import "."
FocusScope {
id: root
property string label
property string placeholder: ""
property alias validator: edit.validator
property alias text: edit.text
property alias suggestedWidthString: metrics.text
readonly property int suggestedWidth: useFullWidth ? root.width
: ((metrics.width == 0) ? Style.strutSize * 4
: metrics.width)
property bool useFullWidth: false
implicitHeight: editFrame.height
TextMetrics {
id: metrics
}
Text {
id: label
text: root.label
anchors.verticalCenter: editFrame.verticalCenter
color: editFrame.activeFocus ? Style.themeColor :
(root.enabled ? "black" : Style.inactiveThemeColor)
}
Rectangle {
id: editFrame
anchors.left: label.right
anchors.margins: Style.margin
height: edit.implicitHeight + Style.margin
width: Math.min(root.width - (label.width + Style.margin * 2), Math.max(suggestedWidth, edit.implicitWidth));
radius: Style.roundRadius
border.color: edit.activeFocus ? Style.frameColor : Style.minorFrameColor
border.width: 1
clip: true
TextInput {
id: edit
enabled: root.enabled
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Style.margin
selectByMouse: true
focus: true
Text {
id: placeholder
visible: (edit.text.length == 0) && !edit.activeFocus
text: root.placeholder
color: Style.baseTextColor
}
}
}
}

View file

@ -30,7 +30,9 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
onClicked: {
_launcher.officialCatalogAction("add-official");
console.warn("Implement me")
// _launcher.officialCatalogAction("add-official");
}
}
@ -40,7 +42,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
onClicked: {
_launcher.officialCatalogAction("hide");
_addOns.officialCatalogAction("hide");
}
}
}

View file

@ -0,0 +1,107 @@
import QtQuick 2.4
import FlightGear.Launcher 1.0
import "."
Item {
id: delegateRoot
signal performDelete();
signal performMove(var newIndex);
property alias deletePromptText: confirmDeletePath.promptText
height: childrenRect.height
Item {
id: divider
visible: model.index > 0 // divider before each item except the first
height: Style.margin
width: parent.width
Rectangle {
color: Style.frameColor
height: 1
width: parent.width - Style.strutSize
anchors.centerIn: parent
}
}
// this is the main visible content
Item {
id: contentRect
anchors.top: divider.bottom
// color: "#cfcfcf"
height: Math.max(label.implicitHeight, pathDeleteButton.height)
width: delegateRoot.width
MouseArea {
id: pathDelegateHover
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
// it's important that the button and text are children of the
// MouseArea, so nested containsMouse logic works
ClickableText {
id: label
text: modelData
onClicked: {
// open the location
_addOns.openDirectory(modelData)
}
anchors.left: parent.left
anchors.right: reorderButton.left
anchors.rightMargin: Style.margin
height: contentRect.height
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
DeleteButton {
id: pathDeleteButton
anchors.right: parent.right
visible: pathDelegateHover.containsMouse
onClicked: {
confirmDeletePath.visible = true
}
}
DragToReorderButton {
id: reorderButton
anchors.right: pathDeleteButton.left
anchors.rightMargin: Style.margin
visible: pathDelegateHover.containsMouse
onMoveUp: {
if (model.index === 0)
return;
delegateRoot.performMove(model.index - 1)
}
onMoveDown: {
delegateRoot.performMove(model.index + 1)
}
}
}
YesNoPanel {
id: confirmDeletePath
visible: false
anchors.fill: parent
yesText: qsTr("Remove")
noText: qsTr("Cancel")
yesIsDestructive: true
onAccepted: {
delegateRoot.performDelete()
}
onRejected: {
confirmDeletePath.visible = false
}
}
}
}

View file

@ -5,17 +5,11 @@ SettingControl {
id: root
implicitHeight: childrenRect.height
property string placeholder: ""
property alias placeholder: edit.placeholder
property alias validation: edit.validator
property alias value: edit.text
property alias suggestedWidthString: metrics.text
readonly property int suggestedWidth: useFullWidth ? root.width
: ((metrics.width == 0) ? Style.strutSize * 4
: metrics.width)
property bool useFullWidth: false
property alias suggestedWidthString: edit.suggestedWidthString
property alias useFullWidth: edit.useFullWidth
property string defaultValue: ""
function apply()
@ -25,57 +19,17 @@ SettingControl {
}
}
TextMetrics {
id: metrics
}
Text {
id: label
text: root.label
anchors.verticalCenter: editFrame.verticalCenter
color: editFrame.activeFocus ? Style.themeColor :
(root.enabled ? "black" : Style.inactiveThemeColor)
}
Rectangle {
id: editFrame
anchors.left: label.right
anchors.margins: Style.margin
height: edit.implicitHeight + Style.margin
width: Math.min(root.width - (label.width + Style.margin * 2), Math.max(suggestedWidth, edit.implicitWidth));
radius: Style.roundRadius
border.color: edit.activeFocus ? Style.frameColor : Style.minorFrameColor
border.width: 1
clip: true
TextInput {
id: edit
enabled: root.enabled
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Style.margin
selectByMouse: true
Text {
id: placeholder
visible: (edit.text.length == 0) && !edit.activeFocus
text: root.placeholder
color: Style.baseTextColor
}
}
LineEdit {
id: edit
label: root.label
width: parent.width
}
SettingDescription {
id: description
enabled: root.enabled
text: root.description
anchors.top: editFrame.bottom
anchors.top: edit.bottom
anchors.topMargin: Style.margin
width: parent.width
}

View file

@ -13,6 +13,8 @@ QtObject
readonly property string minorFrameColor: "#9f9f9f"
readonly property string themeColor: "#1b7ad3"
readonly property string destructiveActionColor: "#c62703"
readonly property string activeColor: Qt.darker(themeColor)
readonly property string inactiveThemeColor: "#9f9f9f"
@ -21,6 +23,7 @@ QtObject
readonly property string baseTextColor: "#3f3f3f"
readonly property int baseFontPixelSize: 12
readonly property int subHeadingFontPixelSize: 14
readonly property int headingFontPixelSize: 18
readonly property string disabledTextColor: "#5f5f5f"

View file

@ -0,0 +1,53 @@
import QtQuick 2.0
Rectangle {
property alias promptText:prompt.text
property alias yesText: yesButton.text
property alias noText: noButton.text
// change UI appearance if yes is a destructive action
property bool yesIsDestructive: false
signal accepted()
signal rejected()
Text {
id: prompt
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: yesButton.left
margins: Style.margin
}
height: parent.height
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
Button {
id: yesButton
anchors {
verticalCenter: parent.verticalCenter
right: noButton.left
rightMargin: Style.margin
}
destructiveAction: parent.yesIsDestructive
onClicked: parent.accepted()
}
Button {
id: noButton
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
rightMargin: Style.margin
}
onClicked: parent.rejected();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

View file

@ -73,6 +73,17 @@
<file>qml/SettingsDateTimePicker.qml</file>
<file>qml/Summary.qml</file>
<file>qml/HistoryPopup.qml</file>
<file>qml/AddOns.qml</file>
<file>qml/DragToReorderButton.qml</file>
<file>qml/AddButton.qml</file>
<file alias="cancel-icon">qml/icons8-cancel-50.png</file>
<file alias="reorder-list-icon">qml/icons8-drag-reorder-filled-50.png</file>
<file alias="add-icon">qml/icons8-plus-26.png</file>
<file>qml/DeleteButton.qml</file>
<file>qml/YesNoPanel.qml</file>
<file>qml/PathListDelegate.qml</file>
<file>qml/AddCatalogPanel.qml</file>
<file>qml/LineEdit.qml</file>
</qresource>
<qresource prefix="/preview">
<file alias="close-icon">preview-close.png</file>