mirror of
				https://github.com/saitohirga/WSJT-X.git
				synced 2025-10-30 20:40:28 -04:00 
			
		
		
		
	Add a sample download dialog and upload sub-system
Samples are downloaded from a web server, currently the SF download server. The samples are stored in the source controlled samples directory and the CMake script there builds a suitable directory tree for upload to the web server under samples/web containing the samples hierarchy and the generated JSON contents database file. The samples CMake script also defines an 'upload-samples' target that uses rsync to efficiently upload the samples and the accompanying contents JSON database file. Any directory structure under the samples directory may be created, to add a new sample file simply add the file to source control and amend the list of sample files (SAMPLE_FILES) in samples/CMakeLists.txt. git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@6308 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
This commit is contained in:
		
							parent
							
								
									0775acf236
								
							
						
					
					
						commit
						0efe9231bb
					
				| @ -51,9 +51,11 @@ set (PROJECT_NAME "WSJT-X") | |||||||
| set (PROJECT_VENDOR "Joe Taylor, K1JT") | set (PROJECT_VENDOR "Joe Taylor, K1JT") | ||||||
| set (PROJECT_CONTACT "Joe Taylor <k1jt@arrl.net>") | set (PROJECT_CONTACT "Joe Taylor <k1jt@arrl.net>") | ||||||
| set (PROJECT_COPYRIGHT "Copyright (C) 2001-2015 by Joe Taylor, K1JT") | 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 wsjtx-main-${wsjtx_VERSION}.html) | ||||||
| set (PROJECT_MANUAL_DIRECTORY_URL http://www.physics.princeton.edu/pulsar/K1JT/wsjtx-doc) | 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_SUMMARY_DESCRIPTION "${PROJECT_NAME} - JT9 and JT65 Modes for LF, MF and HF Amateur Radio.") | ||||||
| set (PROJECT_DESCRIPTION "${PROJECT_SUMMARY_DESCRIPTION} | set (PROJECT_DESCRIPTION "${PROJECT_SUMMARY_DESCRIPTION} | ||||||
|  ${PROJECT_NAME} implements JT9, a new mode designed especially for the LF, MF, |  ${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_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_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON) | ||||||
| option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.") | 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) | 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) | 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 |   MessageClient.cpp | ||||||
|   LettersSpinBox.cpp |   LettersSpinBox.cpp | ||||||
|   HelpTextWindow.cpp |   HelpTextWindow.cpp | ||||||
|  |   SampleDownloader.cpp | ||||||
|  |   SampleDownloader/DirectoryDelegate.cpp | ||||||
|  |   SampleDownloader/Directory.cpp | ||||||
|  |   SampleDownloader/FileNode.cpp | ||||||
|  |   SampleDownloader/RemoteFile.cpp | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
| set (jt9_CXXSRCS | set (jt9_CXXSRCS | ||||||
| @ -533,11 +539,6 @@ set (PALETTE_FILES | |||||||
|   Palettes/ZL1FZ.pal |   Palettes/ZL1FZ.pal | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| set (SAMPLE_FILES |  | ||||||
|   samples/130418_1742.wav |  | ||||||
|   samples/130610_2343.wav |  | ||||||
|   ) |  | ||||||
| 
 |  | ||||||
| if (APPLE) | if (APPLE) | ||||||
|   set (WSJTX_ICON_FILE ${CMAKE_PROJECT_NAME}.icns) |   set (WSJTX_ICON_FILE ${CMAKE_PROJECT_NAME}.icns) | ||||||
|   set (ICONSRCS |   set (ICONSRCS | ||||||
| @ -618,6 +619,11 @@ endif (APPLE) | |||||||
| # | # | ||||||
| find_program(CTAGS ctags) | find_program(CTAGS ctags) | ||||||
| find_program(ETAGS etags) | find_program(ETAGS etags) | ||||||
|  | 
 | ||||||
|  | # | ||||||
|  | # sub-directories | ||||||
|  | # | ||||||
|  | add_subdirectory (samples) | ||||||
| if (WSJT_GENERATE_DOCS) | if (WSJT_GENERATE_DOCS) | ||||||
|   add_subdirectory (doc) |   add_subdirectory (doc) | ||||||
| endif (WSJT_GENERATE_DOCS) | endif (WSJT_GENERATE_DOCS) | ||||||
| @ -855,9 +861,6 @@ endfunction (add_resources resources path) | |||||||
| 
 | 
 | ||||||
| add_resources (wsjtx_RESOURCES "" ${TOP_LEVEL_RESOURCES}) | add_resources (wsjtx_RESOURCES "" ${TOP_LEVEL_RESOURCES}) | ||||||
| add_resources (wsjtx_RESOURCES /Palettes ${PALETTE_FILES}) | 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) | configure_file (wsjtx.qrc.in wsjtx.qrc @ONLY) | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										151
									
								
								SampleDownloader.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								SampleDownloader.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,151 @@ | |||||||
|  | #include "SampleDownloader.hpp" | ||||||
|  | 
 | ||||||
|  | #include <QString> | ||||||
|  | #include <QSettings> | ||||||
|  | #include <QtWidgets> | ||||||
|  | 
 | ||||||
|  | #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; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								SampleDownloader.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								SampleDownloader.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | |||||||
|  | #ifndef SAMPLE_DOWNLOADER_HPP__ | ||||||
|  | #define SAMPLE_DOWNLOADER_HPP__ | ||||||
|  | 
 | ||||||
|  | #include <QObject> | ||||||
|  | 
 | ||||||
|  | #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<impl> m_; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #endif | ||||||
							
								
								
									
										299
									
								
								SampleDownloader/Directory.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								SampleDownloader/Directory.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,299 @@ | |||||||
|  | #include "Directory.hpp" | ||||||
|  | 
 | ||||||
|  | #include <QVariant> | ||||||
|  | #include <QString> | ||||||
|  | #include <QHeaderView> | ||||||
|  | #include <QStringList> | ||||||
|  | #include <QFileInfo> | ||||||
|  | #include <QDir> | ||||||
|  | #include <QNetworkAccessManager> | ||||||
|  | #include <QAuthenticator> | ||||||
|  | #include <QNetworkReply> | ||||||
|  | #include <QTreeWidgetItem> | ||||||
|  | #include <QTreeWidgetItemIterator> | ||||||
|  | #include <QMessageBox> | ||||||
|  | #include <QJsonDocument> | ||||||
|  | #include <QJsonParseError> | ||||||
|  | #include <QJsonArray> | ||||||
|  | #include <QJsonObject> | ||||||
|  | #include <QRegularExpression> | ||||||
|  | 
 | ||||||
|  | #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<FileNode *> (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<FileNode *> (*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"); | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								SampleDownloader/Directory.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								SampleDownloader/Directory.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | #ifndef SAMPLE_DOWNLOADER_DIRECTORY_HPP__ | ||||||
|  | #define SAMPLE_DOWNLOADER_DIRECTORY_HPP__ | ||||||
|  | 
 | ||||||
|  | #include <QObject> | ||||||
|  | #include <QString> | ||||||
|  | #include <QTreeWidget> | ||||||
|  | #include <QIcon> | ||||||
|  | #include <QSize> | ||||||
|  | #include <QDir> | ||||||
|  | #include <QUrl> | ||||||
|  | 
 | ||||||
|  | #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 | ||||||
							
								
								
									
										44
									
								
								SampleDownloader/DirectoryDelegate.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								SampleDownloader/DirectoryDelegate.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | #include "DirectoryDelegate.hpp" | ||||||
|  | 
 | ||||||
|  | #include <QApplication> | ||||||
|  | #include <QVariant> | ||||||
|  | #include <QString> | ||||||
|  | #include <QStyle> | ||||||
|  | #include <QModelIndex> | ||||||
|  | #include <QPainter> | ||||||
|  | #include <QStyleOptionViewItem> | ||||||
|  | #include <QStyleOptionProgressBar> | ||||||
|  | 
 | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								SampleDownloader/DirectoryDelegate.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								SampleDownloader/DirectoryDelegate.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | #ifndef DIRECTORY_DELEGATE_HPP__ | ||||||
|  | #define DIRECTORY_DELEGATE_HPP__ | ||||||
|  | 
 | ||||||
|  | #include <QStyledItemDelegate> | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
							
								
								
									
										51
									
								
								SampleDownloader/DirectoryNode.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								SampleDownloader/DirectoryNode.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | #ifndef DIRECTORY_NODE_HPP__ | ||||||
|  | #define DIRECTORY_NODE_HPP__ | ||||||
|  | 
 | ||||||
|  | #include <QTreeWidgetItem> | ||||||
|  | #include <QString> | ||||||
|  | 
 | ||||||
|  | //
 | ||||||
|  | // 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 | ||||||
							
								
								
									
										73
									
								
								SampleDownloader/FileNode.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								SampleDownloader/FileNode.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | |||||||
|  | #include "FileNode.hpp" | ||||||
|  | 
 | ||||||
|  | #include <QVariant> | ||||||
|  | #include <QUrl> | ||||||
|  | #include <QDir> | ||||||
|  | #include <QFileInfo> | ||||||
|  | #include <QMessageBox> | ||||||
|  | 
 | ||||||
|  | #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<Directory *> (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<Directory *> (treeWidget ())->update (parent ()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void FileNode::abort () | ||||||
|  | { | ||||||
|  |   sync_blocker b {this}; | ||||||
|  |   remote_file_.abort (); | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								SampleDownloader/FileNode.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								SampleDownloader/FileNode.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | |||||||
|  | #ifndef FILE_NODE_HPP__ | ||||||
|  | #define FILE_NODE_HPP__ | ||||||
|  | 
 | ||||||
|  | #include <QTreeWidgetItem> | ||||||
|  | 
 | ||||||
|  | #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 | ||||||
							
								
								
									
										5
									
								
								SampleDownloader/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								SampleDownloader/README
									
									
									
									
									
										Normal file
									
								
							| @ -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. | ||||||
							
								
								
									
										269
									
								
								SampleDownloader/RemoteFile.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								SampleDownloader/RemoteFile.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,269 @@ | |||||||
|  | #include "RemoteFile.hpp" | ||||||
|  | 
 | ||||||
|  | #include <utility> | ||||||
|  | 
 | ||||||
|  | #include <QNetworkAccessManager> | ||||||
|  | #include <QNetworkReply> | ||||||
|  | #include <QDir> | ||||||
|  | #include <QByteArray> | ||||||
|  | 
 | ||||||
|  | #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<QSslError> 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 ())); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										78
									
								
								SampleDownloader/RemoteFile.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								SampleDownloader/RemoteFile.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | |||||||
|  | #ifndef REMOTE_FILE_HPP__ | ||||||
|  | #define REMOTE_FILE_HPP__ | ||||||
|  | 
 | ||||||
|  | #include <QObject> | ||||||
|  | #include <QString> | ||||||
|  | #include <QUrl> | ||||||
|  | #include <QFileInfo> | ||||||
|  | #include <QSaveFile> | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
							
								
								
									
										3
									
								
								main.cpp
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								main.cpp
									
									
									
									
									
								
							| @ -7,6 +7,7 @@ | |||||||
| 
 | 
 | ||||||
| #include <QDateTime> | #include <QDateTime> | ||||||
| #include <QApplication> | #include <QApplication> | ||||||
|  | #include <QNetworkAccessManager> | ||||||
| #include <QRegularExpression> | #include <QRegularExpression> | ||||||
| #include <QObject> | #include <QObject> | ||||||
| #include <QSettings> | #include <QSettings> | ||||||
| @ -206,7 +207,7 @@ int main(int argc, char *argv[]) | |||||||
|                                            ).toBool () ? 1u : 4u; |                                            ).toBool () ? 1u : 4u; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor); |       MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor, new QNetworkAccessManager {&a}); | ||||||
|       w.show(); |       w.show(); | ||||||
| 
 | 
 | ||||||
|       QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit())); |       QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit())); | ||||||
|  | |||||||
| @ -47,6 +47,7 @@ | |||||||
| #include "wsprnet.h" | #include "wsprnet.h" | ||||||
| #include "signalmeter.h" | #include "signalmeter.h" | ||||||
| #include "HelpTextWindow.hpp" | #include "HelpTextWindow.hpp" | ||||||
|  | #include "SampleDownloader.hpp" | ||||||
| 
 | 
 | ||||||
