1 // CatalogListModel.cxx - part of GUI launcher using Qt5
2 //
3 // Written by James Turner, started March 2015.
4 //
5 // Copyright (C) 2015 James Turner <zakalawe@mac.com>
6 //
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 // General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
21 #include "CatalogListModel.hxx"
22
23 #include <QDebug>
24 #include <QUrl>
25 #include <QTimer>
26
27 // Simgear
28 #include <simgear/props/props_io.hxx>
29 #include <simgear/structure/exception.hxx>
30 #include <simgear/misc/sg_path.hxx>
31
32 #include <simgear/package/Package.hxx>
33 #include <simgear/package/Install.hxx>
34
35 // FlightGear
36 #include <Main/globals.hxx>
37 #include <Network/HTTPClient.hxx>
38 #include <Main/sentryIntegration.hxx>
39
40 using namespace simgear::pkg;
41
42 class CatalogDelegate : public simgear::pkg::Delegate
43 {
44 public:
CatalogDelegate(CatalogListModel * outer)45 CatalogDelegate(CatalogListModel* outer) : p(outer) {}
46
catalogRefreshed(CatalogRef catalog,StatusCode)47 void catalogRefreshed(CatalogRef catalog, StatusCode) override
48 {
49 p->onCatalogStatusChanged(catalog);
50 }
51
startInstall(InstallRef)52 void startInstall(InstallRef) override {}
installProgress(InstallRef,unsigned int,unsigned int)53 void installProgress(InstallRef, unsigned int, unsigned int) override {}
finishInstall(InstallRef,StatusCode)54 void finishInstall(InstallRef, StatusCode ) override {}
55 private:
56 CatalogListModel* p = nullptr;
57 };
58
59
CatalogListModel(QObject * pr,const simgear::pkg::RootRef & rootRef)60 CatalogListModel::CatalogListModel(QObject* pr, const
61 simgear::pkg::RootRef& rootRef) :
62 QAbstractListModel(pr),
63 m_packageRoot(rootRef)
64 {
65 m_delegate = new CatalogDelegate(this);
66 m_packageRoot->addDelegate(m_delegate);
67
68 resetData();
69 }
70
~CatalogListModel()71 CatalogListModel::~CatalogListModel()
72 {
73 m_packageRoot->removeDelegate(m_delegate);
74 }
75
resetData()76 void CatalogListModel::resetData()
77 {
78 CatalogList updatedCatalogs = m_packageRoot->allCatalogs();
79 std::sort(updatedCatalogs.begin(), updatedCatalogs.end(),
80 [](const CatalogRef& catA, const CatalogRef& catB)
81 { // lexicographic ordering
82 return catA->name() < catB->name();
83 });
84
85 if (updatedCatalogs == m_catalogs)
86 return;
87
88 beginResetModel();
89 m_catalogs = updatedCatalogs;
90 endResetModel();
91
92 emit catalogsChanged();
93 }
94
rowCount(const QModelIndex & parent) const95 int CatalogListModel::rowCount(const QModelIndex& parent) const
96 {
97 Q_UNUSED(parent)
98 return static_cast<int>(m_catalogs.size());
99 }
100
data(const QModelIndex & index,int role) const101 QVariant CatalogListModel::data(const QModelIndex& index, int role) const
102 {
103 const auto cat = m_catalogs.at(static_cast<size_t>(index.row()));
104 if (role == Qt::DisplayRole) {
105 QString name = QString::fromStdString(cat->name());
106 QString desc;
107 if (cat->isEnabled()) {
108 desc = QString::fromStdString(cat->description()).simplified();
109 } else {
110 switch (cat->status()) {
111 case Delegate::FAIL_NOT_FOUND:
112 desc = tr("The catalog data was not found on the server at the expected location (URL)");
113 break;
114 case Delegate::FAIL_VERSION:
115 desc = tr("The catalog is not compatible with the version of FlightGear");
116 break;
117 case Delegate::FAIL_HTTP_FORBIDDEN:
118 desc = tr("The catalog server is blocking access from some reason (forbidden)");
119 break;
120 default:
121 desc = tr("disabled due to an internal error");
122 }
123 }
124 return tr("%1 - %2").arg(name).arg(desc);
125 } else if (role == CatalogDescriptionRole) {
126 return QString::fromStdString(cat->description());
127 } else if (role == CatalogNameRole) {
128 return QString::fromStdString(cat->name());
129 } else if (role == Qt::ToolTipRole) {
130 return QString::fromStdString(cat->url());
131 } else if (role == CatalogUrlRole) {
132 return QUrl(QString::fromStdString(cat->url()));
133 } else if (role == CatalogIdRole) {
134 return QString::fromStdString(cat->id());
135 } else if (role == CatalogPackageCountRole) {
136 return static_cast<quint32>(cat->packages().size());
137 } else if (role == CatalogInstallCountRole) {
138 return static_cast<quint32>(cat->installedPackages().size());
139 } else if (role == CatalogStatusRole) {
140 return translateStatusForCatalog(cat);
141 } else if (role == CatalogIsNewlyAdded) {
142 return (cat == m_newlyAddedCatalog);
143 } else if (role == CatalogEnabled) {
144 return cat->isUserEnabled();
145 }
146
147 return QVariant();
148 }
149
setData(const QModelIndex & index,const QVariant & value,int role)150 bool CatalogListModel::setData(const QModelIndex &index, const QVariant &value, int role)
151 {
152 auto cat = m_catalogs.at(static_cast<size_t>(index.row()));
153 if (role == CatalogEnabled) {
154 cat->setUserEnabled(value.toBool());
155 return true;
156 }
157 return false;
158 }
159
flags(const QModelIndex & index) const160 Qt::ItemFlags CatalogListModel::flags(const QModelIndex &index) const
161 {
162 Qt::ItemFlags r = Qt::ItemIsSelectable;
163 const auto cat = m_catalogs.at(static_cast<size_t>(index.row()));
164 if (cat->isEnabled()) {
165 r |= Qt::ItemIsEnabled;
166 }
167 return r;
168 }
169
roleNames() const170 QHash<int, QByteArray> CatalogListModel::roleNames() const
171 {
172 QHash<int, QByteArray> result = QAbstractListModel::roleNames();
173 result[CatalogUrlRole] = "url";
174 result[CatalogIdRole] = "id";
175 result[CatalogDescriptionRole] = "description";
176 result[CatalogNameRole] = "name";
177 result[CatalogStatusRole] = "status";
178 result[CatalogIsNewlyAdded] = "isNewlyAdded";
179 result[CatalogEnabled] = "enabled";
180 return result;
181 }
182
removeCatalog(int index)183 void CatalogListModel::removeCatalog(int index)
184 {
185 if ((index < 0) || (index >= static_cast<int>(m_catalogs.size()))) {
186 return;
187 }
188
189 const std::string removeId = m_catalogs.at(static_cast<size_t>(index))->id();
190 m_packageRoot->removeCatalogById(removeId);
191 resetData();
192 }
193
refreshCatalog(int index)194 void CatalogListModel::refreshCatalog(int index)
195 {
196 if ((index < 0) || (index >= static_cast<int>(m_catalogs.size()))) {
197 return;
198 }
199 m_catalogs.at(static_cast<size_t>(index))->refresh();
200 }
201
installDefaultCatalog(bool showAddFeedback)202 void CatalogListModel::installDefaultCatalog(bool showAddFeedback)
203 {
204 FGHTTPClient* http = globals->get_subsystem<FGHTTPClient>();
205 CatalogRef cat = Catalog::createFromUrl(m_packageRoot, http->getDefaultCatalogUrl());
206 if (showAddFeedback) {
207 m_newlyAddedCatalog = cat;
208 emit isAddingCatalogChanged();
209 emit statusOfAddingCatalogChanged();
210 }
211
212 resetData();
213 }
214
addCatalogByUrl(QUrl url)215 void CatalogListModel::addCatalogByUrl(QUrl url)
216 {
217 if (m_newlyAddedCatalog) {
218 qWarning() << Q_FUNC_INFO << "already adding a catalog";
219 return;
220 }
221
222 m_newlyAddedCatalog = Catalog::createFromUrl(m_packageRoot, url.toString().toStdString());
223 flightgear::addSentryBreadcrumb("CatalogListModel: Adding catalog " + url.toString().toStdString(), "info");
224 resetData();
225 emit isAddingCatalogChanged();
226 }
227
indexOf(QUrl url)228 int CatalogListModel::indexOf(QUrl url)
229 {
230 std::string urlString = url.toString().toStdString();
231 auto it = std::find_if(m_catalogs.begin(), m_catalogs.end(),
232 [urlString](simgear::pkg::CatalogRef cat) { return cat->url() == urlString;});
233 if (it == m_catalogs.end())
234 return -1;
235
236 return static_cast<int>(std::distance(m_catalogs.begin(), it));
237 }
238
finalizeAddCatalog()239 void CatalogListModel::finalizeAddCatalog()
240 {
241 if (!m_newlyAddedCatalog) {
242 qWarning() << Q_FUNC_INFO << "no catalog add in progress";
243 return;
244 }
245
246 auto it = std::find(m_catalogs.begin(), m_catalogs.end(), m_newlyAddedCatalog);
247 if (it == m_catalogs.end()) {
248 qWarning() << Q_FUNC_INFO << "couldn't find new catalog in m_catalogs" << QString::fromStdString(m_newlyAddedCatalog->url());
249 return;
250 }
251
252 flightgear::addSentryBreadcrumb("CatalogListModel: finalziing add of:" + m_newlyAddedCatalog->id(), "info");
253
254 const int row = static_cast<int>(std::distance(m_catalogs.begin(), it));
255 m_newlyAddedCatalog.clear();
256 emit isAddingCatalogChanged();
257 emit statusOfAddingCatalogChanged();
258 emit dataChanged(index(row), index(row));
259 }
260
abandonAddCatalog()261 void CatalogListModel::abandonAddCatalog()
262 {
263 if (!m_newlyAddedCatalog)
264 return;
265
266 m_packageRoot->removeCatalog(m_newlyAddedCatalog);
267
268 m_newlyAddedCatalog.clear();
269 emit isAddingCatalogChanged();
270 emit statusOfAddingCatalogChanged();
271
272 resetData();
273 }
274
isAddingCatalog() const275 bool CatalogListModel::isAddingCatalog() const
276 {
277 return m_newlyAddedCatalog.get() != nullptr;
278 }
279
onCatalogStatusChanged(Catalog * cat)280 void CatalogListModel::onCatalogStatusChanged(Catalog* cat)
281 {
282 if (cat == nullptr) {
283 resetData();
284 return;
285 }
286
287 //qInfo() << Q_FUNC_INFO << "for" << QString::fromStdString(cat->url()) << translateStatusForCatalog(cat);
288
289 // download the official catalog often fails with a 404 due to how we
290 // compute the version-specific URL. This is the logic which bounces the UI
291 // to the fallback URL.
292 if (cat->status() == Delegate::FAIL_NOT_FOUND) {
293 FGHTTPClient* http = globals->get_subsystem<FGHTTPClient>();
294 if (cat->url() == http->getDefaultCatalogUrl()) {
295 cat->setUrl(http->getDefaultCatalogFallbackUrl());
296 cat->refresh(); // and trigger another refresh
297 return;
298 }
299 }
300
301 if (cat == m_newlyAddedCatalog) {
302 // defer this signal slightly so that QML calling finalizeAdd or
303 // abandonAdd in response, doesn't re-enter the package code
304 QTimer::singleShot(0, this, &CatalogListModel::statusOfAddingCatalogChanged);
305 return;
306 }
307
308 auto it = std::find(m_catalogs.begin(), m_catalogs.end(), cat);
309 if (it == m_catalogs.end())
310 return;
311
312 int row = std::distance(m_catalogs.begin(), it);
313 emit dataChanged(index(row), index(row));
314 }
315
translateStatusForCatalog(CatalogRef cat) const316 CatalogListModel::CatalogStatus CatalogListModel::translateStatusForCatalog(CatalogRef cat) const
317 {
318 switch (cat->status()) {
319 case Delegate::STATUS_SUCCESS:
320 case Delegate::STATUS_REFRESHED:
321 return Ok;
322
323 case Delegate::FAIL_DOWNLOAD: return NetworkError;
324 case Delegate::STATUS_IN_PROGRESS: return Refreshing;
325 case Delegate::FAIL_NOT_FOUND: return NotFoundOnServer;
326 case Delegate::FAIL_VERSION: return IncompatibleVersion;
327 case Delegate::FAIL_HTTP_FORBIDDEN: return HTTPForbidden;
328 case Delegate::FAIL_VALIDATION:
329 case Delegate::FAIL_EXTRACT:
330 return InvalidData;
331 default:
332 return UnknownError;
333 }
334 }
335
statusOfAddingCatalog() const336 CatalogListModel::CatalogStatus CatalogListModel::statusOfAddingCatalog() const
337 {
338 if (!m_newlyAddedCatalog.get()) {
339 return NoAddInProgress;
340 }
341
342 return translateStatusForCatalog(m_newlyAddedCatalog);
343 }
344