Show highlight notes in the select bar

This commit is contained in:
Kovid Goyal 2020-07-24 12:51:37 +05:30
parent 6436d36f2e
commit 2078309b52
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 115 additions and 141 deletions

View File

@ -150,3 +150,29 @@ def set_selection_to_highlight():
if crw: if crw:
select_crw(crw) select_crw(crw)
return crw or None 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

View File

@ -15,9 +15,13 @@ from read_book.globals import ui_operations
from read_book.shortcuts import shortcut_for_key_event from read_book.shortcuts import shortcut_for_key_event
from widgets import create_button from widgets import create_button
# TODO: # TODO:
# Get rid of view annotations and show notes in selection bar if available # Custom colors for highlights
# Allow adding/removing custom highlight colors # 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: class AnnotationsManager:
@ -61,7 +65,7 @@ class AnnotationsManager:
ui_operations.highlights_changed(Object.values(self.highlights)) ui_operations.highlights_changed(Object.values(self.highlights))
def notes_for_highlight(self, uuid): def notes_for_highlight(self, uuid):
h = self.highlights[uuid] h = self.highlights[uuid] if uuid else None
if h: if h:
return h.notes return h.notes
@ -726,8 +730,6 @@ class CreateAnnotation:
_('Failed to apply highlighting, try adjusting extent of highlight') _('Failed to apply highlighting, try adjusting extent of highlight')
) )
self.annotations_manager.add_highlight(msg, self.current_highlight_style, self.current_notes) 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: else:
print('Ignoring annotations message with unknown type:', msg.type) print('Ignoring annotations message with unknown type:', msg.type)
@ -770,93 +772,3 @@ class CreateAnnotation:
else: else:
self.place_single_handle(self.left_handle, extents.start) self.place_single_handle(self.left_handle, extents.start)
self.place_single_handle(self.right_handle, extents.end) 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()
# }}}

View File

@ -8,14 +8,15 @@ from read_book.globals import annot_id_uuid_map
def get_elements(x, y): def get_elements(x, y):
nonlocal img_id_counter 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): for elem in document.elementsFromPoint(x, y):
if elem.tagName.toLowerCase() is 'a' and elem.getAttribute('href') and not ans.link: if elem.tagName.toLowerCase() is 'a' and elem.getAttribute('href') and not ans.link:
ans.link = elem.getAttribute('href') ans.link = elem.getAttribute('href')
elif elem.tagName.toLowerCase() is 'img' and elem.getAttribute('data-calibre-src') and not ans.img: elif elem.tagName.toLowerCase() is 'img' and elem.getAttribute('data-calibre-src') and not ans.img:
ans.img = elem.getAttribute('data-calibre-src') ans.img = elem.getAttribute('data-calibre-src')
elif elem.dataset?.calibreRangeWrapper: 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: if annot_id:
ans.highlight = annot_id ans.highlight = annot_id
return ans return ans

View File

