mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-01-27 22:27:07 -05:00
774 lines
29 KiB
Plaintext
774 lines
29 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
|
|
from widgets import create_button
|
|
|
|
|
|
# TODO:
|
|
# Custom colors for highlights
|
|
# Google lookup for selections
|
|
# Export all annots as plain text/JSON
|
|
# Remove lookup and create highlight buttons from chrome
|
|
|
|
|
|
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.highlight)
|
|
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 uuid else None
|
|
if h:
|
|
return h.notes
|
|
|
|
def set_notes_for_highlight(self, uuid, notes):
|
|
h = self.highlights[uuid]
|
|
if h:
|
|
if notes:
|
|
h.notes = notes
|
|
else:
|
|
v'delete h.notes'
|
|
if ui_operations.highlights_changed:
|
|
ui_operations.highlights_changed(Object.values(self.highlights))
|
|
return True
|
|
return False
|
|
|
|
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 h in Object.values(self.highlights):
|
|
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'
|
|
builtin_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': builtin_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
|
|
DRAG_SCROLL_ZONE_MIN_HEIGHT = 10
|
|
|
|
|
|
def create_bar():
|
|
style = f'min-height: 1px; min-width: 1px; max-height: {BAR_SIZE}px; height: {BAR_SIZE}px'
|
|
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;',
|
|
E.div(style=style), E.div(style=style), E.div(style=style),
|
|
)
|
|
return ans
|
|
|
|
|
|
class EditNotesAndColors:
|
|
|
|
def __init__(self, container, hide_middle, accept, current_notes, current_style):
|
|
def separator():
|
|
return E.hr(style='max-width: 80em; width: 80vw; border-top: solid 1px; margin: auto; margin-top: 2ex; margin-bottom: 2ex')
|
|
|
|
def finish():
|
|
hide_middle()
|
|
accept()
|
|
|
|
def handle_keypress(ev):
|
|
ev.stopPropagation()
|
|
if ev.key is 'Escape':
|
|
hide_middle()
|
|
elif ev.key is 'Enter' and ev.ctrlKey:
|
|
finish()
|
|
|
|
c = E.div(
|
|
style=f'background: {get_color("window-background")}; margin: auto; padding: 1rem',
|
|
onclick=def(ev): ev.stopPropagation();,
|
|
id=unique_id(),
|
|
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=handle_keypress,
|
|
),
|
|
E.div(
|
|
style='margin: 1ex; font-size: smaller',
|
|
_('Double click or long tap on a highlight to see its notes')
|
|
),
|
|
|
|
separator(),
|
|
|
|
E.h3(_('Choose the color for this highlight'), style='margin-bottom: 2ex'),
|
|
E.div(
|
|
class_='color-block',
|
|
style=f'display: flex; flex-wrap: wrap; max-width: calc({BAR_SIZE}px * 8); margin: auto',
|
|
),
|
|
|
|
separator(),
|
|
|
|
E.div(
|
|
style='max-width: 80em; width: 80vw; margin: auto; display: flex; justify-content: space-between',
|
|
create_button(_('Adjust selection'), 'arrows-h', hide_middle, _('Accept changes and then adjust the selected text') + ' [Esc]'),
|
|
create_button(_('Finish'), 'check', finish, _('Finish editing highlight') + ' [Ctrl+Enter]', True),
|
|
)
|
|
|
|
)
|
|
self.container_id = c.id
|
|
seen_colors = {}
|
|
|
|
def add_color(bg):
|
|
if seen_colors[bg]:
|
|
return
|
|
seen_colors[bg] = True
|
|
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
|
|
)
|
|
if is_current:
|
|
item.classList.add('current-swatch')
|
|
item.dataset.bg = bg
|
|
c.getElementsByClassName('color-block')[0].appendChild(item)
|
|
|
|
custom_highlight_colors = get_session_data().get('custom_highlight_colors')
|
|
for bg in custom_highlight_colors:
|
|
add_color(bg)
|
|
for bg in builtin_highlight_colors:
|
|
add_color(bg)
|
|
if not c.querySelector('.current-swatch'):
|
|
add_color(current_style['background-color'])
|
|
|
|
container.appendChild(c)
|
|
self.notes_edit.focus()
|
|
|
|
@property
|
|
def container(self):
|
|
return document.getElementById(self.container_id)
|
|
|
|
@property
|
|
def notes_edit(self):
|
|
return self.container.getElementsByTagName('textarea')[0]
|
|
|
|
def change_color(self, evt):
|
|
evt.stopPropagation()
|
|
item = evt.currentTarget
|
|
for child in item.parentNode.childNodes:
|
|
child.style.backgroundColor = 'unset'
|
|
child.classList.remove('current-swatch')
|
|
item.style.backgroundColor = get_color('window-background2')
|
|
item.classList.add('current-swatch')
|
|
self.notes_edit.focus()
|
|
|
|
@property
|
|
def current_notes(self):
|
|
return self.notes_edit.value or ''
|
|
|
|
@property
|
|
def current_style(self):
|
|
bg = self.container.getElementsByClassName('current-swatch')[0].dataset.bg
|
|
custom_highlight_colors = get_session_data().get('custom_highlight_colors')
|
|
fg = custom_highlight_colors[bg] or builtin_highlight_colors[bg]
|
|
if not fg:
|
|
rgba = cached_color_to_rgba(bg)
|
|
is_dark = max(rgba[0], rgba[1], rgba[2]) < 115
|
|
fg = light_fg if is_dark else dark_fg
|
|
return {'background-color': bg, 'color': fg}
|
|
|
|
|
|
class CreateAnnotation:
|
|
|
|
container_id = 'create-annotation-overlay'
|
|
|
|
def __init__(self, view):
|
|
self.view = view
|
|
self.active_touch = None
|
|
self.drag_scroll_timer = None
|
|
self.last_drag_scroll_at = -100000
|
|
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(name, bar, icon, tt, action):
|
|
cb = svgicon(icon, bar.style.height, bar.style.height, tt)
|
|
cb.setAttribute('title', tt)
|
|
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.style.marginLeft = '0.5rem'
|
|
cb.style.marginRight = '0.5rem'
|
|
cb.classList.add('simple-link')
|
|
cb.classList.add(f'button-{name}')
|
|
cb.addEventListener('click', def(ev):
|
|
ev.preventDefault(), ev.stopPropagation()
|
|
action()
|
|
)
|
|
bar.appendChild(cb)
|
|
return cb
|
|
|
|
tb = create_bar()
|
|
container.appendChild(tb)
|
|
button('close', tb.firstChild, 'close', _('Cancel creation of highlight') + ' [Esc]', self.hide)
|
|
button('up', tb.firstChild.nextSibling, 'chevron-up', _('Scroll up') + ' [Up]', self.button_scroll.bind(None, True))
|
|
if ui_operations.copy_selection:
|
|
button('copy', tb.lastChild, 'copy', _('Copy to clipboard'), self.copy_to_clipboard)
|
|
button('finish', tb.lastChild, 'check', _('Finish creation of highlight') + ' [Enter]', 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('remove', bb.firstChild, 'trash', _('Remove this highlight'), self.delete_highlight)
|
|
button('down', bb.firstChild.nextSibling, 'chevron-down', _('Scroll down') + ' [Down]', self.button_scroll)
|
|
button('edit', bb.lastChild, 'pencil', _('Edit notes and change highlight color') + ' [e]', self.edit_notes_and_colors)
|
|
|
|
sd = get_session_data()
|
|
style = sd.get('highlight_style') or default_highlight_style()
|
|
if not style['background-color'] or not style['color']:
|
|
style = 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')
|
|
|
|
@property
|
|
def middle(self):
|
|
return document.getElementById(self.middle_id)
|
|
|
|
def edit_notes_and_colors(self):
|
|
if self.editor:
|
|
return
|
|
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)
|
|
current_style = self.current_highlight_style
|
|
self.show_middle(self.editing_done)
|
|
container = self.middle
|
|
clear(container)
|
|
self.editor = EditNotesAndColors(container, self.hide_middle, self.accept, current_notes, current_style)
|
|
|
|
def editing_done(self):
|
|
self.current_notes = self.editor.current_notes
|
|
new_highlight_style = self.editor.current_style
|
|
self.editor = None
|
|
if self.current_highlight_style['background-color'] is not new_highlight_style['background_color']:
|
|
self.current_highlight_style = new_highlight_style
|
|
self.send_message('set-highlight-style', style=self.current_highlight_style)
|
|
get_session_data().set('highlight_style', self.current_highlight_style)
|
|
self.update_handle_colors()
|
|
return True
|
|
|
|
def update_handle_colors(self):
|
|
fill = self.current_highlight_style['background-color']
|
|
stroke = self.view.current_color_scheme.foreground
|
|
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.temporarily_hide_handles()
|
|
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.pre_middle_close_callback = None
|
|
self.unhide_handles()
|
|
m.style.display = 'none'
|
|
self.container.focus()
|
|
|
|
def temporarily_hide_handles(self):
|
|
for h in (self.left_handle, self.right_handle):
|
|
if h.style.display is not 'none':
|
|
h.dataset.savedState = h.style.display
|
|
h.style.display = 'none'
|
|
|
|
def unhide_handles(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()
|
|
if ev.key is 'e' or ev.key is 'E':
|
|
return self.edit_notes_and_colors()
|
|
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'):
|
|
backwards = 'up' in sc_name
|
|
if 'page' in sc_name or not self.in_flow_mode:
|
|
self.paged_scroll(backwards)
|
|
else:
|
|
self.send_drag_scroll_message(backwards, 'left' if backwards else 'right', False)
|
|
elif sc_name in ('left', 'right'):
|
|
if self.in_flow_mode:
|
|
self.send_message('perp-scroll', backwards=bool(sc_name is 'left'))
|
|
else:
|
|
self.paged_scroll(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 button_scroll(self, backwards):
|
|
if self.in_flow_mode:
|
|
self.send_drag_scroll_message(backwards, 'left' if backwards else 'right', False)
|
|
else:
|
|
self.paged_scroll(backwards)
|
|
|
|
def paged_scroll(self, backwards):
|
|
self.send_message('paged-scroll', backwards=backwards)
|
|
|
|
def run_drag_scroll(self, mouse_y, top, bottom):
|
|
backwards = mouse_y <= top
|
|
self.do_one_drag_scroll(backwards, top - mouse_y if backwards else mouse_y - bottom)
|
|
|
|
def do_one_drag_scroll(self, backwards, distance_from_boundary):
|
|
window.clearTimeout(self.drag_scroll_timer)
|
|
self.drag_scroll_timer = None
|
|
if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT):
|
|
return
|
|
sd = get_session_data()
|
|
interval = 1000/sd.get('lines_per_sec_smooth') if self.in_flow_mode else 1200
|
|
self.drag_scroll_timer = window.setTimeout(self.do_one_drag_scroll.bind(None, backwards, distance_from_boundary), interval)
|
|
now = window.performance.now()
|
|
if now - self.last_drag_scroll_at > interval:
|
|
self.send_drag_scroll_message(backwards, 'left' if self.state is DRAGGING_LEFT else 'right', True)
|
|
self.last_drag_scroll_at = now
|
|
|
|
def send_drag_scroll_message(self, backwards, handle, extend_selection):
|
|
self.send_message(
|
|
'drag-scroll', backwards=backwards, handle=handle, extents=self.current_handle_position,
|
|
extend_selection=extend_selection)
|
|
|
|
def end_drag_scroll(self):
|
|
if self.drag_scroll_timer is not None:
|
|
window.clearTimeout(self.drag_scroll_timer)
|
|
self.drag_scroll_timer = None
|
|
self.last_drag_scroll_at = -10000
|
|
|
|
def mouseup_on_container(self, ev):
|
|
if self.state in (DRAGGING_RIGHT, DRAGGING_LEFT):
|
|
self.state = WAITING_FOR_DRAG
|
|
self.end_drag_scroll()
|
|
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
|
|
self.end_drag_scroll()
|
|
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)
|
|
c = self.container
|
|
rect = c.getBoundingClientRect()
|
|
t = document.getElementById('book-top-margin').offsetHeight
|
|
top = rect.top + max(t, DRAG_SCROLL_ZONE_MIN_HEIGHT)
|
|
t = document.getElementById('book-bottom-margin').offsetHeight
|
|
bottom = rect.bottom - max(t, DRAG_SCROLL_ZONE_MIN_HEIGHT)
|
|
if ev.clientY < top or ev.clientY > bottom:
|
|
self.run_drag_scroll(ev.clientY, top, bottom)
|
|
else:
|
|
self.end_drag_scroll()
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
def show(self):
|
|
self.middle.style.display = 'none'
|
|
c = self.container
|
|
c.style.display = 'flex'
|
|
c.focus()
|
|
self.update_handle_colors()
|
|
self.view.selection_bar.hide()
|
|
|
|
def hide(self):
|
|
if self.is_visible:
|
|
self.container.style.display = 'none'
|
|
self.view.focus_iframe()
|
|
self.send_message('set-highlight-style', style=None)
|
|
self.view.selection_bar.update_position()
|
|
|
|
def send_message(self, type, **kw):
|
|
self.view.iframe_wrapper.send_message('annotations', type=type, **kw)
|
|
|
|
def initiate_create_annotation(self, start_in_notes_edit):
|
|
self.send_message('create', start_in_notes_edit=v'!!start_in_notes_edit')
|
|
|
|
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 = msg.existing or None
|
|
self.current_notes = ''
|
|
if self.editing_annot_uuid:
|
|
self.current_notes = self.annotations_manager.notes_for_highlight(self.editing_annot_uuid) or ''
|
|
hs = self.annotations_manager.style_for_highlight(self.editing_annot_uuid)
|
|
if hs:
|
|
self.current_highlight_style = hs
|
|
get_session_data().set('highlight_style', self.current_highlight_style)
|
|
if not self.is_visible:
|
|
self.view.hide_overlays()
|
|
self.state = WAITING_FOR_CLICK
|
|
self.show()
|
|
self.hide_handles()
|
|
if msg.extents and not msg.extents.start.is_empty:
|
|
self.place_handles(msg.extents)
|
|
self.in_flow_mode = msg.in_flow_mode
|
|
self.send_message('set-highlight-style', style=self.current_highlight_style)
|
|
if msg.start_in_notes_edit:
|
|
self.edit_notes_and_colors()
|
|
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 'scrolled':
|
|
self.place_handles_after_scroll(msg.extents, msg.handle, msg.extended)
|
|
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')
|
|
)
|
|
self.annotations_manager.add_highlight(msg, self.current_highlight_style, self.current_notes)
|
|
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_single_handle(self, 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'
|
|
if handle.id is self.left_handle_id:
|
|
s.left = (data.x - width) + 'px'
|
|
else:
|
|
s.left = data.x + 'px'
|
|
|
|
def place_handles(self, extents):
|
|
self.place_single_handle(self.left_handle, extents.start)
|
|
self.place_single_handle(self.right_handle, extents.end)
|
|
self.state = WAITING_FOR_DRAG
|
|
self.left_line_height = extents.start.height
|
|
self.right_line_height = extents.end.height
|
|
|
|
def place_handles_after_scroll(self, extents, handle, extended):
|
|
if extended:
|
|
if handle is 'right':
|
|
h = self.left_handle
|
|
data = extents.start
|
|
else:
|
|
h = self.right_handle
|
|
data = extents.end
|
|
self.place_single_handle(h, data)
|
|
else:
|
|
self.place_single_handle(self.left_handle, extents.start)
|
|
self.place_single_handle(self.right_handle, extents.end)
|