From f85c5228c238b0a37062c12b848d75f79d577b49 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 2 Jul 2018 13:37:40 +0530 Subject: [PATCH] More work on the conversion setting UI for the server --- src/calibre/ebooks/conversion/plumber.py | 12 +- src/calibre/srv/convert.py | 3 +- src/pyj/book_list/conversion_widgets.pyj | 176 ++++++++++++++++++++++- src/pyj/book_list/convert_book.pyj | 36 ++++- 4 files changed, 221 insertions(+), 6 deletions(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 7a50146096..93cd51f58e 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -151,7 +151,7 @@ OptionRecommendation(name='base_font_size', help=_('The base font size in pts. All font sizes in the produced book ' 'will be rescaled based on this size. By choosing a larger ' 'size you can make the fonts in the output bigger and vice ' - 'versa. By default, the base font size is chosen based on ' + 'versa. By default, when the value is zero, the base font size is chosen based on ' 'the output profile you chose.' ) ), @@ -849,6 +849,16 @@ OptionRecommendation(name='search_replace', if help is not None: return help.replace('%default', str(rec.recommended_value)) + def get_all_help(self): + ans = {} + for group in (self.input_options, self.pipeline_options, + self.output_options, self.all_format_options): + for rec in group: + help = getattr(rec, 'help', None) + if help is not None: + ans[rec.option.name] = help + return ans + def merge_plugin_recs(self, plugin): for name, val, level in plugin.recommendations: rec = self.get_option_by_name(name) diff --git a/src/calibre/srv/convert.py b/src/calibre/srv/convert.py index e8b37a8517..22a7007c16 100644 --- a/src/calibre/srv/convert.py +++ b/src/calibre/srv/convert.py @@ -209,7 +209,7 @@ def get_conversion_options(input_fmt, output_fmt, book_id, db): from calibre.customize.conversion import OptionRecommendation plumber = create_dummy_plumber(input_fmt, output_fmt) specifics = load_specifics(db, book_id) - ans = {'options': {}, 'disabled': set(), 'defaults': {}} + ans = {'options': {}, 'disabled': set(), 'defaults': {}, 'help': {}} def merge_group(group_name, option_names): if not group_name or group_name in ('debug', 'metadata'): @@ -227,6 +227,7 @@ def get_conversion_options(input_fmt, output_fmt, book_id, db): ans['options'].update(defs['options']) ans['disabled'] |= set(defs['disabled']) ans['defaults'].update(defaults) + ans['help'] = plumber.get_all_help() for group_name, option_names in OPTIONS['pipe'].iteritems(): merge_group(group_name, option_names) diff --git a/src/pyj/book_list/conversion_widgets.pyj b/src/pyj/book_list/conversion_widgets.pyj index 77525e489a..693784b477 100644 --- a/src/pyj/book_list/conversion_widgets.pyj +++ b/src/pyj/book_list/conversion_widgets.pyj @@ -2,6 +2,178 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal from __python__ import bound_methods, hash_literals +from elementmaker import E +from gettext import gettext as _ -def create_option_group(group_name, container): - pass +from dom import ensure_id, add_extra_css, build_rule + +CLASS_NAME = 'conversion-option-group' + +add_extra_css(def(): + style = '' + sel = '.' + CLASS_NAME + ' ' + style += build_rule(sel + ' [data-option-name]', margin_bottom='1ex') + return style +) + + +# globals {{{ +container_id = None +get_option_value = get_option_default_value = set_option_value = is_option_disabled = get_option_help = None +entry_points = {} +registry = {} +listeners = {} + + +def ep(func): + entry_points[func.name] = func + return func + + +def add_listener(name, callback): + if not listeners[name]: + listeners[name] = v'[]' + listeners[name].push(callback) + + +def on_change(name): + if listeners[name]: + for callback in listeners[name]: + callback(name) + + +def sanitize_accelerator(text): + return text.replace('&', '') +# }}} + +def create_simple_widget(name, text, tooltip, input_widget_, getter, setter, reverse): # {{{ + if not text.endswith(':'): + text = text + ':' + div = E.div( + data_option_name=name, + title=tooltip or get_option_help(name), + ( + E.label(input_widget_, '\xa0' + sanitize_accelerator(text)) if reverse else + E.label(sanitize_accelerator(text) + '\xa0', input_widget_) + ) + ) + + def straight_input_widget(container): + return container.firstChild.lastChild + + def reverse_input_widget(container): + return container.firstChild.firstChild + + input_widget = reverse_input_widget if reverse else straight_input_widget + input_widget(div).addEventListener('change', on_change.bind(None, name)) + + ops = { + 'get': def (container): + return getter(input_widget(container)) + , + 'set': def (container, val): + setter(input_widget(container), val) + , + 'set_disabled': def (container, val): + if val: + container.classList.add('disabled') + input_widget(container).setAttribute('disabled', 'disabled') + else: + container.classList.remove('disabled') + input_widget(container).removeAttribute('disabled') + } + registry[name] = ops + ops.set(div, get_option_value(name)) + ops.set(div, get_option_value(name)) + if is_option_disabled(name): + ops.set_disabled(div, True) + return div +# }}} + +def checkbox(name, text, tooltip): # {{{ + return create_simple_widget(name, text, tooltip, E.input(type='checkbox'), + def getter(w): # noqa: unused-local + return bool(w.checked) + , + def setter(w, val): # noqa: unused-local + w.checked = bool(val) + , + True + ) +# }}} + +def lineedit(name, text, tooltip): # {{{ + return create_simple_widget(name, text, tooltip, E.input(type='text'), + def getter(w): # noqa: unused-local + ans = w.value + if ans and ans.strip(): + return ans.strip() + , + def setter(w, val): # noqa: unused-local + w.value = val or '' + ) +# }}} + +def float_spin(name, text, tooltip=None, step=0.1, min=0, max=100): # {{{ + f = E.input(type='number', step=str(step), min=str(min), max=str(max), required=True) + defval = get_option_default_value(name) + return create_simple_widget(name, text, tooltip, f, + def getter(w): # noqa: unused-local + try: + return float(w.value) + except: + return defval + , + def setter(w, val): # noqa: unused-local + w.value = str(float(val)) + ) +# }}} + +def container_for_option(name): + return document.getElementById(container_id).querySelector(f'[data-option-name="{name}"]') + + +def get(name): + return registry[name].get(container_for_option(name)) + + +def set(name, val): + registry[name].set(container_for_option(name), val) + + +def set_disabled(name, val): + registry[name].set_disabled(container_for_option(name), val) + + +# Look & feel {{{ +@ep +def look_and_feel(container): + def subhead(text): + container.appendChild(E.div( + style='border-bottom: solid 1px currentColor; margin-bottom: 1ex; max-width: 30em', E.b(sanitize_accelerator(text)))) + + subhead(_('&Fonts')) + add_listener('disable_font_rescaling', def (name): + disabled = get('disable_font_rescaling') + for dname in 'font_size_mapping', 'base_font_size': + set_disabled(dname, disabled) + ) + container.appendChild(checkbox('disable_font_rescaling', _('&Disable font size rescaling'))) + container.appendChild(float_spin('base_font_size', _('Base font si&ze:'), step=0.1, min=0, max=50)) + container.appendChild(lineedit('font_size_mapping', _('Font size &key:'))) +# }}} + + +def create_option_group(group_name, container, get_option_value_, get_option_default_value_, is_option_disabled_, get_option_help_): + nonlocal get_option_value, get_option_default_value, set_option_value, is_option_disabled, container_id, registry, listeners, get_option_help + get_option_value, get_option_default_value, is_option_disabled, get_option_help = get_option_value_, get_option_default_value_, is_option_disabled_, get_option_help_ + registry = {} + listeners = {} + container_id = ensure_id(container) + container.classList.add(CLASS_NAME) + entry_points[group_name](container) + + +def commit_changes(set_option_value): + for name in registry: + set_option_value(name, get(name)) diff --git a/src/pyj/book_list/convert_book.pyj b/src/pyj/book_list/convert_book.pyj index 6201c64223..24e02c16a2 100644 --- a/src/pyj/book_list/convert_book.pyj +++ b/src/pyj/book_list/convert_book.pyj @@ -7,7 +7,7 @@ from gettext import gettext as _ from ajax import ajax, ajax_send from book_list.book_details import report_load_failure -from book_list.conversion_widgets import create_option_group +from book_list.conversion_widgets import create_option_group, entry_points, commit_changes from book_list.library_data import download_url, load_status, url_books_query from book_list.router import back, open_book, report_a_load_failure from book_list.top_bar import create_top_bar, set_title @@ -240,6 +240,8 @@ def create_configuring_markup(): ans = E.li(E.a(class_='simple-link', href='javascript: void(0)')) ans.dataset.group = name ans.firstChild.addEventListener('click', show_group) + if not entry_points[name]: + ans.style.display = 'none' return ans GROUP_TITLES = { @@ -290,6 +292,32 @@ def create_configuring_markup(): return ans, initialize +def get_option_value(name, defval): + ans = conversion_data.conversion_options.options[name] + if ans is undefined: + ans = defval + return ans + + +def get_option_default_value(name, defval): + ans = conversion_data.conversion_options.defaults[name] + if ans is undefined: + ans = defval + return ans + + +def set_option_value(name, val): + conversion_data.conversion_options.options[name] = val + + +def is_option_disabled(name): + return conversion_data.disabled_map[name] is True + + +def get_option_help(name): + return conversion_data.conversion_options.help[name] or '' + + def create_configure_group_markup(): ans = E.div() @@ -300,7 +328,7 @@ def create_configure_group_markup(): _('Configuring {} settings').format(conversion_data.configuring_group_title))) panel = E.div() container.appendChild(panel) - create_option_group(conversion_data.configuring_group, container) + create_option_group(conversion_data.configuring_group, container, get_option_value, get_option_default_value, is_option_disabled, get_option_help) return ans, init @@ -320,6 +348,9 @@ def on_data_loaded(end_type, xhr, ev): if end_type is 'load': conversion_data = JSON.parse(xhr.responseText) + conversion_data.disabled_map = {} + for name in conversion_data.conversion_options.disabled: + conversion_data.disabled_map[name] = True elif end_type is 'abort': pass else: @@ -346,6 +377,7 @@ def fetch_conversion_data(book_id, input_fmt, output_fmt): def on_close(container_id): nonlocal current_state if current_state is 'configure-group': + commit_changes(set_option_value) current_state = 'configuring' apply_state_to_markup() return