Actions to copy text/URL and view image

This commit is contained in:
Kovid Goyal 2019-08-28 14:45:28 +05:30
parent ec01392e3a
commit afd483f11a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
10 changed files with 96 additions and 15 deletions

1
imgsrc/srv/copy.svg Normal file
View File

@ -0,0 +1 @@
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1696 384q40 0 68 28t28 68v1216q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-288h-544q-40 0-68-28t-28-68v-672q0-40 20-88t48-76l408-408q28-28 76-48t88-20h416q40 0 68 28t28 68v328q68-40 128-40h416zm-544 213l-299 299h299v-299zm-640-384l-299 299h299v-299zm196 647l316-316v-416h-384v416q0 40-28 68t-68 28h-416v640h512v-256q0-40 20-88t48-76zm956 804v-1152h-384v416q0 40-28 68t-68 28h-416v640h896z"/></svg>

After

Width:  |  Height:  |  Size: 497 B

1
imgsrc/srv/link.svg Normal file
View File

@ -0,0 +1 @@
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1520 1216q0-40-28-68l-208-208q-28-28-68-28-42 0-72 32 3 3 19 18.5t21.5 21.5 15 19 13 25.5 3.5 27.5q0 40-28 68t-68 28q-15 0-27.5-3.5t-25.5-13-19-15-21.5-21.5-18.5-19q-33 31-33 73 0 40 28 68l206 207q27 27 68 27 40 0 68-26l147-146q28-28 28-67zm-703-705q0-40-28-68l-206-207q-28-28-68-28-39 0-68 27l-147 146q-28 28-28 67 0 40 28 68l208 208q27 27 68 27 42 0 72-31-3-3-19-18.5t-21.5-21.5-15-19-13-25.5-3.5-27.5q0-40 28-68t68-28q15 0 27.5 3.5t25.5 13 19 15 21.5 21.5 18.5 19q33-31 33-73zm895 705q0 120-85 203l-147 146q-83 83-203 83-121 0-204-85l-206-207q-83-83-83-203 0-123 88-209l-88-88q-86 88-208 88-120 0-204-84l-208-208q-84-84-84-204t85-203l147-146q83-83 203-83 121 0 204 85l206 207q83 83 83 203 0 123-88 209l88 88q86-88 208-88 120 0 204 84l208 208q84 84 84 204z"/></svg>

After

Width:  |  Height:  |  Size: 868 B

View File

