From 3772cfbfbaff78f07a8796703151956ec5793115 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Aug 2019 13:53:03 +0530 Subject: [PATCH] Work on word lookup --- src/calibre/gui2/viewer/lookup.py | 113 ++++++++++++++++++++++++++++ src/calibre/gui2/viewer/ui.py | 22 ++++-- src/calibre/gui2/viewer/web_view.py | 6 ++ src/pyj/read_book/iframe.pyj | 5 ++ src/pyj/read_book/overlay.pyj | 8 +- src/pyj/read_book/view.pyj | 9 +++ src/pyj/viewer-main.pyj | 4 + 7 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 src/calibre/gui2/viewer/lookup.py diff --git a/src/calibre/gui2/viewer/lookup.py b/src/calibre/gui2/viewer/lookup.py new file mode 100644 index 0000000000..8fd6e08cad --- /dev/null +++ b/src/calibre/gui2/viewer/lookup.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2019, Kovid Goyal + +from __future__ import absolute_import, division, print_function, unicode_literals + +import os + +from PyQt5.Qt import ( + QApplication, QComboBox, QHBoxLayout, QLabel, Qt, QTimer, QUrl, QVBoxLayout, + QWidget +) +from PyQt5.QtWebEngineWidgets import ( + QWebEnginePage, QWebEngineProfile, QWebEngineView +) + +from calibre import random_user_agent +from calibre.constants import cache_dir +from calibre.gui2.viewer.web_view import vprefs +from calibre.gui2.webengine import secure_webengine + +vprefs.defaults['lookup_locations'] = [ + { + 'name': 'Google dictionary', + 'url': 'https://google.com/search?q=define:{word}', + 'langs': [], + }, + + { + 'name': 'Google search', + 'url': 'https://google.com/search?q={word}', + 'langs': [], + }, + + { + 'name': 'Wordnik', + 'url': 'https://www.wordnik.com/words/{word}', + 'langs': ['eng'], + }, +] +vprefs.defaults['lookup_location'] = 'Google dictionary' + + +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')) + s = ans.settings() + s.setDefaultTextEncoding('utf-8') + create_profile.ans = ans + return ans + + +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 = QWebEngineView(self) + self._page = QWebEnginePage(create_profile(), self.view) + 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.update_query) + + def populate_sources(self): + sb = self.source_box + sb.clear() + for item in vprefs['lookup_locations']: + sb.addItem(item['name'], item) + idx = sb.findText(item['name'], Qt.MatchExactly) + if idx > -1: + self.setCurrentIndex(idx) + + def visibility_changed(self, is_visible): + self.is_visible = is_visible + self.update_query() + + @property + def url_template(self): + idx = self.source_box.currentIndex() + if idx > -1: + return self.source_box.itemData(idx)['url'] + + def update_query(self): + self.debounce_timer.stop() + query = self.selected_text or self.current_query + if self.current_query == query and self.current_source == self.url_template: + return + if not self.is_visible: + return + self.current_source = self.url_template + url = self.current_source.format(word=query) + self.view.load(QUrl(url)) + self.current_query = query + + def selected_text_changed(self, text): + self.selected_text = text or '' + self.debounce_timer.start() diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 14da07b55e..f98c9245b3 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -27,6 +27,7 @@ from calibre.gui2.viewer.annotations import ( ) from calibre.gui2.viewer.bookmarks import BookmarkManager from calibre.gui2.viewer.convert_book import prepare_book, update_book +from calibre.gui2.viewer.lookup import Lookup from calibre.gui2.viewer.toc import TOC, TOCSearch, TOCView from calibre.gui2.viewer.web_view import ( WebView, get_session_pref, set_book_path, vprefs @@ -79,6 +80,11 @@ class EbookViewer(MainWindow): w.l.addWidget(self.toc), w.l.addWidget(self.toc_search), w.l.setContentsMargins(0, 0, 0, 0) self.toc_dock.setWidget(w) + self.lookup_dock = create_dock(_('Lookup'), 'lookup-dock', Qt.RightDockWidgetArea) + self.lookup_widget = w = Lookup(self) + self.lookup_dock.visibilityChanged.connect(self.lookup_widget.visibility_changed) + self.lookup_dock.setWidget(w) + self.bookmarks_dock = create_dock(_('Bookmarks'), 'bookmarks-dock', Qt.RightDockWidgetArea) self.bookmarks_widget = w = BookmarkManager(self) connect_lambda( @@ -97,9 +103,11 @@ class EbookViewer(MainWindow): self.web_view.toggle_toc.connect(self.toggle_toc) self.web_view.toggle_bookmarks.connect(self.toggle_bookmarks) self.web_view.toggle_inspector.connect(self.toggle_inspector) + self.web_view.toggle_lookup.connect(self.toggle_lookup) self.web_view.update_current_toc_nodes.connect(self.toc.update_current_toc_nodes) self.web_view.toggle_full_screen.connect(self.toggle_full_screen) self.web_view.ask_for_open.connect(self.ask_for_open, type=Qt.QueuedConnection) + self.web_view.selection_changed.connect(self.lookup_widget.selected_text_changed, type=Qt.QueuedConnection) self.setCentralWidget(self.web_view) self.restore_state() if continue_reading: @@ -146,17 +154,15 @@ class EbookViewer(MainWindow): # }}} # ToC/Bookmarks {{{ + def toggle_toc(self): - if self.toc_dock.isVisible(): - self.toc_dock.setVisible(False) - else: - self.toc_dock.setVisible(True) + self.toc_dock.setVisible(not self.toc_dock.isVisible()) def toggle_bookmarks(self): - if self.bookmarks_dock.isVisible(): - self.bookmarks_dock.setVisible(False) - else: - self.bookmarks_dock.setVisible(True) + self.bookmarks_dock.setVisible(not self.bookmarks_dock.isVisible()) + + def toggle_lookup(self): + self.lookup_dock.setVisible(not self.lookup_dock.isVisible()) def toc_clicked(self, index): item = self.toc_model.itemFromIndex(index) diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 1c9c89f0cc..d8a65d2d2e 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -188,10 +188,12 @@ class ViewerBridge(Bridge): toggle_toc = from_js() toggle_bookmarks = from_js() toggle_inspector = from_js() + toggle_lookup = from_js() update_current_toc_nodes = from_js(object, object) toggle_full_screen = from_js() report_cfi = from_js(object, object) ask_for_open = from_js(object) + selection_changed = from_js(object) create_view = to_js() show_preparing_message = to_js() @@ -307,9 +309,11 @@ class WebView(RestartingWebEngineView): toggle_toc = pyqtSignal() toggle_bookmarks = pyqtSignal() toggle_inspector = pyqtSignal() + toggle_lookup = pyqtSignal() update_current_toc_nodes = pyqtSignal(object, object) toggle_full_screen = pyqtSignal() ask_for_open = pyqtSignal(object) + selection_changed = pyqtSignal(object) def __init__(self, parent=None): self._host_widget = None @@ -328,9 +332,11 @@ class WebView(RestartingWebEngineView): self.bridge.toggle_toc.connect(self.toggle_toc) self.bridge.toggle_bookmarks.connect(self.toggle_bookmarks) self.bridge.toggle_inspector.connect(self.toggle_inspector) + self.bridge.toggle_lookup.connect(self.toggle_lookup) self.bridge.update_current_toc_nodes.connect(self.update_current_toc_nodes) self.bridge.toggle_full_screen.connect(self.toggle_full_screen) self.bridge.ask_for_open.connect(self.ask_for_open) + self.bridge.selection_changed.connect(self.selection_changed) self.bridge.report_cfi.connect(self.call_callback) self.pending_bridge_ready_actions = {} self.setPage(self._page) diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index a078b45cce..1c14332113 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -100,6 +100,7 @@ class IframeBoss: window.addEventListener('wheel', self.onwheel, {'passive': False}) window.addEventListener('keydown', self.onkeydown, {'passive': False}) document.documentElement.addEventListener('contextmenu', self.oncontextmenu, {'passive': False}) + document.addEventListener('selectionchange', self.onselectionchange) self.color_scheme = data.color_scheme create_touch_handlers() @@ -313,6 +314,10 @@ class IframeBoss: return self.onresize_stage2() + def onselectionchange(self): + if self.content_ready: + self.send_message('selectionchange', text=document.getSelection().toString()) + def onresize_stage2(self): if scroll_viewport.width() is self.last_window_width and scroll_viewport.height() is self.last_window_height: # Safari at least, generates lots of spurious resize events diff --git a/src/pyj/read_book/overlay.pyj b/src/pyj/read_book/overlay.pyj index 64831622e6..9de4363ce3 100644 --- a/src/pyj/read_book/overlay.pyj +++ b/src/pyj/read_book/overlay.pyj @@ -284,6 +284,10 @@ class MainOverlay: # {{{ actions_div.appendChild(E.ul(*full_screen_actions)) if runtime.is_standalone_viewer: + actions_div.appendChild(E.ul( + ac(_('Lookup/search word'), _('Lookup or search for the currently selected word'), + def(): self.overlay.hide(), ui_operations.toggle_lookup();, 'library') + )) actions_div.appendChild(E.ul( ac(_('Inspector'), _('Show the content inspector'), def(): self.overlay.hide(), ui_operations.toggle_inspector();, 'bug') @@ -293,8 +297,8 @@ class MainOverlay: # {{{ onclick=def (evt):evt.stopPropagation();, set_css(E.div( # top row - E.div(self.overlay.view.book.metadata.title, style='max-width: 90%; text-overflow: ellipsis; font-weight: bold'), - E.div(self.date_formatter.format(Date()), id=self.timer_id, style='max-width: 9%; text-overflow: ellipsis'), + E.div(self.overlay.view.book.metadata.title, style='max-width: 90%; text-overflow: ellipsis; font-weight: bold; white-space: nowrap'), + E.div(self.date_formatter.format(Date()), id=self.timer_id, style='max-width: 9%; text-overflow: ellipsis; white-space: nowrap'), ), display='flex', justify_content='space-between', align_items='baseline', font_size='smaller', padding='0.5ex 1rem', border_bottom='solid 1px currentColor' ), actions_div, diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 5c0667aec4..dc57e5f190 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -149,6 +149,7 @@ class View: 'show_footnote': self.on_show_footnote, 'print': self.on_print, 'human_scroll': self.on_human_scroll, + 'selectionchange': self.on_selection_change, } entry_point = None if runtime.is_standalone_viewer else 'read_book.iframe' self.iframe_wrapper = IframeWrapper(handlers, document.getElementById(iframe_id), entry_point, _('Bootstrapping book reader...'), runtime.FAKE_PROTOCOL, runtime.FAKE_HOST) @@ -164,6 +165,9 @@ class View: return self.iframe_wrapper.iframe def on_lookup_word(self, data): + if runtime.is_standalone_viewer: + ui_operations.selection_changed(data.word) + return self.overlay.show_word_actions(data.word) def left_margin_clicked(self, event): @@ -211,6 +215,11 @@ class View: amt_scrolled = data.scrolled_by_frac * length self.timers.on_human_scroll(amt_scrolled) + def on_selection_change(self, data): + self.currently_showing.selected_text = data.text + if ui_operations.selection_changed: + ui_operations.selection_changed(data.text) + def find(self, text, backwards): self.iframe_wrapper.send_message('find', text=text, backwards=backwards, searched_in_spine=False) diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index 1478a3c6f4..d860f89f12 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -261,6 +261,10 @@ if window is window.top: to_python.toggle_bookmarks() ui_operations.toggle_inspector = def(): to_python.toggle_inspector() + ui_operations.toggle_lookup = def(): + to_python.toggle_lookup() + ui_operations.selection_changed = def(selected_text): + to_python.selection_changed(selected_text) ui_operations.update_current_toc_nodes = def(current_node_id, top_level_node_id): to_python.update_current_toc_nodes(current_node_id, top_level_node_id) ui_operations.toggle_full_screen = def():