calibre/src/pyj/read_book/create_annotation.pyj
2020-03-24 13:51:32 +05:30

417 lines
14 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 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