# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2020, Kovid Goyal from __python__ import bound_methods, hash_literals from elementmaker import E from gettext import gettext as _ from uuid import short_uuid from book_list.globals import get_session_data from book_list.theme import cached_color_to_rgba, get_color from dom import add_extra_css, clear, ensure_id, svgicon, unique_id from modals import error_dialog from read_book.shortcuts import shortcut_for_key_event WAITING_FOR_CLICK = 1 WAITING_FOR_DRAG = 2 DRAGGING_LEFT = 3 DRAGGING_RIGHT = 4 add_extra_css(def(): ans = '' ans += '.selection-handle { fill: #3cef3d; stroke: black }' ans += '.selection-handle:active { fill: #FCE883; }' return ans ) dark_fg = '#111' light_fg = '#eee' highlight_colors = { '#fce2ae': dark_fg, '#b6ffea': dark_fg, '#ffb3b3': dark_fg, '#ffdcf7': dark_fg, '#cae8d5': dark_fg, '#204051': light_fg, '#3b6978': light_fg, '#2b580c': light_fg, '#512b58': light_fg, } default_highlight_color = '#fce2ae' def selection_handle(invert): ans = svgicon('selection-handle') use = ans.querySelector('use') use.classList.add('selection-handle') s = ans.style if invert: s.transform = 'scaleX(-1)' s.position = 'absolute' s.boxSizing = 'border-box' s.touchAction = 'none' return ans def map_from_iframe_coords(point): l = document.getElementById('book-left-margin') point.x += l.offsetWidth t = document.getElementById('book-top-margin') point.y += t.offsetHeight return point def map_to_iframe_coords(point): l = document.getElementById('book-left-margin') point.x -= l.offsetWidth t = document.getElementById('book-top-margin') point.y -= t.offsetHeight return point BAR_SIZE = 32 def create_bar(): ans = E.div( id=unique_id('annot-bar'), style=f'height: {BAR_SIZE}px; width: 100vw; display: flex; justify-content: space-between;', ) return ans class CreateAnnotation: container_id = 'create-annotation-overlay' def __init__(self, view): self.view = view self.state = WAITING_FOR_CLICK self.left_line_height = self.right_line_height = 8 self.in_flow_mode = False container = self.container container.style.flexDirection = 'column' container.style.justifyContent = 'space-between' self.position_in_handle = {'x': 0, 'y': 0} def button(bar, icon, tt, action): cb = svgicon(icon, bar.style.height, bar.style.height, tt) document.createElement cb.setAttribute('title', tt) cb.classList.add('annot-button') cb.classList.add(f'annot-button-{icon}') cb.style.backgroundColor = get_color('window-background') cb.style.boxSizing = 'border-box' cb.style.padding = '2px' cb.classList.add('simple-link') cb.addEventListener('click', def(ev): ev.preventDefault(), ev.stopPropagation() action() ) bar.appendChild(cb) return cb tb = create_bar() container.appendChild(tb) button(tb, 'close', _('Cancel creation of highlight'), self.hide) button(tb, 'chevron-up', _('Scroll up'), self.scroll_up) button(tb, 'check', _('Finish creation of highlight'), self.accept) middle = E.div(id=unique_id('middle'), style='display: none') self.middle_id = middle.id container.appendChild(middle) bb = create_bar() container.appendChild(bb) button(bb, 'fg', _('Change highlight color'), self.choose_color) button(bb, 'chevron-down', _('Scroll down'), self.scroll_down) button(bb, 'pencil', _('Add a note'), self.add_text) lh = selection_handle(True) self.left_handle_id = ensure_id(lh, 'handle') lh.addEventListener('mousedown', self.mousedown_on_handle, {'passive': False}) container.appendChild(lh) rh = selection_handle(False) self.right_handle_id = ensure_id(rh, 'handle') rh.addEventListener('mousedown', self.mousedown_on_handle, {'passive': False}) container.appendChild(rh) container.addEventListener('click', self.container_clicked, {'passive': False}) container.addEventListener('mouseup', self.mouseup_on_container, {'passive': False}) container.addEventListener('mousemove', self.mousemove_on_container, {'passive': False}) container.addEventListener('keydown', self.on_keydown, {'passive': False}) sd = get_session_data() style = sd.get('highlight_style') or { 'background-color': default_highlight_color, 'color': highlight_colors[default_highlight_color] } self.current_highlight_style = style def scroll_up(self): self.send_message('scroll', backwards=True) def scroll_down(self): self.send_message('scroll', backwards=False) @property def middle(self): return document.getElementById(self.middle_id) def choose_color(self): container = self.middle container.style.display = 'block' container.style.textAlign = 'center' clear(container) c = E.div( E.h3(_('Choose highlight color')), E.div( style=f'display: flex; flex-wrap: wrap; max-width: calc({BAR_SIZE}px * 6); margin: auto', onclick=def(ev): ev.stopPropagation(), ev.preventDefault() ), onclick=def(ev): ev.stopPropagation(), ev.preventDefault() self.hide_middle() , style=f'background: {get_color("window-background")}; margin: auto; padding: 1rem', ) current_style = self.current_highlight_style container.appendChild(c) found_current = False self.save_handle_state() self.handle_state = self.left_handle.display, self.right_hand def add(bg): ic = svgicon('swatch', BAR_SIZE, BAR_SIZE) ic.classList.add('simple-link') is_current = bg.lower() is current_style['background-color'].lower() sqbg = get_color('window-background2') if is_current else 'unset' ic.querySelector('use').style.fill = bg item = E.div( ic, style=f'padding: 4px; background-color: {sqbg}; margin: 4px', onclick=self.change_color.bind(None, bg) ) c.lastChild.appendChild(item) return is_current for bg in highlight_colors: if add(bg): found_current = True if not found_current: add(current_style['background-color']) def change_color(self, new_color): self.hide_middle() c = self.middle c.style.display = 'none' current_style = self.current_highlight_style if not new_color or current_style['background-color'].lower() is new_color.lower(): return fg = highlight_colors[new_color] if not fg: rgba = cached_color_to_rgba(new_color) is_dark = max(rgba[0], rgba[1], rgba[2]) < 115 fg = light_fg if is_dark else dark_fg self.current_highlight_style = {'background-color': new_color, 'color': fg} self.send_message('set-highlight-style', style=self.current_highlight_style) sd = get_session_data() sd.set('highlight_style', self.current_highlight_style) def show_middle(self): self.save_handle_state() self.middle.style.display = 'block' def hide_middle(self): m = self.middle if m.style.display is not 'none': self.restore_handle_state() m.style.display = 'none' def save_handle_state(self): for h in (self.left_handle, self.right_handle): h.dataset.savedState = h.style.display h.style.display = 'none' def restore_handle_state(self): for h in (self.left_handle, self.right_handle): h.style.display = h.dataset.savedState def accept(self): s = self.current_highlight_style style = '' for k in Object.keys(self.current_highlight_style): style += f'{k}: {s[k]}; ' self.send_message( 'apply-highlight', style=style, uuid=short_uuid() ) self.hide() def on_keydown(self, ev): ev.stopPropagation(), ev.preventDefault() if ev.key is 'Enter': return self.accept() sc_name = shortcut_for_key_event(ev, self.view.keyboard_shortcut_map) if sc_name is 'show_chrome': self.hide() elif sc_name in ('up', 'down', 'pageup', 'pagedown'): self.send_message('scroll', backwards=bool('up' in sc_name)) elif sc_name in ('left', 'right'): if self.in_flow_mode: self.send_message('perp-scroll', backwards=bool(sc_name is 'left')) else: self.send_message('scroll', backwards=bool(sc_name is 'left')) def container_clicked(self, ev): ev.stopPropagation(), ev.preventDefault() self.hide_middle() if self.state is WAITING_FOR_CLICK: pt = map_to_iframe_coords({'x': ev.clientX, 'y': ev.clientY}) self.send_message('position-handles-at-point', x=pt.x, y=pt.y) def mousedown_on_handle(self, ev): ev.stopPropagation(), ev.preventDefault() if self.state is WAITING_FOR_CLICK: return q = ev.currentTarget.id if q is self.left_handle_id: self.state = DRAGGING_LEFT handle = self.left_handle elif q is self.right_handle_id: self.state = DRAGGING_RIGHT handle = self.right_handle r = handle.getBoundingClientRect() self.position_in_handle.x = Math.round(ev.clientX - r.left) self.position_in_handle.y = Math.round(ev.clientY - r.top) def mouseup_on_container(self, ev): if self.state in (DRAGGING_RIGHT, DRAGGING_LEFT): self.state = WAITING_FOR_DRAG ev.preventDefault(), ev.stopPropagation() def mousemove_on_container(self, ev): if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT): return ev.stopPropagation(), ev.preventDefault() handle = self.left_handle if self.state is DRAGGING_LEFT else self.right_handle s = handle.style s.left = (ev.clientX - self.position_in_handle.x) + 'px' s.top = (ev.clientY - self.position_in_handle.y) + 'px' pos = self.current_handle_position pos.start = map_to_iframe_coords(pos.start) pos.end = map_to_iframe_coords(pos.end) self.send_message('set-selection', extents=pos) @property def container(self): return document.getElementById(self.container_id) @property def left_handle(self): return document.getElementById(self.left_handle_id) @property def right_handle(self): return document.getElementById(self.right_handle_id) @property def is_visible(self): return self.container.style.display is not 'none' @property def current_handle_position(self): lh, rh = self.left_handle, self.right_handle lbr, rbr = self.left_handle.getBoundingClientRect(), self.right_handle.getBoundingClientRect() return { 'start': { 'onscreen': lh.style.display is not 'none', 'x': Math.round(lbr.right), 'y': Math.round(lbr.bottom - self.left_line_height // 2) }, 'end': { 'onscreen': rh.style.display is not 'none', 'x': Math.round(rbr.left), 'y': Math.round(rbr.bottom - self.right_line_height // 2) } } @property def current_highlight_style(self): return JSON.parse(self.container.querySelector('.annot-button-fg').dataset.style) @current_highlight_style.setter def current_highlight_style(self, val): b = self.container.querySelector('.annot-button-fg') b.dataset.style = JSON.stringify(val) def show(self): c = self.container c.style.display = 'flex' c.focus() def hide(self): if self.is_visible: self.container.style.display = 'none' self.view.focus_iframe() self.send_message('set-highlight-style', style=None) def send_message(self, type, **kw): self.view.iframe_wrapper.send_message('annotations', type=type, **kw) def handle_message(self, msg): if msg.type is 'create-annotation': if not self.is_visible: self.view.hide_overlays() self.state = WAITING_FOR_CLICK self.show() self.hide_handles() if msg.extents.start.x is not None: self.place_handles(msg.extents) self.in_flow_mode = msg.in_flow_mode self.send_message('set-highlight-style', style=self.current_highlight_style) elif msg.type is 'position-handles': if self.state is WAITING_FOR_CLICK: self.place_handles(msg.extents) elif msg.type is 'update-handles': self.place_handles(msg.extents) elif msg.type is 'highlight-applied': if not msg.ok: return error_dialog( _('Highlighting failed'), _('Failed to apply highlighting, try adjusting extent of highlight') ) else: print('Ignoring annotations message with unknown type:', msg.type) def hide_handles(self): self.left_handle.style.display = 'none' self.right_handle.style.display = 'none' def place_handles(self, extents): lh, rh = self.left_handle, self.right_handle def do_it(handle, data): map_from_iframe_coords(data) s = handle.style s.display = 'block' if data.onscreen else 'none' height = data.height * 3 width = data.height * 2 s.width = f'{width}px' s.height = f'{height}px' bottom = data.y + data.height top = bottom - height s.top = f'{top}px' return s, width style, width = do_it(lh, extents.start) style.left = (extents.start.x - width) + 'px' style, width = do_it(rh, extents.end) style.left = extents.end.x + 'px' self.state = WAITING_FOR_DRAG self.left_line_height = extents.start.height self.right_line_height = extents.end.height