mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-10-26 08:12:25 -04:00 
			
		
		
		
	ToC Editor: Location browsing implemented
This commit is contained in:
		
							parent
							
								
									1dc096129b
								
							
						
					
					
						commit
						751a6abd04
					
				
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										41
									
								
								src/calibre/ebooks/oeb/polish/choose.coffee
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/calibre/ebooks/oeb/polish/choose.coffee
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | #!/usr/bin/env coffee | ||||||
|  | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai | ||||||
|  | 
 | ||||||
|  | ### | ||||||
|  |  Copyright 2013, Kovid Goyal <kovid at kovidgoyal.net> | ||||||
|  |  Released under the GPLv3 License | ||||||
|  | ### | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if window?.calibre_utils | ||||||
|  |     log = window.calibre_utils.log | ||||||
|  | 
 | ||||||
|  | class AnchorLocator | ||||||
|  | 
 | ||||||
|  |     ### | ||||||
|  |     # Allow the user to click on any block level element to choose it as the | ||||||
|  |     # location for an anchor. | ||||||
|  |     ### | ||||||
|  |     constructor: () -> | ||||||
|  |         if not this instanceof arguments.callee | ||||||
|  |             throw new Error('AnchorLocator constructor called as function') | ||||||
|  | 
 | ||||||
|  |     find_blocks: () => | ||||||
|  |         for elem in document.body.getElementsByTagName('*') | ||||||
|  |             style = window.getComputedStyle(elem) | ||||||
|  |             if style.display in ['block', 'flex-box', 'box'] | ||||||
|  |                 elem.className += " calibre_toc_hover" | ||||||
|  |                 elem.onclick = this.onclick | ||||||
|  | 
 | ||||||
|  |     onclick: (event) -> | ||||||
|  |         # We dont want this event to trigger onclick on this element's parent | ||||||
|  |         # block, if any. | ||||||
|  |         event.stopPropagation() | ||||||
|  |         frac = window.pageYOffset/document.body.scrollHeight | ||||||
|  |         window.py_bridge.onclick(this, frac) | ||||||
|  |         return false | ||||||
|  | 
 | ||||||
|  | calibre_anchor_locator = new AnchorLocator() | ||||||
|  | calibre_anchor_locator.find_blocks() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @ -72,6 +72,7 @@ class Container(object): | |||||||
|         self.mime_map = {} |         self.mime_map = {} | ||||||
|         self.name_path_map = {} |         self.name_path_map = {} | ||||||
|         self.dirtied = set() |         self.dirtied = set() | ||||||
|  |         self.encoding_map = {} | ||||||
| 
 | 
 | ||||||
|         # 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 | ||||||
| @ -162,27 +163,29 @@ class Container(object): | |||||||
|             data = data[3:] |             data = data[3:] | ||||||
|         if bom_enc is not None: |         if bom_enc is not None: | ||||||
|             try: |             try: | ||||||
|  |                 self.used_encoding = bom_enc | ||||||
|                 return fix_data(data.decode(bom_enc)) |                 return fix_data(data.decode(bom_enc)) | ||||||
|             except UnicodeDecodeError: |             except UnicodeDecodeError: | ||||||
|                 pass |                 pass | ||||||
|         try: |         try: | ||||||
|  |             self.used_encoding = 'utf-8' | ||||||
|             return fix_data(data.decode('utf-8')) |             return fix_data(data.decode('utf-8')) | ||||||
|         except UnicodeDecodeError: |         except UnicodeDecodeError: | ||||||
|             pass |             pass | ||||||
|         data, _ = xml_to_unicode(data) |         data, self.used_encoding = xml_to_unicode(data) | ||||||
|         return fix_data(data) |         return fix_data(data) | ||||||
| 
 | 
 | ||||||
|     def parse_xml(self, data): |     def parse_xml(self, data): | ||||||
|         data = xml_to_unicode(data, strip_encoding_pats=True, assume_utf8=True, |         data, self.used_encoding = xml_to_unicode( | ||||||
|                              resolve_entities=True)[0].strip() |             data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True) | ||||||
|         return etree.fromstring(data, parser=RECOVER_PARSER) |         return etree.fromstring(data, parser=RECOVER_PARSER) | ||||||
| 
 | 
 | ||||||
