Finish up the ToC editor

This commit is contained in:
Kovid Goyal 2013-03-13 21:15:19 +05:30
parent 4928433f75
commit 65083dc40a
4 changed files with 213 additions and 19 deletions

View File

@ -73,6 +73,7 @@ class Container(object):
self.name_path_map = {} self.name_path_map = {}
self.dirtied = set() self.dirtied = set()
self.encoding_map = {} self.encoding_map = {}
self.pretty_print = set()
# 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
@ -414,7 +415,8 @@ class Container(object):
data = self.parsed(name) data = self.parsed(name)
if name == self.opf_name: if name == self.opf_name:
self.format_opf() self.format_opf()
data = serialize(data, self.mime_map[name]) data = serialize(data, self.mime_map[name], pretty_print=name in
self.pretty_print)
if name == self.opf_name: if name == self.opf_name:
# Needed as I can't get lxml to output opf:role and # Needed as I can't get lxml to output opf:role and
# not output <opf:metadata> as well # not output <opf:metadata> as well

View File

@ -248,4 +248,5 @@ def commit_toc(container, toc, lang=None, uid=None):
to_href = partial(container.name_to_href, base=tocname) to_href = partial(container.name_to_href, base=tocname)
root = create_ncx(toc, to_href, title, lang, uid) root = create_ncx(toc, to_href, title, lang, uid)
container.replace(tocname, root) container.replace(tocname, root)
container.pretty_print.add(tocname)

View File

