diff --git a/src/pyj/range_utils.pyj b/src/pyj/range_utils.pyj index 2686a753ea..f521409550 100644 --- a/src/pyj/range_utils.pyj +++ b/src/pyj/range_utils.pyj @@ -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 diff --git a/src/pyj/read_book/create_annotation.pyj b/src/pyj/read_book/create_annotation.pyj index b37121060f..373aa45e62 100644 --- a/src/pyj/read_book/create_annotation.pyj +++ b/src/pyj/read_book/create_annotation.pyj @@ -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() -# }}} diff --git a/src/pyj/read_book/extract.pyj b/src/pyj/read_book/extract.pyj index 20cc26e100..5d3d0359b1 100644 --- a/src/pyj/read_book/extract.pyj +++ b/src/pyj/read_book/extract.pyj @@ -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 diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index f203f988f0..8d984db9ed 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -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 - ev.preventDefault(), ev.stopPropagation() - self.activate_annotation(annot_id_uuid_map[crw]) + if crw: + ev.preventDefault(), ev.stopPropagation() + select_crw(crw) def copy_selection(self): text = window.getSelection().toString() diff --git a/src/pyj/read_book/paged_mode.pyj b/src/pyj/read_book/paged_mode.pyj index 6ebd4a0eea..38065d2155 100644 --- a/src/pyj/read_book/paged_mode.pyj +++ b/src/pyj/read_book/paged_mode.pyj @@ -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 = { diff --git a/src/pyj/read_book/selection_bar.pyj b/src/pyj/read_book/selection_bar.pyj index 33f359b0c2..82a3dc4668 100644 --- a/src/pyj/read_book/selection_bar.pyj +++ b/src/pyj/read_book/selection_bar.pyj @@ -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,9 +67,8 @@ class SelectionBar: self.container.style.display = 'none' def show(self): - if self.view.create_annotation.is_visible: - return - self.container.style.display = 'block' + if not self.view.create_annotation.is_visible: + self.container.style.display = 'block' @property def is_visible(self): @@ -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' diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 6b34005097..00660fc3d5 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -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