From 41c0926f2385bdaab916733756db3783aa983b75 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Dec 2013 18:53:16 +0530 Subject: [PATCH] ToC view for Edit Book --- src/calibre/gui2/tweak_book/__init__.py | 1 + src/calibre/gui2/tweak_book/boss.py | 41 ++++---- src/calibre/gui2/tweak_book/editor/text.py | 7 +- src/calibre/gui2/tweak_book/toc.py | 106 ++++++++++++++++++++- src/calibre/gui2/tweak_book/ui.py | 9 ++ 5 files changed, 143 insertions(+), 21 deletions(-) diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index 27c4549f1a..274fa575cd 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -37,3 +37,4 @@ class NonReplaceDict(dict): actions = NonReplaceDict() editors = NonReplaceDict() +TOP = object() diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 6ad79e786f..214d6645fc 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -104,6 +104,7 @@ class Boss(QObject): self.gui.check_book.item_activated.connect(self.check_item_activated) self.gui.check_book.check_requested.connect(self.check_requested) self.gui.check_book.fix_requested.connect(self.fix_requested) + self.gui.toc_view.navigate_requested.connect(self.link_clicked) def preferences(self): p = Preferences(self.gui) @@ -188,22 +189,24 @@ class Boss(QObject): parse_worker.clear() container = job.result set_current_container(container) - self.current_metadata = self.gui.current_metadata = container.mi - self.global_undo.open_book(container) - self.gui.update_window_title() - self.gui.file_list.current_edited_name = None - self.gui.file_list.build(container, preserve_state=False) - self.gui.action_save.setEnabled(False) - self.update_global_history_actions() - recent_books = list(tprefs.get('recent-books', [])) - path = container.path_to_ebook - if path in recent_books: - recent_books.remove(path) - recent_books.insert(0, path) - tprefs['recent-books'] = recent_books[:10] - self.gui.update_recent_books() - if ef: - self.gui.file_list.request_edit(ef) + with BusyCursor(): + self.current_metadata = self.gui.current_metadata = container.mi + self.global_undo.open_book(container) + self.gui.update_window_title() + self.gui.file_list.current_edited_name = None + self.gui.file_list.build(container, preserve_state=False) + self.gui.action_save.setEnabled(False) + self.update_global_history_actions() + recent_books = list(tprefs.get('recent-books', [])) + path = container.path_to_ebook + if path in recent_books: + recent_books.remove(path) + recent_books.insert(0, path) + tprefs['recent-books'] = recent_books[:10] + self.gui.update_recent_books() + if ef: + self.gui.file_list.request_edit(ef) + self.gui.toc_view.update_if_visible() def update_editors_from_container(self, container=None): c = container or current_container() @@ -291,7 +294,9 @@ class Boss(QObject): if d.exec_() != d.Accepted: self.rewind_savepoint() return - self.update_editors_from_container() + with BusyCursor(): + self.update_editors_from_container() + self.gui.toc_view.update_if_visible() def polish(self, action, name): self.commit_all_editors_to_container() @@ -698,6 +703,8 @@ class Boss(QObject): @in_thread_job def link_clicked(self, name, anchor): + if not name: + return if name in editors: editor = editors[name] self.gui.central.show_editor(editor) diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 605ccd10e2..4eb8e0b378 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -15,7 +15,7 @@ from PyQt4.Qt import ( QTextEdit, QTextFormat, QWidget, QSize, QPainter, Qt, QRect, pyqtSlot, QApplication, QMimeData) -from calibre.gui2.tweak_book import tprefs +from calibre.gui2.tweak_book import tprefs, TOP from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter @@ -332,6 +332,11 @@ class TextEdit(QPlainTextEdit): return True def go_to_anchor(self, anchor): + if anchor is TOP: + c = self.textCursor() + c.movePosition(c.Start) + self.setTextCursor(c) + return True base = r'''%%s\s*=\s*['"]{0,1}%s''' % regex.escape(anchor) raw = unicode(self.toPlainText()) m = regex.search(base % 'id', raw) diff --git a/src/calibre/gui2/tweak_book/toc.py b/src/calibre/gui2/tweak_book/toc.py index fa828a4f0f..52bc6740e3 100644 --- a/src/calibre/gui2/tweak_book/toc.py +++ b/src/calibre/gui2/tweak_book/toc.py @@ -6,12 +6,15 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -from PyQt4.Qt import (QDialog, pyqtSignal, QIcon, QVBoxLayout, QDialogButtonBox, QStackedWidget) +from PyQt4.Qt import ( + QDialog, pyqtSignal, QIcon, QVBoxLayout, QDialogButtonBox, QStackedWidget, + QAction, QMenu, QTreeWidget, QTreeWidgetItem, QGridLayout, QWidget, Qt, + QSize, QStyledItemDelegate, QTimer) -from calibre.ebooks.oeb.polish.toc import commit_toc +from calibre.ebooks.oeb.polish.toc import commit_toc, get_toc from calibre.gui2 import gprefs, error_dialog from calibre.gui2.toc.main import TOCView, ItemEdit -from calibre.gui2.tweak_book import current_container +from calibre.gui2.tweak_book import current_container, TOP class TOCEditor(QDialog): @@ -94,3 +97,100 @@ class TOCEditor(QDialog): commit_toc(current_container(), toc, lang=self.toc_view.toc_lang, uid=self.toc_view.toc_uid) +DEST_ROLE = Qt.UserRole +FRAG_ROLE = DEST_ROLE + 1 + +class Delegate(QStyledItemDelegate): + + def sizeHint(self, *args): + ans = QStyledItemDelegate.sizeHint(self, *args) + return ans + QSize(0, 10) + +class TOCViewer(QWidget): + + navigate_requested = pyqtSignal(object, object) + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.l = l = QGridLayout(self) + self.setLayout(l) + l.setContentsMargins(0, 0, 0, 0) + + self.is_visible = False + self.view = QTreeWidget(self) + self.delegate = Delegate(self.view) + self.view.setItemDelegate(self.delegate) + self.view.setHeaderHidden(True) + self.view.setAnimated(True) + self.view.setContextMenuPolicy(Qt.CustomContextMenu) + self.view.customContextMenuRequested.connect(self.show_context_menu, type=Qt.QueuedConnection) + self.view.itemActivated.connect(self.emit_navigate) + self.view.itemClicked.connect(self.emit_navigate) + l.addWidget(self.view) + + self.refresh_action = QAction(QIcon(I('view-refresh.png')), _('&Refresh'), self) + self.refresh_action.triggered.connect(self.build) + self._last_nav_request = None + + def show_context_menu(self, pos): + menu = QMenu(self) + menu.addAction(self.refresh_action) + menu.addAction(_('&Expand all'), self.view.expandAll) + menu.addAction(_('&Collapse all'), self.view.collapseAll) + menu.exec_(self.view.mapToGlobal(pos)) + + def iteritems(self, parent=None): + if parent is None: + parent = self.invisibleRootItem() + for i in xrange(parent.childCount()): + child = parent.child(i) + yield child + for gc in self.iteritems(parent=child): + yield gc + + def emit_navigate(self, *args): + item = self.view.currentItem() + if item is not None: + dest = unicode(item.data(0, DEST_ROLE).toString()) + frag = unicode(item.data(0, FRAG_ROLE).toString()) + if not frag: + frag = TOP + # Debounce as on some platforms clicking causes both itemActivated + # and itemClicked to be emitted + self._last_nav_request = (dest, frag) + QTimer.singleShot(0, self._emit_navigate) + + def _emit_navigate(self): + if self._last_nav_request is not None: + self.navigate_requested.emit(*self._last_nav_request) + self._last_nav_request = None + + def build(self): + c = current_container() + if c is None: + return + toc = get_toc(c, verify_destinations=False) + + def process_node(toc, parent): + for child in toc: + node = QTreeWidgetItem(parent) + node.setText(0, child.title or '') + node.setData(0, DEST_ROLE, child.dest or '') + node.setData(0, FRAG_ROLE, child.frag or '') + tt = _('File: {0}\nAnchor: {1}').format( + child.dest or '', child.frag or '') + node.setData(0, Qt.ToolTipRole, tt) + process_node(child, node) + + self.view.clear() + process_node(toc, self.view.invisibleRootItem()) + + def visibility_changed(self, visible): + self.is_visible = visible + if visible: + self.build() + + def update_if_visible(self): + if self.is_visible: + self.build() + diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 2169af26fe..2cd54c2907 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -25,6 +25,7 @@ from calibre.gui2.tweak_book.boss import Boss from calibre.gui2.tweak_book.preview import Preview from calibre.gui2.tweak_book.search import SearchPanel from calibre.gui2.tweak_book.check import Check +from calibre.gui2.tweak_book.toc import TOCViewer class Central(QStackedWidget): @@ -197,6 +198,7 @@ class Main(MainWindow): self.central = Central(self) self.setCentralWidget(self.central) self.check_book = Check(self) + self.toc_view = TOCViewer(self) self.create_actions() self.create_toolbars() @@ -506,6 +508,13 @@ class Main(MainWindow): d.close() # By default the inspector window is closed d.setFeatures(d.DockWidgetClosable | d.DockWidgetMovable) # QWebInspector does not work in a floating dock + d = create(_('Table of Contents'), 'toc-viewer') + d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea) + d.setWidget(self.toc_view) + self.addDockWidget(Qt.LeftDockWidgetArea, d) + d.close() # Hidden by default + d.visibilityChanged.connect(self.toc_view.visibility_changed) + def resizeEvent(self, ev): self.blocking_job.resize(ev.size()) return super(Main, self).resizeEvent(ev)