mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-31 14:33:54 -04:00
Implement viewing of highlight notes by clicking on the highlight
This commit is contained in:
parent
be261dcd71
commit
807e5f0403
@ -63,7 +63,7 @@ def select_crw(crw):
|
|||||||
sel.addRange(r)
|
sel.addRange(r)
|
||||||
|
|
||||||
|
|
||||||
def create_wrapper_function(wrapper_elem, r, intersecting_wrappers):
|
def create_wrapper_function(wrapper_elem, r, intersecting_wrappers, process_wrapper):
|
||||||
start_node = r.startContainer
|
start_node = r.startContainer
|
||||||
end_node = r.endContainer
|
end_node = r.endContainer
|
||||||
start_offset = r.startOffset
|
start_offset = r.startOffset
|
||||||
@ -86,6 +86,8 @@ def create_wrapper_function(wrapper_elem, r, intersecting_wrappers):
|
|||||||
if crw:
|
if crw:
|
||||||
intersecting_wrappers[crw] = True
|
intersecting_wrappers[crw] = True
|
||||||
current_range.surroundContents(current_wrapper)
|
current_range.surroundContents(current_wrapper)
|
||||||
|
if process_wrapper:
|
||||||
|
process_wrapper(current_wrapper)
|
||||||
return current_wrapper
|
return current_wrapper
|
||||||
|
|
||||||
return wrap_node
|
return wrap_node
|
||||||
@ -94,9 +96,12 @@ def create_wrapper_function(wrapper_elem, r, intersecting_wrappers):
|
|||||||
wrapper_counter = 0
|
wrapper_counter = 0
|
||||||
|
|
||||||
|
|
||||||
def wrap_text_in_range(style, r):
|
def wrap_text_in_range(style, r, process_wrapper):
|
||||||
if not r:
|
if not r:
|
||||||
r = window.getSelection().getRangeAt(0)
|
sel = window.getSelection()
|
||||||
|
if not sel or not sel.rangeCount:
|
||||||
|
return None, v'[]'
|
||||||
|
r = sel.getRangeAt(0)
|
||||||
if r.isCollapsed:
|
if r.isCollapsed:
|
||||||
return None, v'[]'
|
return None, v'[]'
|
||||||
|
|
||||||
@ -106,7 +111,7 @@ def wrap_text_in_range(style, r):
|
|||||||
wrapper_elem.setAttribute('style', style)
|
wrapper_elem.setAttribute('style', style)
|
||||||
|
|
||||||
intersecting_wrappers = {}
|
intersecting_wrappers = {}
|
||||||
wrap_node = create_wrapper_function(wrapper_elem, r, intersecting_wrappers)
|
wrap_node = create_wrapper_function(wrapper_elem, r, intersecting_wrappers, process_wrapper)
|
||||||
text_nodes_in_range(r).map(wrap_node)
|
text_nodes_in_range(r).map(wrap_node)
|
||||||
crw = wrapper_elem.dataset.calibreRangeWrapper
|
crw = wrapper_elem.dataset.calibreRangeWrapper
|
||||||
v'delete intersecting_wrappers[crw]'
|
v'delete intersecting_wrappers[crw]'
|
||||||
|
@ -42,6 +42,14 @@ class AnnotationsManager:
|
|||||||
if h:
|
if h:
|
||||||
return h.notes
|
return h.notes
|
||||||
|
|
||||||
|
def style_for_highlight(self, uuid):
|
||||||
|
h = self.highlights[uuid]
|
||||||
|
if h:
|
||||||
|
return h.style
|
||||||
|
|
||||||
|
def data_for_highlight(self, uuid):
|
||||||
|
return self.highlights[uuid]
|
||||||
|
|
||||||
def add_highlight(self, msg, style, notes):
|
def add_highlight(self, msg, style, notes):
|
||||||
now = Date().toISOString()
|
now = Date().toISOString()
|
||||||
for uuid in msg.removed_highlights:
|
for uuid in msg.removed_highlights:
|
||||||
@ -95,6 +103,13 @@ highlight_colors = {
|
|||||||
default_highlight_color = '#fce2ae'
|
default_highlight_color = '#fce2ae'
|
||||||
|
|
||||||
|
|
||||||
|
def default_highlight_style():
|
||||||
|
return {
|
||||||
|
'background-color': default_highlight_color,
|
||||||
|
'color': highlight_colors[default_highlight_color]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def selection_handle(invert, style):
|
def selection_handle(invert, style):
|
||||||
ans = svgicon('selection-handle')
|
ans = svgicon('selection-handle')
|
||||||
use = ans.querySelector('use')
|
use = ans.querySelector('use')
|
||||||
@ -191,10 +206,7 @@ class CreateAnnotation:
|
|||||||
button(bb, 'pencil', _('Add a note'), self.add_notes)
|
button(bb, 'pencil', _('Add a note'), self.add_notes)
|
||||||
|
|
||||||
sd = get_session_data()
|
sd = get_session_data()
|
||||||
style = sd.get('highlight_style') or {
|
style = sd.get('highlight_style') or default_highlight_style()
|
||||||
'background-color': default_highlight_color,
|
|
||||||
'color': highlight_colors[default_highlight_color]
|
|
||||||
}
|
|
||||||
self.current_highlight_style = style
|
self.current_highlight_style = style
|
||||||
|
|
||||||
lh = selection_handle(False, style)
|
lh = selection_handle(False, style)
|
||||||
@ -496,6 +508,10 @@ class CreateAnnotation:
|
|||||||
def send_message(self, type, **kw):
|
def send_message(self, type, **kw):
|
||||||
self.view.iframe_wrapper.send_message('annotations', type=type, **kw)
|
self.view.iframe_wrapper.send_message('annotations', type=type, **kw)
|
||||||
|
|
||||||
|
def edit_highlight(self, uuid):
|
||||||
|
self.send_message('edit-highlight', uuid=uuid)
|
||||||
|
self.show()
|
||||||
|
|
||||||
def handle_message(self, msg):
|
def handle_message(self, msg):
|
||||||
if msg.type is 'create-annotation':
|
if msg.type is 'create-annotation':
|
||||||
self.editing_annot_uuid = None
|
self.editing_annot_uuid = None
|
||||||
@ -508,6 +524,7 @@ class CreateAnnotation:
|
|||||||
if msg.extents and msg.extents.start.x is not None:
|
if msg.extents and msg.extents.start.x is not None:
|
||||||
self.place_handles(msg.extents)
|
self.place_handles(msg.extents)
|
||||||
self.in_flow_mode = msg.in_flow_mode
|
self.in_flow_mode = msg.in_flow_mode
|
||||||
|
self.editing_annot_uuid = msg.existing or None
|
||||||
self.send_message('set-highlight-style', style=self.current_highlight_style)
|
self.send_message('set-highlight-style', style=self.current_highlight_style)
|
||||||
elif msg.type is 'position-handles':
|
elif msg.type is 'position-handles':
|
||||||
if self.state is WAITING_FOR_CLICK:
|
if self.state is WAITING_FOR_CLICK:
|
||||||
@ -533,6 +550,8 @@ 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)
|
||||||
|
|
||||||
@ -563,3 +582,88 @@ class CreateAnnotation:
|
|||||||
self.state = WAITING_FOR_DRAG
|
self.state = WAITING_FOR_DRAG
|
||||||
self.left_line_height = extents.start.height
|
self.left_line_height = extents.start.height
|
||||||
self.right_line_height = extents.end.height
|
self.right_line_height = extents.end.height
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def edit_current(self):
|
||||||
|
if self.showing_uuid:
|
||||||
|
self.view.create_annotation.edit_highlight(self.showing_uuid)
|
||||||
|
self.hide()
|
||||||
|
@ -12,7 +12,7 @@ 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, set_selection_to_highlight, unwrap_crw,
|
reset_highlight_counter, select_crw, set_selection_to_highlight, unwrap_crw,
|
||||||
wrap_text_in_range
|
wrap_text_in_range
|
||||||
)
|
)
|
||||||
from read_book.cfi import (
|
from read_book.cfi import (
|
||||||
@ -591,6 +591,12 @@ class IframeBoss:
|
|||||||
if refnum?:
|
if refnum?:
|
||||||
self.scroll_to_ref(refnum)
|
self.scroll_to_ref(refnum)
|
||||||
|
|
||||||
|
def ensure_selection_visible(self):
|
||||||
|
if current_layout_mode() is 'flow':
|
||||||
|
ensure_selection_visible()
|
||||||
|
else:
|
||||||
|
snap_to_selection()
|
||||||
|
|
||||||
def find(self, data, from_load):
|
def find(self, data, from_load):
|
||||||
if data.searched_in_spine:
|
if data.searched_in_spine:
|
||||||
window.getSelection().removeAllRanges()
|
window.getSelection().removeAllRanges()
|
||||||
@ -606,10 +612,7 @@ class IframeBoss:
|
|||||||
|
|
||||||
def show_search_result(self, data, from_load):
|
def show_search_result(self, data, from_load):
|
||||||
if select_search_result(data.search_result):
|
if select_search_result(data.search_result):
|
||||||
if current_layout_mode() is 'flow':
|
self.ensure_selection_visible()
|
||||||
ensure_selection_visible()
|
|
||||||
else:
|
|
||||||
snap_to_selection()
|
|
||||||
else:
|
else:
|
||||||
self.send_message('search_result_not_found', search_result=data.search_result)
|
self.send_message('search_result_not_found', search_result=data.search_result)
|
||||||
|
|
||||||
@ -623,7 +626,7 @@ class IframeBoss:
|
|||||||
else:
|
else:
|
||||||
end_reference_mode()
|
end_reference_mode()
|
||||||
|
|
||||||
def initiate_creation_of_annotation(self):
|
def initiate_creation_of_annotation(self, existing):
|
||||||
self.auto_scroll_action('stop')
|
self.auto_scroll_action('stop')
|
||||||
in_flow_mode = current_layout_mode() is 'flow'
|
in_flow_mode = current_layout_mode() is 'flow'
|
||||||
self.send_message(
|
self.send_message(
|
||||||
@ -631,6 +634,7 @@ class IframeBoss:
|
|||||||
type='create-annotation',
|
type='create-annotation',
|
||||||
in_flow_mode=in_flow_mode,
|
in_flow_mode=in_flow_mode,
|
||||||
extents=selection_extents(in_flow_mode),
|
extents=selection_extents(in_flow_mode),
|
||||||
|
existing=existing or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def annotations_msg_received(self, data):
|
def annotations_msg_received(self, data):
|
||||||
@ -657,13 +661,19 @@ class IframeBoss:
|
|||||||
self.send_message('annotations', type='update-handles', extents=selection_extents(in_flow_mode))
|
self.send_message('annotations', type='update-handles', extents=selection_extents(in_flow_mode))
|
||||||
elif data.type is 'set-highlight-style':
|
elif data.type is 'set-highlight-style':
|
||||||
set_selection_style(data.style)
|
set_selection_style(data.style)
|
||||||
|
elif data.type is 'edit-highlight':
|
||||||
|
crw_ = {v: k for k, v in Object.entries(self.annot_id_uuid_map)}[data.uuid]
|
||||||
|
if crw_:
|
||||||
|
select_crw(crw_)
|
||||||
|
self.ensure_selection_visible()
|
||||||
|
self.initiate_creation_of_annotation(data.uuid)
|
||||||
elif data.type is 'apply-highlight':
|
elif data.type is 'apply-highlight':
|
||||||
sel = window.getSelection()
|
sel = window.getSelection()
|
||||||
text = sel.toString()
|
text = sel.toString()
|
||||||
if not sel.rangeCount:
|
if not sel.rangeCount:
|
||||||
return
|
return
|
||||||
bounds = cfi_for_selection()
|
bounds = cfi_for_selection()
|
||||||
annot_id, intersecting_wrappers = wrap_text_in_range(data.style)
|
annot_id, intersecting_wrappers = wrap_text_in_range(data.style, None, self.add_highlight_listeners)
|
||||||
removed_highlights = v'[]'
|
removed_highlights = v'[]'
|
||||||
if annot_id is not None:
|
if annot_id is not None:
|
||||||
intersecting_uuids = [self.annot_id_uuid_map[x] for x in intersecting_wrappers]
|
intersecting_uuids = [self.annot_id_uuid_map[x] for x in intersecting_wrappers]
|
||||||
@ -704,13 +714,22 @@ class IframeBoss:
|
|||||||
if not r:
|
if not r:
|
||||||
continue
|
continue
|
||||||
style = f'color: {h.style.color}; background-color: {h.style["background-color"]}'
|
style = f'color: {h.style.color}; background-color: {h.style["background-color"]}'
|
||||||
annot_id, intersecting_wrappers = wrap_text_in_range(style, r)
|
annot_id, intersecting_wrappers = wrap_text_in_range(style, r, self.add_highlight_listeners)
|
||||||
if annot_id is not None:
|
if annot_id is not None:
|
||||||
self.annot_id_uuid_map[annot_id] = h.uuid
|
self.annot_id_uuid_map[annot_id] = h.uuid
|
||||||
for crw in intersecting_wrappers:
|
for crw in intersecting_wrappers:
|
||||||
unwrap_crw(crw)
|
unwrap_crw(crw)
|
||||||
v'delete self.annot_id_uuid_map[crw]'
|
v'delete self.annot_id_uuid_map[crw]'
|
||||||
|
|
||||||
|
def add_highlight_listeners(self, wrapper):
|
||||||
|
wrapper.addEventListener('click', self.highlight_wrapper_clicked)
|
||||||
|
|
||||||
|
def highlight_wrapper_clicked(self, ev):
|
||||||
|
crw = ev.currentTarget.dataset.calibreRangeWrapper
|
||||||
|
uuid = self.annot_id_uuid_map[crw]
|
||||||
|
if uuid:
|
||||||
|
self.send_message('annotations', type='annotation-activated', uuid=uuid)
|
||||||
|
|
||||||
def copy_selection(self):
|
def copy_selection(self):
|
||||||
text = window.getSelection().toString()
|
text = window.getSelection().toString()
|
||||||
if text:
|
if text:
|
||||||
|
@ -13,7 +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 AnnotationsManager, CreateAnnotation
|
from read_book.create_annotation import 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
|
||||||
)
|
)
|
||||||
@ -220,6 +220,7 @@ 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(
|
||||||
@ -285,6 +286,7 @@ 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):
|
||||||
@ -567,6 +569,7 @@ 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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user