%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/gui2/viewer/ |
Current File : //usr/lib/calibre/calibre/gui2/viewer/lookup.py |
#!/usr/bin/env python3 # License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net> import os import sys import textwrap from qt.core import ( QApplication, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, QAbstractItemView, QHBoxLayout, QIcon, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QSize, Qt, QTimer, QUrl, QVBoxLayout, QWidget, pyqtSignal ) from qt.webengine import ( QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineView ) from calibre import prints, random_user_agent from calibre.constants import cache_dir from calibre.gui2 import error_dialog from calibre.gui2.viewer.web_view import apply_font_settings, vprefs from calibre.gui2.webengine import create_script, insert_scripts, secure_webengine from calibre.gui2.widgets2 import Dialog vprefs.defaults['lookup_locations'] = [ { 'name': 'Google dictionary', 'url': 'https://www.google.com/search?q=define:{word}', 'langs': [], }, { 'name': 'Google search', 'url': 'https://www.google.com/search?q={word}', 'langs': [], }, { 'name': 'Wordnik', 'url': 'https://www.wordnik.com/words/{word}', 'langs': ['eng'], }, ] vprefs.defaults['lookup_location'] = 'Google dictionary' class SourceEditor(Dialog): def __init__(self, parent, source_to_edit=None): self.all_names = {x['name'] for x in parent.all_entries} self.initial_name = self.initial_url = None self.langs = [] if source_to_edit is not None: self.langs = source_to_edit['langs'] self.initial_name = source_to_edit['name'] self.initial_url = source_to_edit['url'] Dialog.__init__(self, _('Edit lookup source'), 'viewer-edit-lookup-location', parent=parent) self.resize(self.sizeHint()) def setup_ui(self): self.l = l = QFormLayout(self) self.name_edit = n = QLineEdit(self) n.setPlaceholderText(_('The name of the source')) n.setMinimumWidth(450) l.addRow(_('&Name:'), n) if self.initial_name: n.setText(self.initial_name) n.setReadOnly(True) self.url_edit = u = QLineEdit(self) u.setPlaceholderText(_('The URL template of the source')) u.setMinimumWidth(n.minimumWidth()) l.addRow(_('&URL:'), u) if self.initial_url: u.setText(self.initial_url) la = QLabel(_( 'The URL template must starts with https:// and have {word} in it which will be replaced by the actual query')) la.setWordWrap(True) l.addRow(la) l.addRow(self.bb) if self.initial_name: u.setFocus(Qt.FocusReason.OtherFocusReason) @property def source_name(self): return self.name_edit.text().strip() @property def url(self): return self.url_edit.text().strip() def accept(self): q = self.source_name if not q: return error_dialog(self, _('No name'), _( 'You must specify a name'), show=True) if not self.initial_name and q in self.all_names: return error_dialog(self, _('Name already exists'), _( 'A lookup source with the name {} already exists').format(q), show=True) if not self.url: return error_dialog(self, _('No name'), _( 'You must specify a URL'), show=True) if not self.url.startswith('http://') and not self.url.startswith('https://'): return error_dialog(self, _('Invalid URL'), _( 'The URL must start with https://'), show=True) if '{word}' not in self.url: return error_dialog(self, _('Invalid URL'), _( 'The URL must contain the placeholder {word}'), show=True) return Dialog.accept(self) @property def entry(self): return {'name': self.source_name, 'url': self.url, 'langs': self.langs} class SourcesEditor(Dialog): def __init__(self, parent): Dialog.__init__(self, _('Edit lookup sources'), 'viewer-edit-lookup-locations', parent=parent) def setup_ui(self): self.l = l = QVBoxLayout(self) self.la = la = QLabel(_('Double-click to edit an entry')) la.setWordWrap(True) l.addWidget(la) self.entries = e = QListWidget(self) e.setDragEnabled(True) e.itemDoubleClicked.connect(self.edit_source) e.viewport().setAcceptDrops(True) e.setDropIndicatorShown(True) e.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) e.setDefaultDropAction(Qt.DropAction.MoveAction) l.addWidget(e) l.addWidget(self.bb) self.build_entries(vprefs['lookup_locations']) self.add_button = b = self.bb.addButton(_('Add'), QDialogButtonBox.ButtonRole.ActionRole) b.setIcon(QIcon(I('plus.png'))) b.clicked.connect(self.add_source) self.remove_button = b = self.bb.addButton(_('Remove'), QDialogButtonBox.ButtonRole.ActionRole) b.setIcon(QIcon(I('minus.png'))) b.clicked.connect(self.remove_source) self.restore_defaults_button = b = self.bb.addButton(_('Restore defaults'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.restore_defaults) def add_entry(self, entry, prepend=False): i = QListWidgetItem(entry['name']) i.setData(Qt.ItemDataRole.UserRole, entry.copy()) self.entries.insertItem(0, i) if prepend else self.entries.addItem(i) def build_entries(self, entries): self.entries.clear() for entry in entries: self.add_entry(entry) def restore_defaults(self): self.build_entries(vprefs.defaults['lookup_locations']) def add_source(self): d = SourceEditor(self) if d.exec() == QDialog.DialogCode.Accepted: self.add_entry(d.entry, prepend=True) def remove_source(self): idx = self.entries.currentRow() if idx > -1: self.entries.takeItem(idx) def edit_source(self, source_item): d = SourceEditor(self, source_item.data(Qt.ItemDataRole.UserRole)) if d.exec() == QDialog.DialogCode.Accepted: source_item.setData(Qt.ItemDataRole.UserRole, d.entry) source_item.setData(Qt.ItemDataRole.DisplayRole, d.name) @property def all_entries(self): return [self.entries.item(r).data(Qt.ItemDataRole.UserRole) for r in range(self.entries.count())] def accept(self): entries = self.all_entries if not entries: return error_dialog(self, _('No sources'), _( 'You must specify at least one lookup source'), show=True) if entries == vprefs.defaults['lookup_locations']: del vprefs['lookup_locations'] else: vprefs['lookup_locations'] = entries return Dialog.accept(self) def create_profile(): ans = getattr(create_profile, 'ans', None) if ans is None: ans = QWebEngineProfile('viewer-lookup', QApplication.instance()) ans.setHttpUserAgent(random_user_agent(allow_ie=False)) ans.setCachePath(os.path.join(cache_dir(), 'ev2vl')) js = P('lookup.js', data=True, allow_user_override=False) insert_scripts(ans, create_script('lookup.js', js, injection_point=QWebEngineScript.InjectionPoint.DocumentCreation)) s = ans.settings() s.setDefaultTextEncoding('utf-8') create_profile.ans = ans return ans class Page(QWebEnginePage): def javaScriptConsoleMessage(self, level, msg, linenumber, source_id): prefix = { QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel: 'INFO', QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel: 'WARNING' }.get(level, 'ERROR') if source_id == 'userscript:lookup.js': prints(f'{prefix}: {source_id}:{linenumber}: {msg}', file=sys.stderr) sys.stderr.flush() def zoom_in(self): self.setZoomFactor(min(self.zoomFactor() + 0.2, 5)) def zoom_out(self): self.setZoomFactor(max(0.25, self.zoomFactor() - 0.2)) def default_zoom(self): self.setZoomFactor(1) class View(QWebEngineView): inspect_element = pyqtSignal() def contextMenuEvent(self, ev): menu = self.page().createStandardContextMenu() menu.addSeparator() menu.addAction(_('Zoom in'), self.page().zoom_in) menu.addAction(_('Zoom out'), self.page().zoom_out) menu.addAction(_('Default zoom'), self.page().default_zoom) menu.addAction(_('Inspect'), self.do_inspect_element) menu.exec(ev.globalPos()) def do_inspect_element(self): self.inspect_element.emit() def set_sync_override(allowed): li = getattr(set_sync_override, 'instance', None) if li is not None: li.set_sync_override(allowed) class Lookup(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) self.is_visible = False self.selected_text = '' self.current_query = '' self.current_source = '' self.l = l = QVBoxLayout(self) self.h = h = QHBoxLayout() l.addLayout(h) self.debounce_timer = t = QTimer(self) t.setInterval(150), t.timeout.connect(self.update_query) self.source_box = sb = QComboBox(self) self.label = la = QLabel(_('Lookup &in:')) h.addWidget(la), h.addWidget(sb), la.setBuddy(sb) self.view = View(self) self.view.inspect_element.connect(self.show_devtools) self._page = Page(create_profile(), self.view) apply_font_settings(self._page) secure_webengine(self._page, for_viewer=True) self.view.setPage(self._page) l.addWidget(self.view) self.populate_sources() self.source_box.currentIndexChanged.connect(self.source_changed) self.view.setHtml('<p>' + _('Double click on a word in the book\'s text' ' to look it up.')) self.add_button = b = QPushButton(QIcon(I('plus.png')), _('Add sources')) b.setToolTip(_('Add more sources at which to lookup words')) b.clicked.connect(self.add_sources) self.refresh_button = rb = QPushButton(QIcon(I('view-refresh.png')), _('Refresh')) rb.setToolTip(_('Refresh the result to match the currently selected text')) rb.clicked.connect(self.update_query) h = QHBoxLayout() l.addLayout(h) h.addWidget(b), h.addWidget(rb) self.auto_update_query = a = QCheckBox(_('Update on selection change'), self) self.disallow_auto_update = False a.setToolTip(textwrap.fill( _('Automatically update the displayed result when selected text in the book changes. With this disabled' ' the lookup is changed only when clicking the Refresh button.'))) a.setChecked(vprefs['auto_update_lookup']) a.stateChanged.connect(self.auto_update_state_changed) l.addWidget(a) self.update_refresh_button_status() set_sync_override.instance = self def set_sync_override(self, allowed): self.disallow_auto_update = not allowed if self.auto_update_query.isChecked() and allowed: self.update_query() def auto_update_state_changed(self, state): vprefs['auto_update_lookup'] = self.auto_update_query.isChecked() self.update_refresh_button_status() def show_devtools(self): if not hasattr(self, '_devtools_page'): self._devtools_page = QWebEnginePage() self._devtools_view = QWebEngineView(self) self._devtools_view.setPage(self._devtools_page) self._page.setDevToolsPage(self._devtools_page) self._devtools_dialog = d = QDialog(self) d.setWindowTitle('Inspect Lookup page') v = QVBoxLayout(d) v.addWidget(self._devtools_view) d.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) d.bb.rejected.connect(d.reject) v.addWidget(d.bb) d.resize(QSize(800, 600)) d.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) self._devtools_dialog.show() self._page.triggerAction(QWebEnginePage.WebAction.InspectElement) def add_sources(self): if SourcesEditor(self).exec() == QDialog.DialogCode.Accepted: self.populate_sources() self.source_box.setCurrentIndex(0) self.update_query() def source_changed(self): s = self.source if s is not None: vprefs['lookup_location'] = s['name'] self.update_query() def populate_sources(self): sb = self.source_box sb.clear() sb.blockSignals(True) for item in vprefs['lookup_locations']: sb.addItem(item['name'], item) idx = sb.findText(vprefs['lookup_location'], Qt.MatchFlag.MatchExactly) if idx > -1: sb.setCurrentIndex(idx) sb.blockSignals(False) def visibility_changed(self, is_visible): self.is_visible = is_visible self.update_query() @property def source(self): idx = self.source_box.currentIndex() if idx > -1: return self.source_box.itemData(idx) @property def url_template(self): idx = self.source_box.currentIndex() if idx > -1: return self.source_box.itemData(idx)['url'] @property def query_is_up_to_date(self): query = self.selected_text or self.current_query return self.current_query == query and self.current_source == self.url_template def update_refresh_button_status(self): b = self.refresh_button b.setVisible(not self.auto_update_query.isChecked()) b.setEnabled(not self.query_is_up_to_date) def update_query(self): self.debounce_timer.stop() query = self.selected_text or self.current_query if self.query_is_up_to_date: return if not self.is_visible or not query: return self.current_source = self.url_template url = self.current_source.format(word=query) self.view.load(QUrl(url)) self.current_query = query self.update_refresh_button_status() def selected_text_changed(self, text, annot_id): already_has_text = bool(self.current_query) self.selected_text = text or '' if not self.disallow_auto_update and (self.auto_update_query.isChecked() or not already_has_text): self.debounce_timer.start() self.update_refresh_button_status() def on_forced_show(self): self.update_query()