More work on refactoring the panel UI

This commit is contained in:
Kovid Goyal 2017-01-23 12:22:36 +05:30
parent 9a1cca9913
commit bf04a666e9
4 changed files with 104 additions and 333 deletions

View File

@ -0,0 +1,11 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import hash_literals, bound_methods
from book_list.ui import set_default_panel_handler
def init():
pass
set_default_panel_handler(init)

View File

@ -9,6 +9,7 @@ from dom import set_css, get_widget_css
from modals import create_modal_container, error_dialog
from session import get_interface_data, UserSessionData, update_interface_data, get_translations
from gettext import gettext as _, install
from popups import install_event_filters
from utils import parse_url_params
from book_list.constants import book_list_container_id, read_book_container_id
@ -36,6 +37,7 @@ def onerror(msg, script_url, line_number, column_number, error_object):
console.log('There was an error in the unhandled exception handler')
def init_ui():
install_event_filters()
set_default_mode_handler(book_list_mode_handler)
window.onerror = onerror
translations = get_translations()

View File

@ -26,55 +26,41 @@ add_extra_css(def():
return style
)
class TopBar:
def __init__(self, book_list_container):
nonlocal bar_counter
bar_counter += 1
self.current_left_data = {}
self.bar_id, self.dummy_bar_id = 'top-bar-' + bar_counter, 'dummy-top-bar-' + bar_counter
for bid in self.dummy_bar_id, self.bar_id:
def create_markup(container_id):
container = document.getElementById(container_id)
for i in range(2):
bar = E.div(
id=bid, class_=CLASS_NAME,
class_=CLASS_NAME,
E.div(style="white-space:nowrap; overflow:hidden; text-overflow: ellipsis; padding-left: 0.5em;"),
E.div(style="white-space:nowrap; text-align:right; padding-right: 0.5em;")
)
if bid is self.bar_id:
if i is 0:
set_css(bar, position='fixed', left='0', top='0', z_index='1')
set_css(bar,
width='100%', display='flex', flex_direction='row', flex_wrap='wrap', justify_content='space-between',
font_size=get_font_size('title'), user_select='none',
color=get_color('bar-foreground'), background_color=get_color('bar-background')
)
book_list_container.appendChild(bar)
container.appendChild(bar)
@property
def bar(self):
return document.getElementById(self.bar_id)
def get_bars(container_id):
container = document.getElementById(container_id)
return container.getElementsByClassName(CLASS_NAME)
@property
def dummy_bar(self):
return document.getElementById(self.dummy_bar_id)
def set_left(self, title='calibre', icon_name='heart', action=None, tooltip='', run_animation=False):
self.current_left_data = {'title':title, 'icon_name':icon_name, 'action':action, 'tooltip':tooltip}
def set_left_data(container_id, title='calibre', icon_name='heart', action=None, tooltip='', run_animation=False, title_action=None, title_tooltip=None):
bars = get_bars(container_id)
if icon_name is 'heart':
if not tooltip:
tooltip = _('Donate to support calibre development')
title_action = title_tooltip = None
if callable(title):
data = title()
title, title_action, title_tooltip = data.title, data.title_action, data.title_tooltip
for bar in self.bar, self.dummy_bar:
for i, bar in enumerate(bars):
left = bar.firstChild
clear(left)
title_elem = 'a' if callable(title_action) else 'span'
left.appendChild(E.a(title=tooltip, svgicon(icon_name)))
left.appendChild(E(title_elem, title, title=title_tooltip, class_='top-bar-title',
style='margin-left: {0}; font-weight: bold; padding-top: {1}; padding-bottom: {1}; vertical-align: middle'.format(SPACING, VSPACING)))
if bar is self.bar:
if i is 0:
a = left.firstChild
if icon_name is 'heart':
set_css(a,
@ -91,14 +77,19 @@ class TopBar:
a = a.nextSibling
a.addEventListener('click', def(event): event.preventDefault(), title_action();)
def refresh_left(self):
kw = self.current_left_data
self.set_left(**kw)
def set_title(container_id, text):
bars = get_bars(container_id)
for bar in bars:
bar.firstChild.firstChild.nextSibling.textContent = text
def add_button(self, icon_name=None, action=None, tooltip=''):
def create_top_bar(container_id):
create_markup(container_id)
def add_button(container_id, icon_name=None, action=None, tooltip=''):
if not icon_name:
return
for bar in self.bar, self.dummy_bar:
bars = get_bars(container_id)
for bar in bars:
right = bar.firstChild.nextSibling
right.appendChild(E.a(
style="margin-left: " + SPACING,
@ -109,21 +100,9 @@ class TopBar:
if action is not None:
right.lastChild.addEventListener('click', def(event): event.preventDefault(), action();)
def set_button_visibility(self, icon_name, visible):
for bar in self.bar, self.dummy_bar:
def set_button_visibility(container_id, icon_name, visible):
for bar in get_bars(container_id):
right = bar.firstChild.nextSibling
elem = right.querySelector('#{}-bar-icon-{}'.format(('top' if bar is self.bar else 'dummy'), icon_name))
if elem:
elem.style.display = 'inline-block' if visible else 'none'
def apply_state(self, left, buttons):
self.set_left(**left)
for bar in self.bar, self.dummy_bar:
right = bar.firstChild.nextSibling
clear(right)
for button in buttons:
self.add_button(**button)
def set_title(self, text):
for bar in self.bar, self.dummy_bar:
bar.firstChild.firstChild.nextSibling.textContent = text

View File

@ -1,251 +1,13 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import hash_literals
from __python__ import hash_literals, bound_methods
import traceback
from dom import ensure_id, clear
from elementmaker import E
from gettext import gettext as _
from modals import error_dialog, ajax_progress_dialog
from popups import install_event_filters
from book_list.constants import book_list_container_id
from book_list.globals import get_boss, get_session_data, get_current_query
from book_list.search import SearchPanel
from book_list.top_bar import TopBar
from book_list.views import BooksView
from book_list.item_list import ItemsView, create_item
from book_list.prefs import PrefsPanel
from book_list.book_details import BookDetailsPanel
from book_list.globals import get_current_query
class BarState:
def __init__(self, **kw):
self.left_state = kw
self.buttons = v'[]'
def add_button(self, **kw):
self.buttons.push(kw)
class ClosePanelBar(BarState):
def __init__(self, title, tooltip='', close_callback=None):
tooltip = tooltip or _('Close this panel')
def action():
close_panel()
if close_callback is not None:
close_callback()
BarState.__init__(self, title=title, tooltip=tooltip, action=action, icon_name='close')
class UIState:
def __init__(self, top_bar_state=None, main_panel=None, panel_data=None):
self.top_bar_state = top_bar_state
self.main_panel = main_panel
self.panel_data = panel_data
def add_button(self, **kw):
self.top_bar_state.add_button(**kw)
def close_panel():
get_boss().ui.close_panel()
def replace_panel_action(replacement):
return def():
get_boss().ui.replace_panel(replacement)
def show_panel_action(key):
return def():
get_boss().ui.show_panel(key)
def create_book_view_top_bar_state(books_view):
ibs = BarState(run_animation=True, title=def():
q = books_view.interface_data['search_result']['query']
if q:
return {'title': _('Books matching') + ':: ' + q, 'title_tooltip':_('Click to clear this search'),
'title_action':def():
books_view.change_search('')
}
return {'title': 'calibre'}
)
ibs.add_button(icon_name='sort-amount-desc', tooltip=_('Sort books'), action=show_panel_action('booklist-sort-menu'))
ibs.add_button(icon_name='search', tooltip=_('Search for books'), action=show_panel_action('booklist-search'))
ibs.add_button(icon_name='ellipsis-v', tooltip=_('More actions'), action=show_panel_action('more-actions-menu'))
return ibs
def random_book():
get_boss().ui.replace_panel('book-details', extra_query_data={'book-id':'0'})
def change_library_actions():
boss = get_boss()
interface_data = boss.interface_data
ans = []
ans.subtitle = _('Currently showing the library: ') + interface_data.library_map[interface_data.library_id]
for lid in sorted(interface_data.library_map):
if lid is not interface_data.library_id:
library_name = interface_data.library_map[lid]
ans.append({'title':library_name, 'action':boss.ui.change_library.bind(boss.ui, lid)})
return ans
class DevelopPanel:
# To use, go to URL:
# http://localhost:8080/?panel=develop-widgets&widget_module=<module name>
# Implement the develop(container) method in that module.
def __init__(self, interface_data, book_list_container):
c = E.div()
book_list_container.appendChild(c)
self.container_id = ensure_id(c)
@property
def container(self):
return document.getElementById(self.container_id)
@property
def is_visible(self):
self.container.style.display is 'block'
@is_visible.setter
def is_visible(self, val):
self.container.style.display = 'block' if val else 'none'
def init(self):
q = get_current_query()
m = q.widget_module
if m:
m = get_module(m)
if m?.develop:
m.develop(self.container)
else:
self.container.textContent = 'The module {} either does not exist or has no develop method.'.format(q.widget_module)
class UI:
ROOT_PANEL = 'books'
def __init__(self, interface_data, book_list_container):
install_event_filters()
self.top_bar = TopBar(book_list_container)
self.books_view = BooksView(interface_data, book_list_container)
self.items_view = ItemsView(interface_data, book_list_container)
self.prefs_panel = PrefsPanel(interface_data, book_list_container)
self.search_panel = SearchPanel(interface_data, book_list_container)
self.book_details_panel = BookDetailsPanel(interface_data, book_list_container)
self.develop_panel = DevelopPanel(interface_data, book_list_container)
self.panels = [self.books_view, self.items_view, self.search_panel, self.prefs_panel, self.book_details_panel]
self.panel_map = {self.ROOT_PANEL: UIState(create_book_view_top_bar_state(self.books_view), main_panel=self.books_view)}
self.current_panel = self.ROOT_PANEL
num_of_libraries = len(interface_data.library_map)
actions = [
create_item(_('Book List Mode'), replace_panel_action('booklist-mode-menu'), _('Change how the list of books is displayed')),
create_item(_('A Random Book'), random_book, _('Choose a random book from your library')),
]
if num_of_libraries > 1:
actions.push(
create_item(_('Change Library'), replace_panel_action('booklist-change-library'), _('Choose a different library to display'))
)
self.panel_map['booklist-change-library'] = UIState(ClosePanelBar(_('Change Library')), panel_data=change_library_actions)
self.panel_map['more-actions-menu'] = UIState(ClosePanelBar(_('More actions')), panel_data=actions)
self.panel_map['booklist-mode-menu'] = UIState(ClosePanelBar(_('Book List Mode')), panel_data=[])
self.panel_map['booklist-sort-menu'] = UIState(ClosePanelBar(_('Sort books')), panel_data=def():
return self.books_view.sort_panel_data(create_item)
)
self.panel_map['booklist-config-tb'] = self.create_prefences_panel(
_('Configure Tag Browser'), close_callback=self.search_panel.apply_prefs.bind(self.search_panel),
panel_data=self.search_panel.get_prefs.bind(self.search_panel))
self.panel_map['develop-widgets'] = UIState(ClosePanelBar('Develop widgets'), main_panel=self.develop_panel)
bss = ClosePanelBar(_('Search for books'))
bss.add_button(icon_name='cogs', tooltip=_('Configure Tag Browser'), action=show_panel_action('booklist-config-tb'))
self.panel_map['booklist-search'] = UIState(bss, main_panel=self.search_panel)
self.panel_map['book-details'] = bd = UIState(ClosePanelBar(_('Book Details')), main_panel=self.book_details_panel)
bd.add_button(icon_name='random', tooltip=_('Pick another random book'), action=self.book_details_panel.show_random.bind(self.book_details_panel))
# bd.add_button(icon_name='book', tooltip=_('Read this book'), action=self.book_details_panel.read_book.bind(self.book_details_panel))
# bd.add_button(icon_name='cloud-download', tooltip=_('Download this book'), action=self.book_details_panel.download_book.bind(self.book_details_panel))
def create_prefences_panel(self, title, close_callback=None, panel_data=None):
ans = UIState(ClosePanelBar(title), close_callback=close_callback, main_panel=self.prefs_panel, panel_data=panel_data)
ans.add_button(icon_name='refresh', tooltip=_('Restore default settings'), action=self.prefs_panel.reset_to_defaults.bind(self.prefs_panel))
return ans
def set_title(self, text):
self.top_bar.set_title(text)
def set_button_visibility(self, icon_name, visible):
self.top_bar.set_button_visibility(icon_name, visible)
def on_resize(self):
pass
def apply_state(self):
window.scrollTo(0, 0)
state = self.panel_map[self.current_panel]
self.top_bar.apply_state(state.top_bar_state.left_state, state.top_bar_state.buttons)
main_panel = state.main_panel or self.items_view
for panel in self.panels:
panel.is_visible = panel is main_panel
if callable(main_panel.init):
panel_data = state.panel_data() if callable(state.panel_data) else state.panel_data
main_panel.init(panel_data)
if self.current_panel is self.ROOT_PANEL:
# only run the beating heart animation once
window.setTimeout(def(): state.top_bar_state.left_state.run_animation = False;, 10)
def close_panel(self):
if get_boss().has_history:
window.history.back()
else:
self.show_panel(self.ROOT_PANEL)
def replace_panel(self, panel_name, force=False, extra_query_data=None):
action_needed = force or panel_name is not self.current_panel
if action_needed:
self.current_panel = panel_name or self.ROOT_PANEL
get_boss().push_state(replace=True, extra_query_data=extra_query_data)
if action_needed:
self.apply_state()
def show_panel(self, panel_name, push_state=True, force=False, extra_query_data=None):
action_needed = force or panel_name is not self.current_panel
if action_needed:
self.current_panel = panel_name or self.ROOT_PANEL
if push_state:
get_boss().push_state(extra_query_data=extra_query_data)
if action_needed:
self.apply_state()
def refresh_books_view(self):
self.books_view.refresh()
if self.current_panel is self.ROOT_PANEL:
self.top_bar.refresh_left()
def change_library(self, library_id):
data = {'search':'', 'sort':get_session_data().get_library_option(library_id, 'sort'), 'library_id':library_id}
ajax_progress_dialog('interface-data/get-books', self.library_changed.bind(self), _(
'Fetching data from server, please wait') + '…', query=data, extra_data_for_callback={'library_id':library_id})
def library_changed(self, end_type, xhr, ev):
if end_type is 'load':
boss = get_boss()
boss.interface_data.library_id = xhr.extra_data_for_callback.library_id
try:
data = JSON.parse(xhr.responseText)
boss.change_books(data)
except Exception as err:
return error_dialog(_('Could not change library'), err + '', details=traceback.format_exc())
self.show_panel(self.ROOT_PANEL)
window.scrollTo(0, 0)
elif end_type is not 'abort':
msg = xhr.error_html
error_dialog(_('Could not change library'), msg)
panel_handlers = {}
default_panel_handler = None
@ -260,6 +22,21 @@ def set_default_panel_handler(handler):
default_panel_handler = handler
def develop_panel(container):
# To use, go to URL:
# http://localhost:8080/?panel=develop-widgets&widget_module=<module name>
# Implement the develop(container) method in that module.
q = get_current_query()
m = q.widget_module
if m:
m = get_module(m)
if m?.develop:
m.develop(container)
else:
container.textContent = 'The module {} either does not exist or has no develop method.'.format(q.widget_module)
set_panel_handler('develop-widgets', develop_panel)
def apply_url_state(state):
panel = state.panel or 'home'
@ -269,3 +46,5 @@ def apply_url_state(state):
clear(c)
c.appendChild(E.div()), c.appendChild(E.div(style='display:none'))
c.dataset.panel = panel
handler = panel_handlers[panel] or default_panel_handler
handler(ensure_id(c.firstChild, 'panel'))