mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-29 16:20:20 -05:00
764 lines
28 KiB
Plaintext
764 lines
28 KiB
Plaintext
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
|
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 clear, ensure_id, svgicon, unique_id
|
|
from modals import error_dialog, question_dialog
|
|
from read_book.annotations import merge_annotation_maps
|
|
from read_book.globals import ui_operations
|
|
from read_book.shortcuts import shortcut_for_key_event
|
|
|
|
|
|
class AnnotationsManager:
|
|
|
|
def __init__(self, view):
|
|
self.view = view
|
|
self.set_highlights()
|
|
|
|
def set_highlights(self, highlights):
|
|
highlights = highlights or v'[]'
|
|
self.highlights = {h.uuid: h for h in highlights}
|
|
|
|
def merge_highlights(self, highlights):
|
|
highlights = highlights or v'[]'
|
|
updated = False
|
|
if highlights.length:
|
|
base = {'highlight': Object.values(self.highlights)}
|
|
newvals = {'highlight': highlights}
|
|
updated, ans = merge_annotation_maps(base, newvals)
|
|
if updated:
|
|
self.set_highlights(ans)
|
|
return updated
|
|
|
|
def remove_highlight(self, uuid):
|
|
h = self.highlights[uuid]
|
|
if h:
|
|
h.timestamp = Date().toISOString()
|
|
h.removed = True
|
|
v'delete h.style'
|
|
v'delete h.highlighted_text'
|
|
v'delete h.start_cfi'
|
|
v'delete h.end_cfi'
|
|
v'delete h.notes'
|
|
v'delete h.spine_name'
|
|
v'delete h.spine_index'
|
|
return True
|
|
|
|
def delete_highlight(self, uuid):
|
|
if self.remove_highlight(uuid):
|
|
if ui_operations.highlights_changed:
|
|
ui_operations.highlights_changed(Object.values(self.highlights))
|
|
|
|
def notes_for_highlight(self, uuid):
|
|
h = self.highlights[uuid]
|
|
if h:
|
|
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 spine_index_for_highlight(self, uuid, spine):
|
|
h = self.highlights[uuid]
|
|
if not h:
|
|
return -1
|
|
ans = h.spine_index
|
|
name = h.spine_name
|
|
if name:
|
|
idx = spine.indexOf(name)
|
|
if idx > -1:
|
|
ans = idx
|
|
return ans
|
|
|
|
def cfi_for_highlight(self, uuid, spine_index):
|
|
h = self.highlights[uuid]
|
|
if h:
|
|
x = 2 * (spine_index + 1)
|
|
return f'epubcfi(/{x}{h.start_cfi})'
|
|
|
|
def add_highlight(self, msg, style, notes):
|
|
now = Date().toISOString()
|
|
for uuid in msg.removed_highlights:
|
|
self.remove_highlight(uuid)
|
|
|
|
annot = self.highlights[msg.uuid] = {
|
|
'type': 'highlight',
|
|
'timestamp': now,
|
|
'uuid': msg.uuid,
|
|
'highlighted_text': msg.highlighted_text,
|
|
'start_cfi': msg.bounds.start,
|
|
'end_cfi': msg.bounds.end,
|
|
'style': style, # dict with color and background-color
|
|
'spine_name': self.view.currently_showing.name,
|
|
'spine_index': self.view.currently_showing.spine_index,
|
|
}
|
|
if notes:
|
|
annot.notes = notes
|
|
if ui_operations.highlights_changed:
|
|
ui_operations.highlights_changed(Object.values(self.highlights))
|
|
|
|
def highlights_for_currently_showing(self):
|
|
name = self.view.currently_showing.name
|
|
ans = v'[]'
|
|
for uuid in Object.keys(self.highlights):
|
|
h = self.highlights[uuid]
|
|
if h.spine_name is name and not h.removed and h.start_cfi:
|
|
ans.push(h)
|
|
return ans
|
|
|
|
|
|
WAITING_FOR_CLICK = 1
|
|
WAITING_FOR_DRAG = 2
|
|
DRAGGING_LEFT = 3
|
|
DRAGGING_RIGHT = 4
|
|
|
|
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 default_highlight_style():
|
|
return {
|
|
'background-color': default_highlight_color,
|
|
'color': highlight_colors[default_highlight_color]
|
|
}
|
|
|
|
|
|
def selection_handle(invert, style):
|
|
ans = svgicon('selection-handle')
|
|
use = ans.querySelector('use')
|
|
use.style.stroke = style['color']
|
|
use.style.fill = style['background-color']
|
|
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; max-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.active_touch = None
|
|
self.editing_annot_uuid = None
|
|
self.current_notes = ''
|
|
self.annotations_manager = self.view.annotations_manager
|
|
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.style.border = 'solid 2px currentColor'
|
|
cb.style.borderRadius = '4px'
|
|
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)
|
|
tb.appendChild(E.span(style=f'height: {tb.style.height}'))
|
|
button(tb.lastChild, 'trash', _('Remove this highlight'), self.delete_highlight)
|
|
tb.lastChild.appendChild(E.span('\xa0\xa0\xa0'))
|
|
if ui_operations.copy_selection:
|
|
button(tb.lastChild, 'copy', _('Copy to clipboard'), self.copy_to_clipboard)
|
|
tb.lastChild.appendChild(E.span('\xa0\xa0\xa0'))
|
|
button(tb.lastChild, 'check', _('Finish creation of highlight'), self.accept)
|
|
|
|
middle = E.div(id=unique_id('middle'), style='display: none; text-align: center; z-index: 90000')
|
|
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_notes)
|
|
|
|
sd = get_session_data()
|
|
style = sd.get('highlight_style') or default_highlight_style()
|
|
self.current_highlight_style = style
|
|
|
|
lh = selection_handle(False, style)
|
|
self.left_handle_id = ensure_id(lh, 'handle')
|
|
lh.addEventListener('mousedown', self.mousedown_on_handle, {'passive': False})
|
|
lh.addEventListener('touchstart', self.touchstart_on_handle, {'passive': False})
|
|
container.appendChild(lh)
|
|
rh = selection_handle(True, style)
|
|
self.right_handle_id = ensure_id(rh, 'handle')
|
|
rh.addEventListener('mousedown', self.mousedown_on_handle, {'passive': False})
|
|
rh.addEventListener('touchstart', self.touchstart_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('touchmove', self.touchmove_on_container, {'passive': False})
|
|
container.addEventListener('touchend', self.touchend_on_container, {'passive': False})
|
|
container.addEventListener('touchcancel', self.touchend_on_container, {'passive': False})
|
|
container.addEventListener('keydown', self.on_keydown, {'passive': False})
|
|
|
|
def copy_to_clipboard(self):
|
|
self.view.iframe_wrapper.send_message('copy_selection')
|
|
|
|
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 add_notes(self):
|
|
current_notes = self.current_notes
|
|
if not current_notes and self.editing_annot_uuid:
|
|
current_notes = self.annotations_manager.notes_for_highlight(self.editing_annot_uuid)
|
|
self.show_middle(self.apply_notes)
|
|
container = self.middle
|
|
clear(container)
|
|
c = E.div(
|
|
style=f'background: {get_color("window-background")}; margin: auto; padding: 1rem',
|
|
onclick=def(ev):
|
|
ev.stopPropagation(), ev.preventDefault()
|
|
self.hide_middle()
|
|
,
|
|
|
|
E.h3(_('Add notes for this highlight')),
|
|
E.textarea(
|
|
current_notes or '',
|
|
rows='10', spellcheck='true', style='resize: none; width: 80vw; max-width: 80em; margin: 1ex',
|
|
onkeydown=def(ev):
|
|
ev.stopPropagation()
|
|
if ev.key is 'Escape':
|
|
self.hide_middle()
|
|
,
|
|
onclick=def(ev):
|
|
ev.stopPropagation()
|
|
),
|
|
E.div(
|
|
style='display: flex; justify-content: space-between; margin-top: 1ex; align-items: center',
|
|
E.a(
|
|
svgicon('eraser', f'{BAR_SIZE}px', f'{BAR_SIZE}px'),
|
|
href='javascript:void',
|
|
class_='simple-link',
|
|
title=_('Clear'),
|
|
onclick=def(ev):
|
|
ev.preventDefault(), ev.stopPropagation()
|
|
ta = self.middle.querySelector('textarea')
|
|
ta.value = ''
|
|
ta.focus()
|
|
),
|
|
E.div(
|
|
_('To view the notes for a highlight, long-tap or double click on it.'),
|
|
style='font-size-smaller; margin-left: 1rem; margin-right: 1rem'
|
|
),
|
|
E.a(
|
|
svgicon('check', f'{BAR_SIZE}px', f'{BAR_SIZE}px'),
|
|
href='javascript:void',
|
|
class_='simple-link',
|
|
title=_('Done adding notes'),
|
|
onclick=def(ev):
|
|
ev.preventDefault(), ev.stopPropagation()
|
|
self.hide_middle()
|
|
),
|
|
),
|
|
)
|
|
container.appendChild(c)
|
|
c.querySelector('textarea').focus()
|
|
|
|
def apply_notes(self):
|
|
self.current_notes = self.middle.querySelector('textarea').value or ''
|
|
return True
|
|
|
|
def choose_color(self):
|
|
self.show_middle()
|
|
container = self.middle
|
|
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)
|
|
self.update_handle_colors()
|
|
|
|
def update_handle_colors(self):
|
|
fill = self.current_highlight_style['background-color']
|
|
stroke = self.current_highlight_style['color']
|
|
for handle in (self.left_handle, self.right_handle):
|
|
use = handle.querySelector('use')
|
|
use.style.stroke = stroke
|
|
use.style.fill = fill
|
|
|
|
def show_middle(self, pre_close_callback):
|
|
self.pre_middle_close_callback = pre_close_callback
|
|
self.save_handle_state()
|
|
self.middle.style.display = 'block'
|
|
|
|
def hide_middle(self):
|
|
m = self.middle
|
|
if m.style.display is not 'none':
|
|
if self.pre_middle_close_callback:
|
|
if not self.pre_middle_close_callback():
|
|
return
|
|
self.restore_handle_state()
|
|
m.style.display = 'none'
|
|
self.container.focus()
|
|
|
|
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(), existing=self.editing_annot_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 start_handle_drag(self, ev, q):
|
|
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 mousedown_on_handle(self, ev):
|
|
ev.stopPropagation(), ev.preventDefault()
|
|
if self.state is WAITING_FOR_CLICK:
|
|
return
|
|
self.start_handle_drag(ev, ev.currentTarget.id)
|
|
|
|
def touchstart_on_handle(self, ev):
|
|
ev.stopPropagation(), ev.preventDefault()
|
|
if self.state is WAITING_FOR_CLICK:
|
|
return
|
|
for touch in ev.changedTouches:
|
|
self.active_touch = touch.identifier
|
|
self.start_handle_drag(touch, ev.currentTarget.id)
|
|
break
|
|
|
|
def mouseup_on_container(self, ev):
|
|
if self.state in (DRAGGING_RIGHT, DRAGGING_LEFT):
|
|
self.state = WAITING_FOR_DRAG
|
|
ev.preventDefault(), ev.stopPropagation()
|
|
|
|
def touchend_on_container(self, ev):
|
|
if self.state in (DRAGGING_RIGHT, DRAGGING_LEFT):
|
|
ev.preventDefault(), ev.stopPropagation()
|
|
for touch in ev.changedTouches:
|
|
if touch.identifier is self.active_touch:
|
|
self.active_touch = None
|
|
self.state = WAITING_FOR_DRAG
|
|
return
|
|
|
|
def handle_moved(self, ev):
|
|
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)
|
|
|
|
def mousemove_on_container(self, ev):
|
|
if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT):
|
|
return
|
|
ev.stopPropagation(), ev.preventDefault()
|
|
self.handle_moved(ev)
|
|
|
|
def touchmove_on_container(self, ev):
|
|
if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT):
|
|
return
|
|
ev.stopPropagation(), ev.preventDefault()
|
|
for touch in ev.changedTouches:
|
|
if touch.identifier is self.active_touch:
|
|
self.handle_moved(touch)
|
|
return
|
|
|
|
@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):
|
|
self.middle.style.display = 'none'
|
|
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 edit_highlight(self, uuid):
|
|
self.send_message('edit-highlight', uuid=uuid)
|
|
self.show()
|
|
|
|
def remove_highlight(self, uuid):
|
|
self.send_message('remove-highlight', uuid=uuid)
|
|
self.annotations_manager.delete_highlight(uuid)
|
|
self.hide()
|
|
|
|
def delete_highlight(self):
|
|
uuid = self.editing_annot_uuid
|
|
question_dialog(_('Are you sure?'), _('Are you sure you want to delete this highlight permanently?'),
|
|
def (yes):
|
|
if yes:
|
|
self.remove_highlight(uuid) if uuid else self.hide()
|
|
)
|
|
|
|
def handle_message(self, msg):
|
|
if msg.type is 'create-annotation':
|
|
self.editing_annot_uuid = None
|
|
self.current_notes = ''
|
|
if not self.is_visible:
|
|
self.view.hide_overlays()
|
|
self.state = WAITING_FOR_CLICK
|
|
self.show()
|
|
self.hide_handles()
|
|
if msg.extents and msg.extents.start.x is not None:
|
|
self.place_handles(msg.extents)
|
|
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)
|
|
elif msg.type is 'position-handles':
|
|
if self.state is WAITING_FOR_CLICK:
|
|
self.place_handles(msg.extents)
|
|
self.editing_annot_uuid = msg.existing or None
|
|
if self.editing_annot_uuid:
|
|
self.current_notes = self.annotations_manager.notes_for_highlight(self.editing_annot_uuid) or ''
|
|
elif msg.type is 'update-handles':
|
|
self.place_handles(msg.extents)
|
|
if msg.from_scroll and not msg.selection_extended:
|
|
middle = map_from_iframe_coords({
|
|
'x': msg.page_rect.left + msg.page_rect.width // 2,
|
|
'y': msg.page_rect.top + msg.page_rect.height // 2
|
|
})
|
|
handle = self.left_handle if msg.backwards else self.right_handle
|
|
handle.style.display = 'block'
|
|
handle.style.left = f'{middle.x}px'
|
|
handle.style.top = f'{middle.y}px'
|
|
elif msg.type is 'highlight-applied':
|
|
if not msg.ok:
|
|
return error_dialog(
|
|
_('Highlighting failed'),
|
|
_('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)
|
|
|
|
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
|
|
|
|
|
|
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()
|