mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Actions to copy text/URL and view image
This commit is contained in:
parent
ec01392e3a
commit
afd483f11a
1
imgsrc/srv/copy.svg
Normal file
1
imgsrc/srv/copy.svg
Normal 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
1
imgsrc/srv/link.svg
Normal 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 |
@ -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 {{{
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
15
src/pyj/read_book/extract.pyj
Normal file
15
src/pyj/read_book/extract.pyj
Normal 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
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user