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