swift
installxswiftbuscomponent.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2017 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QDesktopServices>
7 #include <QDir>
8 #include <QFileDialog>
9 #include <QFileInfo>
10 #include <QMessageBox>
11 #include <QPointer>
12 #include <QStandardPaths>
13 #include <QTimer>
14 
15 #include "ui_installxswiftbuscomponent.h"
16 
17 #include "config/buildconfig.h"
18 #include "gui/guiapplication.h"
20 #include "misc/compressutils.h"
21 #include "misc/directoryutils.h"
22 #include "misc/fileutils.h"
23 #include "misc/logmessage.h"
25 
26 using namespace swift::config;
27 using namespace swift::misc;
28 using namespace swift::misc::db;
29 using namespace swift::misc::network;
30 using namespace swift::misc::simulation;
31 using namespace swift::misc::simulation::settings;
32 using namespace swift::misc::simulation::xplane;
33 
34 namespace swift::gui::components
35 {
36  CInstallXSwiftBusComponent::CInstallXSwiftBusComponent(QWidget *parent)
38  {
39  ui->setupUi(this);
40 
41  ui->le_XSwiftBusPluginDir->setText(this->getXPlanePluginDirectory());
42  ui->le_DownloadDir->setText(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
43  ui->cb_DownloadFile->setEnabled(false);
44 
45  connect(ui->tb_DialogInstallDir, &QPushButton::pressed, this,
46  &CInstallXSwiftBusComponent::selectPluginDirectory);
47  connect(ui->tb_DialogDownloadDir, &QPushButton::pressed, this,
48  &CInstallXSwiftBusComponent::selectDownloadDirectory);
49  connect(ui->pb_Download, &QPushButton::pressed, this,
50  &CInstallXSwiftBusComponent::triggerDownloadingOfXSwiftBusFile);
51  connect(ui->pb_OpenDownloadDir, &QPushButton::pressed, this, &CInstallXSwiftBusComponent::openDownloadDir);
52  connect(ui->pb_OpenInstallDir, &QPushButton::pressed, this, &CInstallXSwiftBusComponent::openInstallDir);
53 
54  // init upate info
55  this->updatesChanged();
56  }
57 
59 
60  void CInstallXSwiftBusComponent::setDefaultDownloadName(const QString &defaultDownload)
61  {
62  m_defaultDownloadName = defaultDownload;
63  this->updatesChanged();
64  }
65 
66  void CInstallXSwiftBusComponent::selectPluginDirectory()
67  {
68  QString xPlanePluginDir = CFileUtils::fixWindowsUncPath(ui->le_XSwiftBusPluginDir->text().trimmed());
69  xPlanePluginDir = QFileDialog::getExistingDirectory(parentWidget(), tr("Choose your X-Plane plugin directory"),
70  xPlanePluginDir, m_fileDialogOptions);
71 
72  if (xPlanePluginDir.isEmpty()) { return; } // canceled
73  if (!QDir(xPlanePluginDir).exists())
74  {
75  const CStatusMessage msg = CStatusMessage(this, CLogCategories::validation())
76  .warning(u"'%1' is not a valid X-Plane plugin directory")
77  << xPlanePluginDir;
78  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
79  return;
80  }
81  ui->le_XSwiftBusPluginDir->setText(xPlanePluginDir);
82  }
83 
84  void CInstallXSwiftBusComponent::selectDownloadDirectory()
85  {
86  QString downloadDir = CFileUtils::fixWindowsUncPath(ui->le_DownloadDir->text().trimmed());
87  downloadDir = QFileDialog::getExistingDirectory(parentWidget(), tr("Choose your X-Plane plugin directory"),
88  downloadDir, m_fileDialogOptions);
89 
90  if (downloadDir.isEmpty()) { return; } // canceled
91  if (!QDir(downloadDir).exists())
92  {
93  const CStatusMessage msg =
94  CStatusMessage(this, CLogCategories::validation()).warning(u"'%1' is not a valid download directory")
95  << downloadDir;
96  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
97  return;
98  }
99  ui->le_DownloadDir->setText(downloadDir);
100  }
101 
102  void CInstallXSwiftBusComponent::installXSwiftBus()
103  {
104  const CRemoteFile rf = this->getRemoteFileSelected();
105  const QString downloadFileName = CFileUtils::appendFilePathsAndFixUnc(this->downloadDir(), rf.getBaseName());
106  QPointer<CInstallXSwiftBusComponent> myself(this);
107  QFile downloadFile(downloadFileName);
108 
109  if (!downloadFile.exists())
110  {
111  const CStatusMessage msg =
112  CStatusMessage(this, CLogCategories::validation()).error(u"Cannot read downloaded file '%1'")
113  << downloadFileName;
114  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
115  return;
116  }
117 
118  const QString xSwiftBusDirectory = this->xSwiftBusDir();
119  if (xSwiftBusDirectory.isEmpty())
120  {
121  const CStatusMessage msg =
122  CStatusMessage(this, CLogCategories::validation()).error(u"No directory to install to'");
123  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
124  return;
125  }
126 
127  const QDir installDir(xSwiftBusDirectory);
128  if (!installDir.exists())
129  {
130  const CStatusMessage msg =
131  CStatusMessage(this, CLogCategories::validation()).error(u"Directory '%1' does not exist")
132  << xSwiftBusDirectory;
133  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
134  return;
135  }
136 
137  const QString destFileName = CFileUtils::appendFilePathsAndFixUnc(xSwiftBusDirectory, rf.getBaseName());
138  {
139  QFile destFile(destFileName);
140  if (destFile.exists())
141  {
142  const bool removed = destFile.remove();
143  if (!removed)
144  {
145  const CStatusMessage msg =
146  CStatusMessage(this, CLogCategories::validation()).error(u"Cannot remove '%1'") << destFileName;
147  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
148  return;
149  }
150  }
151  }
152 
153  const bool copied = QFile::copy(downloadFileName, destFileName);
154  if (!copied)
155  {
156  const CStatusMessage msg =
157  CStatusMessage(this, CLogCategories::validation()).error(u"Cannot copy '%1' to '%2'")
158  << downloadFileName << destFileName;
159  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
160  return;
161  }
162 
163  // we need to unzip the destination file
164  const QFileInfo destFile(destFileName);
165  if (!destFile.exists())
166  {
167  const CStatusMessage msg =
168  CStatusMessage(this, CLogCategories::validation()).error(u"xswiftbus file '%1' does not exist")
169  << destFileName;
170  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
171  return;
172  }
173 
174  // if possible we will unzip
175  QStringList stdOutAndError;
176  if (CCompressUtils::zip7Uncompress(destFile.absoluteFilePath(), xSwiftBusDirectory, &stdOutAndError))
177  {
178  // capture values by copy!
179  const CStatusMessage msg =
180  CStatusMessage(this, CLogCategories::validation()).info(u"Uncompressed xswiftbus in '%1'")
181  << xSwiftBusDirectory;
182  this->showOverlayMessagesWithConfirmation(msg, false, "Delete downloaded file?", [=] {
183  if (!myself) { return; }
184  QFile downloadFile(downloadFileName);
185  if (!downloadFile.exists()) { return; } // removed in meantime
186  const bool removed = downloadFile.remove();
187  Q_UNUSED(removed);
188  });
189  return;
190  }
191  else
192  {
193  const CStatusMessage msg =
194  CStatusMessage(this, CLogCategories::validation()).warning(u"Unzip failed: stdout '%1' stderr '%2'")
195  << safeAt(stdOutAndError, 0) << safeAt(stdOutAndError, 1);
196  this->showOverlayMessage(msg);
197  }
198 
200  const QMessageBox::StandardButton reply =
201  QMessageBox::question(this, "Install XSwiftXBus",
202  "You need to manually unzip xswiftbus into the plugins directory.\nIt needs to look "
203  "like 'plugin/xswiftbus'.\n\nOpen the archive?",
204  QMessageBox::Yes | QMessageBox::No);
205 
206  if (reply == QMessageBox::Yes) { QDesktopServices::openUrl(QUrl::fromLocalFile(destFile.absoluteFilePath())); }
207  }
208 
209  void CInstallXSwiftBusComponent::triggerDownloadingOfXSwiftBusFile()
210  {
211  using namespace std::chrono_literals;
212  if (!sGui || !sGui->hasWebDataServices() || sGui->isShuttingDown()) { return; }
213  const CRemoteFile rf = this->getRemoteFileSelected();
214  if (!rf.getBaseName().contains(CBuildConfig::getVersionString()))
215  {
216  const QMessageBox::StandardButton reply = QMessageBox::question(
217  this, "Download xswiftbus",
218  QStringLiteral(u"The xswiftbus versions seems to be for a different version\n"
219  u"Your version is '%1'. Use this version.\n\n"
220  u"If not available, you can try the version next to your version number.\n\n"
221  u"Continue with this version?")
222  .arg(CBuildConfig::getVersionString()),
223  QMessageBox::Yes | QMessageBox::No);
224  if (reply != QMessageBox::Yes) { return; }
225  }
226 
227  if (!this->existsDownloadDir())
228  {
229  const CStatusMessage msg =
230  CStatusMessage(this, CLogCategories::validation()).error(u"Invalid download directory");
231  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
232  return;
233  }
234 
235  const CUrl download = rf.getSmartUrl();
236  if (download.isEmpty())
237  {
238  const CStatusMessage msg =
239  CStatusMessage(this, CLogCategories::validation()).error(u"No download URL for file name '%1'")
240  << rf.getBaseNameAndSize();
241  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
242  return;
243  }
244 
245  const QString saveAsFile = CFileUtils::appendFilePathsAndFixUnc(ui->le_DownloadDir->text(), rf.getBaseName());
246  const QFile saveFile(saveAsFile);
247  if (saveFile.exists())
248  {
249  const QMessageBox::StandardButton reply = QMessageBox::question(
250  this, "The file already exists", "Do you want to use the existing '" + saveAsFile + "'?",
251  QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
252  if (reply == QMessageBox::Cancel) { return; }
253  if (reply == QMessageBox::Yes)
254  {
255  const CStatusMessage msg = CStatusMessage(this).info(u"Using existing file '%1'") << saveAsFile;
256  const QPointer<CInstallXSwiftBusComponent> guard(this);
257  QTimer::singleShot(100, this, [=] {
258  if (guard.isNull()) { return; }
259  this->downloadedXSwiftBusFile(msg);
260  });
261  return;
262  }
263  }
264 
265  const QNetworkReply *r = sGui->downloadFromNetwork(
266  download, saveAsFile, { this, &CInstallXSwiftBusComponent::downloadedXSwiftBusFile });
267  if (r)
268  {
269  CLogMessage(this).info(u"Triggered downloading of xswiftbus file from '%1'") << download.getHost();
270  this->showLoading(120s); // timeout in any case
271  }
272  else
273  {
274  const CStatusMessage msg =
275  CStatusMessage(this, CLogCategories::validation()).error(u"Starting download for '%1' failed")
276  << download.getFullUrl();
277  this->showOverlayMessage(msg, CInstallXSwiftBusComponent::OverlayMsgTimeout);
278  }
279  }
280 
281  void CInstallXSwiftBusComponent::downloadedXSwiftBusFile(const CStatusMessage &status)
282  {
283  this->hideLoading();
284  if (sGui && sGui->isShuttingDown()) { return; }
285  if (status.isWarningOrAbove())
286  {
287  this->showOverlayMessage(status);
288  return;
289  }
290  if (!this->existsXSwiftBusPluginDir())
291  {
292  const CStatusMessage msg = CStatusMessage(this).warning(u"No valid install directory, cannot continue.");
293  this->showOverlayMessage(msg);
294  return;
295  }
296 
297  static const QString confirm("Install in '%1'?");
298  this->showOverlayMessagesWithConfirmation(status, false, confirm.arg(ui->le_XSwiftBusPluginDir->text()), [=] {
299  QTimer::singleShot(0, this, &CInstallXSwiftBusComponent::installXSwiftBus);
300  });
301  }
302 
303  CRemoteFile CInstallXSwiftBusComponent::getRemoteFileSelected() const
304  {
305  const QString baseNameAndSize = ui->cb_DownloadFile->currentText();
306  const CUpdateInfo update = m_updates.get();
307  const CRemoteFileList remoteFiles = update.getArtifactsXSwiftBus().asRemoteFiles();
308  return remoteFiles.findFirstByMatchingBaseNameOrDefault(baseNameAndSize);
309  }
310 
311  QString CInstallXSwiftBusComponent::downloadDir() const
312  {
313  return CFileUtils::fixWindowsUncPath(ui->le_DownloadDir->text().trimmed());
314  }
315 
316  QString CInstallXSwiftBusComponent::xSwiftBusDir() const
317  {
318  return CFileUtils::fixWindowsUncPath(ui->le_XSwiftBusPluginDir->text().trimmed());
319  }
320 
321  bool CInstallXSwiftBusComponent::existsDownloadDir() const
322  {
323  const QDir dir(this->downloadDir());
324  return dir.exists() && dir.isReadable();
325  }
326 
327  bool CInstallXSwiftBusComponent::existsXSwiftBusPluginDir() const
328  {
329  const QDir dir(this->xSwiftBusDir());
330  return dir.exists() && dir.isReadable();
331  }
332 
333  QString CInstallXSwiftBusComponent::getXPlanePluginDirectory() const
334  {
335  const CXPlaneSimulatorSettings settings = m_simulatorSettings.getSettings(CSimulatorInfo::XPLANE);
336  return settings.getPluginDirOrDefault();
337  }
338 
339  void CInstallXSwiftBusComponent::updatesChanged()
340  {
341  const CUpdateInfo updateInfo = m_updates.get();
342  if (updateInfo.getArtifactsXSwiftBus().isEmpty()) { return; }
343  const CArtifactList artifacts =
345  if (artifacts.isEmpty()) { return; }
346 
347  const CRemoteFileList remoteFiles = artifacts.asRemoteFiles();
348  if (!remoteFiles.isEmpty())
349  {
350  const QStringList xSwiftBusFiles(remoteFiles.getBaseNamesPlusSize(false));
351  m_xSwiftBusArtifacts = artifacts;
352  ui->cb_DownloadFile->addItems(xSwiftBusFiles);
353 
354  // current text
355  QString current = xSwiftBusFiles.front(); // default latest first
356  if (m_defaultDownloadName.isEmpty())
357  {
358  const CRemoteFile rf =
359  remoteFiles.findFirstContainingNameOrDefault(CBuildConfig::getVersionString(), Qt::CaseInsensitive);
360  if (rf.hasName()) { current = rf.getBaseNameAndSize(); }
361  }
362  else
363  {
364  const CRemoteFile rf = remoteFiles.findFirstByMatchingBaseNameOrDefault(m_defaultDownloadName);
365  if (rf.hasName()) { current = rf.getBaseNameAndSize(); }
366  }
367 
368  ui->cb_DownloadFile->setCurrentText(current.isEmpty() ? remoteFiles.frontOrDefault().getBaseNameAndSize() :
369  current); // latest version
370  }
371  ui->cb_DownloadFile->setEnabled(!remoteFiles.isEmpty());
372  }
373 
374  void CInstallXSwiftBusComponent::openInstallDir()
375  {
376  if (!this->existsXSwiftBusPluginDir()) { return; }
377  const QString file = CFileUtils::fixWindowsUncPath(ui->le_XSwiftBusPluginDir->text());
378  QDesktopServices::openUrl(QUrl::fromLocalFile(file));
379  }
380 
381  void CInstallXSwiftBusComponent::openDownloadDir()
382  {
383  if (!this->existsDownloadDir()) { return; }
384  const QString file = CFileUtils::fixWindowsUncPath(ui->le_DownloadDir->text());
385  QDesktopServices::openUrl(QUrl::fromLocalFile(file));
386  }
387 
389 } // namespace swift::gui::components
QNetworkReply * downloadFromNetwork(const swift::misc::network::CUrl &url, const QString &saveAsFileName, const swift::misc::CSlot< void(const swift::misc::CStatusMessage &)> &callback, int maxRedirects=DefaultMaxRedirects)
Download file from network and store it as passed.
bool hasWebDataServices() const
Web data services available?
bool isShuttingDown() const
Is application shutting down?
Enable widget class for load indicator.
void showLoading(std::chrono::milliseconds timeout=std::chrono::milliseconds(0), bool processEvents=true)
Show load indicator.
void hideLoading()
Hide load indicator.
bool showOverlayMessage(const swift::misc::CStatusMessage &message, std::chrono::milliseconds timeout=std::chrono::milliseconds(0))
Show single message.
void showOverlayMessagesWithConfirmation(const swift::misc::CStatusMessageList &messages, bool appendOldMessages, const QString &confirmationMessage, std::function< void()> okLambda, QMessageBox::StandardButton defaultButton=QMessageBox::Cancel, std::chrono::milliseconds timeout=std::chrono::milliseconds(0))
Show multiple messages with confirmation bar.
Using this class provides a QFrame with the overlay functionality already integrated.
void setDefaultDownloadName(const QString &defaultDownload)
Set a default name for download.
T get() const
Get a copy of the current value.
Definition: valuecache.h:408
Class for emitting a log message.
Definition: logmessage.h:27
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
Derived & error(const char16_t(&format)[N])
Set the severity to error, providing a format string.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
const_reference frontOrDefault() const
Access the first element, or a default-initialized value if the sequence is empty.
Definition: sequence.h:239
bool isEmpty() const
Synonym for empty.
Definition: sequence.h:285
Streamable status message, e.g.
bool isWarningOrAbove() const
Warning or above.
CArtifactList findWithUnrestrictedDistributions() const
Find artifacts with public (unrestricted) distributions.
network::CRemoteFileList asRemoteFiles() const
As remote files.
Update info, i.e. artifacts and distributions.
Definition: updateinfo.h:24
CArtifactList getArtifactsXSwiftBusLatestVersionFirst() const
Artifacts (xswiftbus)
Definition: updateinfo.cpp:42
const CArtifactList & getArtifactsXSwiftBus() const
Artifacts (xswiftbus)
Definition: updateinfo.h:51
QString getBaseName() const
Name with directory stripped.
Definition: remotefile.h:55
CUrl getSmartUrl() const
Automatically concatenates the name if missing.
Definition: remotefile.cpp:39
bool hasName() const
Has name?
Definition: remotefile.h:58
QString getBaseNameAndSize() const
Name + human readable size.
Definition: remotefile.cpp:24
Value object encapsulating a list of remote files.
CRemoteFile findFirstContainingNameOrDefault(const QString &name, Qt::CaseSensitivity cs) const
First by name contained of default.
QStringList getBaseNamesPlusSize(bool sorted=true) const
All file names plus size.
CRemoteFile findFirstByMatchingBaseNameOrDefault(const QString &name) const
Find first matching name of default.
Value object encapsulating information of a location, kind of simplified CValueObject compliant versi...
Definition: url.h:27
bool isEmpty() const
Empty.
Definition: url.cpp:54
const QString & getHost() const
Get host.
Definition: url.h:55
QString getFullUrl(bool withQuery=true) const
Qualified name.
Definition: url.cpp:84
CSimulatorSettings getSettings(const CSimulatorInfo &simulator) const
Settings per simulator.
QString getPluginDirOrDefault() const
Plugin directory or default plugin dir.
SWIFT_GUI_EXPORT swift::gui::CGuiApplication * sGui
Single instance of GUI application object.
High level reusable GUI components.
Definition: aboutdialog.cpp:13
Free functions in swift::misc.
const QString & safeAt(const QStringList &stringList, int index)
Safe "at" function, returns empty string if index does not exists.
Definition: stringutils.h:160
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30