| #include "ui_mainwindow.h" | #include "ui_mainwindow.h" | ||||||
| #include "moc_mainwindow.cpp" | #include "moc_mainwindow.cpp" | ||||||
| @ -130,7 +131,8 @@ namespace | |||||||
| 
 | 
 | ||||||
| //--------------------------------------------------- MainWindow constructor
 | //--------------------------------------------------- MainWindow constructor
 | ||||||
| MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdmem, | MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdmem, | ||||||
|                        unsigned downSampleFactor, QWidget *parent) : |                        unsigned downSampleFactor, QNetworkAccessManager * network_manager, | ||||||
|  |                        QWidget *parent) : | ||||||
|   QMainWindow(parent), |   QMainWindow(parent), | ||||||
|   m_dataDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}, |   m_dataDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}, | ||||||
|   m_revision {revision ()}, |   m_revision {revision ()}, | ||||||
| @ -327,6 +329,14 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme | |||||||
|   ui->actionInclude_averaging->setActionGroup(DepthGroup); |   ui->actionInclude_averaging->setActionGroup(DepthGroup); | ||||||
|   ui->actionInclude_correlation->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; |   QButtonGroup* txMsgButtonGroup = new QButtonGroup; | ||||||
|   txMsgButtonGroup->addButton(ui->txrb1,1); |   txMsgButtonGroup->addButton(ui->txrb1,1); | ||||||
|   txMsgButtonGroup->addButton(ui->txrb2,2); |   txMsgButtonGroup->addButton(ui->txrb2,2); | ||||||
| @ -669,7 +679,7 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme | |||||||
|   progressBar->setMaximum(m_TRperiod); |   progressBar->setMaximum(m_TRperiod); | ||||||
|   m_modulator->setPeriod(m_TRperiod); // TODO - not thread safe
 |   m_modulator->setPeriod(m_TRperiod); // TODO - not thread safe
 | ||||||
