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.dirtied = set()
self.encoding_map = {}
self.pretty_print = set()
# Map of relative paths with '/' separators from root of unzipped ePub
# to absolute paths on filesystem with os-specific separators
@ -414,7 +415,8 @@ class Container(object):
data = self.parsed(name)
if name == self.opf_name:
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:
# Needed as I can't get lxml to output opf:role and
# 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)
root = create_ncx(toc, to_href, title, lang, uid)
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,
pyqtSlot, pyqtSignal, QVBoxLayout, QFrame, QLabel,
QLineEdit)
QLineEdit, QTimer)
from PyQt4.QtWebKit import QWebView, QWebPage, QWebElement
from calibre.ebooks.oeb.display.webview import load_html
@ -88,6 +88,16 @@ class WebView(QWebView): # {{{
def sizeHint(self):
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):
@ -169,22 +179,61 @@ class ItemEdit(QWidget):
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.name.setText(_('(Untitled)'))
dest_index, frag = 0, None
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.current_frag = elem_id or loc
self.dest_list.blockSignals(True)
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))
base = _('Location: A &lt;%s&gt; 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
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>' +
_('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
def result(self):
return (self.current_item, self.current_where, self.current_name,

View File

@ -9,8 +9,9 @@ __docformat__ = 'restructuredtext en'
import sys, os
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,
QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem,
QToolButton, QItemSelectionModel)
@ -27,6 +28,8 @@ ICON_SIZE = 24
class ItemView(QFrame): # {{{
add_new_item = pyqtSignal(object, object)
delete_item = pyqtSignal()
flatten_item = pyqtSignal()
def __init__(self, parent):
QFrame.__init__(self, parent)
@ -38,6 +41,7 @@ class ItemView(QFrame): # {{{
l.addWidget(s)
self.root_pane = rp = QWidget(self)
self.item_pane = ip = QWidget(self)
self.current_item = None
s.addWidget(rp)
s.addWidget(ip)
@ -49,7 +53,7 @@ class ItemView(QFrame): # {{{
' to be fixed.'))
la.setStyleSheet('QLabel { margin-bottom: 20px }')
la.setWordWrap(True)
l = QVBoxLayout()
l = rp.l = QVBoxLayout()
rp.setLayout(l)
l.addWidget(la)
self.add_new_to_root_button = b = QPushButton(_('Create a &new entry'))
@ -57,14 +61,109 @@ class ItemView(QFrame): # {{{
l.addWidget(b)
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):
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):
if item is None:
self.current_item = None
self.stack.setCurrentIndex(0)
else:
self.current_item = item
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)
l.addWidget(hl, col, 2, 1, -1)
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.flatten_item.connect(self.flatten_item)
l.addWidget(i, 0, 4, col, 1)
l.setColumnStretch(2, 10)
@ -139,6 +240,22 @@ class TOCView(QWidget): # {{{
p = item.parent() or self.root
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):
self.tocw.setCurrentItem(item, 0, QItemSelectionModel.ClearAndSelect)
self.tocw.scrollToItem(item)
@ -207,9 +324,18 @@ class TOCView(QWidget): # {{{
toc.title = new_title or _('(Untitled)')
item = self.tocw.itemFromIndex(idx)
self.update_status_tip(item)
self.item_view.data_changed(item)
def create_item(self, parent, child):
c = QTreeWidgetItem(parent)
def create_item(self, parent, child, idx=-1):
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.UserRole, child)
c.setFlags(Qt.ItemIsDragEnabled|Qt.ItemIsEditable|Qt.ItemIsEnabled|
@ -219,9 +345,10 @@ class TOCView(QWidget): # {{{
c.setData(0, Qt.ToolTipRole, _(
'The location this entry point to does not exist:\n%s')
%child.dest_error)
else:
c.setData(0, Qt.ToolTipRole, QVariant())
self.update_status_tip(c)
return c
def __call__(self, ebook):
self.ebook = ebook
@ -250,14 +377,29 @@ class TOCView(QWidget): # {{{
def update_item(self, item, where, name, frag, title):
if isinstance(frag, tuple):
frag = add_id(self.ebook, name, frag)
child = TOC(title, name, frag)
child.dest_exists = True
if item is None:
if where is None:
where = self.tocw.invisibleRootItem()
child = TOC(title, name, frag)
child.dest_exists = True
c = self.create_item(where, child)
# New entry at root level
c = self.create_item(self.root, child)
self.tocw.setCurrentItem(c, 0, QItemSelectionModel.ClearAndSelect)
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):
root = TOC()