@ -12,8 +12,8 @@ from select import (
from fs_images import fix_fullscreen_svg_images from fs_images import fix_fullscreen_svg_images
from iframe_comm import IframeClient from iframe_comm import IframeClient
from range_utils import ( from range_utils import (
reset_highlight_counter, select_crw, set_selection_to_highlight, unwrap_all_crw, highlight_associated_with_selection, reset_highlight_counter, select_crw,
unwrap_crw, wrap_text_in_range set_selection_to_highlight, unwrap_all_crw, unwrap_crw, wrap_text_in_range
) )
from read_book.cfi import ( from read_book.cfi import (
cfi_for_selection, range_from_cfi, scroll_to as scroll_to_cfi 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) self.send_message('view_image', calibre_src=elements.img)
return return
if elements.highlight: if elements.highlight:
self.activate_annotation(elements.highlight) select_crw(elements.crw)
return return
r = word_at_point(gesture.viewport_x, gesture.viewport_y) r = word_at_point(gesture.viewport_x, gesture.viewport_y)
if r: if r:
s = document.getSelection() s = document.getSelection()
s.removeAllRanges() s.removeAllRanges()
s.addRange(r) s.addRange(r)
self.send_message('lookup_word', word=str(r))
def gesture_from_margin(self, data): def gesture_from_margin(self, data):
self.handle_gesture(data.gesture) self.handle_gesture(data.gesture)
@ -523,11 +522,13 @@ class IframeBoss:
return return
sel = window.getSelection() sel = window.getSelection()
text = '' text = ''
annot_id = None
collapsed = not sel or sel.isCollapsed collapsed = not sel or sel.isCollapsed
if not collapsed: if not collapsed:
text = sel.toString() text = sel.toString()
annot_id = highlight_associated_with_selection(sel, annot_id_uuid_map)
self.send_message( 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)) selection_extents=selection_extents(current_layout_mode() is 'flow', True))
def onresize_stage2(self): def onresize_stage2(self):
@ -825,15 +826,11 @@ class IframeBoss:
def add_highlight_listeners(self, wrapper): def add_highlight_listeners(self, wrapper):
wrapper.addEventListener('dblclick', self.highlight_wrapper_dblclicked) 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): def highlight_wrapper_dblclicked(self, ev):
crw = ev.currentTarget.dataset.calibreRangeWrapper crw = ev.currentTarget.dataset.calibreRangeWrapper
ev.preventDefault(), ev.stopPropagation() if crw:
self.activate_annotation(annot_id_uuid_map[crw]) ev.preventDefault(), ev.stopPropagation()
select_crw(crw)
def copy_selection(self): def copy_selection(self):
text = window.getSelection().toString() text = window.getSelection().toString()

View File

