%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/toc/ |
Current File : //lib/calibre/calibre/gui2/toc/location.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net> import json from qt.core import ( QFrame, QGridLayout, QIcon, QLabel, QLineEdit, QListWidget, QPushButton, QSize, QSplitter, Qt, QUrl, QVBoxLayout, QWidget, pyqtSignal ) from qt.webengine import QWebEnginePage, QWebEngineScript, QWebEngineView from calibre.gui2 import error_dialog, gprefs, is_dark_theme, question_dialog from calibre.gui2.palette import dark_color, dark_link_color, dark_text_color from calibre.gui2.webengine import secure_webengine from calibre.utils.logging import default_log from calibre.utils.short_uuid import uuid4 class Page(QWebEnginePage): # {{{ elem_clicked = pyqtSignal(object, object, object, object, object) frag_shown = pyqtSignal(object) def __init__(self, prefs): self.log = default_log self.current_frag = None self.com_id = str(uuid4()) QWebEnginePage.__init__(self) secure_webengine(self.settings(), for_viewer=True) self.titleChanged.connect(self.title_changed) self.loadFinished.connect(self.show_frag) s = QWebEngineScript() s.setName('toc.js') s.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation) s.setRunsOnSubFrames(True) s.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld) js = P('toc.js', allow_user_override=False, data=True).decode('utf-8').replace('COM_ID', self.com_id, 1) if 'preview_background' in prefs.defaults and 'preview_foreground' in prefs.defaults: from calibre.gui2.tweak_book.preview import get_editor_settings settings = get_editor_settings(prefs) else: if is_dark_theme(): settings = { 'is_dark_theme': True, 'bg': dark_color.name(), 'fg': dark_text_color.name(), 'link': dark_link_color.name(), } else: settings = {} js = js.replace('SETTINGS', json.dumps(settings), 1) dark_mode_css = P('dark_mode.css', data=True, allow_user_override=False).decode('utf-8') js = js.replace('CSS', json.dumps(dark_mode_css), 1) s.setSourceCode(js) self.scripts().insert(s) def javaScriptConsoleMessage(self, level, msg, lineno, msgid): self.log('JS:', str(msg)) def javaScriptAlert(self, origin, msg): self.log(str(msg)) def title_changed(self, title): parts = title.split('-', 1) if len(parts) == 2 and parts[0] == self.com_id: self.runJavaScript( 'JSON.stringify(window.calibre_toc_data)', QWebEngineScript.ScriptWorldId.ApplicationWorld, self.onclick) def onclick(self, data): try: tag, elem_id, loc, totals, frac = json.loads(data) except Exception: return elem_id = elem_id or None self.elem_clicked.emit(tag, frac, elem_id, loc, totals) def show_frag(self, ok): if ok and self.current_frag: self.runJavaScript(''' document.location = '#non-existent-anchor'; document.location = '#' + {}; '''.format(json.dumps(self.current_frag))) self.current_frag = None self.runJavaScript('window.pageYOffset/document.body.scrollHeight', QWebEngineScript.ScriptWorldId.ApplicationWorld, self.frag_shown.emit) # }}} class WebView(QWebEngineView): # {{{ elem_clicked = pyqtSignal(object, object, object, object, object) frag_shown = pyqtSignal(object) def __init__(self, parent, prefs): QWebEngineView.__init__(self, parent) self._page = Page(prefs) self._page.elem_clicked.connect(self.elem_clicked) self._page.frag_shown.connect(self.frag_shown) self.setPage(self._page) def load_path(self, path, frag=None): self._page.current_frag = frag self.setUrl(QUrl.fromLocalFile(path)) def sizeHint(self): return QSize(300, 300) def contextMenuEvent(self, ev): pass # }}} class ItemEdit(QWidget): def __init__(self, parent, prefs=None): QWidget.__init__(self, parent) self.prefs = prefs or gprefs self.pending_search = None self.current_frag = None self.setLayout(QVBoxLayout()) self.la = la = QLabel('<b>'+_( 'Select a destination for the Table of Contents entry')) self.layout().addWidget(la) self.splitter = sp = QSplitter(self) self.layout().addWidget(sp) self.layout().setStretch(1, 10) sp.setOpaqueResize(False) sp.setChildrenCollapsible(False) self.dest_list = dl = QListWidget(self) dl.setMinimumWidth(250) dl.currentItemChanged.connect(self.current_changed) sp.addWidget(dl) w = self.w = QWidget(self) l = w.l = QGridLayout() w.setLayout(l) self.view = WebView(self, self.prefs) self.view.elem_clicked.connect(self.elem_clicked) self.view.frag_shown.connect(self.update_dest_label, type=Qt.ConnectionType.QueuedConnection) self.view.loadFinished.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection) l.addWidget(self.view, 0, 0, 1, 3) sp.addWidget(w) self.search_text = s = QLineEdit(self) s.setPlaceholderText(_('Search for text...')) s.returnPressed.connect(self.find_next) l.addWidget(s, 1, 0) self.ns_button = b = QPushButton(QIcon(I('arrow-down.png')), _('Find &next'), self) b.clicked.connect(self.find_next) l.addWidget(b, 1, 1) self.ps_button = b = QPushButton(QIcon(I('arrow-up.png')), _('Find &previous'), self) l.addWidget(b, 1, 2) b.clicked.connect(self.find_previous) self.f = f = QFrame() f.setFrameShape(QFrame.Shape.StyledPanel) f.setMinimumWidth(250) l = f.l = QVBoxLayout() f.setLayout(l) sp.addWidget(f) 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('<b>'+_('Na&me of the ToC entry:')) l.addWidget(la) self.name = QLineEdit(self) self.name.setPlaceholderText(_('(Untitled)')) la.setBuddy(self.name) l.addWidget(self.name) self.base_msg = '<b>'+_('Currently selected destination:')+'</b>' self.dest_label = la = QLabel(self.base_msg) la.setWordWrap(True) la.setStyleSheet('QLabel { margin-top: 20px }') l.addWidget(la) l.addStretch() state = self.prefs.get('toc_edit_splitter_state', None) if state is not None: sp.restoreState(state) def load_finished(self, ok): if self.pending_search: self.pending_search() self.pending_search = None def keyPressEvent(self, ev): if ev.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter) and self.search_text.hasFocus(): # Prevent pressing enter in the search box from triggering the dialog's accept() method ev.accept() return return super().keyPressEvent(ev) def find(self, forwards=True): text = str(self.search_text.text()).strip() flags = QWebEnginePage.FindFlags(0) if forwards else QWebEnginePage.FindFlag.FindBackward self.find_data = text, flags, forwards self.view.findText(text, flags, self.find_callback) def find_callback(self, found): d = self.dest_list text, flags, forwards = self.find_data if not found and text: if d.count() == 1: return error_dialog(self, _('No match found'), _('No match found for: %s')%text, show=True) delta = 1 if forwards else -1 current = str(d.currentItem().data(Qt.ItemDataRole.DisplayRole) or '') next_index = (d.currentRow() + delta)%d.count() next = str(d.item(next_index).data(Qt.ItemDataRole.DisplayRole) or '') msg = '<p>'+_('No matches for %(text)s found in the current file [%(current)s].' ' Do you want to search in the %(which)s file [%(next)s]?') msg = msg%dict(text=text, current=current, next=next, which=_('next') if forwards else _('previous')) if question_dialog(self, _('No match found'), msg): self.pending_search = self.find_next if forwards else self.find_previous d.setCurrentRow(next_index) def find_next(self): return self.find() def find_previous(self): return self.find(forwards=False) 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 = str(item.data(Qt.ItemDataRole.DisplayRole) or '') path = self.container.name_to_abspath(name) # Ensure encoding map is populated root = self.container.parsed(name) nasty = root.xpath('//*[local-name()="head"]/*[local-name()="p"]') if nasty: body = root.xpath('//*[local-name()="body"]') if not body: return error_dialog(self, _('Bad markup'), _('This book has severely broken markup, its ToC cannot be edited.'), show=True) for x in reversed(nasty): body[0].insert(0, x) self.container.commit_item(name, keep_parsed=True) self.view.load_path(path, self.current_frag) self.current_frag = None self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' + name + '<br>' + _('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 self.name.setText('') dest_index, frag = 0, None if item is not None: if where is None: self.name.setText(item.data(0, Qt.ItemDataRole.DisplayRole) or '') self.name.setCursorPosition(0) toc = item.data(0, Qt.ItemDataRole.UserRole) if toc.dest: for i in range(self.dest_list.count()): litem = self.dest_list.item(i) if str(litem.data(Qt.ItemDataRole.DisplayRole) or '') == toc.dest: dest_index = i frag = toc.frag break self.dest_list.blockSignals(True) self.dest_list.setCurrentRow(dest_index) self.dest_list.blockSignals(False) item = self.dest_list.item(dest_index) if frag: self.current_frag = frag self.current_changed(item) def get_loctext(self, frac): frac = int(round(frac * 100)) if frac == 0: loctext = _('Top of the file') else: loctext = _('Approximately %d%% from the top')%frac return loctext def elem_clicked(self, tag, frac, elem_id, loc, totals): self.current_frag = elem_id or (loc, totals) 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.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, self.current_frag, self.name.text().strip() or _('(Untitled)'))