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.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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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.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.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 <%s> 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 <%s> 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,
|
||||||
|
@ -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):
|
||||||
|
if idx == -1:
|
||||||
c = QTreeWidgetItem(parent)
|
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)
|
||||||
if item is None:
|
|
||||||
if where is None:
|
|
||||||
where = self.tocw.invisibleRootItem()
|
|
||||||
child = TOC(title, name, frag)
|
child = TOC(title, name, frag)
|
||||||
child.dest_exists = True
|
child.dest_exists = True
|
||||||
c = self.create_item(where, child)
|
if item is None:
|
||||||
|
# New entry at root level
|
||||||
|
c = self.create_item(self.root, 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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user