ToC Editor: Location browsing implemented

This commit is contained in:
Kovid Goyal 2013-03-06 23:39:08 +05:30
parent 1dc096129b
commit 751a6abd04
5 changed files with 274 additions and 9 deletions

Binary file not shown.

View File

@ -0,0 +1,41 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2013, Kovid Goyal <kovid at kovidgoyal.net>
Released under the GPLv3 License
###
if window?.calibre_utils
log = window.calibre_utils.log
class AnchorLocator
###
# Allow the user to click on any block level element to choose it as the
# location for an anchor.
###
constructor: () ->
if not this instanceof arguments.callee
throw new Error('AnchorLocator constructor called as function')
find_blocks: () =>
for elem in document.body.getElementsByTagName('*')
style = window.getComputedStyle(elem)
if style.display in ['block', 'flex-box', 'box']
elem.className += " calibre_toc_hover"
elem.onclick = this.onclick
onclick: (event) ->
# We dont want this event to trigger onclick on this element's parent
# block, if any.
event.stopPropagation()
frac = window.pageYOffset/document.body.scrollHeight
window.py_bridge.onclick(this, frac)
return false
calibre_anchor_locator = new AnchorLocator()
calibre_anchor_locator.find_blocks()

View File

@ -72,6 +72,7 @@ class Container(object):
self.mime_map = {}
self.name_path_map = {}
self.dirtied = set()
self.encoding_map = {}
# Map of relative paths with '/' separators from root of unzipped ePub
# to absolute paths on filesystem with os-specific separators
@ -162,27 +163,29 @@ class Container(object):
data = data[3:]
if bom_enc is not None:
try:
self.used_encoding = bom_enc
return fix_data(data.decode(bom_enc))
except UnicodeDecodeError:
pass
try:
self.used_encoding = 'utf-8'
return fix_data(data.decode('utf-8'))
except UnicodeDecodeError:
pass
data, _ = xml_to_unicode(data)
data, self.used_encoding = xml_to_unicode(data)
return fix_data(data)
def parse_xml(self, data):
data = xml_to_unicode(data, strip_encoding_pats=True, assume_utf8=True,
resolve_entities=True)[0].strip()
data, self.used_encoding = xml_to_unicode(
data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True)
return etree.fromstring(data, parser=RECOVER_PARSER)
def parse_xhtml(self, data, fname):
try:
return parse_html(data, log=self.log,
decoder=self.decode,
preprocessor=self.html_preprocessor,
filename=fname, non_html_file_tags={'ncx'})
return parse_html(
data, log=self.log, decoder=self.decode,
preprocessor=self.html_preprocessor, filename=fname,
non_html_file_tags={'ncx'})
except NotHTML:
return self.parse_xml(data)
@ -212,9 +215,11 @@ class Container(object):
def parsed(self, name):
ans = self.parsed_cache.get(name, None)
if ans is None:
self.used_encoding = None
mime = self.mime_map.get(name, guess_type(name))
ans = self.parse(self.name_path_map[name], mime)
self.parsed_cache[name] = ans
self.encoding_map[name] = self.used_encoding
return ans
@property

View File

