mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
ToC Editor: Location browsing implemented
This commit is contained in:
parent
1dc096129b
commit
751a6abd04
Binary file not shown.
41
src/calibre/ebooks/oeb/polish/choose.coffee
Normal file
41
src/calibre/ebooks/oeb/polish/choose.coffee
Normal 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()
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
195
src/calibre/gui2/toc/location.py
Normal file
195
src/calibre/gui2/toc/location.py
Normal 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)
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user