|     def parse_xhtml(self, data, fname): |     def parse_xhtml(self, data, fname): | ||||||
|         try: |         try: | ||||||
|             return parse_html(data, log=self.log, |             return parse_html( | ||||||
|                     decoder=self.decode, |                 data, log=self.log, decoder=self.decode, | ||||||
|                     preprocessor=self.html_preprocessor, |                 preprocessor=self.html_preprocessor, filename=fname, | ||||||
|                     filename=fname, non_html_file_tags={'ncx'}) |                 non_html_file_tags={'ncx'}) | ||||||
|         except NotHTML: |         except NotHTML: | ||||||
|             return self.parse_xml(data) |             return self.parse_xml(data) | ||||||
| 
 | 
 | ||||||
| @ -212,9 +215,11 @@ class Container(object): | |||||||
|     def parsed(self, name): |     def parsed(self, name): | ||||||
|         ans = self.parsed_cache.get(name, None) |         ans = self.parsed_cache.get(name, None) | ||||||
|         if ans is None: |         if ans is None: | ||||||
|  |             self.used_encoding = None | ||||||
|             mime = self.mime_map.get(name, guess_type(name)) |             mime = self.mime_map.get(name, guess_type(name)) | ||||||
|             ans = self.parse(self.name_path_map[name], mime) |             ans = self.parse(self.name_path_map[name], mime) | ||||||
|             self.parsed_cache[name] = ans |             self.parsed_cache[name] = ans | ||||||
|  |             self.encoding_map[name] = self.used_encoding | ||||||
|         return ans |         return ans | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|  | |||||||
							
								
								
									
										195
									
								
								src/calibre/gui2/toc/location.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								src/calibre/gui2/toc/location.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,195 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai | ||||||
|  | from __future__ import (unicode_literals, division, absolute_import, | ||||||
|  |                         print_function) | ||||||
|  | 
 | ||||||
|  | __license__   = 'GPL v3' | ||||||
|  | __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' | ||||||
|  | __docformat__ = 'restructuredtext en' | ||||||
|  | 
 | ||||||
|  | from base64 import b64encode | ||||||
|  | 
 | ||||||
|  | from PyQt4.Qt import (QWidget, QGridLayout, QListWidget, QSize, Qt, QUrl, | ||||||
|  |                       pyqtSlot, pyqtSignal, QVBoxLayout, QFrame, QLabel, | ||||||
|  |                       QLineEdit) | ||||||
|  | from PyQt4.QtWebKit import QWebView, QWebPage, QWebElement | ||||||
|  | 
 | ||||||
|  | from calibre.ebooks.oeb.display.webview import load_html | ||||||
|  | from calibre.utils.logging import default_log | ||||||
|  | 
 | ||||||
|  | class Page(QWebPage): # {{{ | ||||||
|  | 
 | ||||||
|  |     elem_clicked = pyqtSignal(object, object, object, object) | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         self.log = default_log | ||||||
|  |         QWebPage.__init__(self) | ||||||
|  |         self.js = None | ||||||
|  |         self.evaljs = self.mainFrame().evaluateJavaScript | ||||||
|  |         self.bridge_value = None | ||||||
|  |         nam = self.networkAccessManager() | ||||||
|  |         nam.setNetworkAccessible(nam.NotAccessible) | ||||||
|  |         self.setLinkDelegationPolicy(self.DelegateAllLinks) | ||||||
|  | 
 | ||||||
|  |     def javaScriptConsoleMessage(self, msg, lineno, msgid): | ||||||
|  |         self.log(u'JS:', unicode(msg)) | ||||||
|  | 
 | ||||||
|  |     def javaScriptAlert(self, frame, msg): | ||||||
|  |         self.log(unicode(msg)) | ||||||
|  | 
 | ||||||