|   m_dialFreqRxWSPR=0; |   m_dialFreqRxWSPR=0; | ||||||
|   wsprNet = new WSPRNet(this); |   wsprNet = new WSPRNet(network_manager, this); | ||||||
|   connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString))); |   connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString))); | ||||||
|   if(m_bFastMode) { |   if(m_bFastMode) { | ||||||
|     int ntr[]={5,10,15,30}; |     int ntr[]={5,10,15,30}; | ||||||
|  | |||||||
| @ -49,6 +49,7 @@ namespace Ui { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class QSettings; | class QSettings; | ||||||
|  | class QNetworkAccessManager; | ||||||
| class QLineEdit; | class QLineEdit; | ||||||
| class QFont; | class QFont; | ||||||
| class QHostInfo; | class QHostInfo; | ||||||
| @ -68,6 +69,7 @@ class SoundOutput; | |||||||
| class Modulator; | class Modulator; | ||||||
| class SoundInput; | class SoundInput; | ||||||
| class Detector; | class Detector; | ||||||
|  | class SampleDownloader; | ||||||
| 
 | 
 | ||||||
| class MainWindow : public QMainWindow | class MainWindow : public QMainWindow | ||||||
| { | { | ||||||
| @ -79,7 +81,8 @@ public: | |||||||
| 
 | 
 | ||||||
|   // Multiple instances: call MainWindow() with *thekey
 |   // Multiple instances: call MainWindow() with *thekey
 | ||||||
|   explicit MainWindow(bool multiple, QSettings *, QSharedMemory *shdmem, |   explicit MainWindow(bool multiple, QSettings *, QSharedMemory *shdmem, | ||||||
|                       unsigned downSampleFactor, QWidget *parent = 0); |                       unsigned downSampleFactor, QNetworkAccessManager * network_manager, | ||||||
|  |                       QWidget *parent = 0); | ||||||
|   ~MainWindow(); |   ~MainWindow(); | ||||||
| 
 | 
 | ||||||
| public slots: | public slots: | ||||||
| @ -283,6 +286,7 @@ private: | |||||||
|   WSPRBandHopping m_WSPR_band_hopping; |   WSPRBandHopping m_WSPR_band_hopping; | ||||||
|   bool m_WSPR_tx_next; |   bool m_WSPR_tx_next; | ||||||
|   QMessageBox m_rigErrorMessageBox; |   QMessageBox m_rigErrorMessageBox; | ||||||
|  |   QScopedPointer<SampleDownloader> m_sampleDownloader; | ||||||
| 
 | 
 | ||||||
|   QScopedPointer<WideGraph> m_wideGraph; |   QScopedPointer<WideGraph> m_wideGraph; | ||||||
|   QScopedPointer<EchoGraph> m_echoGraph; |   QScopedPointer<EchoGraph> m_echoGraph; | ||||||
|  | |||||||
| @ -2351,6 +2351,7 @@ QPushButton[state="ok"] { | |||||||
|     </property> |     </property> | ||||||
|     <addaction name="actionOnline_User_Guide"/> |     <addaction name="actionOnline_User_Guide"/> | ||||||
|     <addaction name="actionLocal_User_Guide"/> |     <addaction name="actionLocal_User_Guide"/> | ||||||
|  |     <addaction name="download_samples_action"/> | ||||||
|     <addaction name="separator"/> |     <addaction name="separator"/> | ||||||
|     <addaction name="actionKeyboard_shortcuts"/> |     <addaction name="actionKeyboard_shortcuts"/> | ||||||
|     <addaction name="actionSpecial_mouse_commands"/> |     <addaction name="actionSpecial_mouse_commands"/> | ||||||
| @ -2816,6 +2817,14 @@ QPushButton[state="ok"] { | |||||||
|     <string>JTMSK</string> |     <string>JTMSK</string> | ||||||
|    </property> |    </property> | ||||||
|   </action> |   </action> | ||||||
|  |   <action name="download_samples_action"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>&Download Samples ...</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="whatsThis"> | ||||||
|  |     <string><html><head/><body><p>Download sample audio files demonstrating the various modes.</p></body></html></string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  </widget> |  </widget> | ||||||
|  <layoutdefault spacing="6" margin="11"/> |  <layoutdefault spacing="6" margin="11"/> | ||||||
|  <customwidgets> |  <customwidgets> | ||||||
|  | |||||||
| @ -76,6 +76,21 @@ QString font_as_stylesheet (QFont const&); | |||||||
| // conditional style sheet updates
 | // conditional style sheet updates
 | ||||||
| void update_dynamic_property (QWidget *, char const * property, QVariant const& value); | void update_dynamic_property (QWidget *, char const * property, QVariant const& value); | ||||||
| 
 | 
 | ||||||
|  | template <class T> | ||||||
|  | class VPtr | ||||||
|  | { | ||||||
|  | public: | ||||||
|  |   static T * asPtr (QVariant v) | ||||||
|  |   { | ||||||
|  |     return reinterpret_cast<T *> (v.value<void *> ()); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static QVariant asQVariant(T * ptr) | ||||||
|  |   { | ||||||
|  |     return qVariantFromValue (reinterpret_cast<void *> (ptr)); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| // Register some useful Qt types with QMetaType
 | // Register some useful Qt types with QMetaType
 | ||||||
| Q_DECLARE_METATYPE (QHostAddress); | Q_DECLARE_METATYPE (QHostAddress); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -63,13 +63,18 @@ QString revision (QString const& svn_rev_string) | |||||||
|   return result.trimmed (); |   return result.trimmed (); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| QString version () | QString version (bool include_patch) | ||||||
| { | { | ||||||
| #if defined (CMAKE_BUILD) | #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) | # if defined (WSJTX_RC) | ||||||
|   v += "-rc" WSJTX_STRINGIZE (WSJTX_RC); |         + "-rc" WSJTX_STRINGIZE (WSJTX_RC) | ||||||
| # endif | # endif | ||||||
|  |         ; | ||||||
|  |     } | ||||||
| #else | #else | ||||||
|   QString v {"Not for Release"}; |   QString v {"Not for Release"}; | ||||||
| #endif | #endif | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ | |||||||
| #include <QString> | #include <QString> | ||||||
| 
 | 
 | ||||||
| QString revision (QString const& svn_rev_string = QString {}); | QString revision (QString const& svn_rev_string = QString {}); | ||||||
| QString version (); | QString version (bool include_patch = true); | ||||||
| QString program_title (QString const& revision = QString {}); | QString program_title (QString const& revision = QString {}); | ||||||
| 
 | 
 | ||||||
| #endif | #endif | ||||||
|  | |||||||
							
								
								
									
										122
									
								
								samples/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								samples/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							| @ -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 () | ||||||
| @ -16,6 +16,7 @@ | |||||||
| #cmakedefine WSJT_DATA_DESTINATION "@WSJT_DATA_DESTINATION@" | #cmakedefine WSJT_DATA_DESTINATION "@WSJT_DATA_DESTINATION@" | ||||||
| #cmakedefine PROJECT_MANUAL "@PROJECT_MANUAL@" | #cmakedefine PROJECT_MANUAL "@PROJECT_MANUAL@" | ||||||
| #cmakedefine PROJECT_MANUAL_DIRECTORY_URL "@PROJECT_MANUAL_DIRECTORY_URL@" | #cmakedefine PROJECT_MANUAL_DIRECTORY_URL "@PROJECT_MANUAL_DIRECTORY_URL@" | ||||||
|  | #cmakedefine PROJECT_SAMPLES_URL "@PROJECT_SAMPLES_URL@" | ||||||
| 
 | 
 | ||||||
| #cmakedefine01 WSJT_SHARED_RUNTIME | #cmakedefine01 WSJT_SHARED_RUNTIME | ||||||
| #cmakedefine01 WSJT_QDEBUG_TO_FILE | #cmakedefine01 WSJT_QDEBUG_TO_FILE | ||||||
|  | |||||||
| @ -22,9 +22,9 @@ namespace | |||||||
|   // char const * const wsprNetUrl = "http://127.0.0.1/post?";
 |   // char const * const wsprNetUrl = "http://127.0.0.1/post?";
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| WSPRNet::WSPRNet(QObject *parent) | WSPRNet::WSPRNet(QNetworkAccessManager * manager, QObject *parent) | ||||||
|   : QObject{parent} |   : QObject{parent} | ||||||
|   , networkManager {new QNetworkAccessManager {this}} |   , networkManager {manager} | ||||||
|   , uploadTimer {new QTimer {this}} |   , uploadTimer {new QTimer {this}} | ||||||
|   , m_urlQueueSize {0} |   , m_urlQueueSize {0} | ||||||
| { | { | ||||||
| @ -74,6 +74,8 @@ void WSPRNet::upload(QString const& call, QString const& grid, QString const& rf | |||||||
| 
 | 
 | ||||||
| void WSPRNet::networkReply(QNetworkReply *reply) | void WSPRNet::networkReply(QNetworkReply *reply) | ||||||
| { | { | ||||||
|  |   // check if request was ours
 | ||||||
|  |   if (m_outstandingRequests.removeOne (reply)) { | ||||||
|     if (QNetworkReply::NoError != reply->error ()) { |     if (QNetworkReply::NoError != reply->error ()) { | ||||||
|       Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ())); |       Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ())); | ||||||
|       // not clearing queue or halting queuing as it may be a transient
 |       // not clearing queue or halting queuing as it may be a transient
 | ||||||
| @ -96,12 +98,12 @@ void WSPRNet::networkReply(QNetworkReply *reply) | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   m_outstandingRequests.removeOne (reply); |  | ||||||
|     qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ()); |     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
 |     // delete request object instance on return to the event loop otherwise it is leaked
 | ||||||
|     reply->deleteLater (); |     reply->deleteLater (); | ||||||
|   } |   } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| bool WSPRNet::decodeLine(QString const& line, QHash<QString,QString> &query) | bool WSPRNet::decodeLine(QString const& line, QHash<QString,QString> &query) | ||||||
| { | { | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ class WSPRNet : public QObject | |||||||
|   Q_OBJECT; |   Q_OBJECT; | ||||||
| 
 | 
 | ||||||
| public: | 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, |     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& mode, QString const& tpct, QString const& dbm, QString const& version, | ||||||
|                 QString const& fileName); |                 QString const& fileName); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user