@ -12,13 +12,15 @@ from hashlib import sha256
from threading import Thread from threading import Thread
from PyQt5.Qt import ( 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 import prints
from calibre.constants import config_dir from calibre.constants import config_dir
from calibre.customize.ui import available_input_formats from calibre.customize.ui import available_input_formats
from calibre.gui2 import choose_files, error_dialog 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.main_window import MainWindow
from calibre.gui2.viewer.annotations import ( from calibre.gui2.viewer.annotations import (
merge_annotations, parse_annotations, save_annots_to_epub, serialize_annotations 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.lookup import Lookup
from calibre.gui2.viewer.toc import TOC, TOCSearch, TOCView from calibre.gui2.viewer.toc import TOC, TOCSearch, TOCView
from calibre.gui2.viewer.web_view import ( 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.date import utcnow
from calibre.utils.ipc.simple_worker import WorkerError from calibre.utils.ipc.simple_worker import WorkerError
@ -68,6 +70,7 @@ class EbookViewer(MainWindow):
self.base_window_title = _('E-book viewer') self.base_window_title = _('E-book viewer')
self.setWindowTitle(self.base_window_title) self.setWindowTitle(self.base_window_title)
self.in_full_screen_mode = None self.in_full_screen_mode = None
self.image_popup = ImagePopup(self)
try: try:
os.makedirs(annotations_dir) os.makedirs(annotations_dir)
except EnvironmentError: except EnvironmentError:
@ -119,6 +122,7 @@ class EbookViewer(MainWindow):
self.web_view.toggle_full_screen.connect(self.toggle_full_screen) 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.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.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.setCentralWidget(self.web_view)
self.restore_state() self.restore_state()
if continue_reading: if continue_reading:
@ -188,6 +192,21 @@ class EbookViewer(MainWindow):
def bookmark_activated(self, cfi): def bookmark_activated(self, cfi):
self.web_view.goto_cfi(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 {{{ # Load book {{{

View File

@ -57,12 +57,18 @@ def set_book_path(path, pathtoebook):
set_book_path.parsed_manifest = json_loads(set_book_path.manifest) 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) bdir = getattr(set_book_path, 'path', None)
if bdir is None: if bdir is None:
return None, None return
path = os.path.abspath(os.path.join(bdir, name)) 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 return None, None
try: try:
with lopen(path, 'rb') as f: with lopen(path, 'rb') as f:
@ -195,6 +201,8 @@ class ViewerBridge(Bridge):
report_cfi = from_js(object, object) report_cfi = from_js(object, object)
ask_for_open = from_js(object) ask_for_open = from_js(object)
selection_changed = from_js(object) selection_changed = from_js(object)
copy_selection = from_js(object)
view_image = from_js(object)
create_view = to_js() create_view = to_js()
show_preparing_message = to_js() show_preparing_message = to_js()
@ -245,6 +253,13 @@ class WebPage(QWebEnginePage):
secure_webengine(self, for_viewer=True) secure_webengine(self, for_viewer=True)
apply_font_settings(self) apply_font_settings(self)
self.bridge = ViewerBridge(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): def javaScriptConsoleMessage(self, level, msg, linenumber, source_id):
if level >= QWebEnginePage.ErrorMessageLevel and source_id == 'userscript:viewer.js': if level >= QWebEnginePage.ErrorMessageLevel and source_id == 'userscript:viewer.js':
@ -319,6 +334,7 @@ class WebView(RestartingWebEngineView):
toggle_full_screen = pyqtSignal() toggle_full_screen = pyqtSignal()
ask_for_open = pyqtSignal(object) ask_for_open = pyqtSignal(object)
selection_changed = pyqtSignal(object) selection_changed = pyqtSignal(object)
view_image = pyqtSignal(object)
def __init__(self, parent=None): def __init__(self, parent=None):
self._host_widget = None self._host_widget = None
@ -342,6 +358,7 @@ class WebView(RestartingWebEngineView):
self.bridge.toggle_full_screen.connect(self.toggle_full_screen) self.bridge.toggle_full_screen.connect(self.toggle_full_screen)
self.bridge.ask_for_open.connect(self.ask_for_open) self.bridge.ask_for_open.connect(self.ask_for_open)
self.bridge.selection_changed.connect(self.selection_changed) 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.bridge.report_cfi.connect(self.call_callback)
self.pending_bridge_ready_actions = {} self.pending_bridge_ready_actions = {}
self.setPage(self._page) self.setPage(self._page)

View File

@ -315,6 +315,7 @@ class Container(ContainerBase):
resource_template = link_uid + '|{}|' resource_template = link_uid + '|{}|'
xlink_xpath = XPath('//*[@xl:href]') xlink_xpath = XPath('//*[@xl:href]')
link_xpath = XPath('//h:a[@href]') link_xpath = XPath('//h:a[@href]')
img_xpath = XPath('//h:img[@src]')
res_link_xpath = XPath('//h:link[@href]') res_link_xpath = XPath('//h:link[@href]')
def link_replacer(base, url): def link_replacer(base, url):
@ -354,6 +355,9 @@ class Container(ContainerBase):
elif mt in OEB_DOCS: elif mt in OEB_DOCS:
self.virtualized_names.add(name) self.virtualized_names.add(name)
root = self.parsed(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): for link in res_link_xpath(root):
ltype = (link.get('type') or 'text/css').lower() ltype = (link.get('type') or 'text/css').lower()
rel = (link.get('rel') or 'stylesheet').lower() rel = (link.get('rel') or 'stylesheet').lower()

View File

@ -0,0 +1,15 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

@ -7,6 +7,7 @@ from gettext import gettext as _
from iframe_comm import IframeClient from iframe_comm import IframeClient
from read_book.cfi import at_current, scroll_to as scroll_to_cfi 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 ( from read_book.flow_mode import (
anchor_funcs as flow_anchor_funcs, flow_onwheel, flow_to_scroll_fraction, anchor_funcs as flow_anchor_funcs, flow_onwheel, flow_to_scroll_fraction,
handle_gesture as flow_handle_gesture, handle_shortcut as flow_handle_shortcut, handle_gesture as flow_handle_gesture, handle_shortcut as flow_handle_shortcut,
@ -91,7 +92,7 @@ class IframeBoss:
'find': self.find, 'find': self.find,
'window_size': self.received_window_size, 'window_size': self.received_window_size,
'get_current_cfi': self.get_current_cfi, '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.comm = IframeClient(handlers)
self.last_window_ypos = 0 self.last_window_ypos = 0
@ -370,7 +371,7 @@ class IframeBoss:
def oncontextmenu(self, evt): def oncontextmenu(self, evt):
if self.content_ready: if self.content_ready:
evt.preventDefault() 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): def send_message(self, action, **data):
self.comm.send_message(action, data) self.comm.send_message(action, data)

View File

@ -203,8 +203,9 @@ def simple_overlay_title(title, overlay, container):
class MainOverlay: # {{{ class MainOverlay: # {{{
def __init__(self, overlay): def __init__(self, overlay, elements):
self.overlay = overlay self.overlay = overlay
self.elements = elements or {}
self.timer = None self.timer = None
self.timer_id = unique_id() self.timer_id = unique_id()
if window.Intl?.DateTimeFormat: if window.Intl?.DateTimeFormat:
@ -286,11 +287,26 @@ class MainOverlay: # {{{
ac(_('Lookup/search word'), _('Lookup or search for the currently selected word'), ac(_('Lookup/search word'), _('Lookup or search for the currently selected word'),
def(): self.overlay.hide(), ui_operations.toggle_lookup();, 'library') 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( actions_div.appendChild(E.ul(
ac(_('Inspector'), _('Show the content inspector'), ac(_('Inspector'), _('Show the content inspector'),
def(): self.overlay.hide(), ui_operations.toggle_inspector();, 'bug') def(): self.overlay.hide(), ui_operations.toggle_inspector();, 'bug')
)) ))
container.appendChild(set_css(E.div(class_=MAIN_OVERLAY_TS_CLASS, # top section container.appendChild(set_css(E.div(class_=MAIN_OVERLAY_TS_CLASS, # top section
onclick=def (evt):evt.stopPropagation();, onclick=def (evt):evt.stopPropagation();,
@ -504,8 +520,8 @@ class Overlay:
self.panels[-1].show(c) self.panels[-1].show(c)
self.update_visibility() self.update_visibility()
def show(self): def show(self, elements):
self.panels = [MainOverlay(self)] self.panels = [MainOverlay(self, elements)]
self.show_current_panel() self.show_current_panel()
def hide(self): def hide(self):

View File

@ -349,14 +349,17 @@ class View:
def focus_iframe(self): def focus_iframe(self):
self.iframe.contentWindow.focus() self.iframe.contentWindow.focus()
def show_chrome(self): def show_chrome(self, data):
self.show_chrome_counter += 1 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.hide_overlays()
self.update_cfi_data(cfi_data) self.update_cfi_data(cfi_data)
self.overlay.show() self.overlay.show(elements)
def show_search(self): def show_search(self):
self.hide_overlays() self.hide_overlays()

View File

@ -284,6 +284,10 @@ if window is window.top:
to_python.report_cfi(request_id, data) to_python.report_cfi(request_id, data)
ui_operations.ask_for_open = def(path): ui_operations.ask_for_open = def(path):
to_python.ask_for_open(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')) document.body.appendChild(E.div(id='view'))
window.onerror = onerror window.onerror = onerror