|  |     def shouldInterruptJavaScript(self): | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     @pyqtSlot(QWebElement, float) | ||||||
|  |     def onclick(self, elem, frac): | ||||||
|  |         elem_id = unicode(elem.attribute('id')) or None | ||||||
|  |         tag = unicode(elem.tagName()).lower() | ||||||
|  |         parent = elem | ||||||
|  |         loc = [] | ||||||
|  |         while unicode(parent.tagName()).lower() != 'body': | ||||||
|  |             num = 0 | ||||||
|  |             sibling = parent.previousSibling() | ||||||
|  |             while not sibling.isNull(): | ||||||
|  |                 num += 1 | ||||||
|  |                 sibling = sibling.previousSibling() | ||||||
|  |             loc.insert(0, num) | ||||||
|  |             parent = parent.parent() | ||||||
|  |         self.elem_clicked.emit(tag, frac, elem_id, tuple(loc)) | ||||||
|  | 
 | ||||||
|  |     def load_js(self): | ||||||
|  |         if self.js is None: | ||||||
|  |             from calibre.utils.resources import compiled_coffeescript | ||||||
|  |             self.js = compiled_coffeescript('ebooks.oeb.display.utils') | ||||||
|  |             self.js += compiled_coffeescript('ebooks.oeb.polish.choose') | ||||||
|  |         self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) | ||||||
|  |         self.evaljs(self.js) | ||||||
|  | # }}} | ||||||
|  | 
 | ||||||
|  | class WebView(QWebView): # {{{ | ||||||
|  | 
 | ||||||
|  |     elem_clicked = pyqtSignal(object, object, object, object) | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent): | ||||||
|  |         QWebView.__init__(self, parent) | ||||||
|  |         self._page = Page() | ||||||
|  |         self._page.elem_clicked.connect(self.elem_clicked) | ||||||
|  |         self.setPage(self._page) | ||||||
|  |         raw = ''' | ||||||
|  |         body { background-color: white  } | ||||||
|  |         .calibre_toc_hover:hover { cursor: pointer !important; border-top: solid 5px green !important } | ||||||
|  |         ''' | ||||||
|  |         raw = '::selection {background:#ffff00; color:#000;}\n'+raw | ||||||
|  |         data = 'data:text/css;charset=utf-8;base64,' | ||||||
|  |         data += b64encode(raw.encode('utf-8')) | ||||||
|  |         self.settings().setUserStyleSheetUrl(QUrl(data)) | ||||||
|  | 
 | ||||||
|  |     def load_js(self): | ||||||
|  |         self.page().load_js() | ||||||
|  | 
 | ||||||
|  |     def sizeHint(self): | ||||||
|  |         return QSize(1500, 300) | ||||||
|  | # }}} | ||||||
|  | 
 | ||||||
|  | class ItemEdit(QWidget): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent): | ||||||
|  |         QWidget.__init__(self, parent) | ||||||
|  |         self.l = l = QGridLayout() | ||||||
|  |         self.setLayout(l) | ||||||
|  | 
 | ||||||
|  |         self.la = la = QLabel('<b>'+_( | ||||||
|  |             'Select a destination for the Table of Contents entry')) | ||||||
|  |         l.addWidget(la, 0, 0, 1, 3) | ||||||
|  | 
 | ||||||
|  |         self.dest_list = dl = QListWidget(self) | ||||||
|  |         dl.setMinimumWidth(250) | ||||||
|  |         dl.currentItemChanged.connect(self.current_changed) | ||||||
|  |         l.addWidget(dl, 1, 0) | ||||||
|  | 
 | ||||||
|  |         self.view = WebView(self) | ||||||
|  |         self.view.elem_clicked.connect(self.elem_clicked) | ||||||
|  |         l.addWidget(self.view, 1, 1) | ||||||
|  | 
 | ||||||
|  |         self.f = f = QFrame() | ||||||
|  |         f.setFrameShape(f.StyledPanel) | ||||||
|  |         f.setMinimumWidth(250) | ||||||
|  |         l.addWidget(f, 1, 2) | ||||||
|  |         l = f.l = QVBoxLayout() | ||||||
|  |         f.setLayout(l) | ||||||
|  | 
 | ||||||