@ -5,7 +5,6 @@ from __python__ import hash_literals
import traceback import traceback
from elementmaker import E from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from select import word_at_point
from dom import set_css from dom import set_css
from read_book.cfi import ( from read_book.cfi import (
@ -667,13 +666,6 @@ def handle_gesture(gesture):
scroll_by_page(True, opts.paged_taps_scroll_by_screen) scroll_by_page(True, opts.paged_taps_scroll_by_screen)
elif gesture.type is 'next-page': elif gesture.type is 'next-page':
scroll_by_page(False, opts.paged_taps_scroll_by_screen) 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 = { anchor_funcs = {

View File

@ -6,21 +6,34 @@ from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from book_list.theme import get_color from book_list.theme import get_color
from dom import svgicon from dom import clear, svgicon
from read_book.globals import ui_operations from read_book.globals import runtime, ui_operations
class SelectionBar: class SelectionBar:
def __init__(self, view): def __init__(self, view):
self.view = view self.view = view
def build_bar(self, notes):
c = self.container c = self.container
bar = E.div( max_width = 'min(50rem, 90vw)' if self.supports_css_min_max else '50rem'
style='position: absolute; height: 4ex; border: solid 1px currentColor; border-radius: 5px; overflow: hidden;' bar_container = E.div(
'display: flex; align-items: center; pointer-events: auto; padding: 5px; left: 0; top: 0;' style='position: absolute; border: solid 1px currentColor; border-radius: 5px;'
'background-color: {}'.format(get_color("window-background")) '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): def cb(icon, tooltip, callback):
ans = svgicon(icon, '3ex', '3ex', tooltip) 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('library', _('Lookup/search selected word'), self.lookup))
bar.appendChild(cb('highlight', _('Highlight selection'), self.create_highlight)) bar.appendChild(cb('highlight', _('Highlight selection'), self.create_highlight))
bar.appendChild(cb('close', _('Clear the selection'), self.clear_selection)) 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 @property
def container(self): def container(self):
@ -48,9 +67,8 @@ class SelectionBar:
self.container.style.display = 'none' self.container.style.display = 'none'
def show(self): def show(self):
if self.view.create_annotation.is_visible: if not self.view.create_annotation.is_visible:
return self.container.style.display = 'block'
self.container.style.display = 'block'
@property @property
def is_visible(self): def is_visible(self):
@ -69,11 +87,46 @@ class SelectionBar:
def create_highlight(self): def create_highlight(self):
self.view.initiate_create_annotation(True) 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): def update_position(self):
container = self.container
clear(container)
cs = self.view.currently_showing cs = self.view.currently_showing
if not cs.has_selection: if not cs.has_selection:
return self.hide() return self.hide()
if not cs.selection_start.onscreen and not cs.selection_end.onscreen:
return self.hide()
margins = { margins = {
'top': document.getElementById('book-top-margin').offsetHeight, 'top': document.getElementById('book-top-margin').offsetHeight,
'bottom': document.getElementById('book-bottom-margin').offsetHeight, 'bottom': document.getElementById('book-bottom-margin').offsetHeight,
@ -84,15 +137,11 @@ class SelectionBar:
def map_boundary(x): 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} 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: bar = self.build_bar(self.view.annotations_manager.notes_for_highlight(cs.selection_annot_id))
return self.hide()
start = map_boundary(cs.selection_start) start = map_boundary(cs.selection_start)
end = map_boundary(cs.selection_end) end = map_boundary(cs.selection_end)
self.show() self.show()
end_after_start = start.y < end.y or (start.y is end.y and start.x < end.x) 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 # vertical position
bar_height = bar.offsetHeight 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 = (end.y + end.height + buffer) if put_below else (end.y - bar_height - buffer)
top = max(buffer, min(top, container.offsetHeight - bar_height - buffer)) top = max(buffer, min(top, container.offsetHeight - bar_height - buffer))
bar.style.top = top + 'px' bar.style.top = top + 'px'
bar.style.flexDirection = 'column' if put_below else 'column-reverse'
# horizontal position # horizontal position
bar_width = bar.offsetWidth bar_width = bar.offsetWidth
left = end.x - bar_width // 2 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' bar.style.left = left + 'px'

View File

@ -13,9 +13,7 @@ from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id
from iframe_comm import IframeWrapper from iframe_comm import IframeWrapper
from modals import error_dialog, warning_dialog from modals import error_dialog, warning_dialog
from read_book.content_popup import ContentPopupOverlay from read_book.content_popup import ContentPopupOverlay
from read_book.create_annotation import ( from read_book.create_annotation import AnnotationsManager, CreateAnnotation
AnnotationsManager, CreateAnnotation, ViewAnnotation
)
from read_book.globals import ( from read_book.globals import (
current_book, runtime, set_current_spine_item, ui_operations 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%; 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', 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%; 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( E.div(
@ -301,7 +298,6 @@ class View:
self.book_scrollbar.apply_visibility() self.book_scrollbar.apply_visibility()
self.annotations_manager = AnnotationsManager(self) self.annotations_manager = AnnotationsManager(self)
self.create_annotation = CreateAnnotation(self) self.create_annotation = CreateAnnotation(self)
self.view_annotation = ViewAnnotation(self)
@property @property
def iframe(self): def iframe(self):
@ -526,6 +522,7 @@ class View:
self.currently_showing.has_selection = not data.empty self.currently_showing.has_selection = not data.empty
self.currently_showing.selection_start = data.selection_extents.start self.currently_showing.selection_start = data.selection_extents.start
self.currently_showing.selection_end = data.selection_extents.end self.currently_showing.selection_end = data.selection_extents.end
self.currently_showing.selection_annot_id = data.annot_id
if ui_operations.selection_changed: if ui_operations.selection_changed:
ui_operations.selection_changed(self.currently_showing.selected_text) ui_operations.selection_changed(self.currently_showing.selected_text)
self.selection_bar.update_position() self.selection_bar.update_position()
@ -610,7 +607,6 @@ class View:
self.content_popup_overlay.hide() self.content_popup_overlay.hide()
self.reference_mode_overlay.style.display = 'none' self.reference_mode_overlay.style.display = 'none'
self.create_annotation.hide() self.create_annotation.hide()
self.view_annotation.hide()
self.focus_iframe() self.focus_iframe()
def focus_iframe(self): def focus_iframe(self):
@ -1020,8 +1016,7 @@ class View:
def set_notes_for_highlight(self, uuid, notes): def set_notes_for_highlight(self, uuid, notes):
if self.annotations_manager.set_notes_for_highlight(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.selection_bar.update_position()
self.view_annotation.show(uuid)
def on_next_spine_item(self, data): def on_next_spine_item(self, data):
spine = self.book.manifest.spine spine = self.book.manifest.spine