diff --git a/setup.py b/setup.py index a1061478ce..948db8039c 100644 --- a/setup.py +++ b/setup.py @@ -145,6 +145,7 @@ if __name__ == '__main__': fb2_xsl = 'ebooks/lrf/fb2/fb2.xsl', metadata_sqlite = 'library/metadata_sqlite.sql', jquery = 'gui2/viewer/jquery.js', + jquery_scrollTo = 'gui2/viewer/jquery_scrollTo.js', ) DEST = os.path.join('src', APPNAME, 'resources.py') @@ -291,7 +292,7 @@ if __name__ == '__main__': if form.endswith('viewer%smain.ui'%os.sep): print 'Promoting WebView' - dat = dat.replace('self.view = QtWebKit.QWebView(self.widget)', 'self.view = DocumentView(self.widget)') + dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(') dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView' open(compiled_form, 'wb').write(dat) diff --git a/src/calibre/ebooks/epub/iterator.py b/src/calibre/ebooks/epub/iterator.py index e41aa47141..4dd7fe1cc8 100644 --- a/src/calibre/ebooks/epub/iterator.py +++ b/src/calibre/ebooks/epub/iterator.py @@ -6,6 +6,7 @@ Iterate over the HTML files in an ebook. Useful for writing viewers. ''' import re, os, math, copy +from cStringIO import StringIO from PyQt4.Qt import QFontDatabase @@ -16,6 +17,8 @@ from calibre.ebooks.metadata.opf2 import OPF from calibre.ptempfile import TemporaryDirectory from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.html import create_dir +from calibre.utils.zipfile import safe_replace, ZipFile +from calibre.utils.config import DynamicConfig def character_count(html): ''' @@ -62,7 +65,8 @@ class EbookIterator(object): CHARACTERS_PER_PAGE = 1000 def __init__(self, pathtoebook): - self.pathtoebook = pathtoebook + self.pathtoebook = os.path.abspath(pathtoebook) + self.config = DynamicConfig(name='iterator') ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower() ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext) map = dict(MAP) @@ -80,6 +84,9 @@ class EbookIterator(object): return i def find_embedded_fonts(self): + ''' + This will become unnecessary once Qt WebKit supports the @font-face rule. + ''' for item in self.opf.manifest: if item.mime_type and 'css' in item.mime_type.lower(): css = open(item.path, 'rb').read().decode('utf-8') @@ -125,9 +132,60 @@ class EbookIterator(object): s.max_page = s.start_page + s.pages - 1 self.toc = self.opf.toc - self.find_embedded_fonts() + self.find_embedded_fonts() + self.read_bookmarks() return self + + def parse_bookmarks(self, raw): + for line in raw.splitlines(): + if line.count('^') > 0: + tokens = line.rpartition('^') + title, ref = tokens[0], tokens[2] + self.bookmarks.append((title, ref)) + + def serialize_bookmarks(self, bookmarks): + dat = [] + for title, bm in bookmarks: + dat.append(u'%s^%s'%(title, bm)) + return (u'\n'.join(dat) +'\n').encode('utf-8') + + def read_bookmarks(self): + self.bookmarks = [] + bmfile = os.path.join(self.base, 'META-INF', 'calibre_bookmarks.txt') + raw = '' + if os.path.exists(bmfile): + raw = open(bmfile, 'rb').read().decode('utf-8') + else: + saved = self.config['bookmarks_'+self.pathtoebook] + if saved: + raw = saved + self.parse_bookmarks(raw) + + def save_bookmarks(self, bookmarks=None): + if bookmarks is None: + bookmarks = self.bookmarks + dat = self.serialize_bookmarks(bookmarks) + if os.path.splitext(self.pathtoebook)[1].lower() == '.epub': + zf = open(self.pathtoebook, 'r+b') + zipf = ZipFile(zf, mode='a') + for name in zipf.namelist(): + if name == 'META-INF/calibre_bookmarks.txt': + safe_replace(zf, 'META-INF/calibre_bookmarks.txt', StringIO(dat)) + return + zipf.writestr('META-INF/calibre_bookmarks.txt', dat) + else: + self.config['bookmarks_'+self.pathtoebook] = dat + + def add_bookmark(self, bm): + dups = [] + for x in self.bookmarks: + if x[0] == bm[0]: + dups.append(x) + for x in dups: + self.bookmarks.remove(x) + self.bookmarks.append(bm) + self.save_bookmarks() def __exit__(self, *args): self._tdir.__exit__(*args) \ No newline at end of file diff --git a/src/calibre/ebooks/html.py b/src/calibre/ebooks/html.py index e10aa9a5b3..a1ec03a16e 100644 --- a/src/calibre/ebooks/html.py +++ b/src/calibre/ebooks/html.py @@ -429,7 +429,7 @@ class Parser(PreProcessor, LoggingInterface): if not m: return ''%raw else: - return match.group().sub(m.group('uri'), "http://www.w3.org/1999/xhtml") + return match.group().replace(m.group('uri'), "http://www.w3.org/1999/xhtml") def save(self): ''' diff --git a/src/calibre/ebooks/lrf/html/convert_from.py b/src/calibre/ebooks/lrf/html/convert_from.py index 76a6825633..f219b92c67 100644 --- a/src/calibre/ebooks/lrf/html/convert_from.py +++ b/src/calibre/ebooks/lrf/html/convert_from.py @@ -809,8 +809,9 @@ class HTMLConverter(object, LoggingInterface): def append_text(src): fp, key, variant = self.font_properties(css) - src = src.replace(u'\xa0', ' ') #Sony's wonderful reading software doesn't handle the nbsp character - + for x, y in [(u'\xa0', ' '), (u'\ufb00', 'ff'), (u'\ufb01', 'fi'), (u'\ufb02', 'fl'), (u'\ufb03', 'ffi'), (u'\ufb04', 'ffl')]: + src = src.replace(x, y) + valigner = lambda x: x if 'vertical-align' in css: valign = css['vertical-align'] diff --git a/src/calibre/gui2/images/bookmarks.svg b/src/calibre/gui2/images/bookmarks.svg new file mode 100644 index 0000000000..2fcd844283 --- /dev/null +++ b/src/calibre/gui2/images/bookmarks.svg @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 949c5c116a..82cb9e7002 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -904,11 +904,7 @@ class Main(MainWindow, Ui_MainWindow): #############################View book###################################### def view_format(self, row, format): - pt = PersistentTemporaryFile('_viewer.'+format.lower()) - pt.write(self.library_view.model().db.format(row, format)) - pt.close() - self.persistent_files.append(pt) - self._view_file(pt.name) + self._view_file(self.library_view.model().db.format(row, format, as_file=True).name) def book_downloaded_for_viewing(self, job): if job.exception: diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 97aa37052f..b0e85ba181 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -13,7 +13,7 @@ from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from calibre.utils.config import Config, StringConfig from calibre.gui2.viewer.config_ui import Ui_Dialog - +from calibre.gui2.viewer.js import bookmarks, referencing def load_builtin_fonts(): from calibre.ebooks.lrf.fonts.liberation import LiberationMono_BoldItalic @@ -134,8 +134,23 @@ class Document(QWebPage): self.load_javascript_libraries) def load_javascript_libraries(self): - from calibre.resources import jquery + from calibre.resources import jquery, jquery_scrollTo self.javascript(jquery) + self.javascript(jquery_scrollTo) + self.javascript(bookmarks) + self.javascript(referencing) + + def reference_mode(self, enable): + self.javascript(('enter' if enable else 'leave')+'_reference_mode()') + + def set_reference_prefix(self, prefix): + self.javascript('reference_prefix = "%s"'%prefix) + + def goto(self, ref): + self.javascript('goto_reference("%s")'%ref) + + def goto_bookmark(self, bm): + self.javascript('scroll_to_bookmark("%s")'%bm) def javascript(self, string, typ=None): ans = self.mainFrame().evaluateJavaScript(string) @@ -162,7 +177,10 @@ class Document(QWebPage): r = self.height%self.window_height if r > 0: self.javascript('document.body.style.paddingBottom = "%dpx"'%r) - + + def bookmark(self): + return self.javascript('calculate_bookmark(%d)'%(self.ypos+25), 'string') + @apply def at_bottom(): def fget(self): @@ -190,6 +208,12 @@ class Document(QWebPage): def fget(self): return self.javascript('window.innerHeight', 'int') return property(fget=fget) + + @apply + def window_width(): + def fget(self): + return self.javascript('window.innerWidth', 'int') + return property(fget=fget) @apply def xpos(): @@ -249,15 +273,29 @@ class DocumentView(QWebView): self.document = Document(self) self.setPage(self.document) self.manager = None + self._reference_mode = False self.connect(self.document, SIGNAL('loadStarted()'), self.load_started) self.connect(self.document, SIGNAL('loadFinished(bool)'), self.load_finished) self.connect(self.document, SIGNAL('linkClicked(QUrl)'), self.link_clicked) self.connect(self.document, SIGNAL('linkHovered(QString,QString,QString)'), self.link_hovered) self.connect(self.document, SIGNAL('selectionChanged()'), self.selection_changed) + def reference_mode(self, enable): + self._reference_mode = enable + self.document.reference_mode(enable) + + def goto(self, ref): + self.document.goto(ref) + + def goto_bookmark(self, bm): + self.document.goto_bookmark(bm) + def config(self, parent=None): self.document.do_config(parent) + def bookmark(self): + return self.document.bookmark() + def selection_changed(self): if self.manager is not None: self.manager.selection_changed(unicode(self.document.selectedText())) @@ -344,8 +382,11 @@ class DocumentView(QWebView): self.initial_pos = 0.0 self.update() self.initialize_scrollbar() + self.document.reference_mode(self._reference_mode) if self.manager is not None: - self.manager.load_finished(bool(ok)) + spine_index = self.manager.load_finished(bool(ok)) + if spine_index > -1: + self.document.set_reference_prefix('%d.'%(spine_index+1)) if scrolled: self.manager.scrolled(self.document.scroll_fraction) diff --git a/src/calibre/gui2/viewer/jquery_scrollTo.js b/src/calibre/gui2/viewer/jquery_scrollTo.js new file mode 100644 index 0000000000..7f4248922f --- /dev/null +++ b/src/calibre/gui2/viewer/jquery_scrollTo.js @@ -0,0 +1,194 @@ +/** + * jQuery.ScrollTo + * Copyright (c) 2007-2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Dual licensed under MIT and GPL. + * Date: 9/11/2008 + * + * @projectDescription Easy element scrolling using jQuery. + * http://flesler.blogspot.com/2007/10/jqueryscrollto.html + * Tested with jQuery 1.2.6. On FF 2/3, IE 6/7, Opera 9.2/5 and Safari 3. on Windows. + * + * @author Ariel Flesler + * @version 1.4 + * + * @id jQuery.scrollTo + * @id jQuery.fn.scrollTo + * @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements. + * The different options for target are: + * - A number position (will be applied to all axes). + * - A string position ('44', '100px', '+=90', etc ) will be applied to all axes + * - A jQuery/DOM element ( logically, child of the element to scroll ) + * - A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc ) + * - A hash { top:x, left:y }, x and y can be any kind of number/string like above. + * @param {Number} duration The OVERALL length of the animation, this argument can be the settings object instead. + * @param {Object,Function} settings Optional set of settings or the onAfter callback. + * @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'. + * @option {Number} duration The OVERALL length of the animation. + * @option {String} easing The easing method for the animation. + * @option {Boolean} margin If true, the margin of the target element will be deducted from the final position. + * @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }. + * @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes. + * @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends. + * @option {Function} onAfter Function to be called after the scrolling ends. + * @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends. + * @return {jQuery} Returns the same jQuery object, for chaining. + * + * @desc Scroll to a fixed position + * @example $('div').scrollTo( 340 ); + * + * @desc Scroll relatively to the actual position + * @example $('div').scrollTo( '+=340px', { axis:'y' } ); + * + * @dec Scroll using a selector (relative to the scrolled element) + * @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } ); + * + * @ Scroll to a DOM element (same for jQuery object) + * @example var second_child = document.getElementById('container').firstChild.nextSibling; + * $('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){ + * alert('scrolled!!'); + * }}); + * + * @desc Scroll on both axes, to different values + * @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } ); + */ +;(function( $ ){ + + var $scrollTo = $.scrollTo = function( target, duration, settings ){ + $(window).scrollTo( target, duration, settings ); + }; + + $scrollTo.defaults = { + axis:'y', + duration:1 + }; + + // Returns the element that needs to be animated to scroll the window. + // Kept for backwards compatibility (specially for localScroll & serialScroll) + $scrollTo.window = function( scope ){ + return $(window).scrollable(); + }; + + // Hack, hack, hack... stay away! + // Returns the real elements to scroll (supports window/iframes, documents and regular nodes) + $.fn.scrollable = function(){ + return this.map(function(){ + // Just store it, we might need it + var win = this.parentWindow || this.defaultView, + // If it's a document, get its iframe or the window if it's THE document + elem = this.nodeName == '#document' ? win.frameElement || win : this, + // Get the corresponding document + doc = elem.contentDocument || (elem.contentWindow || elem).document, + isWin = elem.setInterval; + + return elem.nodeName == 'IFRAME' || isWin && $.browser.safari ? doc.body + : isWin ? doc.documentElement + : this; + }); + }; + + $.fn.scrollTo = function( target, duration, settings ){ + if( typeof duration == 'object' ){ + settings = duration; + duration = 0; + } + if( typeof settings == 'function' ) + settings = { onAfter:settings }; + + settings = $.extend( {}, $scrollTo.defaults, settings ); + // Speed is still recognized for backwards compatibility + duration = duration || settings.speed || settings.duration; + // Make sure the settings are given right + settings.queue = settings.queue && settings.axis.length > 1; + + if( settings.queue ) + // Let's keep the overall duration + duration /= 2; + settings.offset = both( settings.offset ); + settings.over = both( settings.over ); + + return this.scrollable().each(function(){ + var elem = this, + $elem = $(elem), + targ = target, toff, attr = {}, + win = $elem.is('html,body'); + + switch( typeof targ ){ + // A number will pass the regex + case 'number': + case 'string': + if( /^([+-]=)?\d+(px)?$/.test(targ) ){ + targ = both( targ ); + // We are done + break; + } + // Relative selector, no break! + targ = $(targ,this); + case 'object': + // DOMElement / jQuery + if( targ.is || targ.style ) + // Get the real position of the target + toff = (targ = $(targ)).offset(); + } + $.each( settings.axis.split(''), function( i, axis ){ + var Pos = axis == 'x' ? 'Left' : 'Top', + pos = Pos.toLowerCase(), + key = 'scroll' + Pos, + old = elem[key], + Dim = axis == 'x' ? 'Width' : 'Height', + dim = Dim.toLowerCase(); + + if( toff ){// jQuery / DOMElement + attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] ); + + // If it's a dom element, reduce the margin + if( settings.margin ){ + attr[key] -= parseInt(targ.css('margin'+Pos)) || 0; + attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0; + } + + attr[key] += settings.offset[pos] || 0; + + if( settings.over[pos] ) + // Scroll to a fraction of its width/height + attr[key] += targ[dim]() * settings.over[pos]; + }else + attr[key] = targ[pos]; + + // Number or 'number' + if( /^\d+$/.test(attr[key]) ) + // Check the limits + attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max(Dim) ); + + // Queueing axes + if( !i && settings.queue ){ + // Don't waste time animating, if there's no need. + if( old != attr[key] ) + // Intermediate animation + animate( settings.onAfterFirst ); + // Don't animate this axis again in the next iteration. + delete attr[key]; + } + }); + animate( settings.onAfter ); + + function animate( callback ){ + $elem.animate( attr, duration, settings.easing, callback && function(){ + callback.call(this, target, settings); + }); + }; + function max( Dim ){ + var attr ='scroll'+Dim, + doc = elem.ownerDocument; + + return win + ? Math.max( doc.documentElement[attr], doc.body[attr] ) + : elem[attr]; + }; + }).end(); + }; + + function both( val ){ + return typeof val == 'object' ? val : { top:val, left:val }; + }; + +})( jQuery ); \ No newline at end of file diff --git a/src/calibre/gui2/viewer/js.py b/src/calibre/gui2/viewer/js.py new file mode 100644 index 0000000000..af5a2f5e80 --- /dev/null +++ b/src/calibre/gui2/viewer/js.py @@ -0,0 +1,121 @@ +bookmarks = ''' + +function find_enclosing_block(y) { + var elements = $("*", document.body); + var min = 0; + var temp, left, top, elem, width, height, ratio; + for (i = 0; i < elements.length; i++) { + elem = $(elements[i]); + temp = elem.offset(); + left = temp.left; top = temp.top; + width = elem.width(); height = elem.height(); + if (top > y+50) break; + for ( x = 40; x < window.innerWidth; x += 20) { + if (x >= left && x <= left+width && y >= top && y <= top+height) { + if (min == 0 || min.height() > height) { min = elem; break; } + } + } + if (min != 0 && min.height() < 200) break; + } + if (y <= 0) return document.body; + if (min == 0) { return find_enclosing_block(x, y-20); } + return min; +} + +function selector_in_parent(elem) { + var num = elem.prevAll().length; + var sel = " > *:eq("+num+") "; + return sel; +} + +function selector(elem) { + var obj = elem; + var sel = ""; + while (obj[0] != document) { + sel = selector_in_parent(obj) + sel; + obj = obj.parent(); + } + return sel; +} + +function calculate_bookmark(y) { + var elem = find_enclosing_block(y); + var sel = selector(elem); + var ratio = (y - elem.offset().top)/elem.height(); + if (ratio > 1) { ratio = 1; } + if (ratio < 0) { ratio = 0; } + return sel + "|" + ratio; +} + +function scroll_to_bookmark(bookmark) { + bm = bookmark.split("|"); + var ratio = 0.7 * parseFloat(bm[1]); + $.scrollTo($(bm[0]), 1000, {over:ratio}); +} + +''' + +referencing = ''' +var reference_old_bgcol = "transparent"; +var reference_prefix = "1."; + +function show_reference_panel(ref) { + panel = $("#calibre_reference_panel"); + if (panel.length < 1) { + $(document.body).append('
Paragraph

None

') + panel = $("#calibre_reference_panel"); + } + $("> p", panel).text(ref); + panel.css({top:(window.pageYOffset+20)+"px"}); + panel.fadeIn(500); +} + +function toggle_reference(e) { + p = $(this); + if (e.type == "mouseenter") { + reference_old_bgcol = p.css("background-color"); + p.css({backgroundColor:"beige"}); + var i = 0; + var paras = $("p"); + for (j = 0; j < paras.length; j++,i++) { + if (paras[j] == p[0]) break; + } + show_reference_panel(reference_prefix+(i+1) ); + } else { + p.css({backgroundColor:reference_old_bgcol}); + panel = $("#calibre_reference_panel").hide(); + } + return false; +} + +function enter_reference_mode() { + $("p").bind("mouseenter mouseleave", toggle_reference); +} + +function leave_reference_mode() { + $("p").unbind("mouseenter mouseleave", toggle_reference); +} + +function goto_reference(ref) { + var tokens = ref.split("."); + if (tokens.length != 2) {alert("Invalid reference: "+ref); return;} + var num = parseInt(tokens[1]); + if (isNaN(num)) {alert("Invalid reference: "+ref); return;} + num -= 1; + if (num < 0) {alert("Invalid reference: "+ref); return;} + var p = $("p"); + if (num >= p.length) {alert("Reference not found: "+ref); return;} + $.scrollTo($(p[num]), 1000); +} + +''' + +test = ''' +$(document.body).click(function(e) { + bm = calculate_bookmark(e.pageY); + scroll_to_bookmark(bm); +}); + +$(document).ready(enter_reference_mode); + +''' \ No newline at end of file diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index f0f01cbcb2..56ef579465 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -2,11 +2,14 @@ from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' import traceback, os, sys, functools, collections +from functools import partial from threading import Thread from PyQt4.Qt import QMovie, QApplication, Qt, QIcon, QTimer, QWidget, SIGNAL, \ QDesktopServices, QDoubleSpinBox, QLabel, QTextBrowser, \ - QPainter, QBrush, QColor, QStandardItemModel, QStandardItem, QUrl + QPainter, QBrush, QColor, QStandardItemModel, QPalette, \ + QStandardItem, QUrl, QRegExpValidator, QRegExp, QLineEdit, \ + QToolButton, QMenu, QInputDialog from calibre.gui2.viewer.main_ui import Ui_EbookViewer from calibre.gui2.main_window import MainWindow @@ -163,29 +166,63 @@ class DoubleSpinBox(QDoubleSpinBox): self.blockSignals(True) self.setValue(val) self.blockSignals(False) - +class HelpfulLineEdit(QLineEdit): + + HELP_TEXT = _('Go to...') + + def __init__(self, *args): + QLineEdit.__init__(self, *args) + self.default_palette = QApplication.palette(self) + self.gray = QPalette(self.default_palette) + self.gray.setBrush(QPalette.Text, QBrush(QColor('gray'))) + self.connect(self, SIGNAL('editingFinished()'), + lambda : self.emit(SIGNAL('goto(PyQt_PyObject)'), unicode(self.text()))) + self.clear_to_help_mode() + + def focusInEvent(self, ev): + self.setPalette(QApplication.palette(self)) + if self.in_help_mode(): + self.setText('') + return QLineEdit.focusInEvent(self, ev) + + def in_help_mode(self): + return unicode(self.text()) == self.HELP_TEXT + + def clear_to_help_mode(self): + self.setPalette(self.gray) + self.setText(self.HELP_TEXT) + class EbookViewer(MainWindow, Ui_EbookViewer): def __init__(self, pathtoebook=None): MainWindow.__init__(self, None) self.setupUi(self) - self.iterator = None - self.current_page = None - self.pending_search = None - self.pending_anchor = None - self.selected_text = None + self.iterator = None + self.current_page = None + self.pending_search = None + self.pending_anchor = None + self.pending_reference = None + self.pending_bookmark = None + self.selected_text = None self.history = History(self.action_back, self.action_forward) self.metadata = Metadata(self) self.pos = DoubleSpinBox() self.pos.setDecimals(1) + self.pos.setToolTip(_('Position in book')) self.pos.setSuffix(_('/Unknown')+' ') self.pos.setMinimum(1.) self.tool_bar2.insertWidget(self.action_find_next, self.pos) + self.reference = HelpfulLineEdit() + self.reference.setValidator(QRegExpValidator(QRegExp(r'\d+\.\d+'), self.reference)) + self.reference.setToolTip(_('Go to a reference. To get reference numbers, use the reference mode.')) + self.tool_bar2.insertSeparator(self.action_find_next) + self.tool_bar2.insertWidget(self.action_find_next, self.reference) self.tool_bar2.insertSeparator(self.action_find_next) self.setFocusPolicy(Qt.StrongFocus) self.search = SearchBox(self, _('Search')) + self.search.setToolTip(_('Search for text in book')) self.tool_bar2.insertWidget(self.action_find_next, self.search) self.view.set_manager(self) self.pi = ProgressIndicator(self) @@ -193,6 +230,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.action_copy.setDisabled(True) self.action_metadata.setCheckable(True) self.action_table_of_contents.setCheckable(True) + self.action_reference_mode.setCheckable(True) + self.connect(self.action_reference_mode, SIGNAL('triggered(bool)'), + lambda x: self.view.reference_mode(x)) self.connect(self.action_metadata, SIGNAL('triggered(bool)'), lambda x:self.metadata.setVisible(x)) self.connect(self.action_table_of_contents, SIGNAL('triggered(bool)'), lambda x:self.toc.setVisible(x)) self.connect(self.action_copy, SIGNAL('triggered(bool)'), self.copy) @@ -209,6 +249,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.connect(self.action_find_next, SIGNAL('triggered(bool)'), lambda x:self.find(unicode(self.search.text()), True, repeat=True)) self.connect(self.action_back, SIGNAL('triggered(bool)'), self.back) + self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark) self.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward) self.connect(self.action_preferences, SIGNAL('triggered(bool)'), lambda x: self.view.config(self)) self.connect(self.pos, SIGNAL('valueChanged(double)'), self.goto_page) @@ -216,13 +257,39 @@ class EbookViewer(MainWindow, Ui_EbookViewer): lambda x: self.goto_page(x/100.)) self.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find) self.connect(self.toc, SIGNAL('clicked(QModelIndex)'), self.toc_clicked) + self.connect(self.reference, SIGNAL('goto(PyQt_PyObject)'), self.goto) + self.set_bookmarks([]) if pathtoebook is not None: f = functools.partial(self.load_ebook, pathtoebook) QTimer.singleShot(50, f) self.view.setMinimumSize(100, 100) self.splitter.setSizes([1, 300]) self.toc.setCursor(Qt.PointingHandCursor) + self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu) + self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu) + self.tool_bar.widgetForAction(self.action_bookmark).setPopupMode(QToolButton.MenuButtonPopup) + + def goto(self, ref): + if ref: + tokens = ref.split('.') + if len(tokens) > 1: + spine_index = int(tokens[0]) -1 + if spine_index == self.current_index: + self.view.goto(ref) + else: + self.pending_reference = ref + self.load_path(self.iterator.spine[spine_index]) + + def goto_bookmark(self, bm): + m = bm[1].split('#') + if len(m) > 1: + spine_index, m = int(m[0]), m[1] + if self.current_index == spine_index: + self.view.goto_bookmark(m) + else: + self.pending_bookmark = bm + self.load_path(self.iterator.spine[spine_index]) def toc_clicked(self, index): item = self.toc_model.itemFromIndex(index) @@ -263,8 +330,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer): else: self.load_path(page, pos=frac) - - def open_ebook(self, checked): files = choose_files(self, 'ebook viewer open dialog', _('Choose ebook'), @@ -285,6 +350,16 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.action_font_size_smaller.setEnabled(self.view.multiplier() > 0.2) self.set_page_number(frac) + def bookmark(self, *args): + title, ok = QInputDialog.getText(self, _('Add bookmark'), _('Enter title for bookmark:')) + title = unicode(title).strip() + if ok and title: + pos = self.view.bookmark() + bookmark = '%d#%s'%(self.current_index, pos) + self.iterator.add_bookmark((title, bookmark)) + self.set_bookmarks(self.iterator.bookmarks) + + def find(self, text, refinement, repeat=False): if not text: return @@ -308,7 +383,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if self.view.search(text): self.scrolled(self.view.scroll_fraction) - def keyPressEvent(self, event): if event.key() == Qt.Key_F3: text = unicode(self.search.text()) @@ -347,7 +421,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): index = self.iterator.spine.index(path) except ValueError: print path - return + return -1 self.current_page = self.iterator.spine[index] self.current_index = index self.set_page_number(self.view.scroll_fraction) @@ -357,6 +431,13 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if self.pending_anchor is not None: self.view.scroll_to(self.pending_anchor) self.pending_anchor = None + if self.pending_reference is not None: + self.view.goto(self.pending_reference) + self.pending_reference = None + if self.pending_bookmark is not None: + self.goto_bookmark(self.pending_bookmark) + self.pending_bookmark = None + return self.current_index def load_path(self, path, pos=0.0): self.open_progress_indicator(_('Laying out %s')%self.current_title) @@ -385,9 +466,27 @@ class EbookViewer(MainWindow, Ui_EbookViewer): for o in ('tool_bar', 'tool_bar2', 'view', 'horizontal_scrollbar', 'vertical_scrollbar'): getattr(self, o).setEnabled(False) self.setCursor(Qt.BusyCursor) + + def set_bookmarks(self, bookmarks): + menu = QMenu() + current_page = None + for bm in bookmarks: + if bm[0] == 'calibre_current_page_bookmark': + current_page = bm + else: + menu.addAction(bm[0], partial(self.goto_bookmark, bm)) + self.action_bookmark.setMenu(menu) + self._menu = menu + return current_page + def save_current_position(self): + pos = self.view.bookmark() + bookmark = '%d#%s'%(self.current_index, pos) + self.iterator.add_bookmark(('calibre_current_page_bookmark', bookmark)) + def load_ebook(self, pathtoebook): if self.iterator is not None: + self.save_current_position() self.iterator.__exit__() self.iterator = EbookIterator(pathtoebook) self.open_progress_indicator(_('Loading ebook...')) @@ -422,8 +521,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.vertical_scrollbar.setPageStep(100) self.set_vscrollbar_value(1) self.current_index = -1 - self.next_document() QApplication.instance().alert(self, 5000) + previous = self.set_bookmarks(self.iterator.bookmarks) + if previous is not None: + self.goto_bookmark(previous) + else: + self.next_document() def set_vscrollbar_value(self, pagenum): self.vertical_scrollbar.blockSignals(True) @@ -452,7 +555,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer): def __exit__(self, *args): if self.iterator is not None: + self.save_current_position() self.iterator.__exit__(*args) + def config(defaults=None): desc = _('Options to control the ebook viewer') diff --git a/src/calibre/gui2/viewer/main.ui b/src/calibre/gui2/viewer/main.ui index 12eccab399..c409b437a2 100644 --- a/src/calibre/gui2/viewer/main.ui +++ b/src/calibre/gui2/viewer/main.ui @@ -24,7 +24,7 @@ Qt::Horizontal - + @@ -84,6 +84,9 @@ + + + @@ -203,6 +206,24 @@ Preferences + + + + :/images/lookfeel.svg:/images/lookfeel.svg + + + Reference Mode + + + + + + :/images/bookmarks.svg:/images/bookmarks.svg + + + Bookmark + +