@ -0,0 +1,195 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from base64 import b64encode
from PyQt4.Qt import (QWidget, QGridLayout, QListWidget, QSize, Qt, QUrl,
pyqtSlot, pyqtSignal, QVBoxLayout, QFrame, QLabel,
QLineEdit)
from PyQt4.QtWebKit import QWebView, QWebPage, QWebElement
from calibre.ebooks.oeb.display.webview import load_html
from calibre.utils.logging import default_log
class Page(QWebPage): # {{{
elem_clicked = pyqtSignal(object, object, object, object)
def __init__(self):
self.log = default_log
QWebPage.__init__(self)
self.js = None
self.evaljs = self.mainFrame().evaluateJavaScript
self.bridge_value = None
nam = self.networkAccessManager()
nam.setNetworkAccessible(nam.NotAccessible)
self.setLinkDelegationPolicy(self.DelegateAllLinks)
def javaScriptConsoleMessage(self, msg, lineno, msgid):
self.log(u'JS:', unicode(msg))
def javaScriptAlert(self, frame, msg):
self.log(unicode(msg))
def shouldInterruptJavaScript(self):
return True
@pyqtSlot(QWebElement, float)
def onclick(self, elem, frac):
elem_id = unicode(elem.attribute('id')) or None
tag = unicode(elem.tagName()).lower()
parent = elem
loc = []
while unicode(parent.tagName()).lower() != 'body':
num = 0
sibling = parent.previousSibling()
while not sibling.isNull():
num += 1
sibling = sibling.previousSibling()
loc.insert(0, num)
parent = parent.parent()
self.elem_clicked.emit(tag, frac, elem_id, tuple(loc))
def load_js(self):
if self.js is None:
from calibre.utils.resources import compiled_coffeescript
self.js = compiled_coffeescript('ebooks.oeb.display.utils')
self.js += compiled_coffeescript('ebooks.oeb.polish.choose')
self.mainFrame().addToJavaScriptWindowObject("py_bridge", self)
self.evaljs(self.js)
# }}}
class WebView(QWebView): # {{{
elem_clicked = pyqtSignal(object, object, object, object)
def __init__(self, parent):
QWebView.__init__(self, parent)
self._page = Page()
self._page.elem_clicked.connect(self.elem_clicked)
self.setPage(self._page)
raw = '''
body { background-color: white }
.calibre_toc_hover:hover { cursor: pointer !important; border-top: solid 5px green !important }
'''
raw = '::selection {background:#ffff00; color:#000;}\n'+raw
data = 'data:text/css;charset=utf-8;base64,'
data += b64encode(raw.encode('utf-8'))
self.settings().setUserStyleSheetUrl(QUrl(data))
def load_js(self):
self.page().load_js()
def sizeHint(self):
return QSize(1500, 300)
# }}}
class ItemEdit(QWidget):
def __init__(self, parent):
QWidget.__init__(self, parent)
self.l = l = QGridLayout()
self.setLayout(l)
self.la = la = QLabel('<b>'+_(
'Select a destination for the Table of Contents entry'))
l.addWidget(la, 0, 0, 1, 3)
self.dest_list = dl = QListWidget(self)
dl.setMinimumWidth(250)
dl.currentItemChanged.connect(self.current_changed)
l.addWidget(dl, 1, 0)
self.view = WebView(self)
self.view.elem_clicked.connect(self.elem_clicked)
l.addWidget(self.view, 1, 1)
self.f = f = QFrame()
f.setFrameShape(f.StyledPanel)
f.setMinimumWidth(250)
l.addWidget(f, 1, 2)
l = f.l = QVBoxLayout()
f.setLayout(l)
f.la = la = QLabel('<p>'+_(
'Here you can choose a destination for the Table of Contents\' entry'
' to point to. First choose a file from the book in the left-most panel. The'
' file will open in the central panel.<p>'
'Then choose a location inside the file. To do so, simply click on'
' the place in the central panel that you want to use as the'
' destination. As you move the mouse around the central panel, a'
' thick green line appears, indicating the precise location'
' that will be selected when you click.'))
la.setStyleSheet('QLabel { margin-bottom: 20px }')
la.setWordWrap(True)
l.addWidget(la)
f.la2 = la = QLabel(_('&Name of the ToC entry:'))
l.addWidget(la)
self.name = QLineEdit(self)
la.setBuddy(self.name)
l.addWidget(self.name)
self.base_msg = _('Currently selected destination:')
self.dest_label = la = QLabel(self.base_msg)
la.setTextFormat(Qt.PlainText)
la.setWordWrap(True)
la.setStyleSheet('QLabel { margin-top: 20px }')
l.addWidget(la)
l.addStretch()
def load(self, container):
self.container = container
spine_names = [container.abspath_to_name(p) for p in
container.spine_items]
spine_names = [n for n in spine_names if container.has_name(n)]
self.dest_list.addItems(spine_names)
def current_changed(self, item):
name = self.current_name = unicode(item.data(Qt.DisplayRole).toString())
path = self.container.name_to_abspath(name)
# Ensure encoding map is populated
self.container.parsed(name)
encoding = self.container.encoding_map.get(name, None) or 'utf-8'
load_html(path, self.view, codec=encoding,
mime_type=self.container.mime_map[name])
self.view.load_js()
self.dest_label.setText(self.base_msg + '\n' + _('File:') + ' ' +
name + '\n' + _('Top of the file'))
def __call__(self, item, where):
self.current_item, self.current_where = item, where
self.current_name = None
self.current_frag = None
if item is None:
self.dest_list.setCurrentRow(0)
self.name.setText(_('(Untitled)'))
self.dest_label.setText(self.base_msg + '\n' + _('None'))
def elem_clicked(self, tag, frac, elem_id, loc):
self.current_frag = elem_id or loc
frac = int(round(frac * 100))
base = _('Location: A <%s> tag inside the file')%tag
if frac == 0:
loctext = _('Top of the file')
else:
loctext = _('Approximately %d%% from the top')%frac
loctext = base + ' [%s]'%loctext
self.dest_label.setText(self.base_msg + '\n' +
_('File:') + ' ' + self.current_name + '\n' + loctext)
@property
def result(self):
return (self.current_item, self.current_where, self.current_name,
self.current_frag)

