mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
417 lines
14 KiB
Plaintext
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
|