diff --git a/imgsrc/srv/copy.svg b/imgsrc/srv/copy.svg new file mode 100644 index 0000000000..86e0d1eedc --- /dev/null +++ b/imgsrc/srv/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/imgsrc/srv/link.svg b/imgsrc/srv/link.svg new file mode 100644 index 0000000000..6999b4722d --- /dev/null +++ b/imgsrc/srv/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index c4b7acbada..6f9151e7db 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -12,13 +12,15 @@ from hashlib import sha256 from threading import Thread from PyQt5.Qt import ( - QDockWidget, QEvent, QModelIndex, Qt, QVBoxLayout, QWidget, pyqtSignal + QDockWidget, QEvent, QModelIndex, QPixmap, Qt, QUrl, QVBoxLayout, QWidget, + pyqtSignal ) from calibre import prints from calibre.constants import config_dir from calibre.customize.ui import available_input_formats from calibre.gui2 import choose_files, error_dialog +from calibre.gui2.image_popup import ImagePopup from calibre.gui2.main_window import MainWindow from calibre.gui2.viewer.annotations import ( merge_annotations, parse_annotations, save_annots_to_epub, serialize_annotations @@ -28,7 +30,7 @@ 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 + WebView, get_path_for_name, get_session_pref, set_book_path, vprefs ) from calibre.utils.date import utcnow from calibre.utils.ipc.simple_worker import WorkerError @@ -68,6 +70,7 @@ class EbookViewer(MainWindow): self.base_window_title = _('E-book viewer') self.setWindowTitle(self.base_window_title) self.in_full_screen_mode = None + self.image_popup = ImagePopup(self) try: os.makedirs(annotations_dir) except EnvironmentError: @@ -119,6 +122,7 @@ class EbookViewer(MainWindow): 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.web_view.view_image.connect(self.view_image, type=Qt.QueuedConnection) self.setCentralWidget(self.web_view) self.restore_state() if continue_reading: @@ -188,6 +192,21 @@ class EbookViewer(MainWindow): def bookmark_activated(self, cfi): self.web_view.goto_cfi(cfi) + + def view_image(self, name): + path = get_path_for_name(name) + if path: + pmap = QPixmap() + if pmap.load(path): + self.image_popup.current_img = pmap + self.image_popup.current_url = QUrl.fromLocalFile(path) + self.image_popup() + else: + error_dialog(self, _('Invalid image'), _( + "Failed to load the image {}").format(name), show=True) + else: + error_dialog(self, _('Image not found'), _( + "Failed to find the image {}").format(name), show=True) # }}} # Load book {{{ diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 90449a34c6..5913f1fe18 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -57,12 +57,18 @@ def set_book_path(path, pathtoebook): set_book_path.parsed_manifest = json_loads(set_book_path.manifest) -def get_data(name): +def get_path_for_name(name): bdir = getattr(set_book_path, 'path', None) if bdir is None: - return None, None + return path = os.path.abspath(os.path.join(bdir, name)) - if not path.startswith(bdir): + if path.startswith(bdir): + return path + + +def get_data(name): + path = get_path_for_name(name) + if path is None: return None, None try: with lopen(path, 'rb') as f: @@ -195,6 +201,8 @@ class ViewerBridge(Bridge): report_cfi = from_js(object, object) ask_for_open = from_js(object) selection_changed = from_js(object) + copy_selection = from_js(object) + view_image = from_js(object) create_view = to_js() show_preparing_message = to_js() @@ -245,6 +253,13 @@ class WebPage(QWebEnginePage): secure_webengine(self, for_viewer=True) apply_font_settings(self) self.bridge = ViewerBridge(self) + self.bridge.copy_selection.connect(self.trigger_copy) + + def trigger_copy(self, what): + if what: + QApplication.instance().clipboard().setText(what) + else: + self.triggerAction(self.Copy) def javaScriptConsoleMessage(self, level, msg, linenumber, source_id): if level >= QWebEnginePage.ErrorMessageLevel and source_id == 'userscript:viewer.js': @@ -319,6 +334,7 @@ class WebView(RestartingWebEngineView): toggle_full_screen = pyqtSignal() ask_for_open = pyqtSignal(object) selection_changed = pyqtSignal(object) + view_image = pyqtSignal(object) def __init__(self, parent=None): self._host_widget = None @@ -342,6 +358,7 @@ class WebView(RestartingWebEngineView): 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.view_image.connect(self.view_image) self.bridge.report_cfi.connect(self.call_callback) self.pending_bridge_ready_actions = {} self.setPage(self._page) diff --git a/src/calibre/srv/render_book.py b/src/calibre/srv/render_book.py index 12f23bba9c..30f566ef23 100644 --- a/src/calibre/srv/render_book.py +++ b/src/calibre/srv/render_book.py @@ -315,6 +315,7 @@ class Container(ContainerBase): resource_template = link_uid + '|{}|' xlink_xpath = XPath('//*[@xl:href]') link_xpath = XPath('//h:a[@href]') + img_xpath = XPath('//h:img[@src]') res_link_xpath = XPath('//h:link[@href]') def link_replacer(base, url): @@ -354,6 +355,9 @@ class Container(ContainerBase): elif mt in OEB_DOCS: self.virtualized_names.add(name) root = self.parsed(name) + for img in img_xpath(root): + img.set('data-calibre-src', self.href_to_name(img.get('src'), name)) + changed.add(name) for link in res_link_xpath(root): ltype = (link.get('type') or 'text/css').lower() rel = (link.get('rel') or 'stylesheet').lower() diff --git a/src/pyj/read_book/extract.pyj b/src/pyj/read_book/extract.pyj new file mode 100644 index 0000000000..20a632dccb --- /dev/null +++ b/src/pyj/read_book/extract.pyj @@ -0,0 +1,15 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2019, Kovid Goyal +from __python__ import bound_methods, hash_literals + + + +def get_elements(x, y): + nonlocal img_id_counter + ans = {'link': None, 'img': None} + for elem in document.elementsFromPoint(x, y): + if elem.tagName.toLowerCase() is 'a' and elem.getAttribute('href') and not ans.link: + ans.link = elem.getAttribute('href') + elif elem.tagName.toLowerCase() is 'img' and elem.getAttribute('data-calibre-src') and not ans.img: + ans.img = elem.getAttribute('data-calibre-src') + return ans diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 1bef85e35b..a27ce96ad3 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -7,6 +7,7 @@ from gettext import gettext as _ from iframe_comm import IframeClient from read_book.cfi import at_current, scroll_to as scroll_to_cfi +from read_book.extract import get_elements from read_book.flow_mode import ( anchor_funcs as flow_anchor_funcs, flow_onwheel, flow_to_scroll_fraction, handle_gesture as flow_handle_gesture, handle_shortcut as flow_handle_shortcut, @@ -91,7 +92,7 @@ class IframeBoss: 'find': self.find, 'window_size': self.received_window_size, 'get_current_cfi': self.get_current_cfi, - 'set_forward_keypresses': self.set_forward_keypresses + 'set_forward_keypresses': self.set_forward_keypresses, } self.comm = IframeClient(handlers) self.last_window_ypos = 0 @@ -370,7 +371,7 @@ class IframeBoss: def oncontextmenu(self, evt): if self.content_ready: evt.preventDefault() - self.send_message('show_chrome') + self.send_message('show_chrome', elements=get_elements(evt.clientX, evt.clientY)) def send_message(self, action, **data): self.comm.send_message(action, data) diff --git a/src/pyj/read_book/overlay.pyj b/src/pyj/read_book/overlay.pyj index 430f73e6cb..d22798b89b 100644 --- a/src/pyj/read_book/overlay.pyj +++ b/src/pyj/read_book/overlay.pyj @@ -203,8 +203,9 @@ def simple_overlay_title(title, overlay, container): class MainOverlay: # {{{ - def __init__(self, overlay): + def __init__(self, overlay, elements): self.overlay = overlay + self.elements = elements or {} self.timer = None self.timer_id = unique_id() if window.Intl?.DateTimeFormat: @@ -286,11 +287,26 @@ class MainOverlay: # {{{ ac(_('Lookup/search word'), _('Lookup or search for the currently selected word'), def(): self.overlay.hide(), ui_operations.toggle_lookup();, 'library') )) + copy_actions = E.ul() + if self.overlay.view.currently_showing.selected_text: + copy_actions.appendChild(ac(_('Copy selection'), _('Copy the current selection'), def(): + self.overlay.hide(), ui_operations.copy_selection() + , 'copy')) + if self.elements.link: + copy_actions.appendChild(ac(_('Copy link'), _('Copy the current link'), def(): + self.overlay.hide(), ui_operations.copy_selection(self.elements.link) + , 'link')) + if self.elements.img: + copy_actions.appendChild(ac(_('View image'), _('View the current image'), def(): + self.overlay.hide(), ui_operations.view_image(self.elements.img) + , 'image')) + if copy_actions.childNodes.length: + actions_div.appendChild(copy_actions) + actions_div.appendChild(E.ul( ac(_('Inspector'), _('Show the content inspector'), def(): self.overlay.hide(), ui_operations.toggle_inspector();, 'bug') )) - container.appendChild(set_css(E.div(class_=MAIN_OVERLAY_TS_CLASS, # top section onclick=def (evt):evt.stopPropagation();, @@ -504,8 +520,8 @@ class Overlay: self.panels[-1].show(c) self.update_visibility() - def show(self): - self.panels = [MainOverlay(self)] + def show(self, elements): + self.panels = [MainOverlay(self, elements)] self.show_current_panel() def hide(self): diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 0266247524..61621c6cc1 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -349,14 +349,17 @@ class View: def focus_iframe(self): self.iframe.contentWindow.focus() - def show_chrome(self): + def show_chrome(self, data): self.show_chrome_counter += 1 - self.get_current_cfi('show-chrome-' + self.show_chrome_counter, self.do_show_chrome) + elements = {} + if data and data.elements: + elements = data.elements + self.get_current_cfi('show-chrome-' + self.show_chrome_counter, self.do_show_chrome.bind(None, elements)) - def do_show_chrome(self, request_id, cfi_data): + def do_show_chrome(self, elements, request_id, cfi_data): self.hide_overlays() self.update_cfi_data(cfi_data) - self.overlay.show() + self.overlay.show(elements) def show_search(self): self.hide_overlays() diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index c1283c7f0c..9a2be20f14 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -284,6 +284,10 @@ if window is window.top: to_python.report_cfi(request_id, data) ui_operations.ask_for_open = def(path): to_python.ask_for_open(path) + ui_operations.copy_selection = def(text): + to_python.copy_selection(text or None) + ui_operations.view_image = def(name): + to_python.view_image(name) document.body.appendChild(E.div(id='view')) window.onerror = onerror