goldendict-ng/articleview.cc

2840 lines
83 KiB
C++
Raw Normal View History

2012-02-20 21:47:14 +00:00
/* This file is (c) 2008-2012 Konstantin Isakov <ikm@goldendict.org>
* Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */
#include "articleview.hh"
2022-03-27 14:22:42 +00:00
#include "QtCore/qvariant.h"
#include "folding.hh"
#include "fulltextsearch.hh"
#include "gddebug.hh"
#include "gestures.hh"
#include "programs.hh"
#include "utils.hh"
#include "webmultimediadownload.hh"
2021-12-11 16:34:37 +00:00
#include "weburlrequestinterceptor.h"
#include "wildcard.hh"
#include "wstring_qt.hh"
#include <QCryptographicHash>
#include <QDebug>
#include <QDesktopServices>
#include <QFileDialog>
#include <QKeyEvent>
#include <QMenu>
#include <QMessageBox>
#include <QRegularExpression>
#include <QWebChannel>
2021-07-06 13:01:50 +00:00
#include <QWebEngineHistory>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
#include <assert.h>
#include <map>
2022-02-27 05:17:37 +00:00
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0) && QT_VERSION < QT_VERSION_CHECK(6,0,0))
2021-12-28 13:59:49 +00:00
#include <QWebEngineContextMenuData>
2022-02-27 05:17:37 +00:00
#endif
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
2022-02-27 14:42:40 +00:00
#include <QtCore5Compat/QRegExp>
2022-02-27 05:17:37 +00:00
#include <QWebEngineContextMenuRequest>
#include <QWebEngineFindTextResult>
#endif
#ifdef Q_OS_WIN32
#include <windows.h>
#include <QPainter>
#endif
#include <QBuffer>
#if defined( Q_OS_WIN32 ) || defined( Q_OS_MAC )
#include "speechclient.hh"
#endif
#include "globalbroadcaster.h"
using std::map;
using std::list;
/// AccentMarkHandler class
///
/// Remove accent marks from text
/// and mirror position in normalized text to original text
class AccentMarkHandler
{
protected:
QString normalizedString;
QVector< int > accentMarkPos;
public:
AccentMarkHandler()
{}
virtual ~AccentMarkHandler()
{}
static QChar accentMark()
{ return QChar( 0x301 ); }
/// Create text without accent marks
/// and store mark positions
virtual void setText( QString const & baseString )
{
accentMarkPos.clear();
normalizedString.clear();
int pos = 0;
QChar mark = accentMark();
for( int x = 0; x < baseString.length(); x++ )
{
if( baseString.at( x ) == mark )
{
accentMarkPos.append( pos );
continue;
}
normalizedString.append( baseString.at( x ) );
pos++;
}
}
/// Return text without accent marks
QString const & normalizedText() const
{ return normalizedString; }
/// Convert position into position in original text
int mirrorPosition( int const & pos ) const
{
int newPos = pos;
for( int x = 0; x < accentMarkPos.size(); x++ )
{
if( accentMarkPos.at( x ) < pos )
newPos++;
else
break;
}
return newPos;
}
};
/// End of DslAccentMark class
/// DiacriticsHandler class
///
/// Remove diacritics from text
/// and mirror position in normalized text to original text
class DiacriticsHandler : public AccentMarkHandler
{
public:
DiacriticsHandler()
{}
~DiacriticsHandler()
{}
/// Create text without diacriticss
/// and store diacritic marks positions
void setText( QString const & baseString ) override
{
accentMarkPos.clear();
normalizedString.clear();
gd::wstring baseText = gd::toWString( baseString );
gd::wstring normText;
int pos = 0;
normText.reserve( baseText.size() );
gd::wchar const * nextChar = baseText.data();
size_t consumed;
for( size_t left = baseText.size(); left; )
{
if( *nextChar >= 0x10000U )
{
// Will be translated into surrogate pair
normText.push_back( *nextChar );
pos += 2;
nextChar++; left--;
continue;
}
gd::wchar ch = Folding::foldedDiacritic( nextChar, left, consumed );
if( Folding::isCombiningMark( ch ) )
{
accentMarkPos.append( pos );
nextChar++; left--;
continue;
}
if( consumed > 1 )
{
for( size_t i = 1; i < consumed; i++ )
accentMarkPos.append( pos );
}
normText.push_back( ch );
pos += 1;
nextChar += consumed;
left -= consumed;
}
normalizedString = gd::toQString( normText );
}
};
/// End of DiacriticsHandler class
2021-11-24 14:38:37 +00:00
void ArticleView::emitJavascriptFinished(){
emit notifyJavascriptFinished();
}
namespace {
char const * const scrollToPrefix = "gdfrom-";
bool isScrollTo( QString const & id )
{
return id.startsWith( scrollToPrefix );
}
QString dictionaryIdFromScrollTo( QString const & scrollTo )
{
Q_ASSERT( isScrollTo( scrollTo ) );
const int scrollToPrefixLength = 7;
return scrollTo.mid( scrollToPrefixLength );
}
QString searchStatusMessageNoMatches()
{
return ArticleView::tr( "Phrase not found" );
}
QString searchStatusMessage( int activeMatch, int matchCount )
{
Q_ASSERT( matchCount > 0 );
Q_ASSERT( activeMatch > 0 );
Q_ASSERT( activeMatch <= matchCount );
return ArticleView::tr( "%1 of %2 matches" ).arg( activeMatch ).arg( matchCount );
}
} // unnamed namespace
QString ArticleView::scrollToFromDictionaryId( QString const & dictionaryId )
{
Q_ASSERT( !isScrollTo( dictionaryId ) );
return scrollToPrefix + dictionaryId;
}
ArticleView::ArticleView( QWidget * parent, ArticleNetworkAccessManager & nm, AudioPlayerPtr const & audioPlayer_,
std::vector< sptr< Dictionary::Class > > const & allDictionaries_,
Instances::Groups const & groups_, bool popupView_, Config::Class const & cfg_,
QAction & openSearchAction_,
QLineEdit const * translateLine_,
QAction * dictionaryBarToggled_,
GroupComboBox const * groupComboBox_
):
QWidget( parent ),
articleNetMgr( nm ),
audioPlayer( audioPlayer_ ),
allDictionaries( allDictionaries_ ),
groups( groups_ ),
popupView( popupView_ ),
cfg( cfg_ ),
pasteAction( this ),
articleUpAction( this ),
articleDownAction( this ),
goBackAction( this ),
goForwardAction( this ),
selectCurrentArticleAction( this ),
copyAsTextAction( this ),
inspectAction( this ),
2014-04-16 16:18:28 +00:00
openSearchAction( openSearchAction_ ),
2009-05-16 11:14:43 +00:00
searchIsOpened( false ),
dictionaryBarToggled( dictionaryBarToggled_ ),
groupComboBox( groupComboBox_ ),
translateLine( translateLine_ ),
ftsSearchIsOpened( false ),
ftsSearchMatchCase( false ),
ftsPosition( 0 )
{
// setup GUI
webview = new ArticleWebView( this );
// fts Search Panel
ftsSearchPanel = new QWidget( this );
auto * ftsLayout = new QHBoxLayout( ftsSearchPanel );
ftsSearchPrevious = new QPushButton( ftsSearchPanel );
ftsSearchNext = new QPushButton( ftsSearchPanel );
ftsSearchStatusLabel = new QLabel( ftsSearchPanel );
ftsLayout->addWidget( ftsSearchPrevious );
ftsLayout->addWidget( ftsSearchNext );
ftsLayout->addWidget( ftsSearchStatusLabel );
ftsSearchPrevious->setIcon( QIcon( ":/icons/previous.svg" ) );
ftsSearchNext->setIcon( QIcon( ":/icons/next.svg" ) );
ftsSearchPrevious->setText( tr( "&Previous" ) );
ftsSearchNext->setText( tr( "&Next" ) );
ftsLayout->addStretch();
// Search Panel
searchPanel = new QWidget( this );
auto * searchLabel = new QLabel( tr( "Find:" ) );
searchText = new QLineEdit( searchPanel );
searchCloseButton = new QPushButton( searchPanel );
searchCloseButton->setIcon( QIcon( ":/icons/closetab.svg" ) );
searchPrevious = new QPushButton( searchPanel );
searchPrevious->setIcon( QIcon( ":/icons/previous.svg" ) );
searchPrevious->setText( tr( "&Previous" ) );
searchNext = new QPushButton( searchPanel );
searchNext->setIcon( QIcon( ":/icons/next.svg" ) );
searchNext->setText( tr( "&Next" ) );
highlightAllButton = new QCheckBox( searchPanel );
highlightAllButton->setIcon( QIcon( ":/icons/highlighter.png" ) );
highlightAllButton->setText( tr( "Highlight &all" ) );
highlightAllButton->setChecked( true );
searchCaseSensitive = new QCheckBox( searchPanel );
searchCaseSensitive->setText( tr( "&Case Sensitive" ) );
auto * searchEditRow = new QHBoxLayout;
searchEditRow->addWidget( searchLabel );
searchEditRow->addWidget( searchText );
searchEditRow->addWidget( searchCloseButton );
auto * searchButtonsRow = new QHBoxLayout;
searchButtonsRow->addWidget( searchPrevious );
searchButtonsRow->addWidget( searchNext );
searchButtonsRow->addWidget( highlightAllButton );
searchButtonsRow->addWidget( searchCaseSensitive );
searchButtonsRow->addStretch();
auto * searchPanelLayout = new QVBoxLayout( searchPanel );
searchPanelLayout->addLayout( searchEditRow );
searchPanelLayout->addLayout( searchButtonsRow );
// Combine Layouts
auto * mainLayout = new QVBoxLayout( this );
mainLayout->addWidget( webview );
mainLayout->addWidget( ftsSearchPanel );
mainLayout->addWidget( searchPanel );
webview->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
ftsSearchPanel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum );
searchPanel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum );
mainLayout->setContentsMargins( 0, 0, 0, 0 );
// end UI setup
connect( searchPrevious, &QPushButton::clicked, this, &ArticleView::on_searchPrevious_clicked );
connect( searchNext, &QPushButton::clicked, this, &ArticleView::on_searchNext_clicked );
connect( searchCloseButton, &QPushButton::clicked, this, &ArticleView::on_searchCloseButton_clicked );
connect( searchCaseSensitive, &QPushButton::clicked, this, &ArticleView::on_searchCaseSensitive_clicked );
connect( highlightAllButton, &QPushButton::clicked, this, &ArticleView::on_highlightAllButton_clicked );
connect( searchText, &QLineEdit::textEdited, this, &ArticleView::on_searchText_textEdited );
connect( searchText, &QLineEdit::returnPressed, this, &ArticleView::on_searchText_returnPressed );
connect( ftsSearchNext, &QPushButton::clicked, this, &ArticleView::on_ftsSearchNext_clicked );
connect( ftsSearchPrevious, &QPushButton::clicked, this, &ArticleView::on_ftsSearchPrevious_clicked );
//
webview->setUp( const_cast< Config::Class * >( &cfg ) );
goBackAction.setShortcut( QKeySequence( "Alt+Left" ) );
webview->addAction( &goBackAction );
connect( &goBackAction, &QAction::triggered, this, &ArticleView::back );
goForwardAction.setShortcut( QKeySequence( "Alt+Right" ) );
webview->addAction( &goForwardAction );
connect( &goForwardAction, &QAction::triggered, this, &ArticleView::forward );
webview->pageAction( QWebEnginePage::Copy )->setShortcut( QKeySequence::Copy );
webview->addAction( webview->pageAction( QWebEnginePage::Copy ) );
QAction * selectAll = webview->pageAction( QWebEnginePage::SelectAll );
2021-08-21 01:41:40 +00:00
selectAll->setShortcut( QKeySequence::SelectAll );
selectAll->setShortcutContext( Qt::WidgetWithChildrenShortcut );
webview->addAction( selectAll );
webview->setContextMenuPolicy( Qt::CustomContextMenu );
connect( webview, &QWebEngineView::loadFinished, this, &ArticleView::loadFinished );
connect( webview, &QWebEngineView::loadProgress, this, &ArticleView::loadProgress );
connect( webview, &ArticleWebView::linkClicked, this, &ArticleView::linkClicked );
connect( webview->page(), &QWebEnginePage::titleChanged, this, &ArticleView::handleTitleChanged );
connect( webview->page(), &QWebEnginePage::urlChanged, this, &ArticleView::handleUrlChanged );
connect( webview, &QWidget::customContextMenuRequested, this, &ArticleView::contextMenuRequested );
connect( webview->page(), SIGNAL( linkHovered( const QString & ) ), this, SLOT( linkHovered( const QString & ) ) );
connect( webview, &ArticleWebView::doubleClicked, this, &ArticleView::doubleClicked );
pasteAction.setShortcut( QKeySequence::Paste );
webview->addAction( &pasteAction );
connect( &pasteAction, &QAction::triggered, this, &ArticleView::pasteTriggered );
articleUpAction.setShortcut( QKeySequence( "Alt+Up" ) );
webview->addAction( &articleUpAction );
connect( &articleUpAction, &QAction::triggered, this, &ArticleView::moveOneArticleUp );
articleDownAction.setShortcut( QKeySequence( "Alt+Down" ) );
webview->addAction( &articleDownAction );
connect( &articleDownAction, &QAction::triggered, this, &ArticleView::moveOneArticleDown );
webview->addAction( &openSearchAction );
connect( &openSearchAction, &QAction::triggered, this, &ArticleView::openSearch );
2009-05-16 11:14:43 +00:00
selectCurrentArticleAction.setShortcut( QKeySequence( "Ctrl+Shift+A" ) );
selectCurrentArticleAction.setText( tr( "Select Current Article" ) );
webview->addAction( &selectCurrentArticleAction );
connect( &selectCurrentArticleAction, &QAction::triggered, this, &ArticleView::selectCurrentArticle );
copyAsTextAction.setShortcut( QKeySequence( "Ctrl+Shift+C" ) );
copyAsTextAction.setText( tr( "Copy as text" ) );
webview->addAction( &copyAsTextAction );
connect( &copyAsTextAction, &QAction::triggered, this, &ArticleView::copyAsText );
inspectAction.setShortcut( QKeySequence( Qt::Key_F12 ) );
inspectAction.setText( tr( "Inspect" ) );
webview->addAction( &inspectAction );
2021-11-30 03:40:57 +00:00
connect( &inspectAction, &QAction::triggered, this, &ArticleView::inspectElement );
webview->installEventFilter( this );
searchPanel->installEventFilter( this );
ftsSearchPanel->installEventFilter( this );
QWebEngineSettings * settings = webview->settings();
settings->setUnknownUrlSchemePolicy( QWebEngineSettings::UnknownUrlSchemePolicy::DisallowUnknownUrlSchemes );
#if( QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) )
settings->defaultSettings()->setAttribute( QWebEngineSettings::LocalContentCanAccessRemoteUrls, true );
settings->defaultSettings()->setAttribute( QWebEngineSettings::LocalContentCanAccessFileUrls, true );
settings->defaultSettings()->setAttribute( QWebEngineSettings::ErrorPageEnabled, false );
2022-04-26 12:21:45 +00:00
settings->defaultSettings()->setAttribute( QWebEngineSettings::PluginsEnabled, cfg.preferences.enableWebPlugins );
settings->defaultSettings()->setAttribute( QWebEngineSettings::PlaybackRequiresUserGesture, false );
settings->defaultSettings()->setAttribute( QWebEngineSettings::JavascriptCanAccessClipboard, true );
2022-05-24 13:40:53 +00:00
settings->defaultSettings()->setAttribute( QWebEngineSettings::PrintElementBackgrounds, false );
#else
settings->setAttribute( QWebEngineSettings::LocalContentCanAccessRemoteUrls, true );
settings->setAttribute( QWebEngineSettings::LocalContentCanAccessFileUrls, true );
settings->setAttribute( QWebEngineSettings::ErrorPageEnabled, false );
settings->setAttribute( QWebEngineSettings::PluginsEnabled, cfg.preferences.enableWebPlugins );
settings->setAttribute( QWebEngineSettings::PlaybackRequiresUserGesture, false );
2022-04-26 12:21:45 +00:00
settings->setAttribute( QWebEngineSettings::JavascriptCanAccessClipboard, true );
2022-05-24 13:40:53 +00:00
settings->setAttribute( QWebEngineSettings::PrintElementBackgrounds, false );
#endif
webview->load( QUrl( "gdlookup://localhost?word=(untitled)&blank=1" ) );
2022-08-23 11:09:38 +00:00
expandOptionalParts = cfg.preferences.alwaysExpandOptionalParts;
webview->grabGesture( Gestures::GDPinchGestureType );
webview->grabGesture( Gestures::GDSwipeGestureType );
// Variable name for store current selection range
rangeVarName = QString( "sr_%1" ).arg( QString::number( (quint64)this, 16 ) );
connect( GlobalBroadcaster::instance(), &GlobalBroadcaster::dictionaryChanges, this, &ArticleView::setActiveDictIds );
connect( GlobalBroadcaster::instance(), &GlobalBroadcaster::dictionaryClear, this, &ArticleView::dictionaryClear );
channel = new QWebChannel( webview->page() );
agent = new ArticleViewAgent(this);
attachWebChannelToHtml();
ankiConnector = new AnkiConnector( this, cfg );
connect( ankiConnector,
&AnkiConnector::errorText,
this,
[ this ]( QString const & errorText ) { emit statusBarMessage( errorText ); } );
}
// explicitly report the minimum size, to avoid
// sidebar widgets' improper resize during restore
QSize ArticleView::minimumSizeHint() const { return searchPanel->minimumSizeHint(); }
void ArticleView::setGroupComboBox( GroupComboBox const * g )
{
groupComboBox = g;
}
ArticleView::~ArticleView()
{
cleanupTemp();
audioPlayer->stop();
//channel->deregisterObject(this);
webview->ungrabGesture( Gestures::GDPinchGestureType );
webview->ungrabGesture( Gestures::GDSwipeGestureType );
}
void ArticleView::showDefinition( Config::InputPhrase const & phrase, unsigned group,
QString const & scrollTo,
2015-10-28 19:56:58 +00:00
Contexts const & contexts_ )
{
currentWord = phrase.phrase.trimmed();
if( currentWord.isEmpty() )
return;
historyMode = false;
currentActiveDictIds.clear();
// first, let's stop the player
audioPlayer->stop();
QUrl req;
2015-10-28 19:56:58 +00:00
Contexts contexts( contexts_ );
req.setScheme( "gdlookup" );
req.setHost( "localhost" );
Utils::Url::addQueryItem( req, "word", phrase.phrase );
if ( !phrase.punctuationSuffix.isEmpty() )
Utils::Url::addQueryItem( req, "punctuation_suffix", phrase.punctuationSuffix );
Utils::Url::addQueryItem( req, "group", QString::number( group ) );
if( cfg.preferences.ignoreDiacritics )
Utils::Url::addQueryItem( req, "ignore_diacritics", "1" );
if ( scrollTo.size() )
Utils::Url::addQueryItem( req, "scrollto", scrollTo );
if( delayedHighlightText.size() )
{
Utils::Url::addQueryItem( req, "regexp", delayedHighlightText );
delayedHighlightText.clear();
}
2015-10-28 19:56:58 +00:00
Contexts::Iterator pos = contexts.find( "gdanchor" );
if( pos != contexts.end() )
{
Utils::Url::addQueryItem( req, "gdanchor", contexts[ "gdanchor" ] );
2015-10-28 19:56:58 +00:00
contexts.erase( pos );
}
if ( contexts.size() )
{
QBuffer buf;
buf.open( QIODevice::WriteOnly );
QDataStream stream( &buf );
stream << contexts;
buf.close();
Utils::Url::addQueryItem( req, "contexts", QString::fromLatin1( buf.buffer().toBase64() ) );
}
QString mutedDicts = getMutedForGroup( group );
if ( mutedDicts.size() )
Utils::Url::addQueryItem( req, "muted", mutedDicts );
// Update headwords history
emit sendWordToHistory( phrase.phrase );
// Any search opened is probably irrelevant now
closeSearch();
emit setExpandMode( expandOptionalParts );
load( req );
//QApplication::setOverrideCursor( Qt::WaitCursor );
webview->setCursor( Qt::WaitCursor );
}
void ArticleView::showDefinition( QString const & word, unsigned group,
QString const & scrollTo,
Contexts const & contexts_ )
{
showDefinition( Config::InputPhrase::fromPhrase( word ), group, scrollTo, contexts_ );
}
void ArticleView::showDefinition( QString const & word, QStringList const & dictIDs,
QRegExp const & searchRegExp, unsigned group,
bool ignoreDiacritics )
2014-04-16 16:18:28 +00:00
{
if( dictIDs.isEmpty() )
return;
currentWord = word.trimmed();
if( currentWord.isEmpty() )
return;
historyMode = false;
2014-04-16 16:18:28 +00:00
// first, let's stop the player
audioPlayer->stop();
2014-04-16 16:18:28 +00:00
QUrl req;
req.setScheme( "gdlookup" );
req.setHost( "localhost" );
Utils::Url::addQueryItem( req, "word", word );
Utils::Url::addQueryItem( req, "dictionaries", dictIDs.join( ",") );
Utils::Url::addQueryItem( req, "regexp", searchRegExp.pattern() );
2022-03-20 11:27:35 +00:00
if( searchRegExp.caseSensitivity() == Qt::CaseSensitive )
Utils::Url::addQueryItem( req, "matchcase", "1" );
2022-03-20 11:27:35 +00:00
if( searchRegExp.patternSyntax() == QRegExp::WildcardUnix )
Utils::Url::addQueryItem( req, "wildcards", "1" );
Utils::Url::addQueryItem( req, "group", QString::number( group ) );
if( ignoreDiacritics )
Utils::Url::addQueryItem( req, "ignore_diacritics", "1" );
2014-04-16 16:18:28 +00:00
// Update headwords history
2014-04-16 16:18:28 +00:00
emit sendWordToHistory( word );
// Any search opened is probably irrelevant now
closeSearch();
// Clear highlight all button selection
highlightAllButton->setChecked( false );
2014-04-16 16:18:28 +00:00
emit setExpandMode( expandOptionalParts );
load( req );
2014-04-16 16:18:28 +00:00
webview->setCursor( Qt::WaitCursor );
2014-04-16 16:18:28 +00:00
}
void ArticleView::sendToAnki(QString const & word, QString const & text, QString const & sentence ){
ankiConnector->sendToAnki(word,text,sentence);
}
void ArticleView::showAnticipation()
{
webview->setHtml( "" );
webview->setCursor( Qt::WaitCursor );
}
void ArticleView::inspectElement() { emit inspectSignal( webview->page() ); }
void ArticleView::loadFinished( bool result )
{
webview->setFocus();
setZoomFactor( cfg.preferences.zoomFactor );
QUrl url = webview->url();
qDebug() << "article view loaded url:" << url.url().left( 200 ) << result;
if( url.url() == "about:blank" )
{
return;
}
if( !result )
{
qWarning() << "article loaded unsuccessful";
return;
}
Add "Automatically scroll to target article" option When a user clicks on a link in a dictionary or requests translation of a word by double-clicking or translates selection via the context menu, at first the article from the highest-priority dictionary is at the top. Then, after approximately one second, the article from the dictionary, out of which the translation was requested, becomes current and the view scrolls down to this article placing it on top, hiding articles from the dictionaries above it. Such application behavior is inconvenient in some workflows so that the user manually navigates to the top dictionary translation when this automatic scrolling happens. For example: a user has English->Russian dictionaries and English->English dictionaries. The English->Russian dictionaries are higher up in the dictionary order because they provide easier/faster to understand translations. Some rare words and phrases are missing from the English->Russian dictionaries however. Thus the user occasionally reads the English explanation of a word/phrase. When the user double-clicks on a word or follows a link in the English->English dictionary article, she would rather see translations from the preferable English->Russian dictionaries. The new option allows to disable automatic scrolling and ensure that articles from higher-priority dictionaries are visible. The option doesn't affect backward/forward navigation via arrow buttons or Alt+Arrow shortcuts: these still scroll to the stored vertical position among articles. This remaining automatic scrolling happens much faster, is not a problem for the described use case and hopefully for other use cases.
2019-01-11 13:50:40 +00:00
if( cfg.preferences.autoScrollToTargetArticle )
{
QString const scrollTo = Utils::Url::queryItemValue( url, "scrollto" );
if( isScrollTo( scrollTo ) )
{
setCurrentArticle( scrollTo, true );
}
}
webview->unsetCursor();
// Expand collapsed article if only one loaded
webview->page()->runJavaScript( QString( "gdCheckArticlesNumber();" ) );
if( !Utils::Url::queryItemValue( url, "gdanchor" ).isEmpty() )
2015-10-28 19:56:58 +00:00
{
QString anchor = QUrl::fromPercentEncoding( Utils::Url::encodedQueryItemValue( url, "gdanchor" ) );
2015-10-28 19:56:58 +00:00
// Find GD anchor on page
int n = anchor.indexOf( '_' );
if( n == 33 )
// MDict pattern: ("g" + dictionary ID (33 chars total))_(articleID(quint64, hex))_(original anchor)
2015-10-28 19:56:58 +00:00
n = anchor.indexOf( '_', n + 1 );
2015-10-30 18:01:38 +00:00
else
n = 0;
2015-10-28 19:56:58 +00:00
if( n > 0 )
{
QString originalAnchor = anchor.mid( n + 1 );
2021-07-06 13:01:50 +00:00
int end = originalAnchor.indexOf('_');
QString hash=originalAnchor.left(end);
url.clear();
url.setFragment(hash);
webview->page()->runJavaScript(
QString( "window.location.hash = \"%1\"" ).arg( QString::fromUtf8( url.toEncoded() ) ) );
2021-07-06 13:01:50 +00:00
2015-10-28 19:56:58 +00:00
}
2015-10-30 18:01:38 +00:00
else
{
2016-04-01 13:38:07 +00:00
url.clear();
2015-10-30 18:01:38 +00:00
url.setFragment( anchor );
webview->page()->runJavaScript(
QString( "window.location.hash = \"%1\"" ).arg( QString::fromUtf8( url.toEncoded() ) ) );
2015-10-30 18:01:38 +00:00
}
2015-10-28 19:56:58 +00:00
}
//the click audio url such as gdau://xxxx ,webview also emit a pageLoaded signal but with the result is false.need future investigation.
//the audio link click ,no need to emit pageLoaded signal
if(result){
emit pageLoaded( this );
}
if( Utils::Url::hasQueryItem( webview->url(), "regexp" ) )
highlightFTSResults();
}
void ArticleView::loadProgress(int ){
}
void ArticleView::handleTitleChanged( QString const & title )
{
2016-04-01 13:38:07 +00:00
if( !title.isEmpty() ) // Qt 5.x WebKit raise signal titleChanges(QString()) while navigation within page
emit titleChanged( this, title );
}
void ArticleView::handleUrlChanged( QUrl const & url )
{
QIcon icon;
unsigned group = getGroup( url );
if ( group )
{
// Find the group's instance corresponding to the fragment value
for( unsigned x = 0; x < groups.size(); ++x )
if ( groups[ x ].id == group )
{
// Found it
icon = groups[ x ].makeIcon();
break;
}
}
emit iconChanged( this, icon );
}
unsigned ArticleView::getGroup( QUrl const & url )
{
if ( url.scheme() == "gdlookup" && Utils::Url::hasQueryItem( url, "group" ) )
return Utils::Url::queryItemValue( url, "group" ).toUInt();
return 0;
}
QStringList ArticleView::getArticlesList()
2021-12-13 14:45:16 +00:00
{
return currentActiveDictIds;
}
QString ArticleView::getActiveArticleId()
{
return activeDictId;
}
void ArticleView::setActiveArticleId(QString const & dictId){
this->activeDictId=dictId;
}
QString ArticleView::getCurrentArticle()
{
QString dictId=getActiveArticleId();
return scrollToFromDictionaryId( dictId );
}
void ArticleView::jumpToDictionary( QString const & id, bool force )
{
QString targetArticle = scrollToFromDictionaryId( id );
// jump only if neceessary, or when forced
if ( force || targetArticle != getCurrentArticle() )
{
setCurrentArticle( targetArticle, true );
}
}
bool ArticleView::setCurrentArticle( QString const & id, bool moveToIt )
{
if ( !isScrollTo( id ) )
return false; // Incorrect id
if( !webview->isVisible() )
return false; // No action on background page, scrollIntoView there don't work
2021-10-05 01:23:30 +00:00
if(moveToIt){
2022-02-25 14:48:43 +00:00
QString dictId = id.mid( 7 );
if( dictId.isEmpty() )
2022-05-24 23:57:43 +00:00
return false;
2022-02-25 14:48:43 +00:00
QString script = QString( "var elem=document.getElementById('%1'); "
"if(elem!=undefined){elem.scrollIntoView(true);} gdMakeArticleActive('%2',true);" )
.arg( id, dictId );
onJsActiveArticleChanged( id );
webview->page()->runJavaScript( script );
2022-02-25 14:48:43 +00:00
setActiveArticleId( dictId );
2021-10-05 01:23:30 +00:00
}
return true;
}
void ArticleView::selectCurrentArticle()
{
webview->page()->runJavaScript(
QString(
"gdSelectArticle( '%1' );var elem=document.getElementById('%2'); if(elem!=undefined){elem.scrollIntoView(true);}" )
.arg( getActiveArticleId(), getCurrentArticle() ) );
}
2022-03-30 15:08:24 +00:00
void ArticleView::isFramedArticle( QString const & ca, const std::function< void( bool ) > & callback )
{
2022-03-30 15:08:24 +00:00
if( ca.isEmpty() )
callback( false );
webview->page()->runJavaScript( QString( "!!document.getElementById('gdexpandframe-%1');" ).arg( ca.mid( 7 ) ),
[ callback ]( const QVariant & res ) {
callback( res.toBool() );
} );
}
bool ArticleView::isExternalLink( QUrl const & url )
{
return Utils::isExternalLink(url);
}
void ArticleView::tryMangleWebsiteClickedUrl( QUrl & url, Contexts & contexts )
{
// Don't try mangling audio urls, even if they are from the framed websites
2022-03-30 15:08:24 +00:00
if( ( url.scheme() == "http" || url.scheme() == "https" ) && !Dictionary::WebMultimediaDownload::isAudioUrl( url ) )
{
// Maybe a link inside a website was clicked?
QString ca = getCurrentArticle();
2022-07-08 14:13:40 +00:00
isFramedArticle( ca, []( bool framed ){} );
}
}
void ArticleView::load( QUrl const & url ) { webview->load( url ); }
void ArticleView::cleanupTemp()
{
QSet< QString >::iterator it = desktopOpenedTempFiles.begin();
while( it != desktopOpenedTempFiles.end() )
{
if( QFile::remove( *it ) )
it = desktopOpenedTempFiles.erase( it );
else
++it;
}
}
bool ArticleView::handleF3( QObject * /*obj*/, QEvent * ev )
{
if ( ev->type() == QEvent::ShortcutOverride
|| ev->type() == QEvent::KeyPress )
{
QKeyEvent * ke = static_cast<QKeyEvent *>( ev );
if ( ke->key() == Qt::Key_F3 && isSearchOpened() ) {
if ( !ke->modifiers() )
{
if( ev->type() == QEvent::KeyPress )
on_searchNext_clicked();
ev->accept();
return true;
}
if ( ke->modifiers() == Qt::ShiftModifier )
{
if( ev->type() == QEvent::KeyPress )
on_searchPrevious_clicked();
ev->accept();
return true;
}
}
if ( ke->key() == Qt::Key_F3 && ftsSearchIsOpened )
{
if ( !ke->modifiers() )
{
if( ev->type() == QEvent::KeyPress )
on_ftsSearchNext_clicked();
ev->accept();
return true;
}
if ( ke->modifiers() == Qt::ShiftModifier )
{
if( ev->type() == QEvent::KeyPress )
on_ftsSearchPrevious_clicked();
ev->accept();
return true;
}
}
}
return false;
}
bool ArticleView::eventFilter( QObject * obj, QEvent * ev )
{
#ifdef Q_OS_MAC
2022-03-27 14:22:42 +00:00
2022-03-30 15:10:37 +00:00
if( ev->type() == QEvent::NativeGesture )
{
qDebug() << "it's a Native Gesture!";
// handle Qt::ZoomNativeGesture Qt::SmartZoomNativeGesture here
// ignore swipe left/right.
// QWebEngine can handle Qt::SmartZoomNativeGesture.
}
#else
if( ev->type() == QEvent::Gesture )
{
Gestures::GestureResult result;
QPoint pt;
bool handled = Gestures::handleGestureEvent( obj, ev, result, pt );
if( handled )
{
if( result == Gestures::ZOOM_IN )
zoomIn();
else
if( result == Gestures::ZOOM_OUT )
zoomOut();
else
if( result == Gestures::SWIPE_LEFT )
back();
else
if( result == Gestures::SWIPE_RIGHT )
forward();
else
if( result == Gestures::SWIPE_UP || result == Gestures::SWIPE_DOWN )
{
int delta = result == Gestures::SWIPE_UP ? -120 : 120;
QWidget *widget = static_cast< QWidget * >( obj );
QPoint angleDelta(0, delta);
QPoint pixelDetal;
QWidget *child = widget->childAt( widget->mapFromGlobal( pt ) );
if( child )
{
QWheelEvent whev( child->mapFromGlobal( pt ), pt, pixelDetal,angleDelta, Qt::NoButton, Qt::NoModifier,Qt::NoScrollPhase,false);
qApp->sendEvent( child, &whev );
}
else
{
QWheelEvent whev( widget->mapFromGlobal( pt ), pt,pixelDetal, angleDelta,Qt::NoButton, Qt::NoModifier,Qt::NoScrollPhase,false );
qApp->sendEvent( widget, &whev );
}
}
}
return handled;
}
#endif
if( ev->type() == QEvent::MouseMove )
{
if( Gestures::isFewTouchPointsPresented() )
{
ev->accept();
return true;
}
}
if ( handleF3( obj, ev ) )
{
return true;
}
if( obj == webview ) {
if( ev->type() == QEvent::MouseButtonPress ) {
QMouseEvent * event = static_cast< QMouseEvent * >( ev );
if ( event->button() == Qt::XButton1 ) {
back();
return true;
}
if ( event->button() == Qt::XButton2 ) {
forward();
return true;
}
}
else
if ( ev->type() == QEvent::KeyPress || ev->type ()==QEvent::ShortcutOverride)
{
QKeyEvent * keyEvent = static_cast< QKeyEvent * >( ev );
if ( keyEvent->modifiers() &
( Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier ) )
return false; // A non-typing modifier is pressed
if ( Utils::ignoreKeyEvent(keyEvent)||
keyEvent->key() == Qt::Key_Return ||
keyEvent->key() == Qt::Key_Enter )
return false; // Those key have other uses than to start typing
QString text = keyEvent->text();
if ( text.size() )
{
emit typingEvent( text );
return true;
}
}
2022-02-17 16:39:24 +00:00
else if( ev->type() == QEvent::Wheel )
{
QWheelEvent * pe = static_cast< QWheelEvent * >( ev );
if( pe->modifiers().testFlag( Qt::ControlModifier ) )
{
if( pe->angleDelta().y() > 0 )
{
zoomIn();
}
else
{
zoomOut();
}
}
}
}
else
return QWidget::eventFilter( obj, ev );
return false;
}
QString ArticleView::getMutedForGroup( unsigned group )
{
if ( dictionaryBarToggled && dictionaryBarToggled->isChecked() )
{
// Dictionary bar is active -- mute the muted dictionaries
Instances::Group const * groupInstance = groups.findGroup( group );
// Find muted dictionaries for current group
Config::Group const * grp = cfg.getGroup( group );
Config::MutedDictionaries const * mutedDictionaries;
if( group == Instances::Group::AllGroupId )
mutedDictionaries = popupView ? &cfg.popupMutedDictionaries : &cfg.mutedDictionaries;
else
mutedDictionaries = grp ? ( popupView ? &grp->popupMutedDictionaries : &grp->mutedDictionaries ) : 0;
if( !mutedDictionaries )
return QString();
QStringList mutedDicts;
if ( groupInstance )
{
for( unsigned x = 0; x < groupInstance->dictionaries.size(); ++x )
{
QString id = QString::fromStdString(
groupInstance->dictionaries[ x ]->getId() );
if ( mutedDictionaries->contains( id ) )
mutedDicts.append( id );
}
}
if ( mutedDicts.size() )
return mutedDicts.join( "," );
}
return QString();
}
2022-01-01 10:19:11 +00:00
QStringList ArticleView::getMutedDictionaries(unsigned group) {
if (dictionaryBarToggled && dictionaryBarToggled->isChecked()) {
// Dictionary bar is active -- mute the muted dictionaries
Instances::Group const *groupInstance = groups.findGroup(group);
// Find muted dictionaries for current group
Config::Group const *grp = cfg.getGroup(group);
Config::MutedDictionaries const *mutedDictionaries;
if (group == Instances::Group::AllGroupId)
mutedDictionaries = popupView ? &cfg.popupMutedDictionaries : &cfg.mutedDictionaries;
else
mutedDictionaries = grp ? (popupView ? &grp->popupMutedDictionaries : &grp->mutedDictionaries) : 0;
if (!mutedDictionaries)
return QStringList();
QStringList mutedDicts;
if (groupInstance) {
for (unsigned x = 0; x < groupInstance->dictionaries.size(); ++x) {
QString id = QString::fromStdString(groupInstance->dictionaries[x]->getId());
if (mutedDictionaries->contains(id))
mutedDicts.append(id);
}
}
return mutedDicts;
}
return QStringList();
2021-10-03 11:28:26 +00:00
}
2021-07-06 13:01:50 +00:00
void ArticleView::linkHovered ( const QString & link )
{
QString msg;
QUrl url(link);
if ( url.scheme() == "bres" )
{
msg = tr( "Resource" );
}
else
if ( url.scheme() == "gdau" || Dictionary::WebMultimediaDownload::isAudioUrl( url ) )
{
msg = tr( "Audio" );
}
else
2013-04-24 16:01:44 +00:00
if ( url.scheme() == "gdtts" )
{
msg = tr( "TTS Voice" );
}
else
2012-12-07 11:59:29 +00:00
if ( url.scheme() == "gdpicture" )
{
msg = tr( "Picture" );
}
else
2013-06-22 16:36:25 +00:00
if ( url.scheme() == "gdvideo" )
{
if ( url.path().isEmpty() )
{
msg = tr( "Video" );
}
else
{
QString path = url.path();
if ( path.startsWith( '/' ) )
{
path = path.mid( 1 );
}
msg = tr( "Video: %1" ).arg( path );
}
}
else
if (url.scheme() == "gdlookup" || url.scheme().compare( "bword" ) == 0)
{
QString def = url.path();
if (def.startsWith("/"))
{
def = def.mid( 1 );
}
if( Utils::Url::hasQueryItem( url, "dict" ) )
{
// Link to other dictionary
QString dictName( Utils::Url::queryItemValue( url, "dict" ) );
if( !dictName.isEmpty() )
msg = tr( "Definition from dictionary \"%1\": %2" ).arg( dictName , def );
}
if( msg.isEmpty() )
{
if( def.isEmpty() && url.hasFragment() )
msg = '#' + url.fragment(); // this must be a citation, footnote or backlink
else
msg = tr( "Definition: %1").arg( def );
}
}
else
{
msg = link;
}
emit statusBarMessage( msg );
}
void ArticleView::attachWebChannelToHtml() {
// set the web channel to be used by the page
// see http://doc.qt.io/qt-5/qwebenginepage.html#setWebChannel
webview->page()->setWebChannel(channel, QWebEngineScript::MainWorld);
2021-07-06 13:01:50 +00:00
// register QObjects to be exposed to JavaScript
channel->registerObject(QStringLiteral("articleview"), agent);
}
void ArticleView::linkClicked( QUrl const & url_ )
{
Qt::KeyboardModifiers kmod = QApplication::keyboardModifiers();
// Lock jump on links while Alt key is pressed
if( kmod & Qt::AltModifier )
return;
QUrl url( url_ );
Contexts contexts;
tryMangleWebsiteClickedUrl( url, contexts );
if( !popupView && ( webview->isMidButtonPressed() || ( kmod & ( Qt::ControlModifier | Qt::ShiftModifier ) ) )
&& !isAudioLink( url ) ) {
// Mid button or Control/Shift is currently pressed - open the link in new tab
webview->resetMidButtonPressed();
emit openLinkInNewTab( url, webview->url(), getCurrentArticle(), contexts );
}
else
openLink( url, webview->url(), getCurrentArticle(), contexts );
}
void ArticleView::linkClickedInHtml( QUrl const & url_ )
{
emit webview->linkClickedInHtml( url_ );
if( !url_.isEmpty() ) {
linkClicked( url_ );
}
}
void ArticleView::openLink( QUrl const & url, QUrl const & ref, QString const & scrollTo, Contexts const & contexts_ )
{
audioPlayer->stop();
qDebug() << "open link url:" << url;
2022-06-07 00:05:49 +00:00
auto [valid, word] = Utils::Url::getQueryWord( url );
if( valid && word.isEmpty() )
{
// invalid gdlookup url.
return;
}
2015-10-28 19:56:58 +00:00
Contexts contexts( contexts_ );
2012-12-07 11:59:29 +00:00
if( url.scheme().compare( "gdpicture" ) == 0 )
load( url );
2012-12-07 11:59:29 +00:00
else
2022-06-07 00:11:10 +00:00
if ( url.scheme().compare( "bword" ) == 0 || url.scheme().compare( "entry" ) == 0 )
{
if( Utils::Url::hasQueryItem( ref, "dictionaries" ) )
{
QStringList dictsList = Utils::Url::queryItemValue( ref, "dictionaries" )
.split( ",", Qt::SkipEmptyParts );
showDefinition( word, dictsList, QRegExp(), getGroup( ref ), false );
}
else
showDefinition( word,
getGroup( ref ), scrollTo, contexts );
}
else
if ( url.scheme() == "gdlookup" ) // Plain html links inherit gdlookup scheme
{
if ( url.hasFragment() )
{
webview->page()->runJavaScript(
QString( "window.location = \"%1\"" ).arg( QString::fromUtf8( url.toEncoded() ) ) );
}
else
{
if( Utils::Url::hasQueryItem( ref, "dictionaries" ) )
{
// Specific dictionary group from full-text search
QStringList dictsList = Utils::Url::queryItemValue( ref, "dictionaries" )
.split( ",", Qt::SkipEmptyParts );
showDefinition( url.path().mid( 1 ), dictsList, QRegExp(), getGroup( ref ), false );
return;
}
QString newScrollTo( scrollTo );
if( Utils::Url::hasQueryItem( url, "dict" ) )
{
// Link to other dictionary
QString dictName( Utils::Url::queryItemValue( url, "dict" ) );
for( unsigned i = 0; i < allDictionaries.size(); i++ )
{
if( dictName.compare( QString::fromUtf8( allDictionaries[ i ]->getName().c_str() ) ) == 0 )
{
newScrollTo = scrollToFromDictionaryId( QString::fromUtf8( allDictionaries[ i ]->getId().c_str() ) );
break;
}
}
}
2015-10-28 19:56:58 +00:00
if( Utils::Url::hasQueryItem( url, "gdanchor" ) )
contexts[ "gdanchor" ] = Utils::Url::queryItemValue( url, "gdanchor" );
2015-10-28 19:56:58 +00:00
showDefinition( word,
getGroup( ref ), newScrollTo, contexts );
}
}
else
2013-06-22 16:36:25 +00:00
if ( url.scheme() == "bres" || url.scheme() == "gdau" || url.scheme() == "gdvideo" ||
Dictionary::WebMultimediaDownload::isAudioUrl( url ) )
{
// Download it
// Clear any pending ones
resourceDownloadRequests.clear();
resourceDownloadUrl = url;
if ( Dictionary::WebMultimediaDownload::isAudioUrl( url ) )
{
sptr< Dictionary::DataRequest > req =
std::make_shared<Dictionary::WebMultimediaDownload>( url, articleNetMgr );
resourceDownloadRequests.push_back( req );
connect( req.get(), &Dictionary::Request::finished, this, &ArticleView::resourceDownloadFinished );
}
else
if ( url.scheme() == "gdau" && 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 )
2017-04-27 20:55:53 +00:00
{
unsigned preferred = UINT_MAX;
if( url.hasFragment() )
{
// Find sound in the preferred dictionary
QString preferredName = Utils::Url::fragment( url );
try
{
for( unsigned x = 0; x < activeDicts->size(); ++x )
{
if( preferredName.compare( QString::fromUtf8( (*activeDicts)[ x ]->getName().c_str() ) ) == 0 )
{
preferred = x;
sptr< Dictionary::DataRequest > req =
(*activeDicts)[ x ]->getResource(
url.path().mid( 1 ).toUtf8().data() );
resourceDownloadRequests.push_back( req );
if ( !req->isFinished() )
{
// Queued loading
connect( req.get(), &Dictionary::Request::finished, this, &ArticleView::resourceDownloadFinished );
}
else
{
// Immediate loading
if( req->dataSize() > 0 )
{
// Resource already found, stop next search
resourceDownloadFinished();
return;
}
}
break;
}
}
}
catch( std::exception & e )
{
emit statusBarMessage(
tr("ERROR: %1").arg(e.what()),
10000, QPixmap(":/icons/error.svg"));
}
}
for( unsigned x = 0; x < activeDicts->size(); ++x )
{
try
{
if( x == preferred )
continue;
sptr< Dictionary::DataRequest > req =
(*activeDicts)[ x ]->getResource(
url.path().mid( 1 ).toUtf8().data() );
2017-04-27 20:55:53 +00:00
resourceDownloadRequests.push_back( req );
if ( !req->isFinished() )
{
// Queued loading
connect( req.get(), &Dictionary::Request::finished, this, &ArticleView::resourceDownloadFinished );
}
else
{
// Immediate loading
if( req->dataSize() > 0 )
{
// Resource already found, stop next search
break;
}
}
}
catch( std::exception & e )
{
emit statusBarMessage(
tr("ERROR: %1").arg(e.what()),
10000, QPixmap(":/icons/error.svg"));
}
}
2017-04-27 20:55:53 +00:00
}
}
else
{
// Normal resource download
QString contentType;
sptr< Dictionary::DataRequest > req =
articleNetMgr.getResource( url, contentType );
if ( !req.get() )
{
// Request failed, fail
}
else
if ( req->isFinished() && req->dataSize() >= 0 )
{
// Have data ready, handle it
resourceDownloadRequests.push_back( req );
resourceDownloadFinished();
return;
}
else
if ( !req->isFinished() )
{
// Queue to be handled when done
resourceDownloadRequests.push_back( req );
connect( req.get(), &Dictionary::Request::finished, this, &ArticleView::resourceDownloadFinished );
}
}
if ( resourceDownloadRequests.empty() ) // No requests were queued
{
2013-02-01 12:36:01 +00:00
QMessageBox::critical( this, "GoldenDict", tr( "The referenced resource doesn't exist." ) );
return;
}
else
resourceDownloadFinished(); // Check any requests finished already
}
else
if ( url.scheme() == "gdprg" )
{
// Program. Run it.
QString id( url.host() );
for( Config::Programs::const_iterator i = cfg.programs.begin();
i != cfg.programs.end(); ++i )
{
if ( i->id == id )
{
// Found the corresponding program.
Programs::RunInstance * req = new Programs::RunInstance;
connect( req, &Programs::RunInstance::finished, req, &QObject::deleteLater );
QString error;
// Delete the request if it fails to start
if ( !req->start( *i, url.path().mid( 1 ), error ) )
{
delete req;
2013-02-01 12:36:01 +00:00
QMessageBox::critical( this, "GoldenDict",
error );
}
return;
}
}
// Still here? No such program exists.
2013-02-01 12:36:01 +00:00
QMessageBox::critical( this, "GoldenDict",
tr( "The referenced audio program doesn't exist." ) );
}
else
if ( url.scheme() == "gdtts" )
{
// TODO: Port TTS
#if defined( Q_OS_WIN32 ) || defined( Q_OS_MAC )
// Text to speech
QString md5Id = Utils::Url::queryItemValue( url, "engine" );
QString text( url.path().mid( 1 ) );
for ( Config::VoiceEngines::const_iterator i = cfg.voiceEngines.begin();
i != cfg.voiceEngines.end(); ++i )
{
QString itemMd5Id = QString( QCryptographicHash::hash(
i->id.toUtf8(),
2013-04-24 16:01:44 +00:00
QCryptographicHash::Md5 ).toHex() );
2013-04-24 16:01:44 +00:00
if ( itemMd5Id == md5Id )
{
SpeechClient * speechClient = new SpeechClient( *i, this );
connect( speechClient, SIGNAL( finished() ), speechClient, SLOT( deleteLater() ) );
2013-04-24 16:01:44 +00:00
speechClient->tell( text );
break;
}
}
#endif
}
else
if ( isExternalLink( url ) )
{
// Use the system handler for the conventional external links
QDesktopServices::openUrl( url );
}
}
2017-04-27 20:55:53 +00:00
ResourceToSaveHandler * ArticleView::saveResource( const QUrl & url, const QString & fileName )
{
return saveResource( url, webview->url(), fileName );
}
2017-04-27 20:55:53 +00:00
ResourceToSaveHandler * ArticleView::saveResource( const QUrl & url, const QUrl & ref, const QString & fileName )
{
2017-04-27 20:55:53 +00:00
ResourceToSaveHandler * handler = new ResourceToSaveHandler( this, fileName );
sptr< Dictionary::DataRequest > req;
if( url.scheme() == "bres" || url.scheme() == "gico" || url.scheme() == "gdau" || url.scheme() == "gdvideo" )
{
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 )
{
unsigned preferred = UINT_MAX;
if( url.hasFragment() && url.scheme() == "gdau" )
{
// Find sound in the preferred dictionary
QString preferredName = Utils::Url::fragment( url );
for( unsigned x = 0; x < activeDicts->size(); ++x )
{
try
{
if( preferredName.compare( QString::fromUtf8( (*activeDicts)[ x ]->getName().c_str() ) ) == 0 )
{
preferred = x;
sptr< Dictionary::DataRequest > req =
(*activeDicts)[ x ]->getResource(
url.path().mid( 1 ).toUtf8().data() );
handler->addRequest( req );
if( req->isFinished() && req->dataSize() > 0 )
{
handler->downloadFinished();
return handler;
}
break;
}
}
catch( std::exception & e )
{
gdWarning( "getResource request error (%s) in \"%s\"\n", e.what(),
(*activeDicts)[ x ]->getName().c_str() );
}
}
}
for( unsigned x = 0; x < activeDicts->size(); ++x )
{
try
{
if( x == preferred )
continue;
req = (*activeDicts)[ x ]->getResource(
Utils::Url::path( url ).mid( 1 ).toUtf8().data() );
2017-04-27 20:55:53 +00:00
handler->addRequest( req );
if( req->isFinished() && req->dataSize() > 0 )
{
// Resource already found, stop next search
break;
}
}
catch( std::exception & e )
{
gdWarning( "getResource request error (%s) in \"%s\"\n", e.what(),
(*activeDicts)[ x ]->getName().c_str() );
}
}
}
}
else
{
// Normal resource download
QString contentType;
req = articleNetMgr.getResource( url, contentType );
2013-10-18 14:32:58 +00:00
if( req.get() )
{
2017-04-27 20:55:53 +00:00
handler->addRequest( req );
2013-10-18 14:32:58 +00:00
}
}
}
else
{
req = std::make_shared<Dictionary::WebMultimediaDownload>( url, articleNetMgr );
2017-04-27 20:55:53 +00:00
handler->addRequest( req );
}
2017-04-27 20:55:53 +00:00
if ( handler->isEmpty() ) // No requests were queued
{
emit statusBarMessage(
tr("ERROR: %1").arg(tr("The referenced resource doesn't exist.")),
10000, QPixmap(":/icons/error.svg"));
}
2017-04-27 20:55:53 +00:00
// Check already finished downloads
handler->downloadFinished();
return handler;
}
void ArticleView::updateMutedContents()
{
QUrl currentUrl = webview->url();
if ( currentUrl.scheme() != "gdlookup" )
return; // Weird url -- do nothing
unsigned group = getGroup( currentUrl );
if ( !group )
return; // No group in url -- do nothing
QString mutedDicts = getMutedForGroup( group );
if ( Utils::Url::queryItemValue( currentUrl, "muted" ) != mutedDicts )
{
// The list has changed -- update the url
Utils::Url::removeQueryItem( currentUrl, "muted" );
if ( mutedDicts.size() )
Utils::Url::addQueryItem( currentUrl, "muted", mutedDicts );
load( currentUrl );
//QApplication::setOverrideCursor( Qt::WaitCursor );
webview->setCursor( Qt::WaitCursor );
}
}
bool ArticleView::canGoBack()
{
// First entry in a history is always an empty page,
// so we skip it.
return webview->history()->currentItemIndex() > 1;
}
bool ArticleView::canGoForward() { return webview->history()->canGoForward(); }
void ArticleView::setSelectionBySingleClick( bool set ) { webview->setSelectionBySingleClick( set ); }
void ArticleView::setDelayedHighlightText(QString const & text)
{
delayedHighlightText = text;
}
void ArticleView::back()
{
// Don't allow navigating back to page 0, which is usually the initial
// empty page
if ( canGoBack() )
{
currentActiveDictIds.clear();
historyMode = true;
webview->back();
}
}
void ArticleView::forward()
{
currentActiveDictIds.clear();
historyMode = true;
webview->forward();
}
void ArticleView::reload() { webview->reload(); }
2022-03-30 15:08:24 +00:00
void ArticleView::hasSound( const std::function< void( bool ) > & callback )
{
webview->page()->runJavaScript( "if(typeof(gdAudioLinks)!=\"undefined\") gdAudioLinks.first",
[ callback ]( const QVariant & v ) {
bool has = false;
if( v.type() == QVariant::String )
has = !v.toString().isEmpty();
callback( has );
} );
}
2021-12-13 14:45:16 +00:00
//use webengine javascript to playsound
void ArticleView::playSound()
{
QString variable = " (function(){ var link=gdAudioLinks[gdAudioLinks.current]; "
2021-11-24 14:38:37 +00:00
" if(link==undefined){ "
" link=gdAudioLinks.first; "
" } "
" return link;})(); ";
2022-01-31 00:42:36 +00:00
webview->page()->runJavaScript( variable, [ this ]( const QVariant & result ) {
if( result.type() == QVariant::String ) {
QString soundScript = result.toString();
if( !soundScript.isEmpty() )
openLink( QUrl::fromEncoded( soundScript.toUtf8() ), webview->url() );
}
} );
}
2022-03-30 15:08:24 +00:00
void ArticleView::toHtml( const std::function< void( QString & ) > & callback )
2009-05-01 11:17:29 +00:00
{
webview->page()->toHtml( [ = ]( const QString & content ) {
QString html = content;
callback( html );
} );
2009-05-01 11:17:29 +00:00
}
void ArticleView::setHtml( const QString & content, const QUrl & baseUrl )
{
webview->page()->setHtml( content, baseUrl );
2021-08-21 01:41:40 +00:00
}
void ArticleView::setContent( const QByteArray & data, const QString & mimeType, const QUrl & baseUrl )
2009-05-01 11:17:29 +00:00
{
webview->page()->setContent( data, mimeType, baseUrl );
2009-05-01 11:17:29 +00:00
}
QString ArticleView::getTitle() { return webview->page()->title(); }
Config::InputPhrase ArticleView::getPhrase() const
{
const QUrl url = webview->url();
return Config::InputPhrase( Utils::Url::queryItemValue( url, "word" ),
Utils::Url::queryItemValue( url, "punctuation_suffix" ) );
}
2009-05-01 12:20:33 +00:00
void ArticleView::print( QPrinter * printer ) const
{
2022-03-12 10:12:17 +00:00
QEventLoop loop;
bool result;
auto printPreview = [ & ]( bool success )
{
result = success;
loop.quit();
};
2022-03-29 12:34:41 +00:00
#if( QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) )
webview->page()->print( printer, std::move( printPreview ) );
2022-03-29 12:34:41 +00:00
#else
connect( webview, &QWebEngineView::printFinished, &loop, std::move( printPreview ) );
webview->print( printer );
2022-03-29 12:34:41 +00:00
#endif
2022-03-12 10:12:17 +00:00
loop.exec();
if( !result )
{
qDebug() << "print failed";
}
2009-05-01 12:20:33 +00:00
}
void ArticleView::contextMenuRequested( QPoint const & pos )
{
// Is that a link? Is there a selection?
QWebEnginePage * r = webview->page();
QMenu menu( this );
QAction * followLink = 0;
QAction * followLinkExternal = 0;
QAction * followLinkNewTab = 0;
QAction * lookupSelection = 0;
QAction * sendToAnkiAction = 0 ;
QAction * lookupSelectionGr = 0;
QAction * lookupSelectionNewTab = 0;
QAction * lookupSelectionNewTabGr = 0;
QAction * maxDictionaryRefsAction = 0;
QAction * addWordToHistoryAction = 0;
QAction * addHeaderToHistoryAction = 0;
QAction * sendWordToInputLineAction = 0;
QAction * saveImageAction = 0;
2022-02-27 05:17:37 +00:00
QAction * saveSoundAction = 0;
QAction * saveBookmark = 0;
2022-02-27 05:17:37 +00:00
#if( QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) )
const QWebEngineContextMenuData * menuData = &(r->contextMenuData());
#else
QWebEngineContextMenuRequest * menuData = webview->lastContextMenuRequest();
2022-02-27 05:17:37 +00:00
#endif
QUrl targetUrl(menuData->linkUrl());
Contexts contexts;
tryMangleWebsiteClickedUrl( targetUrl, contexts );
2021-12-29 12:09:29 +00:00
if ( !targetUrl.isEmpty() )
{
if ( !isExternalLink( targetUrl ) )
{
followLink = new QAction( tr( "Op&en Link" ), &menu );
menu.addAction( followLink );
if( !popupView && !isAudioLink( targetUrl ) )
{
followLinkNewTab = new QAction( QIcon( ":/icons/addtab.svg" ),
tr( "Open Link in New &Tab" ), &menu );
menu.addAction( followLinkNewTab );
}
}
2021-12-29 12:09:29 +00:00
if ( isExternalLink( targetUrl ) )
{
followLinkExternal = new QAction( tr( "Open Link in &External Browser" ), &menu );
menu.addAction( followLinkExternal );
menu.addAction( webview->pageAction( QWebEnginePage::CopyLinkToClipboard ) );
}
}
2022-01-02 08:30:16 +00:00
QUrl imageUrl;
2022-02-27 05:17:37 +00:00
#if( QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) )
if( !popupView && menuData->mediaType ()==QWebEngineContextMenuData::MediaTypeImage)
#else
if( !popupView && menuData->mediaType ()==QWebEngineContextMenuRequest::MediaType::MediaTypeImage)
#endif
2022-01-02 08:30:16 +00:00
{
imageUrl = menuData->mediaUrl();
if( !imageUrl.isEmpty() ) {
menu.addAction( webview->pageAction( QWebEnginePage::CopyImageToClipboard ) );
saveImageAction = new QAction( tr( "Save &image..." ), &menu );
menu.addAction( saveImageAction );
}
2022-01-02 08:30:16 +00:00
}
if( !popupView && isAudioLink( targetUrl ) )
2022-01-02 08:30:16 +00:00
{
saveSoundAction = new QAction( tr( "Save s&ound..." ), &menu );
menu.addAction( saveSoundAction );
}
QString selectedText = webview->selectedText();
QString text = Utils::trimNonChar( selectedText );
if ( text.size() && text.size() < 60 )
{
// We don't prompt for selections larger or equal to 60 chars, since
// it ruins the menu and it's hardly a single word anyway.
if( text.isRightToLeft() )
{
text.insert( 0, (ushort)0x202E ); // RLE, Right-to-Left Embedding
text.append( (ushort)0x202C ); // PDF, POP DIRECTIONAL FORMATTING
}
lookupSelection = new QAction( tr( "&Look up \"%1\"" ).
arg( text ),
&menu );
menu.addAction( lookupSelection );
if ( !popupView )
{
lookupSelectionNewTab = new QAction( QIcon( ":/icons/addtab.svg" ),
tr( "Look up \"%1\" in &New Tab" ).
arg( text ),
&menu );
menu.addAction( lookupSelectionNewTab );
sendWordToInputLineAction = new QAction( tr( "Send \"%1\" to input line" ).
arg( text ),
&menu );
menu.addAction( sendWordToInputLineAction );
}
addWordToHistoryAction = new QAction( tr( "&Add \"%1\" to history" ).
arg( text ),
&menu );
menu.addAction( addWordToHistoryAction );
Instances::Group const * altGroup =
( groupComboBox && groupComboBox->getCurrentGroup() != getGroup( webview->url() ) ) ?
groups.findGroup( groupComboBox->getCurrentGroup() ) :
0;
if ( altGroup )
{
QIcon icon = altGroup->icon.size() ? QIcon( ":/flags/" + altGroup->icon ) :
QIcon();
2009-05-24 16:40:53 +00:00
lookupSelectionGr = new QAction( icon, tr( "Look up \"%1\" in %2" ).
arg( text ).
arg( altGroup->name ), &menu );
menu.addAction( lookupSelectionGr );
if ( !popupView )
{
lookupSelectionNewTabGr = new QAction( QIcon( ":/icons/addtab.svg" ),
tr( "Look up \"%1\" in %2 in &New Tab" ).
arg( text ).
arg( altGroup->name ), &menu );
menu.addAction( lookupSelectionNewTabGr );
}
}
}
if(text.size())
{
// avoid too long in the menu ,use left 30 characters.
saveBookmark = new QAction( tr( "Save &Bookmark \"%1...\"" ).arg( text.left( 30 ) ), &menu );
menu.addAction( saveBookmark );
}
// add anki menu
if( !text.isEmpty() && cfg.preferences.ankiConnectServer.enabled )
{
QString txt = webview->title();
sendToAnkiAction = new QAction( tr( "&Send \"%1\" to anki with selected text." ).arg( txt ), &menu );
menu.addAction( sendToAnkiAction );
}
if( text.isEmpty() && !cfg.preferences.storeHistory)
{
QString txt = webview->title();
if( txt.size() > 60 )
txt = txt.left( 60 ) + "...";
addHeaderToHistoryAction = new QAction( tr( "&Add \"%1\" to history" ).
arg( txt ),
&menu );
menu.addAction( addHeaderToHistoryAction );
}
if ( selectedText.size() )
{
menu.addAction( webview->pageAction( QWebEnginePage::Copy ) );
menu.addAction( &copyAsTextAction );
}
else
{
menu.addAction( &selectCurrentArticleAction );
menu.addAction( webview->pageAction( QWebEnginePage::SelectAll ) );
}
map< QAction *, QString > tableOfContents;
// Add table of contents
QStringList ids = getArticlesList();
if ( !menu.isEmpty() && ids.size() )
menu.addSeparator();
unsigned refsAdded = 0;
bool maxDictionaryRefsReached = false;
for( QStringList::const_iterator i = ids.constBegin(); i != ids.constEnd();
++i, ++refsAdded )
{
// Find this dictionary
for( unsigned x = allDictionaries.size(); x--; )
{
if ( allDictionaries[ x ]->getId() == i->toUtf8().data() )
{
QAction * action = 0;
if ( refsAdded == cfg.preferences.maxDictionaryRefsInContextMenu )
{
// Enough! Or the menu would become too large.
maxDictionaryRefsAction = new QAction( ".........", &menu );
action = maxDictionaryRefsAction;
maxDictionaryRefsReached = true;
}
else
{
action = new QAction(
allDictionaries[ x ]->getIcon(),
QString::fromUtf8( allDictionaries[ x ]->getName().c_str() ),
&menu );
2018-07-07 09:33:15 +00:00
// Force icons in menu on all platforms,
// since without them it will be much harder
// to find things.
action->setIconVisibleInMenu( true );
}
menu.addAction( action );
tableOfContents[ action ] = *i;
break;
}
}
if( maxDictionaryRefsReached )
break;
}
menu.addSeparator();
if(!popupView||cfg.pinPopupWindow)
menu.addAction( &inspectAction );
if ( !menu.isEmpty() )
{
connect( this, &ArticleView::closePopupMenu, &menu, &QWidget::close );
QAction * result = menu.exec( webview->mapToGlobal( pos ) );
if ( !result )
return;
if( result == followLink )
openLink( targetUrl, webview->url(), getCurrentArticle(), contexts );
else if( result == followLinkExternal )
2021-12-29 12:09:29 +00:00
QDesktopServices::openUrl( targetUrl );
else if( result == lookupSelection )
showDefinition( text, getGroup( webview->url() ), getCurrentArticle() );
else if( result == saveBookmark ) {
emit saveBookmarkSignal( text.left( 60 ) );
}
else if( result == sendToAnkiAction ) {
sendToAnki( webview->title(), webview->selectedText(), translateLine->text() );
}
else
if ( result == lookupSelectionGr && groupComboBox )
showDefinition( selectedText, groupComboBox->getCurrentGroup(), QString() );
else
if ( result == addWordToHistoryAction )
emit forceAddWordToHistory( selectedText );
if( result == addHeaderToHistoryAction )
emit forceAddWordToHistory( webview->title() );
else if( result == sendWordToInputLineAction )
emit sendWordToInputLine( selectedText );
else if( !popupView && result == followLinkNewTab )
emit openLinkInNewTab( targetUrl, webview->url(), getCurrentArticle(), contexts );
else if( !popupView && result == lookupSelectionNewTab )
emit showDefinitionInNewTab( selectedText, getGroup( webview->url() ), getCurrentArticle(), Contexts() );
else if( !popupView && result == lookupSelectionNewTabGr && groupComboBox )
emit showDefinitionInNewTab( selectedText, groupComboBox->getCurrentGroup(), QString(), Contexts() );
else if( result == saveImageAction || result == saveSoundAction ) {
QUrl url = ( result == saveImageAction ) ? imageUrl : targetUrl;
QString savePath;
QString fileName;
if ( cfg.resourceSavePath.isEmpty() )
savePath = QDir::homePath();
else
{
savePath = QDir::fromNativeSeparators( cfg.resourceSavePath );
if ( !QDir( savePath ).exists() )
savePath = QDir::homePath();
}
QString name = Utils::Url::path( url ).section( '/', -1 );
if ( result == saveSoundAction )
{
// Audio data
// if ( name.indexOf( '.' ) < 0 )
// name += ".wav";
fileName = savePath + "/" + name;
fileName = QFileDialog::getSaveFileName( parentWidget(), tr( "Save sound" ),
fileName,
tr( "Sound files (*.wav *.ogg *.oga *.mp3 *.mp4 *.aac *.flac *.mid *.wv *.ape *.spx);;All files (*.*)" ) );
}
else
{
// Image data
// Check for babylon image name
if ( name[ 0 ] == '\x1E' )
name.remove( 0, 1 );
if ( name.length() && 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 (*.*)" ) );
}
if ( !fileName.isEmpty() )
{
QFileInfo fileInfo( fileName );
emit storeResourceSavePath( QDir::toNativeSeparators( fileInfo.absoluteDir().absolutePath() ) );
saveResource( url, webview->url(), fileName );
}
}
else
{
if ( !popupView && result == maxDictionaryRefsAction )
emit showDictsPane();
// Match against table of contents
QString id = tableOfContents[ result ];
if ( id.size() )
setCurrentArticle( scrollToFromDictionaryId( id ), true );
}
}
qDebug()<< "title = "<< r->title();
2021-09-22 04:01:17 +00:00
}
void ArticleView::resourceDownloadFinished()
{
if ( resourceDownloadRequests.empty() )
return; // Stray signal
// Find any finished resources
for( list< sptr< Dictionary::DataRequest > >::iterator i =
resourceDownloadRequests.begin(); i != resourceDownloadRequests.end(); )
{
if ( (*i)->isFinished() )
{
if ( (*i)->dataSize() >= 0 )
{
// Ok, got one finished, all others are irrelevant now
vector< char > const & data = (*i)->getFullData();
if ( resourceDownloadUrl.scheme() == "gdau" ||
Dictionary::WebMultimediaDownload::isAudioUrl( resourceDownloadUrl ) )
{
// Audio data
connect( audioPlayer.data(),
&AudioPlayerInterface::error,
this,
&ArticleView::audioPlayerError,
Qt::UniqueConnection );
QString errorMessage = audioPlayer->play( data.data(), data.size() );
if( !errorMessage.isEmpty() )
QMessageBox::critical( this, "GoldenDict", tr( "Failed to play sound file: %1" ).arg( errorMessage ) );
}
else
{
// Create a temporary file
// Remove the ones previously used, if any
cleanupTemp();
QString fileName;
{
QTemporaryFile tmp(
QDir::temp().filePath( "XXXXXX-" + resourceDownloadUrl.path().section( '/', -1 ) ), this );
2012-10-31 13:58:35 +00:00
if ( !tmp.open() || (size_t) tmp.write( &data.front(), data.size() ) != data.size() )
{
2013-02-01 12:36:01 +00:00
QMessageBox::critical( this, "GoldenDict", tr( "Failed to create temporary file." ) );
return;
}
tmp.setAutoRemove( false );
desktopOpenedTempFiles.insert( fileName = tmp.fileName() );
}
if ( !QDesktopServices::openUrl( QUrl::fromLocalFile( fileName ) ) )
2013-02-01 12:36:01 +00:00
QMessageBox::critical( this, "GoldenDict",
tr( "Failed to auto-open resource file, try opening manually: %1." ).arg( fileName ) );
}
// Ok, whatever it was, it's finished. Remove this and any other
// requests and finish.
resourceDownloadRequests.clear();
return;
}
else
{
// This one had no data. Erase it.
resourceDownloadRequests.erase( i++ );
}
}
2017-04-27 20:55:53 +00:00
else // Unfinished, wait.
break;
}
if ( resourceDownloadRequests.empty() )
{
// emit statusBarMessage(
// tr("WARNING: %1").arg(tr("The referenced resource failed to download.")),
// 10000, QPixmap(":/icons/error.svg"));
}
}
void ArticleView::audioPlayerError( QString const & message )
{
emit statusBarMessage(tr("WARNING: Audio Player: %1").arg(message),
10000, QPixmap(":/icons/error.svg"));
}
void ArticleView::pasteTriggered()
{
Config::InputPhrase phrase = cfg.preferences.sanitizeInputPhrase( QApplication::clipboard()->text() );
if( phrase.isValid() ) {
unsigned groupId = getGroup( webview->url() );
if( groupId == 0 ) {
// We couldn't figure out the group out of the URL,
// so let's try the currently selected group.
groupId = groupComboBox->getCurrentGroup();
}
showDefinition( phrase, groupId, getCurrentArticle() );
}
}
void ArticleView::moveOneArticleUp()
{
QString current = getCurrentArticle();
if ( current.size() )
{
QStringList lst = getArticlesList();
int idx = lst.indexOf( dictionaryIdFromScrollTo( current ) );
if ( idx != -1 )
{
--idx;
if ( idx < 0 )
idx = lst.size() - 1;
setCurrentArticle( scrollToFromDictionaryId( lst[ idx ] ), true );
}
}
}
void ArticleView::moveOneArticleDown()
{
QString current = getCurrentArticle();
QString currentDictId = dictionaryIdFromScrollTo( current );
QStringList lst = getArticlesList();
// if current article is empty .use the first as default.
if( currentDictId.isEmpty() && !lst.isEmpty() )
{
currentDictId = lst[ 0 ];
}
int idx = lst.indexOf( currentDictId );
if( idx != -1 )
{
idx = ( idx + 1 ) % lst.size();
setCurrentArticle( scrollToFromDictionaryId( lst[ idx ] ), true );
}
}
2009-05-16 11:14:43 +00:00
void ArticleView::openSearch()
{
2014-04-16 16:18:28 +00:00
if( !isVisible() )
return;
if( ftsSearchIsOpened )
closeSearch();
2009-05-16 11:14:43 +00:00
if ( !searchIsOpened )
{
searchPanel->show();
searchText->setText( getTitle() );
2009-05-16 11:14:43 +00:00
searchIsOpened = true;
}
searchText->setFocus();
searchText->selectAll();
2009-05-16 11:14:43 +00:00
// Clear any current selection
if( webview->selectedText().size() ) {
webview->page()->runJavaScript( "window.getSelection().removeAllRanges();_=0;" );
2009-05-16 11:14:43 +00:00
}
if( searchText->property( "noResults" ).toBool() ) {
searchText->setProperty( "noResults", false );
// Reload stylesheet
reloadStyleSheet();
2009-05-16 11:14:43 +00:00
}
}
void ArticleView::on_searchPrevious_clicked()
{
if( searchIsOpened )
performFindOperation( false, true );
2009-05-16 11:14:43 +00:00
}
void ArticleView::on_searchNext_clicked()
{
if( searchIsOpened )
performFindOperation( false, false );
2009-05-16 11:14:43 +00:00
}
void ArticleView::on_searchText_textEdited()
{
performFindOperation( true, false );
}
void ArticleView::on_searchText_returnPressed()
{
on_searchNext_clicked();
}
void ArticleView::on_searchCloseButton_clicked()
{
closeSearch();
}
void ArticleView::on_searchCaseSensitive_clicked()
{
performFindOperation( true, false );
}
void ArticleView::on_highlightAllButton_clicked()
{
performFindOperation( false, false, true );
}
//the id start with "gdform-"
void ArticleView::onJsActiveArticleChanged(QString const & id)
{
if ( !isScrollTo( id ) )
return; // Incorrect id
2022-02-25 14:48:43 +00:00
QString dictId = dictionaryIdFromScrollTo( id );
setActiveArticleId( dictId );
emit activeArticleChanged( this, dictId );
}
void ArticleView::doubleClicked( QPoint pos )
{
// We might want to initiate translation of the selected word
audioPlayer->stop();
if( cfg.preferences.doubleClickTranslates ) {
QString selectedText = webview->selectedText();
// ignore empty word;
if( selectedText.isEmpty() )
return;
emit sendWordToInputLine( selectedText );
// Do some checks to make sure there's a sensible selection indeed
if ( Folding::applyWhitespaceOnly( gd::toWString( selectedText ) ).size() &&
selectedText.size() < 60 )
{
// Initiate translation
Qt::KeyboardModifiers kmod = QApplication::keyboardModifiers();
if( kmod & ( Qt::ControlModifier | Qt::ShiftModifier ) ) { // open in new tab
emit showDefinitionInNewTab( selectedText, getGroup( webview->url() ), getCurrentArticle(), Contexts() );
}
else {
QUrl const & ref = webview->url();
if( Utils::Url::hasQueryItem( ref, "dictionaries" ) )
{
QStringList dictsList = Utils::Url::queryItemValue(ref, "dictionaries" )
.split( ",", Qt::SkipEmptyParts );
showDefinition( selectedText, dictsList, QRegExp(), getGroup( ref ), false );
}
else
showDefinition( selectedText, getGroup( ref ), getCurrentArticle() );
}
}
}
}
void ArticleView::performFindOperation( bool restart, bool backwards, bool checkHighlight )
2009-05-16 11:14:43 +00:00
{
QString text = searchText->text();
2009-05-16 11:14:43 +00:00
if ( restart || checkHighlight )
2009-05-16 11:14:43 +00:00
{
if( restart ) {
// Anyone knows how we reset the search position?
// For now we resort to this hack:
if( webview->selectedText().size() ) {
webview->page()->runJavaScript( "window.getSelection().removeAllRanges();_=0;" );
}
2009-05-16 11:14:43 +00:00
}
2021-07-06 13:01:50 +00:00
QWebEnginePage::FindFlags f( 0 );
if( searchCaseSensitive->isChecked() )
2021-07-06 13:01:50 +00:00
f |= QWebEnginePage::FindCaseSensitively;
webview->findText( "", f );
if( highlightAllButton->isChecked() )
webview->findText( text, f );
if( checkHighlight )
return;
2009-05-16 11:14:43 +00:00
}
2021-07-06 13:01:50 +00:00
QWebEnginePage::FindFlags f( 0 );
2009-05-16 11:14:43 +00:00
if( searchCaseSensitive->isChecked() )
2021-07-06 13:01:50 +00:00
f |= QWebEnginePage::FindCaseSensitively;
2009-05-16 11:14:43 +00:00
if( backwards )
2021-07-06 13:01:50 +00:00
f |= QWebEnginePage::FindBackward;
2009-05-16 11:14:43 +00:00
findText( text,
f,
2022-08-17 18:36:17 +00:00
[ text, this ]( bool match )
{
bool setMark = !text.isEmpty() && !match;
2009-05-16 11:14:43 +00:00
if( searchText->property( "noResults" ).toBool() != setMark ) {
searchText->setProperty( "noResults", setMark );
// Reload stylesheet
reloadStyleSheet();
}
} );
}
void ArticleView::findText( QString & text,
const QWebEnginePage::FindFlags & f,
const std::function< void( bool match ) > & callback )
2021-07-06 13:01:50 +00:00
{
#if( QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) )
webview->findText( text, f, [ callback ]( const QWebEngineFindTextResult & result ) {
auto r = result.numberOfMatches() > 0;
if( callback )
callback( r );
} );
2022-02-27 05:17:37 +00:00
#else
webview->findText( text, f, [ callback ]( bool result ) {
if( callback )
callback( result );
} );
2022-02-27 05:17:37 +00:00
#endif
2021-07-06 13:01:50 +00:00
}
void ArticleView::reloadStyleSheet()
{
for( QWidget * w = parentWidget(); w; w = w->parentWidget() )
{
if ( w->styleSheet().size() )
{
w->setStyleSheet( w->styleSheet() );
break;
}
2009-05-16 11:14:43 +00:00
}
}
2009-05-16 11:14:43 +00:00
bool ArticleView::closeSearch()
{
if( searchIsOpened ) {
searchPanel->hide();
webview->setFocus();
2009-05-16 11:14:43 +00:00
searchIsOpened = false;
return true;
}
else
if( ftsSearchIsOpened )
{
allMatches.clear();
uniqueMatches.clear();
ftsPosition = 0;
ftsSearchIsOpened = false;
ftsSearchPanel->hide();
webview->setFocus();
2021-07-06 13:01:50 +00:00
QWebEnginePage::FindFlags flags ( 0 );
webview->findText( "", flags );
return true;
}
2009-05-16 11:14:43 +00:00
else
return false;
}
bool ArticleView::isSearchOpened() { return searchIsOpened; }
void ArticleView::showEvent( QShowEvent * ev )
{
QWidget::showEvent( ev );
if( !searchIsOpened )
searchPanel->hide();
if( !ftsSearchIsOpened )
ftsSearchPanel->hide();
}
void ArticleView::receiveExpandOptionalParts( bool expand )
{
if( expandOptionalParts != expand )
switchExpandOptionalParts();
}
void ArticleView::switchExpandOptionalParts()
{
expandOptionalParts = !expandOptionalParts;
emit setExpandMode( expandOptionalParts );
reload();
}
void ArticleView::copyAsText()
{
QString text = webview->selectedText();
if( !text.isEmpty() )
QApplication::clipboard()->setText( text );
}
void ArticleView::highlightFTSResults()
{
closeSearch();
// Clear any current selection
if( webview->selectedText().size() ) {
webview->page()->runJavaScript( "window.getSelection().removeAllRanges();_=0;" );
}
webview->page()->toPlainText(
[ & ]( const QString pageText ) {
const QUrl & url = webview->url();
QString regString = Utils::Url::queryItemValue( url, "regexp" );
2022-07-02 11:13:53 +00:00
if( regString.isEmpty() )
return;
bool ignoreDiacritics = Utils::Url::hasQueryItem( url, "ignore_diacritics" );
if( ignoreDiacritics )
regString = gd::toQString( Folding::applyDiacriticsOnly( gd::toWString( regString ) ) );
else
regString = regString.remove( AccentMarkHandler::accentMark() );
//<div><i>watch</i>out</div> to plainText will return "watchout".
//if application goes here,that means the article text must contains the search text.
//whole word match regString will contain \b . can not match the above senario.
//workaround ,remove \b from the regstring="(\bwatch\b)"
regString.remove( QRegularExpression( "\\\\b" ) );
QRegularExpression regexp;
if( Utils::Url::hasQueryItem( url, "wildcards" ) )
regexp.setPattern( wildcardsToRegexp( regString ) );
else
regexp.setPattern( regString );
QRegularExpression::PatternOptions patternOptions =
QRegularExpression::DotMatchesEverythingOption | QRegularExpression::UseUnicodePropertiesOption |
QRegularExpression::MultilineOption | QRegularExpression::InvertedGreedinessOption;
if( !Utils::Url::hasQueryItem( url, "matchcase" ) )
patternOptions |= QRegularExpression::CaseInsensitiveOption;
regexp.setPatternOptions( patternOptions );
if( regexp.pattern().isEmpty() || !regexp.isValid() )
return;
sptr< AccentMarkHandler > marksHandler = ignoreDiacritics ? std::make_shared<DiacriticsHandler>() : std::make_shared<AccentMarkHandler>();
2022-03-30 15:08:24 +00:00
marksHandler->setText( pageText );
2022-03-30 15:08:24 +00:00
QRegularExpressionMatchIterator it = regexp.globalMatch( marksHandler->normalizedText() );
while( it.hasNext() )
{
QRegularExpressionMatch match = it.next();
2022-03-30 15:08:24 +00:00
// Mirror pos and matched length to original string
int pos = match.capturedStart();
int spos = marksHandler->mirrorPosition( pos );
int matched = marksHandler->mirrorPosition( pos + match.capturedLength() ) - spos;
2022-03-30 15:08:24 +00:00
// Add mark pos (if presented)
while( spos + matched < pageText.length() && pageText[ spos + matched ].category() == QChar::Mark_NonSpacing )
matched++;
2022-03-30 15:08:24 +00:00
if( matched > FTS::MaxMatchLengthForHighlightResults )
{
gdWarning( "ArticleView::highlightFTSResults(): Too long match - skipped (matched length %i, allowed %i)",
match.capturedLength(),
FTS::MaxMatchLengthForHighlightResults );
2022-03-28 09:29:00 +00:00
}
2022-03-30 15:08:24 +00:00
else
allMatches.append( pageText.mid( spos, matched ) );
}
2022-03-30 15:08:24 +00:00
ftsSearchMatchCase = Utils::Url::hasQueryItem( url, "matchcase" );
QWebEnginePage::FindFlags flags( QWebEnginePage::FindBackward );
2022-03-30 15:08:24 +00:00
if( ftsSearchMatchCase )
flags |= QWebEnginePage::FindCaseSensitively;
if( allMatches.isEmpty() )
ftsSearchStatusLabel->setText( searchStatusMessageNoMatches() );
else {
// highlightAllFtsOccurences( flags );
webview->findText( allMatches.at( 0 ), flags );
// if( webview->findText( allMatches.at( 0 ), flags ) )
// {
// webview->page()->runJavaScript(
// QString( "%1=window.getSelection().getRangeAt(0);_=0;" ).arg( rangeVarName ) );
// }
Q_ASSERT( ftsPosition == 0 );
ftsSearchStatusLabel->setText( searchStatusMessage( 1, allMatches.size() ) );
2022-03-30 15:08:24 +00:00
}
ftsSearchPanel->show();
ftsSearchPrevious->setEnabled( false );
ftsSearchNext->setEnabled( allMatches.size() > 1 );
2022-03-30 15:08:24 +00:00
ftsSearchIsOpened = true;
} );
2021-07-06 13:01:50 +00:00
}
2022-05-24 12:21:40 +00:00
void ArticleView::highlightAllFtsOccurences( QWebEnginePage::FindFlags flags )
{
// Usually allMatches contains mostly duplicates. Thus searching for each element of
// allMatches to highlight them takes a long time => collect unique elements into a
// set and search for them instead.
// Don't use QList::toSet() or QSet's range constructor because they reserve space
// for QList::size() elements, whereas the final QSet size is likely 1 or 2.
QSet< QString > uniqueMatches;
for( int x = 0; x < allMatches.size(); ++x )
{
QString const & match = allMatches.at( x );
// Consider words that differ only in case equal if the search is case-insensitive.
uniqueMatches.insert( ftsSearchMatchCase ? match : match.toLower() );
}
for( QSet< QString >::const_iterator it = uniqueMatches.constBegin(); it != uniqueMatches.constEnd(); ++it )
webview->findText( *it, flags );
}
void ArticleView::setActiveDictIds(ActiveDictIds ad) {
if (ad.word == currentWord || historyMode) {
// ignore all other signals.
qDebug() << "receive dicts, current word:" << currentWord << ad.word << ":" << ad.dictIds;
currentActiveDictIds << ad.dictIds;
currentActiveDictIds.removeDuplicates();
2022-01-09 04:54:50 +00:00
emit updateFoundInDictsList();
}
}
void ArticleView::dictionaryClear( ActiveDictIds ad )
{
// ignore all other signals.
if( ad.word == currentWord )
{
qDebug() << "clear current dictionaries:" << currentWord;
currentActiveDictIds.clear();
}
}
2021-12-13 14:45:16 +00:00
//todo ,futher refinement?
void ArticleView::performFtsFindOperation( bool backwards )
{
if( !ftsSearchIsOpened )
return;
if( allMatches.isEmpty() ) {
ftsSearchStatusLabel->setText( searchStatusMessageNoMatches() );
ftsSearchNext->setEnabled( false );
ftsSearchPrevious->setEnabled( false );
return;
}
2021-07-06 13:01:50 +00:00
QWebEnginePage::FindFlags flags( 0 );
if( ftsSearchMatchCase )
2021-07-06 13:01:50 +00:00
flags |= QWebEnginePage::FindCaseSensitively;
// Restore saved highlighted selection
webview->page()->runJavaScript(
QString( "var sel=window.getSelection();sel.removeAllRanges();sel.addRange(%1);_=0;" ).arg( rangeVarName ) );
if( backwards ) {
if( ftsPosition > 0 ) {
ftsPosition -= 1;
}
2022-02-27 05:17:37 +00:00
#if( QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) )
webview->findText( allMatches.at( ftsPosition ),
flags | QWebEnginePage::FindBackward,
[ this ]( const QWebEngineFindTextResult & result ) {
if( result.numberOfMatches() == 0 )
return;
ftsSearchPrevious->setEnabled( true );
if( !ftsSearchNext->isEnabled() )
ftsSearchNext->setEnabled( true );
} );
2022-02-27 05:17:37 +00:00
#else
webview->findText( allMatches.at( ftsPosition ), flags | QWebEnginePage::FindBackward, [ this ]( bool res ) {
ftsSearchPrevious->setEnabled( res );
if( !ftsSearchNext->isEnabled() )
ftsSearchNext->setEnabled( res );
} );
2022-02-27 05:17:37 +00:00
#endif
}
else {
if( ftsPosition < allMatches.size() - 1 ) {
ftsPosition += 1;
}
2022-02-27 05:17:37 +00:00
#if( QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) )
webview->findText( allMatches.at( ftsPosition ), flags, [ this ]( const QWebEngineFindTextResult & result ) {
if( result.numberOfMatches() == 0 )
return;
ftsSearchNext->setEnabled( true );
if( !ftsSearchPrevious->isEnabled() )
ftsSearchPrevious->setEnabled( true );
} );
2022-02-27 05:17:37 +00:00
}
#else
webview->findText( allMatches.at( ftsPosition ), flags, [ this ]( bool res ) {
ftsSearchNext->setEnabled( res );
if( !ftsSearchPrevious->isEnabled() )
ftsSearchPrevious->setEnabled( res );
} );
}
2022-02-27 05:17:37 +00:00
#endif
ftsSearchStatusLabel->setText( searchStatusMessage( ftsPosition + 1, allMatches.size() ) );
}
void ArticleView::on_ftsSearchPrevious_clicked()
{
performFtsFindOperation( true );
}
void ArticleView::on_ftsSearchNext_clicked()
{
performFtsFindOperation( false );
}
2017-04-27 20:55:53 +00:00
ResourceToSaveHandler::ResourceToSaveHandler(ArticleView * view, QString const & fileName ) :
QObject( view ),
fileName( fileName ),
2017-04-27 20:55:53 +00:00
alreadyDone( false )
{
connect( this, &ResourceToSaveHandler::statusBarMessage, view, &ArticleView::statusBarMessage );
2017-04-27 20:55:53 +00:00
}
void ResourceToSaveHandler::addRequest( sptr< Dictionary::DataRequest > req )
2017-04-27 20:55:53 +00:00
{
if( !alreadyDone )
{
2017-04-27 20:55:53 +00:00
downloadRequests.push_back( req );
connect( req.get(), &Dictionary::Request::finished, this, &ResourceToSaveHandler::downloadFinished );
}
}
void ResourceToSaveHandler::downloadFinished()
{
2017-04-27 20:55:53 +00:00
if ( downloadRequests.empty() )
return; // Stray signal
2017-04-27 20:55:53 +00:00
// Find any finished resources
for( list< sptr< Dictionary::DataRequest > >::iterator i =
downloadRequests.begin(); i != downloadRequests.end(); )
{
2017-04-27 20:55:53 +00:00
if ( (*i)->isFinished() )
{
if ( (*i)->dataSize() >= 0 && !alreadyDone )
2017-04-27 20:55:53 +00:00
{
QByteArray resourceData;
vector< char > const & data = (*i)->getFullData();
resourceData = QByteArray( data.data(), data.size() );
2017-04-27 20:55:53 +00:00
// Write data to file
2017-04-27 20:55:53 +00:00
if ( !fileName.isEmpty() )
{
QFileInfo fileInfo( fileName );
QDir().mkpath( fileInfo.absoluteDir().absolutePath() );
2017-04-27 20:55:53 +00:00
QFile file( fileName );
if ( file.open( QFile::WriteOnly ) )
{
file.write( resourceData.data(), resourceData.size() );
file.close();
}
2017-04-27 20:55:53 +00:00
if ( file.error() )
{
emit statusBarMessage(
tr("ERROR: %1").arg(tr("Resource saving error: ") + file.errorString()),
10000, QPixmap(":/icons/error.svg"));
2017-04-27 20:55:53 +00:00
}
}
alreadyDone = true;
// Clear other requests
downloadRequests.clear();
break;
}
else
{
// This one had no data. Erase it.
downloadRequests.erase( i++ );
}
}
2017-04-27 20:55:53 +00:00
else // Unfinished, wait.
break;
}
2017-04-27 20:55:53 +00:00
if ( downloadRequests.empty() )
{
2017-04-27 20:55:53 +00:00
if( !alreadyDone )
{
emit statusBarMessage(
tr("WARNING: %1").arg(tr("The referenced resource failed to download.")),
10000, QPixmap(":/icons/error.svg"));
}
2017-04-27 20:55:53 +00:00
emit done();
deleteLater();
}
}
ArticleViewAgent::ArticleViewAgent( ArticleView * articleView ) : QObject( articleView ), articleView( articleView )
{
}
void ArticleViewAgent::onJsActiveArticleChanged( QString const & id )
{
articleView->onJsActiveArticleChanged( id );
}
void ArticleViewAgent::linkClickedInHtml( QUrl const & url )
{
articleView->linkClickedInHtml( url );
}