View File

@ -19,6 +19,7 @@ from calibre.ebooks.oeb.polish.container import get_container
from calibre.ebooks.oeb.polish.toc import get_toc
from calibre.gui2 import Application
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.toc.location import ItemEdit
from calibre.utils.logging import GUILog
ICON_SIZE = 24
@ -46,13 +47,15 @@ class ItemView(QFrame): # {{{
'Entries with a green tick next to them point to a location that has '
'been verified to exist. Entries with a red dot are broken and may need'
' to be fixed.'))
la.setStyleSheet('QLabel { margin-bottom: 20px }')
la.setWordWrap(True)
l = QVBoxLayout()
rp.setLayout(l)
l.addWidget(la, alignment=Qt.AlignTop)
l.addWidget(la)
self.add_new_to_root_button = b = QPushButton(_('Create a &new entry'))
b.clicked.connect(self.add_new_to_root)
l.addWidget(b, alignment=Qt.AlignTop)
l.addWidget(b)
l.addStretch()
def add_new_to_root(self):
self.add_new_item.emit(None, None)
@ -267,7 +270,10 @@ class TOCEditor(QDialog): # {{{
la.setStyleSheet('QLabel { font-size: 20pt }')
ll.addWidget(la, alignment=Qt.AlignHCenter|Qt.AlignTop)
self.toc_view = TOCView(self)
self.toc_view.add_new_item.connect(self.add_new_item)
s.addWidget(self.toc_view)
self.item_edit = ItemEdit(self)
s.addWidget(self.item_edit)
bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
l.addWidget(bb)
@ -278,6 +284,23 @@ class TOCEditor(QDialog): # {{{
self.resize(950, 630)
def add_new_item(self, item, where):
self.item_edit(item, where)
self.stacks.setCurrentIndex(2)
def accept(self):
if self.stacks.currentIndex() == 2:
self.toc_view.update_item(self.item_edit.result)
self.stacks.setCurrentIndex(1)
else:
super(TOCEditor, self).accept()
def reject(self):
if self.stacks.currentIndex() == 2:
self.stacks.setCurrentIndex(1)
else:
super(TOCEditor, self).accept()
def start(self):
t = Thread(target=self.explode)
t.daemon = True
@ -293,6 +316,7 @@ class TOCEditor(QDialog): # {{{
def read_toc(self):
self.pi.stopAnimation()
self.toc_view(self.ebook)
self.item_edit.load(self.ebook)
self.stacks.setCurrentIndex(1)
# }}}