From ea9e907298dc4f2928bcc6807f5f5a404834c4c0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 21 Dec 2015 09:01:20 +0530 Subject: [PATCH] Refactor panel architecture to use browser history This means that the panels work with the browsers back and forward actions --- src/pyj/book_list/boss.pyj | 31 ++++++-- src/pyj/book_list/ui.pyj | 137 ++++++++++++++++-------------------- src/pyj/book_list/views.pyj | 11 ++- src/pyj/srv.pyj | 5 +- 4 files changed, 94 insertions(+), 90 deletions(-) diff --git a/src/pyj/book_list/boss.pyj b/src/pyj/book_list/boss.pyj index 54f14a5d10..a9ba415e74 100644 --- a/src/pyj/book_list/boss.pyj +++ b/src/pyj/book_list/boss.pyj @@ -9,13 +9,14 @@ from gettext import gettext as _ from widgets import get_widget_css 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.ui import UI class Boss: def __init__(self, interface_data): + set_boss(self) document.head.appendChild(E.style(get_widget_css())) set_css(document.body, background_color=get_color('window-background'), color=get_color('window-foreground')) create_modal_container() @@ -28,10 +29,20 @@ class Boss: document.body.appendChild(div) self.ui = UI(interface_data, div) 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(): 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 + @property + def has_history(self): + return self.history_count > 0 + def update_window_title(self): document.title = 'calibre :: ' + self.current_library_name @@ -50,10 +61,13 @@ class Boss: def onpopstate(self, ev): data = parse_url_params() mode = data.mode or 'book_list' + self.history_count -= 1 if mode == 'book_list': 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: - 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): 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.ui.refresh_books_view() - def push_state(self): + def push_state(self, replace=False): 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 idata = self.interface_data if idata.library_id != idata.default_library: @@ -77,4 +94,8 @@ class Boss: if sq: query.search = sq 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 diff --git a/src/pyj/book_list/ui.pyj b/src/pyj/book_list/ui.pyj index 099e40aaae..3c0e7f4d67 100644 --- a/src/pyj/book_list/ui.pyj +++ b/src/pyj/book_list/ui.pyj @@ -26,118 +26,103 @@ class ClosePanelBar(BarState): 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.main_panel = main_panel or get_boss().ui.items_view - if type(self.main_panel) == 'string': - self.main_panel = getattr(get_boss().ui, self.main_panel) + self.main_panel = main_panel self.panel_data = panel_data - self.is_cacheable = is_cacheable def add_button(self, **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(): get_boss().ui.close_panel() def replace_panel_action(replacement): return def(): - get_boss().ui.replace_panel(panel(replacement)) + get_boss().ui.replace_panel(replacement) def show_panel_action(key): 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: + ROOT_PANEL = 'books' + def __init__(self, interface_data, book_list_container): - self.states, self.panels = [], [] 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.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.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): 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) + 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: - panel.is_visible = panel is state.main_panel - if callable(state.main_panel.init): - state.main_panel.init(state.panel_data) + 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) def close_panel(self): - if len(self.states) > 1: - self.states.pop() - self.apply_state(self.states[-1]) + if get_boss().has_history: + window.history.back() + else: + self.show_panel(self.ROOT_PANEL) - def close_all_panels(self): - if len(self.states) > 1: - while len(self.states) > 1: - self.states.pop() - self.apply_state(self.states[-1]) + def replace_panel(self, panel_name, force=False): + if force or panel_name != self.current_panel: + self.current_panel = panel_name or self.ROOT_PANEL + self.apply_state() + get_boss().push_state(replace=True) - def replace_panel(self, state): - if len(self.states) > 1: - self.states.pop() - self.states.append(state) - self.apply_state(self.states[-1]) - - def show_panel(self, state): - self.states.append(state) - self.apply_state(self.states[-1]) + def show_panel(self, panel_name, push_state=True, force=False): + if force or panel_name != self.current_panel: + self.current_panel = panel_name or self.ROOT_PANEL + self.apply_state() + if push_state: + get_boss().push_state() def refresh_books_view(self): self.books_view.refresh() - if len(self.states) == 1: + if self.current_panel == self.ROOT_PANEL: self.top_bar.refresh_left() diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj index ccfb577479..2d79dd7b6a 100644 --- a/src/pyj/book_list/views.pyj +++ b/src/pyj/book_list/views.pyj @@ -227,18 +227,18 @@ class BooksView: boss.change_books(data) except Exception as err: 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) elif end_type != 'abort': 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() query = query or '' sd = get_session_data() 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), _( - '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): if end_type == 'load': @@ -248,10 +248,9 @@ class BooksView: boss.change_books(data) except Exception as err: 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() + 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) elif end_type != 'abort': msg = xhr.error_html diff --git a/src/pyj/srv.pyj b/src/pyj/srv.pyj index 2053ef60fe..d4e1ccdd8c 100644 --- a/src/pyj/srv.pyj +++ b/src/pyj/srv.pyj @@ -8,7 +8,7 @@ from session import UserSessionData from utils import parse_url_params 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): p = document.getElementById('page_load_progress') @@ -17,8 +17,7 @@ def on_library_loaded(end_type, xhr, ev): interface_data = JSON.parse(xhr.responseText) sd = UserSessionData(interface_data['username'], interface_data['user_session_data']) set_session_data(sd) - boss = Boss(interface_data) - set_boss(boss) + Boss(interface_data) else: p = E.p(style='color:red; font-weight: bold; font-size:1.5em') if xhr.status == 401: