mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Finish up the ToC editor
This commit is contained in:
parent
4928433f75
commit
65083dc40a
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 <%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
|
||||
return loctext
|
||||
|
||||
|
||||
def elem_clicked(self, tag, frac, elem_id, loc):
|
||||
self.current_frag = elem_id or loc
|
||||
base = _('Location: A <%s> 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,
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user