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.mime_map = {}
self.name_path_map = {} self.name_path_map = {}
self.dirtied = set() self.dirtied = set()
self.encoding_map = {}
# Map of relative paths with '/' separators from root of unzipped ePub # Map of relative paths with '/' separators from root of unzipped ePub
# to absolute paths on filesystem with os-specific separators # to absolute paths on filesystem with os-specific separators
@ -162,27 +163,29 @@ class Container(object):
data = data[3:] data = data[3:]
if bom_enc is not None: if bom_enc is not None:
try: try:
self.used_encoding = bom_enc
return fix_data(data.decode(bom_enc)) return fix_data(data.decode(bom_enc))
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
try: try:
self.used_encoding = 'utf-8'
return fix_data(data.decode('utf-8')) return fix_data(data.decode('utf-8'))
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
data, _ = xml_to_unicode(data) data, self.used_encoding = xml_to_unicode(data)
return fix_data(data) return fix_data(data)
def parse_xml(self, data): def parse_xml(self, data):
data = xml_to_unicode(data, strip_encoding_pats=True, assume_utf8=True, data, self.used_encoding = xml_to_unicode(
resolve_entities=True)[0].strip() data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True)
return etree.fromstring(data, parser=RECOVER_PARSER) return etree.fromstring(data, parser=RECOVER_PARSER)
def parse_xhtml(self, data, fname): def parse_xhtml(self, data, fname):
try: try:
return parse_html(data, log=self.log, return parse_html(
decoder=self.decode, data, log=self.log, decoder=self.decode,
preprocessor=self.html_preprocessor, preprocessor=self.html_preprocessor, filename=fname,
filename=fname, non_html_file_tags={'ncx'}) non_html_file_tags={'ncx'})
except NotHTML: except NotHTML:
return self.parse_xml(data) return self.parse_xml(data)
@ -212,9 +215,11 @@ class Container(object):
def parsed(self, name): def parsed(self, name):
ans = self.parsed_cache.get(name, None) ans = self.parsed_cache.get(name, None)
if ans is None: if ans is None:
self.used_encoding = None
mime = self.mime_map.get(name, guess_type(name)) mime = self.mime_map.get(name, guess_type(name))
ans = self.parse(self.name_path_map[name], mime) ans = self.parse(self.name_path_map[name], mime)
self.parsed_cache[name] = ans self.parsed_cache[name] = ans
self.encoding_map[name] = self.used_encoding
return ans return ans
@property @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.ebooks.oeb.polish.toc import get_toc
from calibre.gui2 import Application from calibre.gui2 import Application
from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.toc.location import ItemEdit
from calibre.utils.logging import GUILog from calibre.utils.logging import GUILog
ICON_SIZE = 24 ICON_SIZE = 24
@ -46,13 +47,15 @@ class ItemView(QFrame): # {{{
'Entries with a green tick next to them point to a location that has ' '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' 'been verified to exist. Entries with a red dot are broken and may need'
' to be fixed.')) ' to be fixed.'))
la.setStyleSheet('QLabel { margin-bottom: 20px }')
la.setWordWrap(True) la.setWordWrap(True)
l = QVBoxLayout() l = QVBoxLayout()
rp.setLayout(l) rp.setLayout(l)
l.addWidget(la, alignment=Qt.AlignTop) l.addWidget(la)
self.add_new_to_root_button = b = QPushButton(_('Create a &new entry')) self.add_new_to_root_button = b = QPushButton(_('Create a &new entry'))
b.clicked.connect(self.add_new_to_root) 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): def add_new_to_root(self):
self.add_new_item.emit(None, None) self.add_new_item.emit(None, None)
@ -267,7 +270,10 @@ class TOCEditor(QDialog): # {{{
la.setStyleSheet('QLabel { font-size: 20pt }') la.setStyleSheet('QLabel { font-size: 20pt }')
ll.addWidget(la, alignment=Qt.AlignHCenter|Qt.AlignTop) ll.addWidget(la, alignment=Qt.AlignHCenter|Qt.AlignTop)
self.toc_view = TOCView(self) self.toc_view = TOCView(self)
self.toc_view.add_new_item.connect(self.add_new_item)
s.addWidget(self.toc_view) s.addWidget(self.toc_view)
self.item_edit = ItemEdit(self)
s.addWidget(self.item_edit)
bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
l.addWidget(bb) l.addWidget(bb)
@ -278,6 +284,23 @@ class TOCEditor(QDialog): # {{{
self.resize(950, 630) 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): def start(self):
t = Thread(target=self.explode) t = Thread(target=self.explode)
t.daemon = True t.daemon = True
@ -293,6 +316,7 @@ class TOCEditor(QDialog): # {{{
def read_toc(self): def read_toc(self):
self.pi.stopAnimation() self.pi.stopAnimation()
self.toc_view(self.ebook) self.toc_view(self.ebook)
self.item_edit.load(self.ebook)
self.stacks.setCurrentIndex(1) self.stacks.setCurrentIndex(1)
# }}} # }}}