From 0ac060576d939b6bdd313c5ea3cc9a3c5fdb15c8 Mon Sep 17 00:00:00 2001 From: Abs62 Date: Fri, 22 Feb 2013 16:44:23 +0400 Subject: [PATCH] Save images and sounds via context menu (issue #223) --- articleview.cc | 243 +++++++++++++++++++++++++++++++++++++++++++++++++ articleview.hh | 8 ++ config.cc | 10 ++ config.hh | 1 + mainwindow.cc | 10 +- mainwindow.hh | 2 + 6 files changed, 273 insertions(+), 1 deletion(-) diff --git a/articleview.cc b/articleview.cc index f6b49c3d..b34c41b8 100644 --- a/articleview.cc +++ b/articleview.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include "folding.hh" #include "wstring_qt.hh" #include "webmultimediadownload.hh" @@ -954,6 +955,221 @@ void ArticleView::openLink( QUrl const & url, QUrl const & ref, } } +void ArticleView::saveResource( const QUrl & url, QUrl const & ref ) +{ + resourceToSaveDownloadRequests.clear(); + resourceToSaveUrl = url; + sptr< Dictionary::DataRequest > req; + + if( url.scheme() == "bres" || url.scheme() == "gico" || url.scheme() == "gdau") + { + if ( url.host() == "search" ) + { + // Since searches should be limited to current group, we just do them + // here ourselves since otherwise we'd need to pass group id to netmgr + // and it should've been having knowledge of the current groups, too. + + unsigned currentGroup = getGroup( ref ); + + std::vector< sptr< Dictionary::Class > > const * activeDicts = 0; + + if ( groups.size() ) + { + for( unsigned x = 0; x < groups.size(); ++x ) + if ( groups[ x ].id == currentGroup ) + { + activeDicts = &( groups[ x ].dictionaries ); + break; + } + } + else + activeDicts = &allDictionaries; + + if ( activeDicts ) + { + for( unsigned x = 0; x < activeDicts->size(); ++x ) + { + req = (*activeDicts)[ x ]->getResource( + url.path().mid( 1 ).toUtf8().data() ); + + if ( req->isFinished() && req->dataSize() >= 0 ) + { + // A request was instantly finished with success. + // If we've managed to spawn some lingering requests already, + // erase them. + resourceToSaveDownloadRequests.clear(); + + // Handle the result + resourceToSaveDownloadRequests.push_back( req ); + resourceToSaveDownloadFinished(); + return; + } + else + if ( !req->isFinished() ) + { + resourceToSaveDownloadRequests.push_back( req ); + connect( req.get(), SIGNAL( finished() ), + this, SLOT( resourceToSaveDownloadFinished() ) ); + } + } + } + } + else + { + // Normal resource download + QString contentType; + req = articleNetMgr.getResource( url, contentType ); + } + } + else + req = new Dictionary::WebMultimediaDownload( url, articleNetMgr ); + + if( url.host().compare( "search" ) != 0 ) + { + if ( !req.get() ) + { + // Request failed, fail + } + else + if ( req->isFinished() && req->dataSize() >= 0 ) + { + // Have data ready, handle it + resourceToSaveDownloadRequests.push_back( req ); + resourceToSaveDownloadFinished(); + return; + } + else + if ( !req->isFinished() ) + { + // Queue to be handled when done + + resourceToSaveDownloadRequests.push_back( req ); + connect( req.get(), SIGNAL( finished() ), + this, SLOT( resourceToSaveDownloadFinished() ) ); + } + } + + if ( resourceToSaveDownloadRequests.empty() ) // No requests were queued + { + emit statusBarMessage( + tr( "ERROR: %1" ).arg( tr( "The referenced resource doesn't exist." ) ), + 10000, QPixmap( ":/icons/error.png" ) ); + return; + } + else + resourceToSaveDownloadFinished(); // Check any requests finished already +} + +void ArticleView::resourceToSaveDownloadFinished() +{ + if ( resourceToSaveDownloadRequests.empty() ) + return; // Stray signal + + QByteArray resourceData; + + // Find any finished resources + + for( list< sptr< Dictionary::DataRequest > >::iterator i = + resourceToSaveDownloadRequests.begin(); i != resourceToSaveDownloadRequests.end(); ) + { + if ( (*i)->isFinished() ) + { + if ( (*i)->dataSize() >= 0 ) + { + // Ok, got one finished, all others are irrelevant now + + vector< char > const & data = (*i)->getFullData(); + resourceData = QByteArray::fromRawData( data.data(), data.size() ); + break; + } + else + { + // This one had no data. Erase it. + resourceToSaveDownloadRequests.erase( i++ ); + } + } + else // Unfinished, try the next one. + i++; + } + + if( !resourceData.isEmpty() ) + { + QString fileName; + QString savePath; + if( cfg.resourceSavePath.isEmpty() ) + savePath = QDir::homePath(); + else + { + savePath = QDir::fromNativeSeparators( cfg.resourceSavePath ); + if( !QDir( savePath ).exists() ) + savePath = QDir::homePath(); + } + + QString name = resourceToSaveUrl.path().section( '/', -1 ); + + if ( resourceToSaveUrl.scheme() == "gdau" || + Dictionary::WebMultimediaDownload::isAudioUrl( resourceToSaveUrl ) ) + { + // Audio data + if( name.indexOf( '.' ) < 0 ) + name += ".wav"; + + fileName = savePath + "/" + name; + fileName = QFileDialog::getSaveFileName( parentWidget(), tr( "Save sound" ), + fileName, + tr( "Sound files (*.wav *.ogg *.mp3 *.mp4 *.aac *.flac *.mid *.wv *.ape);;All files (*.*)" ) ); + } + else + { + // Image data + + // Check for babylon image name + if( name[ 0 ] == '\x1E' ) + name.remove( 0, 1 ); + if( name[ name.length() - 1 ] == '\x1F' ) + name.chop( 1 ); + + fileName = savePath + "/" + name; + fileName = QFileDialog::getSaveFileName( parentWidget(), tr( "Save image" ), + fileName, + tr( "Image files (*.bmp *.jpg *.png *.tif);;All files (*.*)" ) ); + } + + // Write data to file + + if( !fileName.isEmpty() ) + { + QFileInfo fileInfo( fileName ); + emit storeResourceSavePath( QDir::toNativeSeparators( fileInfo.absoluteDir().absolutePath() ) ); + QFile file( fileName ); + if ( file.open( QFile::WriteOnly ) ) + { + file.write( resourceData.data(), resourceData.size() ); + file.close(); + } + if( file.error() ) + { + emit statusBarMessage( + tr( "ERROR: %1" ).arg( tr( "Resource saving error: " ) + file.errorString() ), + 10000, QPixmap( ":/icons/error.png" ) ); + } + } + + // Ok, whatever it was, it's finished. Remove this and any other + // requests and finish. + + resourceToSaveDownloadRequests.clear(); + return; + } + + if ( resourceToSaveDownloadRequests.empty() ) + { + emit statusBarMessage( + tr( "ERROR: %1" ).arg( tr( "The referenced resource failed to download." ) ), + 10000, QPixmap( ":/icons/error.png" ) ); + } +} + void ArticleView::updateMutedContents() { QUrl currentUrl = ui.definition->url(); @@ -1073,6 +1289,8 @@ void ArticleView::contextMenuRequested( QPoint const & pos ) QAction * addWordToHistoryAction = 0; QAction * addHeaderToHistoryAction = 0; QAction * sendWordToInputLineAction = 0; + QAction * saveImageAction = 0; + QAction * saveSoundAction = 0; QUrl targetUrl( r.linkUrl() ); Contexts contexts; @@ -1102,6 +1320,25 @@ void ArticleView::contextMenuRequested( QPoint const & pos ) } } + QWebElement el = r.element(); + QUrl imageUrl; + if( !popupView && el.tagName().compare( "img", Qt::CaseInsensitive ) == 0 ) + { + imageUrl = QUrl::fromPercentEncoding( el.attribute( "src" ).toLatin1() ); + if( !imageUrl.isEmpty() ) + { + saveImageAction = new QAction( tr( "Save &image" ), &menu ); + menu.addAction( saveImageAction ); + } + } + + if( !popupView && ( targetUrl.scheme() == "gdau" + || Dictionary::WebMultimediaDownload::isAudioUrl( targetUrl ) ) ) + { + saveSoundAction = new QAction( tr( "Save s&ound" ), &menu ); + menu.addAction( saveSoundAction ); + } + QString selectedText = ui.definition->selectedText(); if ( selectedText.size() && selectedText.size() < 60 ) @@ -1265,6 +1502,12 @@ void ArticleView::contextMenuRequested( QPoint const & pos ) emit showDefinitionInNewTab( selectedText, groupComboBox->getCurrentGroup(), QString(), Contexts() ); else + if( result == saveImageAction ) + saveResource( imageUrl, ui.definition->url() ); + else + if( result == saveSoundAction ) + saveResource( targetUrl, ui.definition->url() ); + else { if ( !popupView && result == maxDictionaryRefsAction ) emit showDictsPane(); diff --git a/articleview.hh b/articleview.hh index c984bdfc..dca20c6c 100644 --- a/articleview.hh +++ b/articleview.hh @@ -46,6 +46,9 @@ class ArticleView: public QFrame /// Url of the resourceDownloadRequests QUrl resourceDownloadUrl; + std::list< sptr< Dictionary::DataRequest > > resourceToSaveDownloadRequests; + QUrl resourceToSaveUrl; + /// For resources opened via desktop services QString desktopOpenedTempFile; @@ -209,6 +212,8 @@ signals: void sendWordToInputLine( QString const & word ); + void storeResourceSavePath(QString const & ); + public slots: void on_searchPrevious_clicked(); @@ -237,6 +242,7 @@ private slots: void contextMenuRequested( QPoint const & ); void resourceDownloadFinished(); + void resourceToSaveDownloadFinished(); /// We handle pasting by attempting to define the word in clipboard. void pasteTriggered(); @@ -307,6 +313,8 @@ private: /// for the given group. If there are none, returns empty string. QString getMutedForGroup( unsigned group ); + void saveResource( QUrl const &, QUrl const & ); + protected: // We need this to hide the search bar when we're showed diff --git a/config.cc b/config.cc index cd653598..2d45a9e3 100644 --- a/config.cc +++ b/config.cc @@ -777,6 +777,9 @@ Class load() throw( exError ) if ( !root.namedItem( "historyExportPath" ).isNull() ) c.historyExportPath = root.namedItem( "historyExportPath" ).toElement().text(); + if ( !root.namedItem( "resourceSavePath" ).isNull() ) + c.resourceSavePath = root.namedItem( "resourceSavePath" ).toElement().text(); + if ( !root.namedItem( "editDictionaryCommandLine" ).isNull() ) c.editDictionaryCommandLine = root.namedItem( "editDictionaryCommandLine" ).toElement().text(); @@ -1470,6 +1473,13 @@ void save( Class const & c ) throw( exError ) root.appendChild( opt ); } + if( !c.resourceSavePath.isEmpty() ) + { + opt = dd.createElement( "resourceSavePath" ); + opt.appendChild( dd.createTextNode( c.resourceSavePath ) ); + root.appendChild( opt ); + } + opt = dd.createElement( "editDictionaryCommandLine" ); opt.appendChild( dd.createTextNode( c.editDictionaryCommandLine ) ); root.appendChild( opt ); diff --git a/config.hh b/config.hh index 027903a0..02e2b59b 100644 --- a/config.hh +++ b/config.hh @@ -404,6 +404,7 @@ struct Class QByteArray dictInfoGeometry; // Geometry of "Dictionary info" window QString historyExportPath; // Path for export/import history + QString resourceSavePath; // Path to save images/audio bool pinPopupWindow; // Last pin status diff --git a/mainwindow.cc b/mainwindow.cc index 0ad1f274..9a8fcce0 100644 --- a/mainwindow.cc +++ b/mainwindow.cc @@ -1335,7 +1335,10 @@ ArticleView * MainWindow::createNewTab( bool switchToIt, this, SLOT( addWordToHistory( QString ) ) ); connect( view, SIGNAL( sendWordToInputLine( QString const & ) ), - this, SLOT(sendWordToInputLine( QString const & ) ) ); + this, SLOT( sendWordToInputLine( QString const & ) ) ); + + connect( view, SIGNAL( storeResourceSavePath( QString const & ) ), + this, SLOT( storeResourceSavePath( QString const & ) ) ); view->setSelectionBySingleClick( cfg.preferences.selectWordBySingleClick ); @@ -3340,6 +3343,11 @@ void MainWindow::sendWordToInputLine( const QString & word ) translateLine->setText( word ); } +void MainWindow::storeResourceSavePath( const QString & newPath ) +{ + cfg.resourceSavePath = newPath; +} + #ifdef Q_OS_WIN32 bool MainWindow::winEvent( MSG * message, long * result ) diff --git a/mainwindow.hh b/mainwindow.hh index e9c42b73..67510c10 100644 --- a/mainwindow.hh +++ b/mainwindow.hh @@ -385,6 +385,8 @@ private slots: void sendWordToInputLine( QString const & word ); + void storeResourceSavePath( QString const & ); + signals: /// Set optional parts expand mode for all tabs void setExpandOptionalParts( bool expand );