Merge pull request #2285 from brendandahl/async-find

Allow find to highlight matches without extracting all text.
This commit is contained in:
Yury Delendik 2012-10-19 16:49:18 -07:00
commit 01fe460273

View File

@ -230,7 +230,9 @@ var cache = new Cache(kCacheSize);
var currentPageNumber = 1; var currentPageNumber = 1;
var PDFFindController = { var PDFFindController = {
extractTextPromise: null, startedTextExtraction: false,
extractTextPromises: [],
// If active, find results will be highlighted. // If active, find results will be highlighted.
active: false, active: false,
@ -240,11 +242,22 @@ var PDFFindController = {
pageMatches: [], pageMatches: [],
// Currently selected match.
selected: { selected: {
pageIdx: 0, pageIdx: -1,
matchIdx: 0 matchIdx: -1
}, },
// Where find algorithm currently is in the document.
offset: {
pageIdx: null,
matchIdx: null
},
resumePageIdx: null,
resumeCallback: null,
state: null, state: null,
dirtyMatch: false, dirtyMatch: false,
@ -266,13 +279,16 @@ var PDFFindController = {
} }
}, },
calcFindMatch: function(pageContent) { calcFindMatch: function(pageIndex) {
var pageContent = this.pageContents[pageIndex];
var query = this.state.query; var query = this.state.query;
var caseSensitive = this.state.caseSensitive; var caseSensitive = this.state.caseSensitive;
var queryLen = query.length; var queryLen = query.length;
if (queryLen === 0) if (queryLen === 0) {
return []; // Do nothing the matches should be wiped out already.
return;
}
if (!caseSensitive) { if (!caseSensitive) {
pageContent = pageContent.toLowerCase(); pageContent = pageContent.toLowerCase();
@ -290,14 +306,26 @@ var PDFFindController = {
matches.push(matchIdx); matches.push(matchIdx);
} }
return matches; this.pageMatches[pageIndex] = matches;
this.updatePage(pageIndex);
if (this.resumePageIdx === pageIndex) {
var callback = this.resumeCallback;
this.resumePageIdx = null;
this.resumeCallback = null;
callback();
}
}, },
extractText: function() { extractText: function() {
if (this.extractTextPromise) { if (this.startedTextExtraction) {
return this.extractTextPromise; return;
}
this.startedTextExtraction = true;
this.pageContents = [];
for (var i = 0, ii = PDFView.pdfDocument.numPages; i < ii; i++) {
this.extractTextPromises.push(new PDFJS.Promise());
} }
this.extractTextPromise = new PDFJS.Promise();
var self = this; var self = this;
function extractPageText(pageIndex) { function extractPageText(pageIndex) {
@ -313,13 +341,10 @@ var PDFFindController = {
// Store the pageContent as a string. // Store the pageContent as a string.
self.pageContents.push(str); self.pageContents.push(str);
// Ensure there is a empty array of matches.
self.pageMatches.push([]);
self.extractTextPromises[pageIndex].resolve(pageIndex);
if ((pageIndex + 1) < PDFView.pages.length) if ((pageIndex + 1) < PDFView.pages.length)
extractPageText(pageIndex + 1); extractPageText(pageIndex + 1);
else
self.extractTextPromise.resolve();
} }
); );
} }
@ -334,16 +359,14 @@ var PDFFindController = {
this.state = e.detail; this.state = e.detail;
this.updateUIState(FindStates.FIND_PENDING); this.updateUIState(FindStates.FIND_PENDING);
var promise = this.extractText(); this.extractText();
clearTimeout(this.findTimeout); clearTimeout(this.findTimeout);
if (e.type === 'find') { if (e.type === 'find') {
// Only trigger the find action after 250ms of silence. // Only trigger the find action after 250ms of silence.
this.findTimeout = setTimeout(function() { this.findTimeout = setTimeout(this.nextMatch.bind(this), 250);
promise.then(this.performFind.bind(this));
}.bind(this), 250);
} else { } else {
promise.then(this.performFind.bind(this)); this.nextMatch();
} }
}, },
@ -362,118 +385,147 @@ var PDFFindController = {
} }
}, },
performFind: function() { nextMatch: function() {
// Recalculate all the matches.
// TODO: Make one match show up as the current match
var pages = PDFView.pages; var pages = PDFView.pages;
var pageContents = this.pageContents; var previous = this.state.findPrevious;
var pageMatches = this.pageMatches; var numPages = PDFView.pages.length;
this.active = true; this.active = true;
if (this.dirtyMatch) { if (this.dirtyMatch) {
// Need to recalculate the matches. // Need to recalculate the matches, reset everything.
this.dirtyMatch = false; this.dirtyMatch = false;
this.selected.pageIdx = this.selected.matchIdx = -1;
this.offset.pageIdx = previous ? numPages - 1 : 0;
this.offset.matchIdx = null;
this.hadMatch = false;
this.resumeCallback = null;
this.resumePageIdx = null;
this.pageMatches = [];
var self = this;
this.selected = { for (var i = 0; i < numPages; i++) {
pageIdx: -1, // Wipe out any previous highlighted matches.
matchIdx: -1 this.updatePage(i);
};
// TODO: Make this way more lasily (aka. efficient) - e.g. calculate only // As soon as the text is extracted start finding the matches.
// the matches for the current visible pages. this.extractTextPromises[i].onData(function(pageIdx) {
var firstMatch = true; // Use a timeout since all the pages may already be extracted and we
for (var i = 0; i < pageContents.length; i++) { // want to start highlighting before finding all the matches.
var matches = pageMatches[i] = this.calcFindMatch(pageContents[i]); setTimeout(function() {
if (firstMatch && matches.length !== 0) { self.calcFindMatch(pageIdx);
firstMatch = false; });
this.selected = { });
pageIdx: i,
matchIdx: 0
};
} }
this.updatePage(i, true);
} }
if (!firstMatch || !this.state.query) {
// If there's no query there's no point in searching.
if (this.state.query === '') {
this.updateUIState(FindStates.FIND_FOUND); this.updateUIState(FindStates.FIND_FOUND);
} else {
this.updateUIState(FindStates.FIND_NOTFOUND);
}
} else {
// If there is NO selection, then there is no match at all -> no sense to
// handle previous/next action.
if (this.selected.pageIdx === -1) {
this.updateUIState(FindStates.FIND_NOTFOUND);
return; return;
} }
// Handle findAgain case. // If we're waiting on a page, we return since we can't do anything else.
if (this.resumeCallback) {
return;
}
var offset = this.offset;
// If there's already a matchIdx that means we are iterating through a
// page's matches.
if (offset.matchIdx !== null) {
var numPageMatches = this.pageMatches[offset.pageIdx].length;
if ((!previous && offset.matchIdx + 1 < numPageMatches) ||
(previous && offset.matchIdx > 0)) {
// The simple case, we just have advance the matchIdx to select the next
// match on the page.
this.hadMatch = true;
offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1;
this.updateMatch(true);
return;
}
// We went beyond the current page's matches, so we advance to the next
// page.
this.advanceOffsetPage(previous);
}
// Start searching through the page.
this.nextPageMatch();
},
nextPageMatch: function() {
if (this.resumePageIdx !== null)
console.error('There can only be one pending page.');
var matchesReady = function(matches) {
var offset = this.offset;
var numMatches = matches.length;
var previous = this.state.findPrevious; var previous = this.state.findPrevious;
var sPageIdx = this.selected.pageIdx; if (numMatches) {
var sMatchIdx = this.selected.matchIdx; // There were matches for the page, so initialize the matchIdx.
var findState = FindStates.FIND_FOUND; this.hadMatch = true;
offset.matchIdx = previous ? numMatches - 1 : 0;
if (previous) { this.updateMatch(true);
// Select previous match.
if (sMatchIdx !== 0) {
this.selected.matchIdx -= 1;
} else { } else {
var len = pageMatches.length; // No matches attempt to search the next page.
for (var i = sPageIdx - 1; i != sPageIdx; i--) { this.advanceOffsetPage(previous);
if (i < 0) if (offset.wrapped) {
i += len; offset.matchIdx = null;
if (!this.hadMatch) {
// No point in wrapping there were no matches.
this.updateMatch(false);
return;
}
}
// Search the next page.
this.nextPageMatch();
}
}.bind(this);
if (pageMatches[i].length !== 0) { var pageIdx = this.offset.pageIdx;
this.selected = { var pageMatches = this.pageMatches;
pageIdx: i, if (!pageMatches[pageIdx]) {
matchIdx: pageMatches[i].length - 1 // The matches aren't ready setup a callback so we can be notified,
// when they are ready.
this.resumeCallback = function() {
matchesReady(pageMatches[pageIdx]);
}; };
break; this.resumePageIdx = pageIdx;
return;
} }
} // The matches are finished already.
// If pageIdx stayed the same, select last match on the page. matchesReady(pageMatches[pageIdx]);
if (this.selected.pageIdx === sPageIdx) { },
this.selected.matchIdx = pageMatches[sPageIdx].length - 1;
findState = FindStates.FIND_WRAPPED;
} else if (this.selected.pageIdx > sPageIdx) {
findState = FindStates.FIND_WRAPPED;
}
}
} else {
// Select next match.
if (pageMatches[sPageIdx].length !== sMatchIdx + 1) { advanceOffsetPage: function(previous) {
this.selected.matchIdx += 1; var offset = this.offset;
} else { var numPages = this.extractTextPromises.length;
var len = pageMatches.length; offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1;
for (var i = sPageIdx + 1; i < len + sPageIdx; i++) { offset.matchIdx = null;
if (pageMatches[i % len].length !== 0) { if (offset.pageIdx >= numPages || offset.pageIdx < 0) {
this.selected = { offset.pageIdx = previous ? numPages - 1 : 0;
pageIdx: i % len, offset.wrapped = true;
matchIdx: 0 return;
};
break;
}
} }
},
// If pageIdx stayed the same, select first match on the page. updateMatch: function(found) {
if (this.selected.pageIdx === sPageIdx) { var state = FindStates.FIND_NOTFOUND;
this.selected.matchIdx = 0; var wrapped = this.offset.wrapped;
findState = FindStates.FIND_WRAPPED; this.offset.wrapped = false;
} else if (this.selected.pageIdx < sPageIdx) { if (found) {
findState = FindStates.FIND_WRAPPED; var previousPage = this.selected.pageIdx;
this.selected.pageIdx = this.offset.pageIdx;
this.selected.matchIdx = this.offset.matchIdx;
state = wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND;
// Update the currently selected page to wipe out any selected matches.
if (previousPage !== -1 && previousPage !== this.selected.pageIdx) {
this.updatePage(previousPage);
} }
} }
} this.updateUIState(state, this.state.findPrevious);
if (this.selected.pageIdx !== -1) {
this.updateUIState(findState, previous);
this.updatePage(sPageIdx, sPageIdx === this.selected.pageIdx);
if (sPageIdx !== this.selected.pageIdx) {
this.updatePage(this.selected.pageIdx, true); this.updatePage(this.selected.pageIdx, true);
} }
}
}, },
updateUIState: function(state, previous) { updateUIState: function(state, previous) {