|  |         f.la = la = QLabel('<p>'+_( | ||||||
|  |             'Here you can choose a destination for the Table of Contents\' entry' | ||||||
|  |             ' to point to. First choose a file from the book in the left-most panel. The' | ||||||
|  |             ' file will open in the central panel.<p>' | ||||||
|  | 
 | ||||||
|  |             'Then choose a location inside the file. To do so, simply click on' | ||||||
|  |             ' the place in the central panel that you want to use as the' | ||||||
|  |             ' destination. As you move the mouse around the central panel, a' | ||||||
|  |             ' thick green line appears, indicating the precise location' | ||||||
|  |             ' that will be selected when you click.')) | ||||||
|  |         la.setStyleSheet('QLabel { margin-bottom: 20px }') | ||||||
|  |         la.setWordWrap(True) | ||||||
|  |         l.addWidget(la) | ||||||
|  | 
 | ||||||
|  |         f.la2 = la = QLabel(_('&Name of the ToC entry:')) | ||||||
|  |         l.addWidget(la) | ||||||
|  |         self.name = QLineEdit(self) | ||||||
|  |         la.setBuddy(self.name) | ||||||
|  |         l.addWidget(self.name) | ||||||
|  | 
 | ||||||
|  |         self.base_msg = _('Currently selected destination:') | ||||||
|  |         self.dest_label = la = QLabel(self.base_msg) | ||||||
|  |         la.setTextFormat(Qt.PlainText) | ||||||
|  |         la.setWordWrap(True) | ||||||
|  |         la.setStyleSheet('QLabel { margin-top: 20px }') | ||||||
|  |         l.addWidget(la) | ||||||
|  | 
 | ||||||
|  |         l.addStretch() | ||||||
|  | 
 | ||||||
|  |     def load(self, container): | ||||||
|  |         self.container = container | ||||||
|  |         spine_names = [container.abspath_to_name(p) for p in | ||||||
|  |                        container.spine_items] | ||||||
|  |         spine_names = [n for n in spine_names if container.has_name(n)] | ||||||
|  |         self.dest_list.addItems(spine_names) | ||||||
|  | 
 | ||||||
|  |     def current_changed(self, item): | ||||||
|  |         name = self.current_name = unicode(item.data(Qt.DisplayRole).toString()) | ||||||
|  |         path = self.container.name_to_abspath(name) | ||||||
|  |         # Ensure encoding map is populated | ||||||
|  |         self.container.parsed(name) | ||||||
|  |         encoding = self.container.encoding_map.get(name, None) or 'utf-8' | ||||||
|  | 
 | ||||||
|  |         load_html(path, self.view, codec=encoding, | ||||||
|  |                   mime_type=self.container.mime_map[name]) | ||||||
|  |         self.view.load_js() | ||||||
|  |         self.dest_label.setText(self.base_msg + '\n' + _('File:') + ' ' + | ||||||
|  |                                 name + '\n' + _('Top of the file')) | ||||||
|  | 
 | ||||||
|  |     def __call__(self, item, where): | ||||||
|  |         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.dest_label.setText(self.base_msg + '\n' + _('None')) | ||||||
|  | 
 | ||||||
|  |     def elem_clicked(self, tag, frac, elem_id, loc): | ||||||
|  |         self.current_frag = elem_id or loc | ||||||
|  |         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 | ||||||
|  |         self.dest_label.setText(self.base_msg + '\n' + | ||||||
|  |                     _('File:') + ' ' + self.current_name + '\n' + loctext) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def result(self): | ||||||
|  |         return (self.current_item, self.current_where, self.current_name, | ||||||
|  |                 self.current_frag) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @ -19,6 +19,7 @@ from calibre.ebooks.oeb.polish.container import get_container | |||||||
| from calibre.ebooks.oeb.polish.toc import get_toc | from calibre.ebooks.oeb.polish.toc import get_toc | ||||||
| from calibre.gui2 import Application | from calibre.gui2 import Application | ||||||
| from calibre.gui2.progress_indicator import ProgressIndicator | from calibre.gui2.progress_indicator import ProgressIndicator | ||||||
|  | from calibre.gui2.toc.location import ItemEdit | ||||||
| from calibre.utils.logging import GUILog | from calibre.utils.logging import GUILog | ||||||
| 
 | 
 | ||||||