@ -11,7 +11,7 @@ from base64 import b64encode
from PyQt4.Qt import (QWidget, QGridLayout, QListWidget, QSize, Qt, QUrl, from PyQt4.Qt import (QWidget, QGridLayout, QListWidget, QSize, Qt, QUrl,
pyqtSlot, pyqtSignal, QVBoxLayout, QFrame, QLabel, pyqtSlot, pyqtSignal, QVBoxLayout, QFrame, QLabel,
QLineEdit) QLineEdit, QTimer)
from PyQt4.QtWebKit import QWebView, QWebPage, QWebElement from PyQt4.QtWebKit import QWebView, QWebPage, QWebElement
from calibre.ebooks.oeb.display.webview import load_html from calibre.ebooks.oeb.display.webview import load_html
@ -88,6 +88,16 @@ class WebView(QWebView): # {{{
def sizeHint(self): def sizeHint(self):
return QSize(1500, 300) return QSize(1500, 300)
def show_frag(self, frag):
self.page().mainFrame().scrollToAnchor(frag)
@property
def scroll_frac(self):
val, ok = self.page().evaljs('window.pageYOffset/document.body.scrollHeight').toFloat()
if not ok:
val = 0
return val
# }}} # }}}
class ItemEdit(QWidget): class ItemEdit(QWidget):
@ -169,22 +179,61 @@ class ItemEdit(QWidget):
self.current_item, self.current_where = item, where self.current_item, self.current_where = item, where
self.current_name = None self.current_name = None
self.current_frag = None self.current_frag = None
if item is None: self.name.setText(_('(Untitled)'))
self.dest_list.setCurrentRow(0) dest_index, frag = 0, None
self.name.setText(_('(Untitled)')) if item is not None:
if where is None:
self.name.setText(item.data(0, Qt.DisplayRole).toString())
toc = item.data(0, Qt.UserRole).toPyObject()
if toc.dest:
for i in xrange(self.dest_list.count()):
litem = self.dest_list.item(i)
if unicode(litem.data(Qt.DisplayRole).toString()) == toc.dest:
dest_index = i
frag = toc.frag
break
def elem_clicked(self, tag, frac, elem_id, loc): self.dest_list.blockSignals(True)
self.current_frag = elem_id or loc self.dest_list.setCurrentRow(dest_index)
self.dest_list.blockSignals(False)
item = self.dest_list.item(dest_index)
self.current_changed(item)
if frag:
self.current_frag = frag
QTimer.singleShot(1, self.show_frag)
def show_frag(self):
self.view.show_frag(self.current_frag)
QTimer.singleShot(1, self.check_frag)
def check_frag(self):
pos = self.view.scroll_frac
if pos == 0:
self.current_frag = None
self.update_dest_label()
def get_loctext(self, frac):
frac = int(round(frac * 100)) frac = int(round(frac * 100))
base = _('Location: A &lt;%s&gt; tag inside the file')%tag
if frac == 0: if frac == 0:
loctext = _('Top of the file') loctext = _('Top of the file')
else: else:
loctext = _('Approximately %d%% from the top')%frac loctext = _('Approximately %d%% from the top')%frac
loctext = base + ' [%s]'%loctext return loctext
def elem_clicked(self, tag, frac, elem_id, loc):
self.current_frag = elem_id or loc
base = _('Location: A &lt;%s&gt; tag inside the file')%tag
loctext = base + ' [%s]'%self.get_loctext(frac)
self.dest_label.setText(self.base_msg + '<br>' + self.dest_label.setText(self.base_msg + '<br>' +
_('File:') + ' ' + self.current_name + '<br>' + loctext) _('File:') + ' ' + self.current_name + '<br>' + loctext)
def update_dest_label(self):
val = self.view.scroll_frac
self.dest_label.setText(self.base_msg + '<br>' +
_('File:') + ' ' + self.current_name + '<br>' +
self.get_loctext(val))
@property @property
def result(self): def result(self):
return (self.current_item, self.current_where, self.current_name, return (self.current_item, self.current_where, self.current_name,

View File

@ -9,8 +9,9 @@ __docformat__ = 'restructuredtext en'
import sys, os import sys, os
from threading import Thread from threading import Thread
from functools import partial
from PyQt4.Qt import (QPushButton, QFrame, from PyQt4.Qt import (QPushButton, QFrame, QVariant,
QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget, QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget,
QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem, QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem,
QToolButton, QItemSelectionModel) QToolButton, QItemSelectionModel)
@ -27,6 +28,8 @@ ICON_SIZE = 24
class ItemView(QFrame): # {{{ class ItemView(QFrame): # {{{
add_new_item = pyqtSignal(object, object) add_new_item = pyqtSignal(object, object)
delete_item = pyqtSignal()
flatten_item = pyqtSignal()
def __init__(self, parent): def __init__(self, parent):
QFrame.__init__(self, parent) QFrame.__init__(self, parent)
@ -38,6 +41,7 @@ class ItemView(QFrame): # {{{
l.addWidget(s) l.addWidget(s)
self.root_pane = rp = QWidget(self) self.root_pane = rp = QWidget(self)
self.item_pane = ip = QWidget(self) self.item_pane = ip = QWidget(self)
self.current_item = None
s.addWidget(rp) s.addWidget(rp)
s.addWidget(ip) s.addWidget(ip)
@ -49,7 +53,7 @@ class ItemView(QFrame): # {{{
' to be fixed.')) ' to be fixed.'))
la.setStyleSheet('QLabel { margin-bottom: 20px }') la.setStyleSheet('QLabel { margin-bottom: 20px }')
la.setWordWrap(True) la.setWordWrap(True)
l = QVBoxLayout() l = rp.l = QVBoxLayout()
rp.setLayout(l) rp.setLayout(l)
l.addWidget(la) 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'))
@ -57,14 +61,109 @@ class ItemView(QFrame): # {{{
l.addWidget(b) l.addWidget(b)
l.addStretch() l.addStretch()
l = ip.l = QGridLayout()
ip.setLayout(l)
la = ip.heading = QLabel('')
l.addWidget(la, 0, 0, 1, 2)
la.setWordWrap(True)
la = ip.la = QLabel(_(
'You can move this entry around the Table of Contents by drag '
'and drop or using the up and down buttons to the left'))
la.setWordWrap(True)
l.addWidget(la, 1, 0, 1, 2)
# Item status
ip.hl1 = hl = QFrame()
hl.setFrameShape(hl.HLine)
l.addWidget(hl, l.rowCount(), 0, 1, 2)
self.icon_label = QLabel()
self.status_label = QLabel()
self.status_label.setWordWrap(True)
l.addWidget(self.icon_label, l.rowCount(), 0)
l.addWidget(self.status_label, l.rowCount()-1, 1)
ip.hl2 = hl = QFrame()
hl.setFrameShape(hl.HLine)
l.addWidget(hl, l.rowCount(), 0, 1, 2)
# Edit/remove item
rs = l.rowCount()
ip.b1 = b = QPushButton(QIcon(I('edit_input.png')),
_('Change the &location this entry points to'), self)
b.clicked.connect(self.edit_item)
l.addWidget(b, l.rowCount()+1, 0, 1, 2)
ip.b2 = b = QPushButton(QIcon(I('trash.png')),
_('&Remove this entry'), self)
l.addWidget(b, l.rowCount(), 0, 1, 2)
b.clicked.connect(self.delete_item)
ip.hl3 = hl = QFrame()
hl.setFrameShape(hl.HLine)
l.addWidget(hl, l.rowCount(), 0, 1, 2)
l.setRowMinimumHeight(rs, 20)
# Add new item
rs = l.rowCount()
ip.b3 = b = QPushButton(QIcon(I('plus.png')), _('New entry &inside this entry'))
b.clicked.connect(partial(self.add_new, 'inside'))
l.addWidget(b, l.rowCount()+1, 0, 1, 2)
ip.b4 = b = QPushButton(QIcon(I('plus.png')), _('New entry &above this entry'))
b.clicked.connect(partial(self.add_new, 'before'))
l.addWidget(b, l.rowCount(), 0, 1, 2)
ip.b5 = b = QPushButton(QIcon(I('plus.png')), _('New entry &below this entry'))
b.clicked.connect(partial(self.add_new, 'after'))
l.addWidget(b, l.rowCount(), 0, 1, 2)
ip.hl4 = hl = QFrame()
hl.setFrameShape(hl.HLine)
l.addWidget(hl, l.rowCount(), 0, 1, 2)
l.setRowMinimumHeight(rs, 20)
# Flatten entry
rs = l.rowCount()
ip.b3 = b = QPushButton(QIcon(I('heuristics.png')), _('&Flatten this entry'))
b.clicked.connect(self.flatten_item)
b.setToolTip(_('All children of this entry are brought to the same '
'level as this entry.'))
l.addWidget(b, l.rowCount()+1, 0, 1, 2)
l.setRowMinimumHeight(rs, 20)
l.addWidget(QLabel(), l.rowCount(), 0, 1, 2)
l.setColumnStretch(1, 10)
l.setRowStretch(l.rowCount()-1, 10)
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)
def add_new(self, where):
self.add_new_item.emit(self.current_item, where)
def edit_item(self):
self.add_new_item.emit(self.current_item, None)
def __call__(self, item): def __call__(self, item):
if item is None: if item is None:
self.current_item = None
self.stack.setCurrentIndex(0) self.stack.setCurrentIndex(0)
else: else:
self.current_item = item
self.stack.setCurrentIndex(1) self.stack.setCurrentIndex(1)
self.populate_item_pane()
def populate_item_pane(self):
item = self.current_item
name = unicode(item.data(0, Qt.DisplayRole).toString())
self.item_pane.heading.setText('<h2>%s</h2>'%name)
self.icon_label.setPixmap(item.data(0, Qt.DecorationRole
).toPyObject().pixmap(32, 32))
tt = _('This entry points to an existing destination')
toc = item.data(0, Qt.UserRole).toPyObject()
if toc.dest_exists is False:
tt = _('The location this entry points to does not exist')
elif toc.dest_exists is None:
tt = ''
self.status_label.setText(tt)
def data_changed(self, item):
if item is self.current_item:
self.populate_item_pane()
# }}} # }}}
@ -120,7 +219,9 @@ class TOCView(QWidget): # {{{
self.hl = hl = QLabel(self.default_msg) self.hl = hl = QLabel(self.default_msg)
l.addWidget(hl, col, 2, 1, -1) l.addWidget(hl, col, 2, 1, -1)
self.item_view = i = ItemView(self) self.item_view = i = ItemView(self)
self.item_view.delete_item.connect(self.delete_current_item)
i.add_new_item.connect(self.add_new_item) i.add_new_item.connect(self.add_new_item)
i.flatten_item.connect(self.flatten_item)
l.addWidget(i, 0, 4, col, 1) l.addWidget(i, 0, 4, col, 1)
l.setColumnStretch(2, 10) l.setColumnStretch(2, 10)
@ -139,6 +240,22 @@ class TOCView(QWidget): # {{{
p = item.parent() or self.root p = item.parent() or self.root
p.removeChild(item) p.removeChild(item)
def delete_current_item(self):
item = self.tocw.currentItem()
if item is not None:
p = item.parent() or self.root
p.removeChild(item)
def flatten_item(self):
item = self.tocw.currentItem()
if item is not None:
p = item.parent() or self.root
idx = p.indexOfChild(item)
children = [item.child(i) for i in xrange(item.childCount())]
for child in reversed(children):
item.removeChild(child)
p.insertChild(idx+1, child)
def highlight_item(self, item): def highlight_item(self, item):
self.tocw.setCurrentItem(item, 0, QItemSelectionModel.ClearAndSelect) self.tocw.setCurrentItem(item, 0, QItemSelectionModel.ClearAndSelect)
self.tocw.scrollToItem(item) self.tocw.scrollToItem(item)
@ -207,9 +324,18 @@ class TOCView(QWidget): # {{{
toc.title = new_title or _('(Untitled)') toc.title = new_title or _('(Untitled)')
item = self.tocw.itemFromIndex(idx) item = self.tocw.itemFromIndex(idx)
self.update_status_tip(item) self.update_status_tip(item)
self.item_view.data_changed(item)
def create_item(self, parent, child): def create_item(self, parent, child, idx=-1):
c = QTreeWidgetItem(parent) if idx == -1:
c = QTreeWidgetItem(parent)
else:
c = QTreeWidgetItem()
parent.insertChild(idx, c)
self.populate_item(c, child)
return c
def populate_item(self, c, child):
c.setData(0, Qt.DisplayRole, child.title or _('(Untitled)')) c.setData(0, Qt.DisplayRole, child.title or _('(Untitled)'))
c.setData(0, Qt.UserRole, child) c.setData(0, Qt.UserRole, child)
c.setFlags(Qt.ItemIsDragEnabled|Qt.ItemIsEditable|Qt.ItemIsEnabled| c.setFlags(Qt.ItemIsDragEnabled|Qt.ItemIsEditable|Qt.ItemIsEnabled|
@ -219,9 +345,10 @@ class TOCView(QWidget): # {{{
c.setData(0, Qt.ToolTipRole, _( c.setData(0, Qt.ToolTipRole, _(
'The location this entry point to does not exist:\n%s') 'The location this entry point to does not exist:\n%s')
%child.dest_error) %child.dest_error)
else:
c.setData(0, Qt.ToolTipRole, QVariant())
self.update_status_tip(c) self.update_status_tip(c)
return c
def __call__(self, ebook): def __call__(self, ebook):
self.ebook = ebook self.ebook = ebook
@ -250,14 +377,29 @@ class TOCView(QWidget): # {{{
def update_item(self, item, where, name, frag, title): def update_item(self, item, where, name, frag, title):
if isinstance(frag, tuple): if isinstance(frag, tuple):
frag = add_id(self.ebook, name, frag) frag = add_id(self.ebook, name, frag)
child = TOC(title, name, frag)
child.dest_exists = True
if item is None: if item is None:
if where is None: # New entry at root level
where = self.tocw.invisibleRootItem() c = self.create_item(self.root, child)
child = TOC(title, name, frag)
child.dest_exists = True
c = self.create_item(where, child)
self.tocw.setCurrentItem(c, 0, QItemSelectionModel.ClearAndSelect) self.tocw.setCurrentItem(c, 0, QItemSelectionModel.ClearAndSelect)
self.tocw.scrollToItem(c) self.tocw.scrollToItem(c)
else:
if where is None:
# Editing existing entry
self.populate_item(item, child)
else:
if where == 'inside':
parent = item
idx = -1
else:
parent = item.parent() or self.root
idx = parent.indexOfChild(item)
if where == 'after': idx += 1
c = self.create_item(parent, child, idx=idx)
self.tocw.setCurrentItem(c, 0, QItemSelectionModel.ClearAndSelect)
self.tocw.scrollToItem(c)
def create_toc(self): def create_toc(self):
root = TOC() root = TOC()