512 рядки
13 KiB
C++
512 рядки
13 KiB
C++
/*
|
|
|
|
Copyright 2014 S. Razi Alavizadeh
|
|
Copyright 2014-2015 Adam Reichold
|
|
|
|
This file is part of qpdfview.
|
|
|
|
qpdfview is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
qpdfview is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with qpdfview. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
#include "searchmodel.h"
|
|
|
|
#include <QApplication>
|
|
#include <QtConcurrentRun>
|
|
|
|
#include "documentview.h"
|
|
|
|
static inline bool operator<(int page, const QPair< int, QRectF >& result) { return page < result.first; }
|
|
static inline bool operator<(const QPair< int, QRectF >& result, int page) { return result.first < page; }
|
|
|
|
namespace qpdfview
|
|
{
|
|
|
|
SearchModel* SearchModel::s_instance = 0;
|
|
|
|
SearchModel* SearchModel::instance()
|
|
{
|
|
if(s_instance == 0)
|
|
{
|
|
s_instance = new SearchModel(qApp);
|
|
}
|
|
|
|
return s_instance;
|
|
}
|
|
|
|
SearchModel::~SearchModel()
|
|
{
|
|
foreach(TextWatcher* watcher, m_textWatchers)
|
|
{
|
|
watcher->waitForFinished();
|
|
watcher->deleteLater();
|
|
}
|
|
|
|
qDeleteAll(m_results);
|
|
|
|
s_instance = 0;
|
|
}
|
|
|
|
QModelIndex SearchModel::index(int row, int column, const QModelIndex& parent) const
|
|
{
|
|
if(hasIndex(row, column, parent))
|
|
{
|
|
if(!parent.isValid())
|
|
{
|
|
return createIndex(row, column);
|
|
}
|
|
else
|
|
{
|
|
DocumentView* view = m_views.value(parent.row(), 0);
|
|
|
|
return createIndex(row, column, view);
|
|
}
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex SearchModel::parent(const QModelIndex& child) const
|
|
{
|
|
if(child.internalPointer() != 0)
|
|
{
|
|
DocumentView* view = static_cast< DocumentView* >(child.internalPointer());
|
|
|
|
return findView(view);
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
int SearchModel::rowCount(const QModelIndex& parent) const
|
|
{
|
|
if(!parent.isValid())
|
|
{
|
|
return m_views.count();
|
|
}
|
|
else if(parent.internalPointer() == 0)
|
|
{
|
|
DocumentView* view = m_views.value(parent.row(), 0);
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
if(results != 0)
|
|
{
|
|
return results->count();
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int SearchModel::columnCount(const QModelIndex&) const
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
QVariant SearchModel::data(const QModelIndex& index, int role) const
|
|
{
|
|
if(!index.isValid())
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
if(index.internalPointer() == 0)
|
|
{
|
|
DocumentView* view = m_views.value(index.row(), 0);
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
if(results == 0)
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
switch(role)
|
|
{
|
|
default:
|
|
return QVariant();
|
|
case CountRole:
|
|
return results->count();
|
|
case ProgressRole:
|
|
return view->searchProgress();
|
|
case Qt::DisplayRole:
|
|
switch(index.column())
|
|
{
|
|
case 0:
|
|
return view->title();
|
|
case 1:
|
|
return results->count();
|
|
}
|
|
case Qt::ToolTipRole:
|
|
return tr("<b>%1</b> occurrences").arg(results->count());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DocumentView* view = static_cast< DocumentView* >(index.internalPointer());
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
if(results == 0 || index.row() >= results->count())
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
const Result& result = results->at(index.row());
|
|
|
|
switch(role)
|
|
{
|
|
default:
|
|
return QVariant();
|
|
case PageRole:
|
|
return result.first;
|
|
case RectRole:
|
|
return result.second;
|
|
case TextRole:
|
|
return view->searchText();
|
|
case MatchCaseRole:
|
|
return view->searchMatchCase();
|
|
case WholeWordsRole:
|
|
return view->searchWholeWords();
|
|
case MatchedTextRole:
|
|
return fetchMatchedText(view, result);
|
|
case SurroundingTextRole:
|
|
return fetchSurroundingText(view, result);
|
|
case Qt::DisplayRole:
|
|
switch(index.column())
|
|
{
|
|
case 0:
|
|
return QVariant();
|
|
case 1:
|
|
return result.first;
|
|
}
|
|
case Qt::ToolTipRole:
|
|
return tr("<b>%1</b> occurrences on page <b>%2</b>").arg(numberOfResultsOnPage(view, result.first)).arg(result.first);
|
|
}
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
DocumentView* SearchModel::viewForIndex(const QModelIndex& index) const
|
|
{
|
|
if(index.internalPointer() == 0)
|
|
{
|
|
return m_views.value(index.row(), 0);
|
|
}
|
|
else
|
|
{
|
|
return static_cast< DocumentView* >(index.internalPointer());
|
|
}
|
|
}
|
|
|
|
bool SearchModel::hasResults(DocumentView* view) const
|
|
{
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
return results != 0 && !results->isEmpty();
|
|
}
|
|
|
|
bool SearchModel::hasResultsOnPage(DocumentView* view, int page) const
|
|
{
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
return results != 0 && qBinaryFind(results->begin(), results->end(), page) != results->end();
|
|
}
|
|
|
|
int SearchModel::numberOfResultsOnPage(DocumentView* view, int page) const
|
|
{
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
if(results == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
const Results::const_iterator pageBegin = qLowerBound(results->constBegin(), results->constEnd(), page);
|
|
const Results::const_iterator pageEnd = qUpperBound(pageBegin, results->constEnd(), page);
|
|
|
|
return pageEnd - pageBegin;
|
|
}
|
|
|
|
QList< QRectF > SearchModel::resultsOnPage(DocumentView* view, int page) const
|
|
{
|
|
QList< QRectF > resultsOnPage;
|
|
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
if(results != 0)
|
|
{
|
|
const Results::const_iterator pageBegin = qLowerBound(results->constBegin(), results->constEnd(), page);
|
|
const Results::const_iterator pageEnd = qUpperBound(pageBegin, results->constEnd(), page);
|
|
|
|
for(Results::const_iterator iterator = pageBegin; iterator != pageEnd; ++iterator)
|
|
{
|
|
resultsOnPage.append(iterator->second);
|
|
}
|
|
}
|
|
|
|
return resultsOnPage;
|
|
}
|
|
|
|
QPersistentModelIndex SearchModel::findResult(DocumentView* view, const QPersistentModelIndex& currentResult, int currentPage, FindDirection direction) const
|
|
{
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
if(results == 0 || results->isEmpty())
|
|
{
|
|
return QPersistentModelIndex();
|
|
}
|
|
|
|
const int rows = results->count();
|
|
int row;
|
|
|
|
if(currentResult.isValid())
|
|
{
|
|
switch(direction)
|
|
{
|
|
default:
|
|
case FindNext:
|
|
row = (currentResult.row() + 1) % rows;
|
|
break;
|
|
case FindPrevious:
|
|
row = (currentResult.row() + rows - 1) % rows;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch(direction)
|
|
{
|
|
default:
|
|
case FindNext:
|
|
{
|
|
Results::const_iterator lowerBound = qLowerBound(results->constBegin(), results->constEnd(), currentPage);
|
|
|
|
row = (lowerBound - results->constBegin()) % rows;
|
|
break;
|
|
}
|
|
case FindPrevious:
|
|
{
|
|
Results::const_iterator upperBound = qUpperBound(results->constBegin(), results->constEnd(), currentPage);
|
|
|
|
row = ((upperBound - results->constBegin()) + rows - 1) % rows;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return createIndex(row, 0, view);
|
|
}
|
|
|
|
void SearchModel::insertResults(DocumentView* view, int page, const QList< QRectF >& resultsOnPage)
|
|
{
|
|
if(resultsOnPage.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const QModelIndex parent = findOrInsertView(view);
|
|
|
|
Results* results = m_results.value(view);
|
|
|
|
Results::iterator at = qLowerBound(results->begin(), results->end(), page);
|
|
const int row = at - results->begin();
|
|
|
|
beginInsertRows(parent, row, row + resultsOnPage.size() - 1);
|
|
|
|
for(int index = resultsOnPage.size() - 1; index >= 0; --index)
|
|
{
|
|
at = results->insert(at, qMakePair(page, resultsOnPage.at(index)));
|
|
}
|
|
|
|
endInsertRows();
|
|
}
|
|
|
|
void SearchModel::clearResults(DocumentView* view)
|
|
{
|
|
typedef QHash< TextCacheKey, TextWatcher* >::iterator WatcherIterator;
|
|
|
|
for(WatcherIterator iterator = m_textWatchers.begin(); iterator != m_textWatchers.end(); ++iterator)
|
|
{
|
|
const TextCacheKey& key = iterator.key();
|
|
|
|
if(key.first == view)
|
|
{
|
|
TextWatcher* const watcher = iterator.value();
|
|
watcher->cancel();
|
|
watcher->waitForFinished();
|
|
watcher->deleteLater();
|
|
|
|
iterator = m_textWatchers.erase(iterator);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
foreach(const TextCacheKey& key, m_textCache.keys())
|
|
{
|
|
if(key.first == view)
|
|
{
|
|
m_textCache.remove(key);
|
|
}
|
|
}
|
|
|
|
const QVector< DocumentView* >::iterator at = qBinaryFind(m_views.begin(), m_views.end(), view);
|
|
const int row = at - m_views.begin();
|
|
|
|
if(at == m_views.end())
|
|
{
|
|
return;
|
|
}
|
|
|
|
beginRemoveRows(QModelIndex(), row, row);
|
|
|
|
m_views.erase(at);
|
|
delete m_results.take(view);
|
|
|
|
endRemoveRows();
|
|
}
|
|
|
|
void SearchModel::updateProgress(DocumentView* view)
|
|
{
|
|
QModelIndex index = findView(view);
|
|
|
|
if(index.isValid())
|
|
{
|
|
emit dataChanged(index, index);
|
|
}
|
|
}
|
|
|
|
void SearchModel::on_fetchSurroundingText_finished()
|
|
{
|
|
TextWatcher* watcher = dynamic_cast< TextWatcher* >(sender());
|
|
|
|
if(watcher == 0 || watcher->isCanceled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const TextJob job = watcher->result();
|
|
|
|
m_textWatchers.remove(job.key);
|
|
watcher->deleteLater();
|
|
|
|
DocumentView* view = job.key.first;
|
|
const Results* results = m_results.value(view, 0);
|
|
|
|
if(results == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const int cost = job.object->first.length() + job.object->second.length();
|
|
|
|
m_textCache.insert(job.key, job.object, cost);
|
|
|
|
emit dataChanged(createIndex(0, 0, view), createIndex(results->count() - 1, 0, view));
|
|
}
|
|
|
|
SearchModel::SearchModel(QObject* parent) : QAbstractItemModel(parent),
|
|
m_views(),
|
|
m_results(),
|
|
m_textCache(1 << 16),
|
|
m_textWatchers()
|
|
{
|
|
}
|
|
|
|
QModelIndex SearchModel::findView(DocumentView *view) const
|
|
{
|
|
const QVector< DocumentView* >::const_iterator at = qBinaryFind(m_views.constBegin(), m_views.constEnd(), view);
|
|
const int row = at - m_views.constBegin();
|
|
|
|
if(at == m_views.constEnd())
|
|
{
|
|
return QModelIndex();
|
|
}
|
|
|
|
return createIndex(row, 0);
|
|
}
|
|
|
|
QModelIndex SearchModel::findOrInsertView(DocumentView* view)
|
|
{
|
|
const QVector< DocumentView* >::iterator at = qLowerBound(m_views.begin(), m_views.end(), view);
|
|
const int row = at - m_views.begin();
|
|
|
|
if(at == m_views.end() || *at != view)
|
|
{
|
|
beginInsertRows(QModelIndex(), row, row);
|
|
|
|
m_views.insert(at, view);
|
|
m_results.insert(view, new Results);
|
|
|
|
endInsertRows();
|
|
}
|
|
|
|
return createIndex(row, 0);
|
|
}
|
|
|
|
QString SearchModel::fetchMatchedText(DocumentView* view, const SearchModel::Result& result) const
|
|
{
|
|
const TextCacheObject* object = fetchText(view, result);
|
|
|
|
return object != 0 ? object->first : QString();
|
|
}
|
|
|
|
QString SearchModel::fetchSurroundingText(DocumentView* view, const Result& result) const
|
|
{
|
|
const TextCacheObject* object = fetchText(view, result);
|
|
|
|
return object != 0 ? object->second : QString();
|
|
}
|
|
|
|
const SearchModel::TextCacheObject* SearchModel::fetchText(DocumentView* view, const SearchModel::Result& result) const
|
|
{
|
|
const TextCacheKey key = textCacheKey(view, result);
|
|
|
|
if(const TextCacheObject* object = m_textCache.object(key))
|
|
{
|
|
return object;
|
|
}
|
|
|
|
if(m_textWatchers.size() < 20 && !m_textWatchers.contains(key))
|
|
{
|
|
TextWatcher* watcher = new TextWatcher();
|
|
m_textWatchers.insert(key, watcher);
|
|
|
|
connect(watcher, SIGNAL(finished()), SLOT(on_fetchSurroundingText_finished()));
|
|
|
|
watcher->setFuture(QtConcurrent::run(textJob, key, result));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
inline SearchModel::TextCacheKey SearchModel::textCacheKey(DocumentView* view, const Result& result)
|
|
{
|
|
QByteArray key;
|
|
|
|
QDataStream(&key, QIODevice::WriteOnly)
|
|
<< result.first
|
|
<< result.second;
|
|
|
|
return qMakePair(view, key);
|
|
}
|
|
|
|
SearchModel::TextJob SearchModel::textJob(const TextCacheKey& key, const Result& result)
|
|
{
|
|
const QPair< QString, QString >& text = key.first->searchContext(result.first, result.second);
|
|
|
|
return TextJob(key, new TextCacheObject(text));
|
|
}
|
|
|
|
} // qpdfview
|