| ICON_SIZE = 24 | ICON_SIZE = 24 | ||||||
| @ -46,13 +47,15 @@ class ItemView(QFrame): # {{{ | |||||||
|             'Entries with a green tick next to them point to a location that has ' |             'Entries with a green tick next to them point to a location that has ' | ||||||
|             'been verified to exist. Entries with a red dot are broken and may need' |             'been verified to exist. Entries with a red dot are broken and may need' | ||||||
|             ' to be fixed.')) |             ' to be fixed.')) | ||||||
|  |         la.setStyleSheet('QLabel { margin-bottom: 20px }') | ||||||
|         la.setWordWrap(True) |         la.setWordWrap(True) | ||||||
|         l = QVBoxLayout() |         l = QVBoxLayout() | ||||||
|         rp.setLayout(l) |         rp.setLayout(l) | ||||||
|         l.addWidget(la, alignment=Qt.AlignTop) |         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')) | ||||||
|         b.clicked.connect(self.add_new_to_root) |         b.clicked.connect(self.add_new_to_root) | ||||||
|         l.addWidget(b, alignment=Qt.AlignTop) |         l.addWidget(b) | ||||||
|  |         l.addStretch() | ||||||
| 
 | 
 | ||||||
|     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) | ||||||
| @ -267,7 +270,10 @@ class TOCEditor(QDialog): # {{{ | |||||||
|         la.setStyleSheet('QLabel { font-size: 20pt }') |         la.setStyleSheet('QLabel { font-size: 20pt }') | ||||||
|         ll.addWidget(la, alignment=Qt.AlignHCenter|Qt.AlignTop) |         ll.addWidget(la, alignment=Qt.AlignHCenter|Qt.AlignTop) | ||||||
|         self.toc_view = TOCView(self) |         self.toc_view = TOCView(self) | ||||||
|  |         self.toc_view.add_new_item.connect(self.add_new_item) | ||||||
|         s.addWidget(self.toc_view) |         s.addWidget(self.toc_view) | ||||||
|  |         self.item_edit = ItemEdit(self) | ||||||
|  |         s.addWidget(self.item_edit) | ||||||
| 
 | 
 | ||||||
|         bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) |         bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) | ||||||
|         l.addWidget(bb) |         l.addWidget(bb) | ||||||
| @ -278,6 +284,23 @@ class TOCEditor(QDialog): # {{{ | |||||||
| 
 | 
 | ||||||
|         self.resize(950, 630) |         self.resize(950, 630) | ||||||
| 
 | 
 | ||||||
|  |     def add_new_item(self, item, where): | ||||||
|  |         self.item_edit(item, where) | ||||||
|  |         self.stacks.setCurrentIndex(2) | ||||||
|  | 
 | ||||||
|  |     def accept(self): | ||||||
|  |         if self.stacks.currentIndex() == 2: | ||||||
|  |             self.toc_view.update_item(self.item_edit.result) | ||||||
|  |             self.stacks.setCurrentIndex(1) | ||||||
|  |         else: | ||||||
|  |             super(TOCEditor, self).accept() | ||||||
|  | 
 | ||||||
|  |     def reject(self): | ||||||
|  |         if self.stacks.currentIndex() == 2: | ||||||
|  |             self.stacks.setCurrentIndex(1) | ||||||
|  |         else: | ||||||
|  |             super(TOCEditor, self).accept() | ||||||
|  | 
 | ||||||
|     def start(self): |     def start(self): | ||||||
|         t = Thread(target=self.explode) |         t = Thread(target=self.explode) | ||||||
|         t.daemon = True |         t.daemon = True | ||||||
| @ -293,6 +316,7 @@ class TOCEditor(QDialog): # {{{ | |||||||
|     def read_toc(self): |     def read_toc(self): | ||||||
|         self.pi.stopAnimation() |         self.pi.stopAnimation() | ||||||
|         self.toc_view(self.ebook) |         self.toc_view(self.ebook) | ||||||
|  |         self.item_edit.load(self.ebook) | ||||||
|         self.stacks.setCurrentIndex(1) |         self.stacks.setCurrentIndex(1) | ||||||
| 
 | 
 | ||||||
| # }}} | # }}} | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user