mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-01-08 05:00:18 -05:00
400 lines
16 KiB
Plaintext
400 lines
16 KiB
Plaintext
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
|
from __python__ import hash_literals
|
|
|
|
from elementmaker import E
|
|
from gettext import gettext as _
|
|
|
|
from ajax import ajax, ajax_send
|
|
from book_list.theme import get_color, get_font_size
|
|
from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id
|
|
from popups import MODAL_Z_INDEX
|
|
from utils import safe_set_inner_html
|
|
from widgets import create_button
|
|
from book_list.globals import get_session_data
|
|
|
|
modal_container = None
|
|
modal_count = 0
|
|
|
|
add_extra_css(def():
|
|
style = build_rule(
|
|
'#modal-container > div > a:hover',
|
|
color=get_color('dialog-foreground') + ' !important',
|
|
background_color=get_color('dialog-background') + ' !important'
|
|
)
|
|
style += build_rule(
|
|
'.button-box', display='flex', justify_content='flex-end', padding='1rem 0rem', overflow='hidden'
|
|
)
|
|
style += build_rule('.button-box .calibre-push-button', transform_origin='right')
|
|
return style
|
|
)
|
|
|
|
class Modal:
|
|
|
|
def __init__(self, create_func, on_close, show_close, onkeydown):
|
|
nonlocal modal_count
|
|
self.create_func, self.on_close, self.show_close = create_func, on_close, show_close
|
|
self.onkeydown = onkeydown
|
|
modal_count += 1
|
|
self.id = modal_count
|
|
|
|
class ModalContainer:
|
|
|
|
def __init__(self):
|
|
div = E.div(id='modal-container', tabindex='0',
|
|
E.div( # popup
|
|
E.div(), # content area
|
|
E.a(svgicon('close'), title=_('Close'))
|
|
)
|
|
)
|
|
div.addEventListener('keydown', self.onkeydown.bind(self), {'passive': False})
|
|
document.body.appendChild(div)
|
|
|
|
# Container style
|
|
set_css(div,
|
|
position='fixed', top='0', right='0', bottom='0', left='0', # Stretch over entire window
|
|
background_color='rgba(0,0,0,0.8)', z_index=MODAL_Z_INDEX + '',
|
|
display='none', text_align='center', user_select='none'
|
|
)
|
|
|
|
# Popup style
|
|
set_css(div.firstChild,
|
|
position='relative', display='inline-block', top='50vh', transform='translateY(-50%)',
|
|
min_width='25vw', max_width='70vw', # Needed for iPhone 5
|
|
border_radius='1em', padding='1em 2em', margin_right='1em', margin_left='1em',
|
|
background=get_color('dialog-background'), color=get_color('dialog-foreground'),
|
|
background_image=get_color('dialog-background-image'), get_color('dialog-background'), get_color('dialog-background2')
|
|
)
|
|
|
|
# Close button style
|
|
set_css(div.firstChild.lastChild,
|
|
font_size='1.5em', line_height='100%', cursor='pointer', position='absolute',
|
|
right='-0.5em', top='-0.5em', width='1em', height='1em',
|
|
background_color=get_color('window-foreground'), color=get_color('window-background'), display='inline-box',
|
|
border_radius='50%', padding='4px', text_align='center', box_shadow='1px 1px 3px black'
|
|
)
|
|
div.firstChild.lastChild.addEventListener('click', def(event): event.preventDefault(), self.close_current_modal(event);)
|
|
|
|
# Content container style
|
|
set_css(div.firstChild.firstChild, user_select='text', max_height='60vh', overflow='auto')
|
|
|
|
self.modals = v'[]'
|
|
self.current_modal = None
|
|
self.hide = self.close_current_modal.bind(self)
|
|
|
|
@property
|
|
def modal_container(self):
|
|
return document.getElementById('modal-container')
|
|
|
|
def show_modal(self, create_func, on_close=None, show_close=True, onkeydown=None):
|
|
self.modals.push(Modal(create_func, on_close, show_close, onkeydown))
|
|
modal_id = self.modals[-1].id
|
|
self.update()
|
|
window.setTimeout(def(): self.modal_container.focus();, 0)
|
|
return modal_id
|
|
|
|
def hide_modal(self, modal_id):
|
|
if self.current_modal is not None and self.current_modal.id is modal_id:
|
|
self.clear_current_modal()
|
|
else:
|
|
doomed_modal = None
|
|
for i, modal in enumerate(self.modals):
|
|
if modal.id is modal_id:
|
|
doomed_modal = i
|
|
break
|
|
if doomed_modal is not None:
|
|
self.modals.splice(doomed_modal, 1)
|
|
|
|
def update(self):
|
|
if self.current_modal is None and self.modals:
|
|
self.current_modal = self.modals.shift()
|
|
c = self.modal_container
|
|
try:
|
|
self.current_modal.create_func(c.firstChild.firstChild, self.hide)
|
|
except:
|
|
self.current_modal = None
|
|
raise
|
|
if c.style.display is 'none':
|
|
set_css(c, display='block')
|
|
c.firstChild.lastChild.style.visibility = 'visible' if self.current_modal.show_close else 'hidden'
|
|
|
|
def clear_current_modal(self):
|
|
self.current_modal = None
|
|
c = self.modal_container
|
|
clear(c.firstChild.firstChild)
|
|
if self.modals.length is 0:
|
|
set_css(c, display='none')
|
|
else:
|
|
self.update()
|
|
|
|
def close_current_modal(self, event):
|
|
if self.current_modal is not None:
|
|
if self.current_modal.on_close is not None and self.current_modal.on_close(event) is True:
|
|
return
|
|
self.clear_current_modal()
|
|
|
|
def close_all_modals(self):
|
|
while self.current_modal is not None:
|
|
self.close_current_modal()
|
|
|
|
def onkeydown(self, event):
|
|
if self.current_modal is not None and self.current_modal.onkeydown:
|
|
return self.current_modal.onkeydown(event, self.clear_current_modal.bind(self))
|
|
if (event.key is 'Escape' or event.key is 'Esc') and not event.altKey and not event.ctrlKey and not event.metaKey and not event.shiftKey:
|
|
event.preventDefault(), event.stopPropagation()
|
|
self.close_current_modal(event)
|
|
|
|
|
|
def create_simple_dialog_markup(title, msg, details, icon, prefix, parent):
|
|
details = details or ''
|
|
show_details = E.a(class_='blue-link', style='padding-top:1em; display:inline-block; margin-left: auto', _('Show details'))
|
|
show_details.addEventListener('click', def():
|
|
show_details.style.display = 'none'
|
|
show_details.nextSibling.style.display = 'block'
|
|
)
|
|
is_html_msg = /<[a-zA-Z]/.test(msg)
|
|
html_container = E.div()
|
|
if is_html_msg:
|
|
safe_set_inner_html(html_container, msg)
|
|
details_container = E.span()
|
|
if /<[a-zA-Z]/.test(details):
|
|
safe_set_inner_html(details_container, details)
|
|
else:
|
|
details_container.textContent = details
|
|
if prefix:
|
|
prefix = E.span(' ' + prefix + ' ', style='white-space:pre; font-variant: small-caps')
|
|
else:
|
|
prefix = '\xa0'
|
|
parent.appendChild(
|
|
E.div(
|
|
style='max-width:40em; text-align: left',
|
|
E.h2(
|
|
E.span(svgicon(icon), style='color:red'), prefix, title,
|
|
style='font-weight: bold; font-size: ' + get_font_size('title')
|
|
),
|
|
E.div((html_container if is_html_msg else msg), style='padding-top: 1em; margin-top: 1em; border-top: 1px solid currentColor'),
|
|
E.div(style='display: ' + ('block' if details else 'none'),
|
|
show_details,
|
|
E.div(details_container,
|
|
style='display:none; white-space:pre-wrap; font-size: smaller; margin-top: 1em; border-top: solid 1px currentColor; padding-top: 1em'
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
def create_simple_dialog(title, msg, details, icon, prefix, on_close=None):
|
|
show_modal(create_simple_dialog_markup.bind(None, title, msg, details, icon, prefix), on_close=on_close)
|
|
|
|
|
|
def create_custom_dialog(title, content_generator_func, on_close=None, onkeydown=None):
|
|
def create_func(parent, close_modal):
|
|
content_div = E.div()
|
|
content_generator_func(content_div, close_modal)
|
|
parent.appendChild(
|
|
E.div(
|
|
style='max-width:60em; text-align: left',
|
|
E.h2(title, style='font-weight: bold; font-size: ' + get_font_size('title')),
|
|
E.div(content_div, style='padding-top: 1em; margin-top: 1em; border-top: 1px solid currentColor'),
|
|
))
|
|
show_modal(create_func, on_close=on_close, onkeydown=onkeydown)
|
|
|
|
|
|
def get_text_dialog(title, callback, initial_text=None, msg=None, rows=12):
|
|
called = {}
|
|
cid = unique_id()
|
|
|
|
def keyaction(ok, close_modal):
|
|
if called.done:
|
|
return
|
|
called.done = True
|
|
text = document.getElementById(cid).value or ''
|
|
if close_modal:
|
|
close_modal()
|
|
callback(ok, text)
|
|
|
|
def on_keydown(event, close_modal):
|
|
if event.altKey or event.ctrlKey or event.metaKey or event.shiftKey:
|
|
return
|
|
if event.key is 'Escape' or event.key is 'Esc':
|
|
event.preventDefault(), event.stopPropagation()
|
|
keyaction(False, close_modal)
|
|
|
|
create_custom_dialog(
|
|
title, def(parent, close_modal):
|
|
parent.appendChild(E.div(
|
|
E.textarea(initial_text or '', placeholder=msg or '', id=cid, rows=rows + '', style='min-width: min(40rem, 60vw)'),
|
|
E.div(class_='button-box',
|
|
create_button(_('OK'), 'check', keyaction.bind(None, True, close_modal), highlight=True),
|
|
'\xa0',
|
|
create_button(_('Cancel'), 'close', keyaction.bind(None, False, close_modal))
|
|
),
|
|
))
|
|
window.setTimeout(def(): parent.querySelector('textarea').focus();, 10)
|
|
,
|
|
on_close=keyaction.bind(None, False, None),
|
|
onkeydown=on_keydown
|
|
)
|
|
|
|
|
|
def question_dialog(
|
|
title, msg, callback, yes_text=None, no_text=None,
|
|
skip_dialog_name=None, skip_dialog_msg=None,
|
|
skip_dialog_skipped_value=True, skip_dialog_skip_precheck=True,
|
|
):
|
|
yes_text = yes_text or _('Yes')
|
|
no_text = no_text or _('No')
|
|
called = {}
|
|
|
|
def keyaction(yes, close_modal):
|
|
if called.done:
|
|
return
|
|
called.done = True
|
|
if skip_dialog_name:
|
|
if not skip_box.querySelector('input').checked:
|
|
sd = get_session_data()
|
|
skipped_dialogs = Object.assign(v'{}', sd.get('skipped_dialogs', v'{}'))
|
|
skipped_dialogs[skip_dialog_name] = Date().toISOString()
|
|
sd.set('skipped_dialogs', skipped_dialogs)
|
|
if close_modal:
|
|
close_modal()
|
|
callback(yes)
|
|
|
|
def on_keydown(event, close_modal):
|
|
if event.altKey or event.ctrlKey or event.metaKey or event.shiftKey:
|
|
return
|
|
if event.key is 'Escape' or event.key is 'Esc':
|
|
event.preventDefault(), event.stopPropagation()
|
|
keyaction(False, close_modal)
|
|
if event.key is 'Enter' or event.key is 'Return' or event.key is 'Space':
|
|
event.preventDefault(), event.stopPropagation()
|
|
keyaction(True, close_modal)
|
|
|
|
skip_box = E.div(style='margin-top: 2ex;')
|
|
if skip_dialog_name:
|
|
sd = get_session_data()
|
|
skipped_dialogs = sd.get('skipped_dialogs', v'{}')
|
|
if skipped_dialogs[skip_dialog_name]:
|
|
return callback(skip_dialog_skipped_value)
|
|
skip_dialog_msg = skip_dialog_msg or _('Show this confirmation again')
|
|
skip_box.appendChild(E.label(E.input(type='checkbox', name='skip_dialog'), '\xa0', skip_dialog_msg))
|
|
if skip_dialog_skip_precheck:
|
|
skip_box.querySelector('input').checked = True
|
|
else:
|
|
skip_box.style.display = 'none'
|
|
|
|
create_custom_dialog(
|
|
title, def(parent, close_modal):
|
|
parent.appendChild(E.div(
|
|
E.div(msg),
|
|
skip_box,
|
|
E.div(class_='button-box',
|
|
create_button(yes_text, 'check', keyaction.bind(None, True, close_modal), highlight=True),
|
|
'\xa0',
|
|
create_button(no_text, 'close', keyaction.bind(None, False, close_modal))
|
|
))
|
|
)
|
|
,
|
|
on_close=keyaction.bind(None, False, None),
|
|
onkeydown=on_keydown
|
|
)
|
|
|
|
|
|
|
|
def create_progress_dialog(msg, on_close):
|
|
msg = msg or _('Loading, please wait...')
|
|
pbar, msg_div = E.progress(style='display:inline-block'), E.div(msg, style='text-align:center; padding-top:1ex')
|
|
def create_func(parent):
|
|
parent.appendChild(E.div(style='text-align: center', pbar, msg_div))
|
|
show_close = on_close is not None
|
|
modal_id = show_modal(create_func, on_close, show_close)
|
|
return {
|
|
'close': def(): modal_container.hide_modal(modal_id);,
|
|
'update_progress': def(amount, total): pbar.max, pbar.value = total, amount;,
|
|
'set_msg': def(new_msg): safe_set_inner_html(msg_div, new_msg);,
|
|
}
|
|
|
|
# def test_progress():
|
|
# counter = 0
|
|
# pd = progress_dialog('Testing progress dialog, please wait...', def():
|
|
# nonlocal counter
|
|
# counter = 101
|
|
# console.log('pd canceled')
|
|
# )
|
|
# def update():
|
|
# nonlocal counter
|
|
# counter += 1
|
|
# pd.update_progress(counter, 100)
|
|
# if counter < 100:
|
|
# setTimeout(update, 150)
|
|
# else:
|
|
# pd.close()
|
|
# update()
|
|
#
|
|
# def test_error():
|
|
# error_dialog(
|
|
# 'Hello, world!',
|
|
# 'Some long text to test rendering/line breaking in error dialogs that contain lots of text like this test error dialog from hell in a handbasket with fruits and muppets making a scene.',
|
|
# ['a word ' for i in range(10000)].join(' ')
|
|
# )
|
|
|
|
def create_modal_container():
|
|
nonlocal modal_container
|
|
if modal_container is None:
|
|
modal_container = ModalContainer()
|
|
# window.setTimeout(def():
|
|
# document.getElementById('books-view-1').addEventListener('click', test_progress)
|
|
# , 10)
|
|
return modal_container
|
|
|
|
def show_modal(create_func, on_close=None, show_close=True, onkeydown=None):
|
|
return modal_container.show_modal(create_func, on_close, show_close, onkeydown)
|
|
|
|
|
|
def close_all_modals():
|
|
return modal_container.close_all_modals()
|
|
|
|
|
|
def error_dialog(title, msg, details=None, on_close=None):
|
|
create_simple_dialog(title, msg, details, 'bug', _('Error:'), on_close)
|
|
|
|
def warning_dialog(title, msg, details=None, on_close=None):
|
|
create_simple_dialog(title, msg, details, 'warning', _('Warning:'), on_close)
|
|
|
|
def progress_dialog(msg, on_close=None):
|
|
# Show a modal dialog with a progress bar and an optional close button.
|
|
# If the user clicks the close button, on_close is called and the dialog is closed.
|
|
# Returns an object with the methods: close(), update_progress(amount, total), set_msg()
|
|
# Call update_progress() to update the progress bar
|
|
# Call set_msg() to change the displayed message
|
|
# Call close() once the task being performed is finished
|
|
return create_progress_dialog(msg, on_close)
|
|
|
|
def ajax_progress_dialog(path, on_complete, msg, extra_data_for_callback=None, **kw):
|
|
pd = None
|
|
def on_complete_callback(event_type, xhr, ev):
|
|
nonlocal pd
|
|
pd.close()
|
|
pd = undefined
|
|
return on_complete(event_type, xhr, ev)
|
|
def on_progress_callback(loaded, total, xhr):
|
|
pd.update_progress(loaded, total)
|
|
xhr = ajax(path, on_complete_callback, on_progress=on_progress_callback, **kw)
|
|
xhr.extra_data_for_callback = extra_data_for_callback
|
|
pd = progress_dialog(msg, xhr.abort.bind(xhr))
|
|
xhr.send()
|
|
return xhr, pd
|
|
|
|
def ajax_send_progress_dialog(path, data, on_complete, msg, **kw):
|
|
pd = None
|
|
def on_complete_callback(event_type, xhr, ev):
|
|
nonlocal pd
|
|
pd.close()
|
|
pd = undefined
|
|
return on_complete(event_type, xhr, ev)
|
|
def on_progress_callback(loaded, total, xhr):
|
|
pd.update_progress(loaded, total)
|
|
xhr = ajax_send(path, data, on_complete_callback, on_progress=on_progress_callback, **kw)
|
|
pd = progress_dialog(msg, xhr.abort.bind(xhr))
|
|
return xhr, pd
|