diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cf9030b6..cab8fd26c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,9 +51,11 @@ set (PROJECT_NAME "WSJT-X") set (PROJECT_VENDOR "Joe Taylor, K1JT") set (PROJECT_CONTACT "Joe Taylor ") set (PROJECT_COPYRIGHT "Copyright (C) 2001-2015 by Joe Taylor, K1JT") -set (PROJECT_HOMEPAGE "http://www.physics.princeton.edu/pulsar/K1JT/wsjtx.html") +set (PROJECT_HOMEPAGE http://www.physics.princeton.edu/pulsar/K1JT/wsjtx.html) set (PROJECT_MANUAL wsjtx-main-${wsjtx_VERSION}.html) set (PROJECT_MANUAL_DIRECTORY_URL http://www.physics.princeton.edu/pulsar/K1JT/wsjtx-doc) +set (PROJECT_SAMPLES_URL http://downloads.sourceforge.net/project/wsjt/) +set (PROJECT_SAMPLES_UPLOAD_DEST frs.sourceforge.net:/home/frs/project/wsjt/) set (PROJECT_SUMMARY_DESCRIPTION "${PROJECT_NAME} - JT9 and JT65 Modes for LF, MF and HF Amateur Radio.") set (PROJECT_DESCRIPTION "${PROJECT_SUMMARY_DESCRIPTION} ${PROJECT_NAME} implements JT9, a new mode designed especially for the LF, MF, @@ -111,7 +113,6 @@ option (WSJT_TRACE_CAT_POLLS "Debugging option that turns on CAT diagnostics dur option (WSJT_HAMLIB_TRACE "Debugging option that turns on minimal Hamlib internal diagnostics.") option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON) option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.") -option (WSJT_EMBED_SAMPLES "Embed sample files into WSJT-X resources." ON) option (WSJT_GENERATE_DOCS "Generate documentation files." ON) CMAKE_DEPENDENT_OPTION (WSJT_HAMLIB_VERBOSE_TRACE "Debugging option that turns on full Hamlib internal diagnostics." OFF WSJT_HAMLIB_TRACE OFF) @@ -209,6 +210,11 @@ set (wsjt_qt_CXXSRCS MessageClient.cpp LettersSpinBox.cpp HelpTextWindow.cpp + SampleDownloader.cpp + SampleDownloader/DirectoryDelegate.cpp + SampleDownloader/Directory.cpp + SampleDownloader/FileNode.cpp + SampleDownloader/RemoteFile.cpp ) set (jt9_CXXSRCS @@ -533,11 +539,6 @@ set (PALETTE_FILES Palettes/ZL1FZ.pal ) -set (SAMPLE_FILES - samples/130418_1742.wav - samples/130610_2343.wav - ) - if (APPLE) set (WSJTX_ICON_FILE ${CMAKE_PROJECT_NAME}.icns) set (ICONSRCS @@ -618,6 +619,11 @@ endif (APPLE) # find_program(CTAGS ctags) find_program(ETAGS etags) + +# +# sub-directories +# +add_subdirectory (samples) if (WSJT_GENERATE_DOCS) add_subdirectory (doc) endif (WSJT_GENERATE_DOCS) @@ -855,9 +861,6 @@ endfunction (add_resources resources path) add_resources (wsjtx_RESOURCES "" ${TOP_LEVEL_RESOURCES}) add_resources (wsjtx_RESOURCES /Palettes ${PALETTE_FILES}) -if (WSJT_EMBED_SAMPLES) - add_resources (wsjtx_RESOURCES /samples ${SAMPLE_FILES}) -endif (WSJT_EMBED_SAMPLES) configure_file (wsjtx.qrc.in wsjtx.qrc @ONLY) diff --git a/SampleDownloader.cpp b/SampleDownloader.cpp new file mode 100644 index 000000000..b7c6686ac --- /dev/null +++ b/SampleDownloader.cpp @@ -0,0 +1,151 @@ +#include "SampleDownloader.hpp" + +#include +#include +#include + +#include "SettingsGroup.hpp" +#include "SampleDownloader/Directory.hpp" + +#include "pimpl_impl.hpp" + +#include "moc_SampleDownloader.cpp" + +namespace +{ + char const * const title = "Download Samples"; +} + +class SampleDownloader::impl final + : public QDialog +{ + Q_OBJECT + +public: + explicit impl (QSettings * settings, Configuration const *, QNetworkAccessManager *, QWidget * parent); + ~impl () {save_window_state ();} + + void refresh () + { + show (); + raise (); + activateWindow (); + directory_.refresh (); + } + +protected: + void closeEvent (QCloseEvent * e) override + { + save_window_state (); + QDialog::closeEvent (e); + } + +private: + void save_window_state () + { + SettingsGroup g (settings_, title); + settings_->setValue ("geometry", saveGeometry ()); + settings_->setValue ("SamplesURL", url_line_edit_.text ()); + } + + Q_SLOT void button_clicked (QAbstractButton *); + + QSettings * settings_; + Directory directory_; + QGridLayout main_layout_; + QVBoxLayout left_layout_; + QDialogButtonBox button_box_; + QWidget details_widget_; + QFormLayout details_layout_; + QLineEdit url_line_edit_; +}; + +#include "SampleDownloader.moc" + +SampleDownloader::SampleDownloader (QSettings * settings, Configuration const * configuration + , QNetworkAccessManager * network_manager, QWidget * parent) + : m_ {settings, configuration, network_manager, parent} +{ +} + +SampleDownloader::~SampleDownloader () +{ +} + +void SampleDownloader::show () +{ + m_->refresh (); +} + +SampleDownloader::impl::impl (QSettings * settings + , Configuration const * configuration + , QNetworkAccessManager * network_manager + , QWidget * parent) + : QDialog {parent, Qt::Window | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint} + , settings_ {settings} + , directory_ {configuration, network_manager} + , button_box_ {QDialogButtonBox::Close, Qt::Vertical} +{ + setWindowTitle (windowTitle () + ' ' + tr (title)); + resize (500, 600); + { + SettingsGroup g {settings_, title}; + restoreGeometry (settings_->value ("geometry", saveGeometry ()).toByteArray ()); + url_line_edit_.setText (settings_->value ("SamplesURL", PROJECT_SAMPLES_URL).toString ()); + directory_.url_root (url_line_edit_.text ()); + } + + setWindowTitle (QApplication::applicationName () + " - " + tr ("Download Samples")); + + button_box_.button (QDialogButtonBox::Close)->setDefault (true); + button_box_.addButton ("&Abort", QDialogButtonBox::DestructiveRole); + button_box_.addButton ("&Refresh", QDialogButtonBox::ResetRole); + left_layout_.addWidget (&directory_); + + auto details_button = button_box_.addButton ("&Details", QDialogButtonBox::HelpRole); + details_button->setCheckable (true); + details_widget_.hide (); + details_layout_.setMargin (0); + details_layout_.addRow ("Base URL for samples:", &url_line_edit_); + details_widget_.setLayout (&details_layout_); + + main_layout_.addLayout (&left_layout_, 0, 0); + main_layout_.addWidget (&button_box_, 0, 1); + main_layout_.addWidget (&details_widget_, 1, 0, 1, 2); + main_layout_.setRowStretch (1, 2); + setLayout (&main_layout_); + + connect (&button_box_, &QDialogButtonBox::clicked, this, &SampleDownloader::impl::button_clicked); + connect (details_button, &QAbstractButton::clicked, &details_widget_, &QWidget::setVisible); + connect (&url_line_edit_, &QLineEdit::editingFinished, [this] () { + if (directory_.url_root (url_line_edit_.text ())) + { + directory_.refresh (); + } + else + { + QMessageBox::warning (this, "Input Error", "Invalid URL format"); + } + }); +} + +void SampleDownloader::impl::button_clicked (QAbstractButton * button) +{ + switch (button_box_.buttonRole (button)) + { + case QDialogButtonBox::RejectRole: + hide (); + break; + + case QDialogButtonBox::DestructiveRole: + directory_.abort (); + break; + + case QDialogButtonBox::ResetRole: + directory_.refresh (); + break; + + default: + break; + } +} diff --git a/SampleDownloader.hpp b/SampleDownloader.hpp new file mode 100644 index 000000000..0dfa05be9 --- /dev/null +++ b/SampleDownloader.hpp @@ -0,0 +1,50 @@ +#ifndef SAMPLE_DOWNLOADER_HPP__ +#define SAMPLE_DOWNLOADER_HPP__ + +#include + +#include "pimpl_h.hpp" + +class QSettings; +class QWidget; +class QNetworkAccessManager; +class Configuration; + +// +// SampleDownloader - A Dialog to maintain sample files +// +// This uses a Qt Dialog window that contains a tree view of the +// available sample files on a web or ftp server. The files can be +// installed locally by ticking a check box or removed from the local +// machine by un-ticking the check boxes. +// +// The class requires a pointer to an open QSettings instance where it +// will save its persistent state, a pointer to a WSJT-X Configuration +// instance that is used to obtain configuration information like the +// current file save location and, a pointer to a +// QNetworkAccessManager instance which is used for network requests. +// +// An instance of SampleDownloader need not be destroyed after use, +// just call SampleDownloader::show() to make the dialog visible +// again. +// +class SampleDownloader final + : public QObject +{ + Q_OBJECT; + +public: + SampleDownloader (QSettings * settings + , Configuration const * + , QNetworkAccessManager * + , QWidget * parent = nullptr); + ~SampleDownloader (); + + Q_SLOT void show (); + +private: + class impl; + pimpl m_; +}; + +#endif diff --git a/SampleDownloader/Directory.cpp b/SampleDownloader/Directory.cpp new file mode 100644 index 000000000..da175c02d --- /dev/null +++ b/SampleDownloader/Directory.cpp @@ -0,0 +1,299 @@ +#include "Directory.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Configuration.hpp" +#include "DirectoryNode.hpp" +#include "FileNode.hpp" +#include "revision_utils.hpp" + +#include "moc_Directory.cpp" + +namespace +{ + char const * samples_dir_name = "samples"; + QString const contents_file_name = "contents_" + version (false) + ".json"; +} + +Directory::Directory (Configuration const * configuration + , QNetworkAccessManager * network_manager + , QWidget * parent) + : QTreeWidget {parent} + , configuration_ {configuration} + , network_manager_ {network_manager} + , root_dir_ {configuration_->save_directory ()} + , contents_ {this + , network_manager_ + , QDir {root_dir_.absoluteFilePath (samples_dir_name)}.absoluteFilePath (contents_file_name)} +{ + dir_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_DirClosedIcon), QIcon::Normal, QIcon::Off); + dir_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_DirOpenIcon), QIcon::Normal, QIcon::On); + file_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_FileIcon)); + + setColumnCount (2); + setHeaderLabels ({"File", "Progress"}); + header ()->setSectionResizeMode (QHeaderView::ResizeToContents); + setItemDelegate (&item_delegate_); + + connect (network_manager_, &QNetworkAccessManager::authenticationRequired + , this, &Directory::authentication); + connect (this, &Directory::itemChanged, [this] (QTreeWidgetItem * item) { + switch (item->type ()) + { + case FileNode::Type: + { + FileNode * node = static_cast (item); + if (!node->sync (node->checkState (0) == Qt::Checked)) + { + FileNode::sync_blocker b {node}; + node->setCheckState (0, node->checkState (0) == Qt::Checked ? Qt::Unchecked : Qt::Checked); + } + } + break; + + default: + break; + } + }); +} + +bool Directory::url_root (QUrl root) +{ + if (!root.path ().endsWith ('/')) + { + root.setPath (root.path () + '/'); + } + if (root.isValid ()) + { + url_root_ = root; + refresh (); + } + return root.isValid (); +} + +void Directory::error (QString const& title, QString const& message) +{ + QMessageBox::warning (this, title, message); +} + +bool Directory::refresh () +{ + abort (); + clear (); + // update locations + root_dir_ = configuration_->save_directory (); + QDir contents_dir {root_dir_.absoluteFilePath (samples_dir_name)}; + contents_.local_file_path (contents_dir.absoluteFilePath (contents_file_name)); + QUrl url {url_root_.resolved (QDir {root_dir_.relativeFilePath (samples_dir_name)}.filePath (contents_file_name))}; + if (url.isValid ()) + { + return contents_.sync (url, true, true); // attempt to fetch contents + } + else + { + QMessageBox::warning (this + , tr ("URL Error") + , tr ("Invalid URL:\n\"%1\"") + .arg (url.toDisplayString ())); + } + return false; +} + +void Directory::download_finished (bool success) +{ + if (success) + { + QFile contents {contents_.local_file_path ()}; + if (contents.open (QFile::ReadOnly | QFile::Text)) + { + QJsonParseError json_status; + auto content = QJsonDocument::fromJson (contents.readAll (), &json_status); + if (json_status.error) + { + QMessageBox::warning (this + , tr ("JSON Error") + , tr ("Contents file syntax error %1 at character offset %2") + .arg (json_status.errorString ()).arg (json_status.offset)); + return; + } + if (!content.isArray ()) + { + QMessageBox::warning (this, tr ("JSON Error") + , tr ("Contents file top level must be a JSON array")); + return; + } + QTreeWidgetItem * parent {invisibleRootItem ()}; + parent = new DirectoryNode {parent, samples_dir_name}; + parent->setIcon (0, dir_icon_); + parent->setExpanded (true); + parse_entries (content.array (), root_dir_.relativeFilePath (samples_dir_name), parent); + } + else + { + QMessageBox::warning (this, tr ("File System Error") + , tr ("Failed to open \"%1\"\nError: %2 - %3") + .arg (contents.fileName ()) + .arg (contents.error ()) + .arg (contents.errorString ())); + } + } +} + +void Directory::parse_entries (QJsonArray const& entries, QDir const& dir, QTreeWidgetItem * parent) +{ + if (dir.isRelative () && !dir.path ().startsWith ('.')) + { + for (auto const& value: entries) + { + if (value.isObject ()) + { + auto const& entry = value.toObject (); + auto const& name = entry["name"].toString (); + if (name.size () && !name.contains (QRegularExpression {R"([/:;])"})) + { + auto const& type = entry["type"].toString (); + if ("file" == type) + { + QUrl url {url_root_.resolved (dir.filePath (name))}; + if (url.isValid ()) + { + auto node = new FileNode {parent, network_manager_ + , QDir {root_dir_.filePath (dir.path ())}.absoluteFilePath (name) + , url}; + FileNode::sync_blocker b {node}; + node->setIcon (0, file_icon_); + node->setCheckState (0, node->local () ? Qt::Checked : Qt::Unchecked); + update (parent); + } + else + { + QMessageBox::warning (this + , tr ("URL Error") + , tr ("Invalid URL:\n\"%1\"") + .arg (url.toDisplayString ())); + } + } + else if ("directory" == type) + { + auto node = new DirectoryNode {parent, name}; + node->setIcon (0, dir_icon_); + auto const& entries = entry["entries"]; + if (entries.isArray ()) + { + parse_entries (entries.toArray () + , QDir {root_dir_.relativeFilePath (dir.path ())}.filePath (name) + , node); + } + else + { + QMessageBox::warning (this, tr ("JSON Error") + , tr ("Contents entries must be a JSON array")); + } + } + else + { + QMessageBox::warning (this, tr ("JSON Error") + , tr ("Contents entries must have a valid type")); + } + } + else + { + QMessageBox::warning (this, tr ("JSON Error") + , tr ("Contents entries must have a valid name")); + } + } + else + { + QMessageBox::warning (this, tr ("JSON Error") + , tr ("Contents entries must be JSON objects")); + } + } + } + else + { + QMessageBox::warning (this, tr ("JSON Error") + , tr ("Contents directories must be relative and within \"%1\"") + .arg (samples_dir_name)); + } +} + +void Directory::abort () +{ + QTreeWidgetItemIterator iter {this}; + while (*iter) + { + if ((*iter)->type () == FileNode::Type) + { + auto * node = static_cast (*iter); + node->abort (); + } + ++iter; + } +} + +void Directory::update (QTreeWidgetItem * item) +{ + // iterate the tree under item and accumulate the progress + if (item) + { + Q_ASSERT (item->type () == DirectoryNode::Type); + qint64 max {0}; + qint64 bytes {0}; + + // reset progress + item->setData (1, Qt::UserRole, max); + item->setData (1, Qt::DisplayRole, bytes); + int items {0}; + int counted {0}; + QTreeWidgetItemIterator iter {item}; + // iterate sub tree only + while (*iter && (*iter == item || (*iter)->parent () != item->parent ())) + { + if ((*iter)->type () == FileNode::Type) // only count files + { + ++items; + if (auto size = (*iter)->data (1, Qt::UserRole).toLongLong ()) + { + max += size; + ++counted; + } + bytes += (*iter)->data (1, Qt::DisplayRole).toLongLong (); + } + ++iter; + } + // estimate size of items not yet downloaded as average of + // those actually present + if (counted) + { + max += (items - counted) * max / counted; + } + // save as our progress + item->setData (1, Qt::UserRole, max); + item->setData (1, Qt::DisplayRole, bytes); + + // recurse up to top + update (item->parent ()); + } +} + +void Directory::authentication (QNetworkReply * /* reply */ + , QAuthenticator * /* authenticator */) +{ + QMessageBox::warning (this, "Network Error", "Authentication required"); +} diff --git a/SampleDownloader/Directory.hpp b/SampleDownloader/Directory.hpp new file mode 100644 index 000000000..3e933c9f3 --- /dev/null +++ b/SampleDownloader/Directory.hpp @@ -0,0 +1,59 @@ +#ifndef SAMPLE_DOWNLOADER_DIRECTORY_HPP__ +#define SAMPLE_DOWNLOADER_DIRECTORY_HPP__ + +#include +#include +#include +#include +#include +#include +#include + +#include "DirectoryDelegate.hpp" +#include "RemoteFile.hpp" + +class Configuration; +class QNetworkAccessManager; +class QTreeWidgetItem; +class QNetworkReply; +class QAuthenticator; +class QJsonArray; + +class Directory final + : public QTreeWidget + , protected RemoteFile::ListenerInterface +{ + Q_OBJECT + +public: + explicit Directory (Configuration const * configuration + , QNetworkAccessManager * network_manager + , QWidget * parent = nullptr); + + QSize sizeHint () const override {return {400, 500};} + + bool url_root (QUrl); + bool refresh (); + void abort (); + void update (QTreeWidgetItem * item); + +protected: + void error (QString const& title, QString const& message) override; + bool redirect_request (QUrl const&) override {return true;} // allow + void download_finished (bool success) override; + +private: + Q_SLOT void authentication (QNetworkReply *, QAuthenticator *); + void parse_entries (QJsonArray const& entries, QDir const& dir, QTreeWidgetItem * parent); + + Configuration const * configuration_; + QNetworkAccessManager * network_manager_; + QDir root_dir_; + QUrl url_root_; + RemoteFile contents_; + DirectoryDelegate item_delegate_; + QIcon dir_icon_; + QIcon file_icon_; +}; + +#endif diff --git a/SampleDownloader/DirectoryDelegate.cpp b/SampleDownloader/DirectoryDelegate.cpp new file mode 100644 index 000000000..f64e98703 --- /dev/null +++ b/SampleDownloader/DirectoryDelegate.cpp @@ -0,0 +1,44 @@ +#include "DirectoryDelegate.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +void DirectoryDelegate::paint (QPainter * painter, QStyleOptionViewItem const& option + , QModelIndex const& index) const +{ + if (1 == index.column ()) + { + QStyleOptionProgressBar progress_bar_option; + progress_bar_option.rect = option.rect; + progress_bar_option.state = QStyle::State_Enabled; + progress_bar_option.direction = QApplication::layoutDirection (); + progress_bar_option.fontMetrics = QApplication::fontMetrics (); + progress_bar_option.minimum = 0; + progress_bar_option.maximum = 100; + auto progress = index.data ().toLongLong (); + if (progress > 0) + { + auto percent = int (progress * 100 / index.data (Qt::UserRole).toLongLong ()); + progress_bar_option.progress = percent; + progress_bar_option.text = QString::number (percent) + '%'; + progress_bar_option.textVisible = true; + progress_bar_option.textAlignment = Qt::AlignCenter; + } + else + { + // not started + progress_bar_option.progress = -1; + } + QApplication::style ()->drawControl (QStyle::CE_ProgressBar, &progress_bar_option, painter); + } + else + { + QStyledItemDelegate::paint (painter, option, index); + } +} diff --git a/SampleDownloader/DirectoryDelegate.hpp b/SampleDownloader/DirectoryDelegate.hpp new file mode 100644 index 000000000..65dfd0016 --- /dev/null +++ b/SampleDownloader/DirectoryDelegate.hpp @@ -0,0 +1,30 @@ +#ifndef DIRECTORY_DELEGATE_HPP__ +#define DIRECTORY_DELEGATE_HPP__ + +#include + +class QObject; +class QStyleOptionVoew; +class QModelIndex; +class QPainter; + +// +// Styled item delegate that renders a progress bar in column #1 +// +// model column #1 DisplayRole is the progress in bytes +// model column #1 UserRole is the expected number of bytes +// +class DirectoryDelegate final + : public QStyledItemDelegate +{ +public: + explicit DirectoryDelegate (QObject * parent = nullptr) + : QStyledItemDelegate {parent} + { + } + + void paint (QPainter * painter, QStyleOptionViewItem const& option + , QModelIndex const& index) const override; +}; + +#endif diff --git a/SampleDownloader/DirectoryNode.hpp b/SampleDownloader/DirectoryNode.hpp new file mode 100644 index 000000000..5d26b7207 --- /dev/null +++ b/SampleDownloader/DirectoryNode.hpp @@ -0,0 +1,51 @@ +#ifndef DIRECTORY_NODE_HPP__ +#define DIRECTORY_NODE_HPP__ + +#include +#include + +// +// Tree widget item representing a file system directory. +// +// It renders the directory name in the first column and progress +// information in the 2nd column. The progress information consists of +// two 64 bit integer values, the 1st in the DisplayRole is the number +// of bytes received and the 2nd in the UserRole the total bytes +// expected. The progress information is not automatically +// maintained, see the Directory class for an example of how to +// dynamically maintain the DirectoryNode progress values. The 1st +// column also renders a tristate check box that controls the first +// column check boxes of child items. +// +class DirectoryNode final + : public QTreeWidgetItem +{ +public: + explicit DirectoryNode (QTreeWidgetItem * parent, QString const& name) + : QTreeWidgetItem {parent, Type} + { + setFlags (flags () | Qt::ItemIsUserCheckable | Qt::ItemIsTristate); + setText (0, name); + setCheckState (0, Qt::Unchecked); + + // initialize as empty, the owning QTreeWidget must maintain these + // progress values + setData (1, Qt::DisplayRole, 0ll); // progress in bytes + setData (1, Qt::UserRole, 0ll); // expected bytes + } + + bool operator == (QString const& name) const + { + return name == text (0); + } + + static int const Type {UserType}; +}; + +inline +bool operator == (QString const& lhs, DirectoryNode const& rhs) +{ + return rhs == lhs; +} + +#endif diff --git a/SampleDownloader/FileNode.cpp b/SampleDownloader/FileNode.cpp new file mode 100644 index 000000000..c165c5fb1 --- /dev/null +++ b/SampleDownloader/FileNode.cpp @@ -0,0 +1,73 @@ +#include "FileNode.hpp" + +#include +#include +#include +#include +#include + +#include "Directory.hpp" + +FileNode::FileNode (QTreeWidgetItem * parent + , QNetworkAccessManager * network_manager + , QString const& local_file_path + , QUrl const& url) + : QTreeWidgetItem {parent, Type} + , remote_file_ {this, network_manager, local_file_path} + , block_sync_ {false} +{ + sync_blocker b {this}; + setFlags (flags () | Qt::ItemIsUserCheckable); + setText (0, QFileInfo {local_file_path}.fileName ()); // display + setData (0, Qt::UserRole, url); + setData (0, Qt::UserRole + 1, local_file_path); // local absolute path + setCheckState (0, Qt::Unchecked); +} + +void FileNode::error (QString const& title, QString const& message) +{ + QMessageBox::warning (treeWidget (), title, message); +} + +bool FileNode::sync (bool local) +{ + if (block_sync_) + { + return true; + } + return remote_file_.sync (data (0, Qt::UserRole).toUrl (), local); +} + +void FileNode::download_progress (qint64 bytes_received, qint64 total_bytes) +{ + sync_blocker b {this}; + setData (1, Qt::UserRole, total_bytes); + if (bytes_received < 0) + { + setData (1, Qt::DisplayRole, 0ll); + setCheckState (0, Qt::Unchecked); + } + else + { + setData (1, Qt::DisplayRole, bytes_received); + } + static_cast (treeWidget ())->update (parent ()); +} + +void FileNode::download_finished (bool success) +{ + sync_blocker b {this}; + if (!success) + { + setData (1, Qt::UserRole, 0ll); + setData (1, Qt::DisplayRole, 0ll); + } + setCheckState (0, success ? Qt::Checked : Qt::Unchecked); + static_cast (treeWidget ())->update (parent ()); +} + +void FileNode::abort () +{ + sync_blocker b {this}; + remote_file_.abort (); +} diff --git a/SampleDownloader/FileNode.hpp b/SampleDownloader/FileNode.hpp new file mode 100644 index 000000000..c6046de72 --- /dev/null +++ b/SampleDownloader/FileNode.hpp @@ -0,0 +1,67 @@ +#ifndef FILE_NODE_HPP__ +#define FILE_NODE_HPP__ + +#include + +#include "RemoteFile.hpp" + +class QNetworkAccessManager; +class QString; +class QUrl; + +// +// A holder for a RemoteFile object linked to a QTreeWidget row. +// +// It renders the file name in first column and holds download +// progress data in the second column. The progress information is a +// 64 bit integer number of bytes in the DisplayRole and a total bytes +// expected in the UserRole. The first column also renders a check box +// that downloads the file when checked and removes the downloaded +// file when unchecked. The URL and local absolute file path are +// stored in the UserData and UserData+1 roles of the first column. +// +class FileNode final + : public QTreeWidgetItem + , protected RemoteFile::ListenerInterface +{ +public: + explicit FileNode (QTreeWidgetItem * parent + , QNetworkAccessManager * network_manager + , QString const& local_path + , QUrl const& url); + + bool local () const {return remote_file_.local ();} + bool sync (bool local); + void abort (); + + static int const Type {UserType + 1}; + + // + // Clients may use this RAII class to block nested calls to sync + // which may be troublesome, e.g. when UI updates cause recursion. + // + struct sync_blocker + { + sync_blocker (FileNode * node) : node_ {node} {node_->block_sync_ = true;} + sync_blocker (sync_blocker const&) = delete; + sync_blocker& operator = (sync_blocker const&) = delete; + ~sync_blocker () {node_->block_sync_ = false;} + private: + FileNode * node_; + }; + +protected: + void error (QString const& title, QString const& message) override; + bool redirect_request (QUrl const&) override {return true;} // allow + void download_progress (qint64 bytes_received, qint64 total_bytes) override; + void download_finished (bool success) override; + +private: + QNetworkAccessManager * network_manager_; + RemoteFile remote_file_; // active download + bool block_sync_; + + friend struct sync_blocker; +}; + +#endif diff --git a/SampleDownloader/README b/SampleDownloader/README new file mode 100644 index 000000000..d11bfc72f --- /dev/null +++ b/SampleDownloader/README @@ -0,0 +1,5 @@ +A UI for downloading sample files from a web server. + +Works in concert with samples/CMakeLists.txt which generates the JSON +contents description file and has a build target upload-samples that +uploads the samples and content file to the project files server. diff --git a/SampleDownloader/RemoteFile.cpp b/SampleDownloader/RemoteFile.cpp new file mode 100644 index 000000000..57d33463c --- /dev/null +++ b/SampleDownloader/RemoteFile.cpp @@ -0,0 +1,269 @@ +#include "RemoteFile.hpp" + +#include + +#include +#include +#include +#include + +#include "moc_RemoteFile.cpp" + +RemoteFile::RemoteFile (ListenerInterface * listener, QNetworkAccessManager * network_manager + , QString const& local_file_path, QObject * parent) + : QObject {parent} + , listener_ {listener} + , network_manager_ {network_manager} + , local_file_ {local_file_path} + , reply_ {nullptr} + , is_valid_ {false} + , redirect_count_ {0} + , file_ {local_file_path} +{ + local_file_.setCaching (false); +} + +void RemoteFile::local_file_path (QString const& name) +{ + QFileInfo new_file {name}; + new_file.setCaching (false); + if (new_file != local_file_) + { + if (local_file_.exists ()) + { + QFile file {local_file_.absoluteFilePath ()}; + if (!file.rename (new_file.absoluteFilePath ())) + { + listener_->error (tr ("File System Error") + , tr ("Cannot rename file:\n\"%1\"\nto: \"%2\"\nError(%3): %4") + .arg (file.fileName ()) + .arg (new_file.absoluteFilePath ()) + .arg (file.error ()) + .arg (file.errorString ())); + } + } + std::swap (local_file_, new_file); + } +} + +bool RemoteFile::local () const +{ + auto is_local = (reply_ && !reply_->isFinished ()) || local_file_.exists (); + if (is_local) + { + auto size = local_file_.size (); + listener_->download_progress (size, size); + listener_->download_finished (true); + } + else + { + listener_->download_progress (-1, 0); + } + return is_local; +} + +bool RemoteFile::sync (QUrl const& url, bool local, bool force) +{ + if (local) + { + if (!reply_ || reply_->isFinished ()) // not active download + { + if (force || !local_file_.exists () || url != url_) + { + url_ = url; + redirect_count_ = 0; + Q_ASSERT (!is_valid_); + download (url_); + } + } + else + { + return false; + } + } + else + { + if (reply_ && reply_->isRunning ()) + { + reply_->abort (); + } + if (local_file_.exists ()) + { + auto path = local_file_.absoluteDir (); + if (path.remove (local_file_.fileName ())) + { + listener_->download_progress (-1, 0); + } + else + { + listener_->error (tr ("File System Error") + , tr ("Cannot delete file:\n\"%1\"") + .arg (local_file_.absoluteFilePath ())); + return false; + } + path.rmpath ("."); + } + } + return true; +} + +void RemoteFile::download (QUrl const& url) +{ + QNetworkRequest request {url}; + request.setRawHeader ("User-Agent", "WSJT Sample Downloader"); + request.setOriginatingObject (this); + + // this blocks for a second or two the first time it is used on + // Windows - annoying + if (!is_valid_) + { + reply_ = network_manager_->head (request); + } + else + { + reply_ = network_manager_->get (request); + } + + connect (reply_, &QNetworkReply::finished, this, &RemoteFile::reply_finished); + connect (reply_, &QNetworkReply::readyRead, this, &RemoteFile::store); + connect (reply_, &QNetworkReply::downloadProgress + , [this] (qint64 bytes_received, qint64 total_bytes) { + // report progress of wanted file + if (is_valid_) + { + listener_->download_progress (bytes_received, total_bytes); + } + }); + connect (reply_, &QNetworkReply::sslErrors, [this] (QList const& errors) { + QString message; + for (auto const& error: errors) + { + message += '\n' + reply_->request ().url ().toDisplayString () + ": " + + error.errorString (); + } + listener_->error ("Network SSL Errors", message); + }); +} + +void RemoteFile::abort () +{ + if (reply_ && reply_->isRunning ()) + { + reply_->abort (); + } +} + +void RemoteFile::reply_finished () +{ + auto saved_reply = reply_; + auto redirect_url = reply_->attribute (QNetworkRequest::RedirectionTargetAttribute).toUrl (); + if (!redirect_url.isEmpty ()) + { + if (listener_->redirect_request (redirect_url)) + { + if (++redirect_count_ < 10) // maintain sanity + { + // follow redirect + download (reply_->url ().resolved (redirect_url)); + } + else + { + listener_->download_finished (false); + listener_->error (tr ("Network Error") + , tr ("Too many redirects: %1") + .arg (redirect_url.toDisplayString ())); + is_valid_ = false; // reset + } + } + else + { + listener_->download_finished (false); + listener_->error (tr ("Network Error") + , tr ("Redirect not followed: %1") + .arg (redirect_url.toDisplayString ())); + is_valid_ = false; // reset + } + } + else if (reply_->error () != QNetworkReply::NoError) + { + file_.cancelWriting (); + file_.commit (); + listener_->download_finished (false); + is_valid_ = false; // reset + // report errors that are not due to abort + if (QNetworkReply::OperationCanceledError != reply_->error ()) + { + listener_->error (tr ("Network Error"), reply_->errorString ()); + } + } + else + { + auto path = QFileInfo {file_.fileName ()}.absoluteDir (); + if (is_valid_ && !file_.commit ()) + { + listener_->error (tr ("File System Error") + , tr ("Cannot commit changes to:\n\"%1\"") + .arg (file_.fileName ())); + path.rmpath ("."); // tidy empty directories + listener_->download_finished (false); + is_valid_ = false; // reset + } + else + { + if (!is_valid_) + { + // now get the body content + is_valid_ = true; + download (reply_->url () .resolved (redirect_url)); + } + else + { + listener_->download_finished (true); + is_valid_ = false; // reset + } + } + } + if (reply_->isFinished ()) reply_ = nullptr; + disconnect (saved_reply); + saved_reply->deleteLater (); // finished with QNetworkReply +} + +void RemoteFile::store () +{ + if (is_valid_) + { + if (!file_.isOpen ()) + { + // create temporary file in the final location + auto path = QFileInfo {file_.fileName ()}.absoluteDir (); + if (path.mkpath (".")) + { + if (!file_.open (QSaveFile::WriteOnly)) + { + abort (); + listener_->error (tr ("File System Error") + , tr ("Cannot open file:\n\"%1\"\nError(%2): %3") + .arg (path.path ()) + .arg (file_.error ()) + .arg (file_.errorString ())); + } + } + else + { + abort (); + listener_->error (tr ("File System Error") + , tr ("Cannot make path:\n\"%1\"") + .arg (path.path ())); + } + } + if (file_.write (reply_->read (reply_->bytesAvailable ())) < 0) + { + abort (); + listener_->error (tr ("File System Error") + , tr ("Cannot write to file:\n\"%1\"\nError(%2): %3") + .arg (file_.fileName ()) + .arg (file_.error ()) + .arg (file_.errorString ())); + } + } +} diff --git a/SampleDownloader/RemoteFile.hpp b/SampleDownloader/RemoteFile.hpp new file mode 100644 index 000000000..e1f0dbddd --- /dev/null +++ b/SampleDownloader/RemoteFile.hpp @@ -0,0 +1,78 @@ +#ifndef REMOTE_FILE_HPP__ +#define REMOTE_FILE_HPP__ + +#include +#include +#include +#include +#include + +class QNetworkAccessManager; +class QNetworkReply; + +// +// Synchronize an individual file specified by a URL to the local file +// system +// +class RemoteFile final + : public QObject +{ + Q_OBJECT + +public: + // + // Clients of RemoteFile must provide an instance of this + // interface. It may be used to receive information and requests + // from the RemoteFile instance as it does its work. + // + class ListenerInterface + { + protected: + ListenerInterface () {} + + public: + virtual void error (QString const& title, QString const& message) = 0; + virtual bool redirect_request (QUrl const&) {return false;} // disallow + virtual void download_progress (qint64 /* bytes_received */, qint64 /* total_bytes */) {} + virtual void download_finished (bool /* success */) {} + }; + + explicit RemoteFile (ListenerInterface * listener, QNetworkAccessManager * network_manager + , QString const& local_file_path, QObject * parent = nullptr); + + // true if local file exists or will do very soon + bool local () const; + + // download/remove the local file + bool sync (QUrl const& url, bool local = true, bool force = false); + + // abort an active download + void abort (); + + // change the local location, this will rename if the file exists locally + void local_file_path (QString const&); + + QString local_file_path () const {return local_file_.absoluteFilePath ();} + QUrl url () const {return url_;} + +private: + void download (QUrl const& url); + void reply_finished (); + + Q_SLOT void store (); + + Q_SIGNAL void redirect (QUrl const&, unsigned redirect_count); + Q_SIGNAL void downloadProgress (qint64 bytes_received, qint64 total_bytes); + Q_SIGNAL void finished (); + + ListenerInterface * listener_; + QNetworkAccessManager * network_manager_; + QFileInfo local_file_; + QUrl url_; + QNetworkReply * reply_; + bool is_valid_; + unsigned redirect_count_; + QSaveFile file_; +}; + +#endif diff --git a/main.cpp b/main.cpp index 94d71134f..752176d4b 100644 --- a/main.cpp +++ b/main.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -206,7 +207,7 @@ int main(int argc, char *argv[]) ).toBool () ? 1u : 4u; } - MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor); + MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor, new QNetworkAccessManager {&a}); w.show(); QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit())); diff --git a/mainwindow.cpp b/mainwindow.cpp index de42f0cb0..3cc0b4dc9 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -47,6 +47,7 @@ #include "wsprnet.h" #include "signalmeter.h" #include "HelpTextWindow.hpp" +#include "SampleDownloader.hpp" #include "ui_mainwindow.h" #include "moc_mainwindow.cpp" @@ -130,7 +131,8 @@ namespace //--------------------------------------------------- MainWindow constructor MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdmem, - unsigned downSampleFactor, QWidget *parent) : + unsigned downSampleFactor, QNetworkAccessManager * network_manager, + QWidget *parent) : QMainWindow(parent), m_dataDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}, m_revision {revision ()}, @@ -327,6 +329,14 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme ui->actionInclude_averaging->setActionGroup(DepthGroup); ui->actionInclude_correlation->setActionGroup(DepthGroup); + connect (ui->download_samples_action, &QAction::triggered, [this, network_manager] () { + if (!m_sampleDownloader) + { + m_sampleDownloader.reset (new SampleDownloader {m_settings, &m_config, network_manager, this}); + } + m_sampleDownloader->show (); + }); + QButtonGroup* txMsgButtonGroup = new QButtonGroup; txMsgButtonGroup->addButton(ui->txrb1,1); txMsgButtonGroup->addButton(ui->txrb2,2); @@ -669,7 +679,7 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme progressBar->setMaximum(m_TRperiod); m_modulator->setPeriod(m_TRperiod); // TODO - not thread safe m_dialFreqRxWSPR=0; - wsprNet = new WSPRNet(this); + wsprNet = new WSPRNet(network_manager, this); connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString))); if(m_bFastMode) { int ntr[]={5,10,15,30}; diff --git a/mainwindow.h b/mainwindow.h index 189c05a19..e2b52eafd 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -49,6 +49,7 @@ namespace Ui { } class QSettings; +class QNetworkAccessManager; class QLineEdit; class QFont; class QHostInfo; @@ -68,6 +69,7 @@ class SoundOutput; class Modulator; class SoundInput; class Detector; +class SampleDownloader; class MainWindow : public QMainWindow { @@ -79,7 +81,8 @@ public: // Multiple instances: call MainWindow() with *thekey explicit MainWindow(bool multiple, QSettings *, QSharedMemory *shdmem, - unsigned downSampleFactor, QWidget *parent = 0); + unsigned downSampleFactor, QNetworkAccessManager * network_manager, + QWidget *parent = 0); ~MainWindow(); public slots: @@ -283,6 +286,7 @@ private: WSPRBandHopping m_WSPR_band_hopping; bool m_WSPR_tx_next; QMessageBox m_rigErrorMessageBox; + QScopedPointer m_sampleDownloader; QScopedPointer m_wideGraph; QScopedPointer m_echoGraph; diff --git a/mainwindow.ui b/mainwindow.ui index 4452526a1..12d4da251 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -2351,6 +2351,7 @@ QPushButton[state="ok"] { + @@ -2816,6 +2817,14 @@ QPushButton[state="ok"] { JTMSK + + + &Download Samples ... + + + <html><head/><body><p>Download sample audio files demonstrating the various modes.</p></body></html> + + diff --git a/qt_helpers.hpp b/qt_helpers.hpp index ec417dc3c..847731ce2 100644 --- a/qt_helpers.hpp +++ b/qt_helpers.hpp @@ -76,6 +76,21 @@ QString font_as_stylesheet (QFont const&); // conditional style sheet updates void update_dynamic_property (QWidget *, char const * property, QVariant const& value); +template +class VPtr +{ +public: + static T * asPtr (QVariant v) + { + return reinterpret_cast (v.value ()); + } + + static QVariant asQVariant(T * ptr) + { + return qVariantFromValue (reinterpret_cast (ptr)); + } +}; + // Register some useful Qt types with QMetaType Q_DECLARE_METATYPE (QHostAddress); diff --git a/revision_utils.cpp b/revision_utils.cpp index fe31beb7b..81b12a6ca 100644 --- a/revision_utils.cpp +++ b/revision_utils.cpp @@ -63,13 +63,18 @@ QString revision (QString const& svn_rev_string) return result.trimmed (); } -QString version () +QString version (bool include_patch) { #if defined (CMAKE_BUILD) - QString v {WSJTX_STRINGIZE (WSJTX_VERSION_MAJOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_MINOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_PATCH)}; + QString v {WSJTX_STRINGIZE (WSJTX_VERSION_MAJOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_MINOR)}; + if (include_patch) + { + v += "." WSJTX_STRINGIZE (WSJTX_VERSION_PATCH) # if defined (WSJTX_RC) - v += "-rc" WSJTX_STRINGIZE (WSJTX_RC); + + "-rc" WSJTX_STRINGIZE (WSJTX_RC) # endif + ; + } #else QString v {"Not for Release"}; #endif diff --git a/revision_utils.hpp b/revision_utils.hpp index 838c36e25..ba237255c 100644 --- a/revision_utils.hpp +++ b/revision_utils.hpp @@ -4,7 +4,7 @@ #include QString revision (QString const& svn_rev_string = QString {}); -QString version (); +QString version (bool include_patch = true); QString program_title (QString const& revision = QString {}); #endif diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt new file mode 100644 index 000000000..b658e9e74 --- /dev/null +++ b/samples/CMakeLists.txt @@ -0,0 +1,122 @@ +set (SAMPLE_FILES + JT9+JT65/130610_2343.wav + JT9/130418_1742.wav + ) + +#set_directory_properties (PROPERTIES EXCLUDE_FROM_ALL ON) + +set (contents_file_ ${CMAKE_CURRENT_BINARY_DIR}/contents_${WSJTX_VERSION_MAJOR}.${WSJTX_VERSION_MINOR}.json) + +function (indent_) + foreach (temp_ RANGE ${level_}) + file (APPEND ${contents_file_} " ") + endforeach () +endfunction () + +function (end_entry_) + file (APPEND ${contents_file_} "\n") + set(first_ 0 PARENT_SCOPE) + math (EXPR level_ "${level_} - 1") + indent_ () + file (APPEND ${contents_file_} "]\n") + math (EXPR level_ "${level_} - 2") + indent_ () + file (APPEND ${contents_file_} "}") + string (FIND "${dirs_}" "${cwd_}" pos_) + set (level_ ${level_} PARENT_SCOPE) +endfunction () + +file (WRITE ${contents_file_} "[") + +set (cwd_) +set (level_ 0) +set (first_ 1) +list (SORT SAMPLE_FILES) +foreach (sample_ IN LISTS SAMPLE_FILES) + string (REGEX MATCHALL "[^/]*/" dirs_ "${sample_}") + string (REPLACE "/" "" dirs_ "${dirs_}") + string (REGEX MATCH "[^/]*$" name_ "${sample_}") + string (FIND "${dirs_}" "${cwd_}" pos_) + list (LENGTH cwd_ cwd_count_) + if (${pos_} EQUAL 0) + # same root + while (${cwd_count_} GREATER 0) + list (REMOVE_AT dirs_ 0) + math (EXPR cwd_count_ "${cwd_count_} - 1") + endwhile () + else () + # reduce cwd_ until matched + while ((NOT ${pos_} EQUAL 0) AND ${cwd_count_} GREATER 0) + math (EXPR cwd_count_ "${cwd_count_} - 1") + list (REMOVE_AT cwd_ ${cwd_count_}) + end_entry_ () + endwhile () + # back to same root + while (${cwd_count_} GREATER 0) + list (REMOVE_AT dirs_ 0) + math (EXPR cwd_count_ "${cwd_count_} - 1") + endwhile () + endif () + list (LENGTH cwd_ cwd_count_) + list (LENGTH dirs_ path_count_) + while (${path_count_} GREATER 0) + list (GET dirs_ 0 dir_) + list (APPEND cwd_ "${dir_}") + list (REMOVE_AT dirs_ 0) + if (${first_}) + file (APPEND ${contents_file_} "\n") + set (first 0) + else () + file (APPEND ${contents_file_} ",\n") + endif () + indent_ () + file (APPEND ${contents_file_} "{\n") + math (EXPR level_ "${level_} + 1") + indent_ () + file (APPEND ${contents_file_} "\"type\": \"directory\",\n") + indent_ () + file (APPEND ${contents_file_} "\"name\": \"${dir_}\",\n") + indent_ () + file (APPEND ${contents_file_} "\"entries\": [") + set (first_ 1) + math (EXPR level_ "${level_} + 2") + math (EXPR path_count_ "${path_count_} - 1") + endwhile () + file (COPY ${sample_} DESTINATION web/samples/${cwd_}) + if (${first_}) + file (APPEND ${contents_file_} "\n") + set (first 0) + else () + file (APPEND ${contents_file_} ",\n") + endif () + indent_ () + file (APPEND ${contents_file_} "{\n") + math (EXPR level_ "${level_} + 1") + indent_ () + file (APPEND ${contents_file_} "\"type\": \"file\",\n") + indent_ () + file (APPEND ${contents_file_} "\"name\": \"${name_}\"\n") + math (EXPR level_ "${level_} - 1") + indent_ () + file (APPEND ${contents_file_} "}") + set (first_ 0) +endforeach () +if (${level_} GREATER 1) + end_entry_ () +endif () + +file (APPEND ${contents_file_} "\n]\n") + +file (COPY ${contents_file_} DESTINATION web/samples) + +find_program (RSYNC_EXECUTABLE rsync) +if (RSYNC_EXECUTABLE) + add_custom_command ( + OUTPUT upload.timestamp + COMMAND ${RSYNC_EXECUTABLE} ARGS -avz ${CMAKE_CURRENT_BINARY_DIR}/web/samples ${PROJECT_SAMPLES_UPLOAD_DEST} + COMMAND ${CMAKE_COMMAND} ARGS touch upload.timestamp + DEPENDS ${contents_file_} ${SAMPLE_FILES} + COMMENT "Uploading WSJT-X samples to web server" + ) + add_custom_target (upload-samples DEPENDS upload.timestamp) +endif () diff --git a/samples/130610_2343.wav b/samples/JT9+JT65/130610_2343.wav similarity index 100% rename from samples/130610_2343.wav rename to samples/JT9+JT65/130610_2343.wav diff --git a/samples/130418_1742.wav b/samples/JT9/130418_1742.wav similarity index 100% rename from samples/130418_1742.wav rename to samples/JT9/130418_1742.wav diff --git a/wsjtx_config.h.in b/wsjtx_config.h.in index 9d4ec1212..4fc873726 100644 --- a/wsjtx_config.h.in +++ b/wsjtx_config.h.in @@ -16,6 +16,7 @@ #cmakedefine WSJT_DATA_DESTINATION "@WSJT_DATA_DESTINATION@" #cmakedefine PROJECT_MANUAL "@PROJECT_MANUAL@" #cmakedefine PROJECT_MANUAL_DIRECTORY_URL "@PROJECT_MANUAL_DIRECTORY_URL@" +#cmakedefine PROJECT_SAMPLES_URL "@PROJECT_SAMPLES_URL@" #cmakedefine01 WSJT_SHARED_RUNTIME #cmakedefine01 WSJT_QDEBUG_TO_FILE diff --git a/wsprnet.cpp b/wsprnet.cpp index 8a72965d3..ddbacb6d8 100644 --- a/wsprnet.cpp +++ b/wsprnet.cpp @@ -22,9 +22,9 @@ namespace // char const * const wsprNetUrl = "http://127.0.0.1/post?"; }; -WSPRNet::WSPRNet(QObject *parent) +WSPRNet::WSPRNet(QNetworkAccessManager * manager, QObject *parent) : QObject{parent} - , networkManager {new QNetworkAccessManager {this}} + , networkManager {manager} , uploadTimer {new QTimer {this}} , m_urlQueueSize {0} { @@ -74,33 +74,35 @@ void WSPRNet::upload(QString const& call, QString const& grid, QString const& rf void WSPRNet::networkReply(QNetworkReply *reply) { - if (QNetworkReply::NoError != reply->error ()) { - Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ())); - // not clearing queue or halting queuing as it may be a transient - // one off request error - } - else { - QString serverResponse = reply->readAll(); - if( m_uploadType == 2) { - if (!serverResponse.contains(QRegExp("spot\\(s\\) added"))) { - emit uploadStatus("Upload Failed"); - urlQueue.clear(); + // check if request was ours + if (m_outstandingRequests.removeOne (reply)) { + if (QNetworkReply::NoError != reply->error ()) { + Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ())); + // not clearing queue or halting queuing as it may be a transient + // one off request error + } + else { + QString serverResponse = reply->readAll(); + if( m_uploadType == 2) { + if (!serverResponse.contains(QRegExp("spot\\(s\\) added"))) { + emit uploadStatus("Upload Failed"); + urlQueue.clear(); + uploadTimer->stop(); + } + } + + if (urlQueue.isEmpty()) { + emit uploadStatus("done"); + QFile::remove(m_file); uploadTimer->stop(); } } - if (urlQueue.isEmpty()) { - emit uploadStatus("done"); - QFile::remove(m_file); - uploadTimer->stop(); - } + qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ()); + + // delete request object instance on return to the event loop otherwise it is leaked + reply->deleteLater (); } - - m_outstandingRequests.removeOne (reply); - qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ()); - - // delete request object instance on return to the event loop otherwise it is leaked - reply->deleteLater (); } bool WSPRNet::decodeLine(QString const& line, QHash &query) diff --git a/wsprnet.h b/wsprnet.h index 0494e73c6..d712d5273 100644 --- a/wsprnet.h +++ b/wsprnet.h @@ -16,7 +16,7 @@ class WSPRNet : public QObject Q_OBJECT; public: - explicit WSPRNet(QObject *parent = nullptr); + explicit WSPRNet(QNetworkAccessManager *, QObject *parent = nullptr); void upload(QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq, QString const& mode, QString const& tpct, QString const& dbm, QString const& version, QString const& fileName);