Refactor panel architecture to use browser history

This means that the panels work with the browsers back and forward
actions
This commit is contained in:
Kovid Goyal 2015-12-21 09:01:20 +05:30
parent debb1a3968
commit ea9e907298
4 changed files with 94 additions and 90 deletions

View File

@ -9,13 +9,14 @@ from gettext import gettext as _
from widgets import get_widget_css from widgets import get_widget_css
from utils import parse_url_params from utils import parse_url_params
from book_list.globals import get_session_data from book_list.globals import get_session_data, set_boss
from book_list.theme import get_color from book_list.theme import get_color
from book_list.ui import UI from book_list.ui import UI
class Boss: class Boss:
def __init__(self, interface_data): def __init__(self, interface_data):
set_boss(self)
document.head.appendChild(E.style(get_widget_css())) document.head.appendChild(E.style(get_widget_css()))
set_css(document.body, background_color=get_color('window-background'), color=get_color('window-foreground')) set_css(document.body, background_color=get_color('window-background'), color=get_color('window-foreground'))
create_modal_container() create_modal_container()
@ -28,10 +29,20 @@ class Boss:
document.body.appendChild(div) document.body.appendChild(div)
self.ui = UI(interface_data, div) self.ui = UI(interface_data, div)
window.onerror = self.onerror.bind(self) window.onerror = self.onerror.bind(self)
self.history_count = 0
self.ui.apply_state() # Render the book list
data = parse_url_params()
if not data.mode or data.mode == 'book_list':
if data.panel != self.ui.current_panel:
self.ui.show_panel(data.panel, push_state=False)
setTimeout(def(): setTimeout(def():
window.onpopstate = self.onpopstate.bind(self) window.onpopstate = self.onpopstate.bind(self)
, 0) # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load , 0) # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load
@property
def has_history(self):
return self.history_count > 0
def update_window_title(self): def update_window_title(self):
document.title = 'calibre :: ' + self.current_library_name document.title = 'calibre :: ' + self.current_library_name
@ -50,10 +61,13 @@ class Boss:
def onpopstate(self, ev): def onpopstate(self, ev):
data = parse_url_params() data = parse_url_params()
mode = data.mode or 'book_list' mode = data.mode or 'book_list'
self.history_count -= 1
if mode == 'book_list': if mode == 'book_list':
search = data.search or '' search = data.search or ''
if data.panel != self.ui.current_panel:
self.ui.show_panel(data.panel, push_state=False)
if search != self.ui.books_view.interface_data.search_result.query: if search != self.ui.books_view.interface_data.search_result.query:
self.ui.books_view.change_search(search, push_state=False) self.ui.books_view.change_search(search, push_state=False, panel_to_show=data.panel)
def change_books(self, data): def change_books(self, data):
data.search_result.sort = str.split(data.search_result.sort, ',')[:2].join(',') data.search_result.sort = str.split(data.search_result.sort, ',')[:2].join(',')
@ -66,9 +80,12 @@ class Boss:
self.interface_data.search_result = data.search_result self.interface_data.search_result = data.search_result
self.ui.refresh_books_view() self.ui.refresh_books_view()
def push_state(self): def push_state(self, replace=False):
query = {} query = {}
if self.current_mode != 'book_list': if self.current_mode == 'book_list':
if self.ui.current_panel != self.ui.ROOT_PANEL:
query.panel = self.ui.current_panel
else:
query.current_mode = self.current_mode query.current_mode = self.current_mode
idata = self.interface_data idata = self.interface_data
if idata.library_id != idata.default_library: if idata.library_id != idata.default_library:
@ -77,4 +94,8 @@ class Boss:
if sq: if sq:
query.search = sq query.search = sq
query = encode_query(query) or '?' query = encode_query(query) or '?'
window.history.pushState(None, '', query) if replace:
window.history.replaceState(None, '', query)
else:
window.history.pushState(None, '', query)
self.history_count += 1

View File

@ -26,118 +26,103 @@ class ClosePanelBar(BarState):
class UIState: class UIState:
def __init__(self, top_bar_state=None, main_panel=None, panel_data=None, is_cacheable=True): def __init__(self, top_bar_state=None, main_panel=None, panel_data=None):
self.top_bar_state = top_bar_state self.top_bar_state = top_bar_state
self.main_panel = main_panel or get_boss().ui.items_view self.main_panel = main_panel
if type(self.main_panel) == 'string':
self.main_panel = getattr(get_boss().ui, self.main_panel)
self.panel_data = panel_data self.panel_data = panel_data
self.is_cacheable = is_cacheable
def add_button(self, **kw): def add_button(self, **kw):
self.top_bar_state.add_button(**kw) self.top_bar_state.add_button(**kw)
panels = {}
def panel(key):
ans = panels[key]
if not ans:
ans = create_panel[key]()
if ans.is_cacheable:
panels[key] = ans
return ans
def close_panel(): def close_panel():
get_boss().ui.close_panel() get_boss().ui.close_panel()
def replace_panel_action(replacement): def replace_panel_action(replacement):
return def(): return def():
get_boss().ui.replace_panel(panel(replacement)) get_boss().ui.replace_panel(replacement)
def show_panel_action(key): def show_panel_action(key):
return def(): return def():
get_boss().ui.show_panel(panel(key)) get_boss().ui.show_panel(key)
create_panel = {
'more-actions-menu': def more_actions_menu():
return UIState(ClosePanelBar(_('More actions')), panel_data=[
create_item(_('Book List Mode'), replace_panel_action('booklist-mode-menu'), _('Change how the list of books is displayed')),
])
,
'booklist-mode-menu': def booklist_mode_menu():
return UIState(ClosePanelBar(_('Book List Mode')), panel_data=[])
,
'booklist-sort-menu': def change_booklist_sort():
data = get_boss().ui.books_view.sort_panel_data(create_item)
return UIState(ClosePanelBar(_('Sort books')), panel_data=data, is_cacheable=False)
,
'booklist-search': def search_panel():
return UIState(ClosePanelBar(_('Search for books')), main_panel='search_panel')
,
}
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
class UI: class UI:
ROOT_PANEL = 'books'
def __init__(self, interface_data, book_list_container): def __init__(self, interface_data, book_list_container):
self.states, self.panels = [], []
self.top_bar = TopBar(book_list_container) self.top_bar = TopBar(book_list_container)
self.books_view = BooksView(interface_data, book_list_container) self.books_view = BooksView(interface_data, book_list_container)
self.items_view = ItemsView(interface_data, book_list_container) self.items_view = ItemsView(interface_data, book_list_container)
self.search_panel = SearchPanel(interface_data, book_list_container) self.search_panel = SearchPanel(interface_data, book_list_container)
ibs = BarState(run_animation=True, title=def():
q = self.books_view.interface_data['search_result']['query']
if q:
return {'title': _('Books matching') + ':: ' + q, 'title_tooltip':_('Click to clear this search'),
'title_action':def():
self.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'))
self.states.append(UIState(ibs, self.books_view))
self.apply_state(self.states[0])
ibs.left_state.run_animation = False
window.addEventListener('resize', debounce(self.on_resize.bind(self), 250))
self.panels = [self.books_view, self.items_view, self.search_panel] self.panels = [self.books_view, self.items_view, self.search_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
window.addEventListener('resize', debounce(self.on_resize.bind(self), 250))
self.panel_map['more-actions-menu'] = UIState(ClosePanelBar(_('More actions')), panel_data=[
create_item(_('Book List Mode'), replace_panel_action('booklist-mode-menu'), _('Change how the list of books is displayed')),
])
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-search'] = UIState(ClosePanelBar(_('Search for books')), main_panel=self.search_panel)
def on_resize(self): def on_resize(self):
pass pass
def apply_state(self, state): def apply_state(self):
state = self.panel_map[self.current_panel]
self.top_bar.apply_state(state.top_bar_state.left_state, state.top_bar_state.buttons) self.top_bar.apply_state(state.top_bar_state.left_state, state.top_bar_state.buttons)
if self.current_panel == self.ROOT_PANEL:
# only run the beating heart animation once
state.top_bar_state.left_state.run_animation = False
main_panel = state.main_panel or self.items_view
for panel in self.panels: for panel in self.panels:
panel.is_visible = panel is state.main_panel panel.is_visible = panel is main_panel
if callable(state.main_panel.init): if callable(main_panel.init):
state.main_panel.init(state.panel_data) panel_data = state.panel_data() if callable(state.panel_data) else state.panel_data
main_panel.init(panel_data)
def close_panel(self): def close_panel(self):
if len(self.states) > 1: if get_boss().has_history:
self.states.pop() window.history.back()
self.apply_state(self.states[-1]) else:
self.show_panel(self.ROOT_PANEL)
def close_all_panels(self): def replace_panel(self, panel_name, force=False):
if len(self.states) > 1: if force or panel_name != self.current_panel:
while len(self.states) > 1: self.current_panel = panel_name or self.ROOT_PANEL
self.states.pop() self.apply_state()
self.apply_state(self.states[-1]) get_boss().push_state(replace=True)
def replace_panel(self, state): def show_panel(self, panel_name, push_state=True, force=False):
if len(self.states) > 1: if force or panel_name != self.current_panel:
self.states.pop() self.current_panel = panel_name or self.ROOT_PANEL
self.states.append(state) self.apply_state()
self.apply_state(self.states[-1]) if push_state:
get_boss().push_state()
def show_panel(self, state):
self.states.append(state)
self.apply_state(self.states[-1])
def refresh_books_view(self): def refresh_books_view(self):
self.books_view.refresh() self.books_view.refresh()
if len(self.states) == 1: if self.current_panel == self.ROOT_PANEL:
self.top_bar.refresh_left() self.top_bar.refresh_left()

View File

@ -227,18 +227,18 @@ class BooksView:
boss.change_books(data) boss.change_books(data)
except Exception as err: except Exception as err:
return error_dialog(_('Could not change sort order'), err + '', details=err.stack) return error_dialog(_('Could not change sort order'), err + '', details=err.stack)
boss.ui.close_panel() boss.ui.show_panel(boss.ui.ROOT_PANEL)
window.scrollTo(0, 0) window.scrollTo(0, 0)
elif end_type != 'abort': elif end_type != 'abort':
error_dialog(_('Could not change sort order'), xhr.error_html) error_dialog(_('Could not change sort order'), xhr.error_html)
def change_search(self, query, push_state=True): def change_search(self, query, push_state=True, panel_to_show=None):
self.abort_get_more_books() self.abort_get_more_books()
query = query or '' query = query or ''
sd = get_session_data() sd = get_session_data()
data = {'search':query, 'sort':sd.get('sort'), 'library_id':self.interface_data.library_id} data = {'search':query, 'sort':sd.get('sort'), 'library_id':self.interface_data.library_id}
ajax_progress_dialog('interface-data/get-books', self.search_change_completed.bind(self), _( ajax_progress_dialog('interface-data/get-books', self.search_change_completed.bind(self), _(
'Fetching data from server, please wait') + '…', query=data, extra_data_for_callback={'push_state':push_state}) 'Fetching data from server, please wait') + '…', query=data, extra_data_for_callback={'push_state':push_state, 'panel_to_show': panel_to_show})
def search_change_completed(self, end_type, xhr, ev): def search_change_completed(self, end_type, xhr, ev):
if end_type == 'load': if end_type == 'load':
@ -248,10 +248,9 @@ class BooksView:
boss.change_books(data) boss.change_books(data)
except Exception as err: except Exception as err:
return error_dialog(_('Could not change search query'), err + '', details=err.stack) return error_dialog(_('Could not change search query'), err + '', details=err.stack)
boss.ui.close_all_panels()
if xhr.extra_data_for_callback.push_state:
boss.push_state()
self.update_fetching_status() self.update_fetching_status()
ed = xhr.extra_data_for_callback
boss.ui.show_panel(ed.panel_to_show or boss.ui.ROOT_PANEL, push_state=ed.push_state)
window.scrollTo(0, 0) window.scrollTo(0, 0)
elif end_type != 'abort': elif end_type != 'abort':
msg = xhr.error_html msg = xhr.error_html

View File

@ -8,7 +8,7 @@ from session import UserSessionData
from utils import parse_url_params from utils import parse_url_params
from book_list.boss import Boss from book_list.boss import Boss
from book_list.globals import set_boss, set_session_data from book_list.globals import set_session_data
def on_library_loaded(end_type, xhr, ev): def on_library_loaded(end_type, xhr, ev):
p = document.getElementById('page_load_progress') p = document.getElementById('page_load_progress')
@ -17,8 +17,7 @@ def on_library_loaded(end_type, xhr, ev):
interface_data = JSON.parse(xhr.responseText) interface_data = JSON.parse(xhr.responseText)
sd = UserSessionData(interface_data['username'], interface_data['user_session_data']) sd = UserSessionData(interface_data['username'], interface_data['user_session_data'])
set_session_data(sd) set_session_data(sd)
boss = Boss(interface_data) Boss(interface_data)
set_boss(boss)
else: else:
p = E.p(style='color:red; font-weight: bold; font-size:1.5em') p = E.p(style='color:red; font-weight: bold; font-size:1.5em')
if xhr.status == 401: if xhr.status == 401: