mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Show highlight notes in the select bar
This commit is contained in:
parent
6436d36f2e
commit
2078309b52
@ -150,3 +150,29 @@ def set_selection_to_highlight():
|
||||
if crw:
|
||||
select_crw(crw)
|
||||
return crw or None
|
||||
|
||||
|
||||
def highlight_associated_with_selection(sel, annot_id_uuid_map):
|
||||
# Return the annotation id for a highlight that contains the focus or
|
||||
# anchor of the selection
|
||||
|
||||
def get_annot_id_for(node, offset):
|
||||
if not node:
|
||||
return
|
||||
if node.nodeType is Node.ELEMENT_NODE:
|
||||
if node.dataset.calibreRangeWrapper:
|
||||
return annot_id_uuid_map[node.dataset.calibreRangeWrapper]
|
||||
if offset is 0:
|
||||
if node.firstChild?.nodeType is Node.ELEMENT_NODE and node.firstChild.dataset.calibreRangeWrapper:
|
||||
return annot_id_uuid_map[node.firstChild.dataset.calibreRangeWrapper]
|
||||
elif offset < node.childNodes.length:
|
||||
node = node.childNodes[offset]
|
||||
return get_annot_id_for(node, 0)
|
||||
elif node.nodeType is Node.TEXT_NODE:
|
||||
if node.parentNode?.nodeType is Node.ELEMENT_NODE and node.parentNode.dataset.calibreRangeWrapper:
|
||||
return annot_id_uuid_map[node.parentNode.dataset.calibreRangeWrapper]
|
||||
|
||||
annot_id = get_annot_id_for(sel.focusNode, sel.focusOffset)
|
||||
if not annot_id:
|
||||
annot_id = get_annot_id_for(sel.anchorNode, sel.anchorOffset)
|
||||
return annot_id
|
||||
|
@ -15,9 +15,13 @@ from read_book.globals import ui_operations
|
||||
from read_book.shortcuts import shortcut_for_key_event
|
||||
from widgets import create_button
|
||||
|
||||
|
||||
# TODO:
|
||||
# Get rid of view annotations and show notes in selection bar if available
|
||||
# Allow adding/removing custom highlight colors
|
||||
# Custom colors for highlights
|
||||
# Google lookup for selections
|
||||
# Export all annots as plain text/JSON
|
||||
# Remove lookup and create highlight buttons from chrome
|
||||
# position bar at mouse x during drag
|
||||
|
||||
|
||||
class AnnotationsManager:
|
||||
@ -61,7 +65,7 @@ class AnnotationsManager:
|
||||
ui_operations.highlights_changed(Object.values(self.highlights))
|
||||
|
||||
def notes_for_highlight(self, uuid):
|
||||
h = self.highlights[uuid]
|
||||
h = self.highlights[uuid] if uuid else None
|
||||
if h:
|
||||
return h.notes
|
||||
|
||||
@ -726,8 +730,6 @@ class CreateAnnotation:
|
||||
_('Failed to apply highlighting, try adjusting extent of highlight')
|
||||
)
|
||||
self.annotations_manager.add_highlight(msg, self.current_highlight_style, self.current_notes)
|
||||
elif msg.type is 'annotation-activated':
|
||||
self.view.view_annotation.show(msg.uuid)
|
||||
else:
|
||||
print('Ignoring annotations message with unknown type:', msg.type)
|
||||
|
||||
@ -770,93 +772,3 @@ class CreateAnnotation:
|
||||
else:
|
||||
self.place_single_handle(self.left_handle, extents.start)
|
||||
self.place_single_handle(self.right_handle, extents.end)
|
||||
|
||||
|
||||
class ViewAnnotation: # {{{
|
||||
|
||||
container_id = 'view-annotation-overlay'
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
self.annotations_manager = view.annotations_manager
|
||||
c = self.container
|
||||
self.showing_uuid = None
|
||||
c.style.flexDirection = 'column'
|
||||
c.style.justifyContent = 'flex-end'
|
||||
c.appendChild(E.div(
|
||||
style='pointer-events: auto; padding: 1ex 1rem; box-sizing: border-box; border-top: solid 2px currentColor; overflow: hidden',
|
||||
onclick=def(ev):
|
||||
ev.preventDefault(), ev.stopPropagation()
|
||||
,
|
||||
E.div(
|
||||
style='display: flex; justify-content: space-between; align-items: flex-start',
|
||||
E.a(
|
||||
svgicon('close', f'{BAR_SIZE}px', f'{BAR_SIZE}px'),
|
||||
class_='simple-link', href='javascript: void', title=_('Close'),
|
||||
onclick=def(ev):
|
||||
self.hide()
|
||||
),
|
||||
E.div(
|
||||
style='margin-left: 2rem; margin-right: 2rem; max-height: 20vh; overflow-y: auto; overflow-x: hidden; box-sizing: border-box; padding-top: 1ex',
|
||||
class_='highlight-notes-viewer'
|
||||
),
|
||||
E.a(
|
||||
svgicon('pencil', f'{BAR_SIZE}px', f'{BAR_SIZE}px'),
|
||||
class_='simple-link', href='javascript: void', title=_('Edit this highlight'),
|
||||
onclick=def(ev):
|
||||
self.edit_current()
|
||||
),
|
||||
)
|
||||
))
|
||||
|
||||
@property
|
||||
def container(self):
|
||||
return document.getElementById(self.container_id)
|
||||
|
||||
def show(self, uuid):
|
||||
c = self.container
|
||||
c.style.display = 'flex'
|
||||
self.showing_uuid = uuid
|
||||
s = self.annotations_manager.style_for_highlight(uuid) or default_highlight_style()
|
||||
c = c.firstChild
|
||||
c.style.color = s.color
|
||||
c.style.backgroundColor = s['background-color']
|
||||
text = self.annotations_manager.notes_for_highlight(uuid) or ''
|
||||
self.display_text(text)
|
||||
|
||||
def display_text(self, text):
|
||||
text = text or _('This highlight has no added notes')
|
||||
text = text.strip()
|
||||
div = self.container.querySelector('.highlight-notes-viewer')
|
||||
clear(div)
|
||||
current_block = ''
|
||||
|
||||
def add_para():
|
||||
nonlocal current_block
|
||||
div.appendChild(E.p(current_block))
|
||||
if div.childNodes.length > 1:
|
||||
div.lastChild.style.marginTop = '2ex'
|
||||
current_block = ''
|
||||
|
||||
for line in text.splitlines():
|
||||
if not line or not line.strip():
|
||||
if current_block:
|
||||
add_para()
|
||||
continue
|
||||
current_block += line + '\n'
|
||||
if current_block:
|
||||
add_para()
|
||||
|
||||
def hide(self):
|
||||
self.container.style.display = 'none'
|
||||
self.showing_uuid = None
|
||||
|
||||
@property
|
||||
def is_visible(self):
|
||||
return self.container.style.display is not 'none'
|
||||
|
||||
def edit_current(self):
|
||||
if self.showing_uuid:
|
||||
self.view.create_annotation.edit_highlight(self.showing_uuid)
|
||||
self.hide()
|
||||
# }}}
|
||||
|
@ -8,14 +8,15 @@ from read_book.globals import annot_id_uuid_map
|
||||
|
||||
def get_elements(x, y):
|
||||
nonlocal img_id_counter
|
||||
ans = {'link': None, 'img': None, 'highlight': None}
|
||||
ans = {'link': None, 'img': None, 'highlight': None, 'crw': 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')
|
||||
elif elem.dataset?.calibreRangeWrapper:
|
||||
annot_id = annot_id_uuid_map[elem.dataset.calibreRangeWrapper]
|
||||
ans.crw = elem.dataset.calibreRangeWrapper
|
||||
annot_id = annot_id_uuid_map[ans.crw]
|
||||
if annot_id:
|
||||
ans.highlight = annot_id
|
||||
return ans
|
||||
|
@ -12,8 +12,8 @@ from select import (
|
||||
from fs_images import fix_fullscreen_svg_images
|
||||
from iframe_comm import IframeClient
|
||||
from range_utils import (
|
||||
reset_highlight_counter, select_crw, set_selection_to_highlight, unwrap_all_crw,
|
||||
unwrap_crw, wrap_text_in_range
|
||||
highlight_associated_with_selection, reset_highlight_counter, select_crw,
|
||||
set_selection_to_highlight, unwrap_all_crw, unwrap_crw, wrap_text_in_range
|
||||
)
|
||||
from read_book.cfi import (
|
||||
cfi_for_selection, range_from_cfi, scroll_to as scroll_to_cfi
|
||||
@ -292,14 +292,13 @@ class IframeBoss:
|
||||
self.send_message('view_image', calibre_src=elements.img)
|
||||
return
|
||||
if elements.highlight:
|
||||
self.activate_annotation(elements.highlight)
|
||||
select_crw(elements.crw)
|
||||
return
|
||||
r = word_at_point(gesture.viewport_x, gesture.viewport_y)
|
||||
if r:
|
||||
s = document.getSelection()
|
||||
s.removeAllRanges()
|
||||
s.addRange(r)
|
||||
self.send_message('lookup_word', word=str(r))
|
||||
|
||||
def gesture_from_margin(self, data):
|
||||
self.handle_gesture(data.gesture)
|
||||
@ -523,11 +522,13 @@ class IframeBoss:
|
||||
return
|
||||
sel = window.getSelection()
|
||||
text = ''
|
||||
annot_id = None
|
||||
collapsed = not sel or sel.isCollapsed
|
||||
if not collapsed:
|
||||
text = sel.toString()
|
||||
annot_id = highlight_associated_with_selection(sel, annot_id_uuid_map)
|
||||
self.send_message(
|
||||
'selectionchange', text=text, empty=v'!!collapsed',
|
||||
'selectionchange', text=text, empty=v'!!collapsed', annot_id=annot_id,
|
||||
selection_extents=selection_extents(current_layout_mode() is 'flow', True))
|
||||
|
||||
def onresize_stage2(self):
|
||||
@ -825,15 +826,11 @@ class IframeBoss:
|
||||
def add_highlight_listeners(self, wrapper):
|
||||
wrapper.addEventListener('dblclick', self.highlight_wrapper_dblclicked)
|
||||
|
||||
def activate_annotation(self, uuid):
|
||||
if uuid:
|
||||
self.send_message('annotations', type='annotation-activated', uuid=uuid)
|
||||
window.getSelection().removeAllRanges()
|
||||
|
||||
def highlight_wrapper_dblclicked(self, ev):
|
||||
crw = ev.currentTarget.dataset.calibreRangeWrapper
|
||||
if crw:
|
||||
ev.preventDefault(), ev.stopPropagation()
|
||||
self.activate_annotation(annot_id_uuid_map[crw])
|
||||
select_crw(crw)
|
||||
|
||||
def copy_selection(self):
|
||||
text = window.getSelection().toString()
|
||||
|
@ -5,7 +5,6 @@ from __python__ import hash_literals
|
||||
import traceback
|
||||
from elementmaker import E
|
||||
from gettext import gettext as _
|
||||
from select import word_at_point
|
||||
|
||||
from dom import set_css
|
||||
from read_book.cfi import (
|
||||
@ -667,13 +666,6 @@ def handle_gesture(gesture):
|
||||
scroll_by_page(True, opts.paged_taps_scroll_by_screen)
|
||||
elif gesture.type is 'next-page':
|
||||
scroll_by_page(False, opts.paged_taps_scroll_by_screen)
|
||||
elif gesture.type is 'long-tap':
|
||||
r = word_at_point(gesture.viewport_x, gesture.viewport_y)
|
||||
if r:
|
||||
s = document.getSelection()
|
||||
s.removeAllRanges()
|
||||
s.addRange(r)
|
||||
get_boss().send_message('lookup_word', word=str(r))
|
||||
|
||||
|
||||
anchor_funcs = {
|
||||
|
@ -6,21 +6,34 @@ from elementmaker import E
|
||||
from gettext import gettext as _
|
||||
|
||||
from book_list.theme import get_color
|
||||
from dom import svgicon
|
||||
from read_book.globals import ui_operations
|
||||
from dom import clear, svgicon
|
||||
from read_book.globals import runtime, ui_operations
|
||||
|
||||
|
||||
class SelectionBar:
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
|
||||
def build_bar(self, notes):
|
||||
c = self.container
|
||||
bar = E.div(
|
||||
style='position: absolute; height: 4ex; border: solid 1px currentColor; border-radius: 5px; overflow: hidden;'
|
||||
'display: flex; align-items: center; pointer-events: auto; padding: 5px; left: 0; top: 0;'
|
||||
'background-color: {}'.format(get_color("window-background"))
|
||||
max_width = 'min(50rem, 90vw)' if self.supports_css_min_max else '50rem'
|
||||
bar_container = E.div(
|
||||
style='position: absolute; border: solid 1px currentColor; border-radius: 5px;'
|
||||
'left: 0; top: 0; pointer-events: auto; display: flex; flex-direction: column;'
|
||||
'background-color: {}; max-width: {}'.format(get_color("window-background"), max_width),
|
||||
|
||||
E.div(style='height: 4ex; display: flex; align-items: center; padding: 5px; justify-content: center'),
|
||||
|
||||
E.hr(style='border-top: solid 1px; margin: 0; padding: 0'),
|
||||
|
||||
E.div(
|
||||
style='display: none; padding: 5px;',
|
||||
E.div(),
|
||||
),
|
||||
)
|
||||
c.appendChild(bar)
|
||||
bar = bar_container.firstChild
|
||||
c.appendChild(bar_container)
|
||||
|
||||
def cb(icon, tooltip, callback):
|
||||
ans = svgicon(icon, '3ex', '3ex', tooltip)
|
||||
@ -35,6 +48,12 @@ class SelectionBar:
|
||||
bar.appendChild(cb('library', _('Lookup/search selected word'), self.lookup))
|
||||
bar.appendChild(cb('highlight', _('Highlight selection'), self.create_highlight))
|
||||
bar.appendChild(cb('close', _('Clear the selection'), self.clear_selection))
|
||||
self.show_notes(bar_container, notes)
|
||||
return bar_container
|
||||
|
||||
@property
|
||||
def supports_css_min_max(self):
|
||||
return not runtime.is_standalone_viewer or runtime.QT_VERSION >= 0x050f00
|
||||
|
||||
@property
|
||||
def container(self):
|
||||
@ -48,8 +67,7 @@ class SelectionBar:
|
||||
self.container.style.display = 'none'
|
||||
|
||||
def show(self):
|
||||
if self.view.create_annotation.is_visible:
|
||||
return
|
||||
if not self.view.create_annotation.is_visible:
|
||||
self.container.style.display = 'block'
|
||||
|
||||
@property
|
||||
@ -69,11 +87,46 @@ class SelectionBar:
|
||||
def create_highlight(self):
|
||||
self.view.initiate_create_annotation(True)
|
||||
|
||||
def show_notes(self, bar, notes):
|
||||
notes = (notes or "").strip()
|
||||
if not notes:
|
||||
return
|
||||
notes_container = bar.lastChild
|
||||
c = notes_container.lastChild
|
||||
notes_container.style.display = notes_container.previousSibling.style.display = 'block'
|
||||
c.style.overflow = 'auto'
|
||||
if self.supports_css_min_max:
|
||||
c.style.maxHeight = 'min(20ex, 40vh)'
|
||||
else:
|
||||
c.style.maxHeight = '20ex'
|
||||
current_block = ''
|
||||
|
||||
def add_para():
|
||||
nonlocal current_block
|
||||
c.appendChild(E.p(current_block))
|
||||
if c.childNodes.length > 1:
|
||||
c.lastChild.style.marginTop = '2ex'
|
||||
current_block = ''
|
||||
|
||||
for line in notes.splitlines():
|
||||
if not line or not line.strip():
|
||||
if current_block:
|
||||
add_para()
|
||||
continue
|
||||
current_block += line + '\n'
|
||||
if current_block:
|
||||
add_para()
|
||||
|
||||
def update_position(self):
|
||||
container = self.container
|
||||
clear(container)
|
||||
cs = self.view.currently_showing
|
||||
if not cs.has_selection:
|
||||
return self.hide()
|
||||
|
||||
if not cs.selection_start.onscreen and not cs.selection_end.onscreen:
|
||||
return self.hide()
|
||||
|
||||
margins = {
|
||||
'top': document.getElementById('book-top-margin').offsetHeight,
|
||||
'bottom': document.getElementById('book-bottom-margin').offsetHeight,
|
||||
@ -84,15 +137,11 @@ class SelectionBar:
|
||||
def map_boundary(x):
|
||||
return {'x': (x.x or 0) + margins.left, 'y': (x.y or 0) + margins.top, 'height': x.height or 0, 'onscreen': x.onscreen}
|
||||
|
||||
if not cs.selection_start.onscreen and not cs.selection_end.onscreen:
|
||||
return self.hide()
|
||||
bar = self.build_bar(self.view.annotations_manager.notes_for_highlight(cs.selection_annot_id))
|
||||
start = map_boundary(cs.selection_start)
|
||||
end = map_boundary(cs.selection_end)
|
||||
|
||||
self.show()
|
||||
end_after_start = start.y < end.y or (start.y is end.y and start.x < end.x)
|
||||
container = self.container
|
||||
bar = self.bar
|
||||
|
||||
# vertical position
|
||||
bar_height = bar.offsetHeight
|
||||
@ -106,9 +155,11 @@ class SelectionBar:
|
||||
top = (end.y + end.height + buffer) if put_below else (end.y - bar_height - buffer)
|
||||
top = max(buffer, min(top, container.offsetHeight - bar_height - buffer))
|
||||
bar.style.top = top + 'px'
|
||||
bar.style.flexDirection = 'column' if put_below else 'column-reverse'
|
||||
|
||||
# horizontal position
|
||||
bar_width = bar.offsetWidth
|
||||
left = end.x - bar_width // 2
|
||||
left = max(buffer, min(left, container.offsetWidth - bar_width - buffer))
|
||||
# - 10 ensures we dont cover scroll bar
|
||||
left = max(buffer, min(left, container.offsetWidth - bar_width - buffer - 10))
|
||||
bar.style.left = left + 'px'
|
||||
|
@ -13,9 +13,7 @@ from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id
|
||||
from iframe_comm import IframeWrapper
|
||||
from modals import error_dialog, warning_dialog
|
||||
from read_book.content_popup import ContentPopupOverlay
|
||||
from read_book.create_annotation import (
|
||||
AnnotationsManager, CreateAnnotation, ViewAnnotation
|
||||
)
|
||||
from read_book.create_annotation import AnnotationsManager, CreateAnnotation
|
||||
from read_book.globals import (
|
||||
current_book, runtime, set_current_spine_item, ui_operations
|
||||
)
|
||||
@ -229,7 +227,6 @@ class View:
|
||||
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; overflow: auto; display:none', id='book-overlay'), # main overlay
|
||||
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='controls-help-overlay'), # controls help overlay
|
||||
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none; overflow: hidden', id=CreateAnnotation.container_id, tabindex='0'), # create annotation overlay
|
||||
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; pointer-events:none; display:none; z-index: 4000', id=ViewAnnotation.container_id), # view annotation overlay
|
||||
)
|
||||
),
|
||||
E.div(
|
||||
@ -301,7 +298,6 @@ class View:
|
||||
self.book_scrollbar.apply_visibility()
|
||||
self.annotations_manager = AnnotationsManager(self)
|
||||
self.create_annotation = CreateAnnotation(self)
|
||||
self.view_annotation = ViewAnnotation(self)
|
||||
|
||||
@property
|
||||
def iframe(self):
|
||||
@ -526,6 +522,7 @@ class View:
|
||||
self.currently_showing.has_selection = not data.empty
|
||||
self.currently_showing.selection_start = data.selection_extents.start
|
||||
self.currently_showing.selection_end = data.selection_extents.end
|
||||
self.currently_showing.selection_annot_id = data.annot_id
|
||||
if ui_operations.selection_changed:
|
||||
ui_operations.selection_changed(self.currently_showing.selected_text)
|
||||
self.selection_bar.update_position()
|
||||
@ -610,7 +607,6 @@ class View:
|
||||
self.content_popup_overlay.hide()
|
||||
self.reference_mode_overlay.style.display = 'none'
|
||||
self.create_annotation.hide()
|
||||
self.view_annotation.hide()
|
||||
self.focus_iframe()
|
||||
|
||||
def focus_iframe(self):
|
||||
@ -1020,8 +1016,7 @@ class View:
|
||||
|
||||
def set_notes_for_highlight(self, uuid, notes):
|
||||
if self.annotations_manager.set_notes_for_highlight(uuid, notes):
|
||||
if self.view_annotation.is_visible and self.view_annotation.showing_uuid is uuid:
|
||||
self.view_annotation.show(uuid)
|
||||
self.selection_bar.update_position()
|
||||
|
||||
def on_next_spine_item(self, data):
|
||||
spine = self.book.manifest.spine
|
||||
|
Loading…
x
Reference in New Issue
Block a user