From 88ed3546bebcf87582b61f7c8828ef3b664422a9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 8 Oct 2019 15:42:14 +0530 Subject: [PATCH] Viewer: Use Qt for errors and the loading spinner Makes the UI more consistent with the rest of calibre. Also, change the initial loading text depending on whether the prepared book is already cached or not. --- src/calibre/gui2/viewer/convert_book.py | 4 +- src/calibre/gui2/viewer/overlay.py | 86 +++++++++++++++++++++++++ src/calibre/gui2/viewer/ui.py | 28 +++++++- src/calibre/gui2/viewer/web_view.py | 15 ++--- src/pyj/read_book/overlay.pyj | 16 +++-- src/pyj/viewer-main.pyj | 15 ++--- 6 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 src/calibre/gui2/viewer/overlay.py diff --git a/src/calibre/gui2/viewer/convert_book.py b/src/calibre/gui2/viewer/convert_book.py index 7907fefd9d..a0026ffb19 100644 --- a/src/calibre/gui2/viewer/convert_book.py +++ b/src/calibre/gui2/viewer/convert_book.py @@ -133,7 +133,7 @@ def save_metadata(metadata, f): f.seek(0), f.truncate(), f.write(as_bytes(json.dumps(metadata, indent=2))) -def prepare_book(path, convert_func=do_convert, max_age=30 * DAY, force=False): +def prepare_book(path, convert_func=do_convert, max_age=30 * DAY, force=False, prepare_notify=None): st = os.stat(path) key = book_hash(path, st.st_size, st.st_mtime) finished_path = safe_makedirs(os.path.join(book_cache_dir(), 'f')) @@ -155,6 +155,8 @@ def prepare_book(path, convert_func=do_convert, max_age=30 * DAY, force=False): instance['atime'] = time.time() save_metadata(metadata, f) return os.path.join(finished_path, instance['path']) + if prepare_notify: + prepare_notify() instance = prepare_convert(temp_path, key, st) instances.append(instance) save_metadata(metadata, f) diff --git a/src/calibre/gui2/viewer/overlay.py b/src/calibre/gui2/viewer/overlay.py new file mode 100644 index 0000000000..a63986a817 --- /dev/null +++ b/src/calibre/gui2/viewer/overlay.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2019, Kovid Goyal + +from __future__ import absolute_import, division, print_function, unicode_literals + +from PyQt5.Qt import QPainter, QPalette, QPoint, QRect, QTimer, QWidget, Qt, QFontInfo, QLabel + +from calibre.gui2.progress_indicator import draw_snake_spinner + + +class LoadingOverlay(QWidget): + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.setVisible(False) + self.label = QLabel(self) + self.label.setText('testing') + self.label.setTextFormat(Qt.RichText) + self.label.setAlignment(Qt.AlignTop | Qt.AlignHCenter) + self.resize(parent.size()) + self.move(0, 0) + self.angle = 0 + self.timer = t = QTimer(self) + t.setInterval(10) + t.timeout.connect(self.tick) + f = self.font() + f.setBold(True) + fm = QFontInfo(f) + f.setPixelSize(int(fm.pixelSize() * 1.5)) + self.label.setFont(f) + self.calculate_rects() + + def tick(self): + self.angle -= 2 + self.angle %= 360 + self.update() + + def __call__(self, msg=''): + self.label.setText(msg) + self.resize(self.parent().size()) + self.move(0, 0) + self.setVisible(True) + self.raise_() + self.setFocus(Qt.OtherFocusReason) + + def hide(self): + self.parent().web_view.setFocus(Qt.OtherFocusReason) + return QWidget.hide(self) + + def showEvent(self, ev): + self.timer.start() + + def hideEvent(self, ev): + self.timer.stop() + + def calculate_rects(self): + rect = self.rect() + self.spinner_rect = r = QRect(0, 0, 96, 96) + r.moveCenter(rect.center() - QPoint(0, r.height() // 2)) + r = QRect(r) + r.moveTop(r.center().y() + 20 + r.height() // 2) + r.setLeft(0), r.setRight(self.width()) + self.label.setGeometry(r) + + def resizeEvent(self, ev): + self.calculate_rects() + return QWidget.resizeEvent(self, ev) + + def do_paint(self, painter): + pal = self.palette() + color = pal.color(QPalette.Window) + color.setAlphaF(0.8) + painter.fillRect(self.rect(), color) + draw_snake_spinner(painter, self.spinner_rect, self.angle, pal.color(QPalette.Window), pal.color(QPalette.WindowText)) + + def paintEvent(self, ev): + painter = QPainter(self) + painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing) + try: + self.do_paint(painter) + except Exception: + import traceback + traceback.print_exc() + finally: + painter.end() diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 54925b0bcb..dd37b8bc77 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -27,6 +27,7 @@ from calibre.gui2.viewer.annotations import ( from calibre.gui2.viewer.bookmarks import BookmarkManager from calibre.gui2.viewer.convert_book import prepare_book, update_book from calibre.gui2.viewer.lookup import Lookup +from calibre.gui2.viewer.overlay import LoadingOverlay from calibre.gui2.viewer.toc import TOC, TOCSearch, TOCView from calibre.gui2.viewer.web_view import ( WebView, get_path_for_name, get_session_pref, set_book_path, viewer_config_dir, @@ -69,11 +70,14 @@ class ScrollBar(QScrollBar): class EbookViewer(MainWindow): msg_from_anotherinstance = pyqtSignal(object) + book_preparation_started = pyqtSignal() book_prepared = pyqtSignal(object, object) MAIN_WINDOW_STATE_VERSION = 1 def __init__(self, open_at=None, continue_reading=None): MainWindow.__init__(self, None) + connect_lambda(self.book_preparation_started, self, lambda self: self.loading_overlay(_( + 'Preparing book for first read, please wait')), type=Qt.QueuedConnection) self.maximized_at_last_fullscreen = False self.pending_open_at = open_at self.base_window_title = _('E-book viewer') @@ -134,7 +138,10 @@ class EbookViewer(MainWindow): self.web_view.selection_changed.connect(self.lookup_widget.selected_text_changed, type=Qt.QueuedConnection) self.web_view.view_image.connect(self.view_image, type=Qt.QueuedConnection) self.web_view.copy_image.connect(self.copy_image, type=Qt.QueuedConnection) + self.web_view.show_loading_message.connect(self.show_loading_message) + self.web_view.show_error.connect(self.show_error) self.setCentralWidget(self.web_view) + self.loading_overlay = LoadingOverlay(self) self.restore_state() if continue_reading: self.continue_reading() @@ -143,6 +150,10 @@ class EbookViewer(MainWindow): visible = self.inspector_dock.toggleViewAction().isChecked() self.inspector_dock.setVisible(not visible) + def resizeEvent(self, ev): + self.loading_overlay.resize(self.size()) + return MainWindow.resizeEvent(self, ev) + # IPC {{{ def handle_commandline_arg(self, arg): if arg: @@ -242,6 +253,16 @@ class EbookViewer(MainWindow): # Load book {{{ + def show_loading_message(self, msg): + if msg: + self.loading_overlay(msg) + else: + self.loading_overlay.hide() + + def show_error(self, title, msg, details): + self.loading_overlay.hide() + error_dialog(self, title, msg, det_msg=details or None, show=True) + def ask_for_open(self, path=None): if path is None: files = choose_files( @@ -263,7 +284,7 @@ class EbookViewer(MainWindow): if open_at: self.pending_open_at = open_at self.setWindowTitle(_('Loading book') + '… — {}'.format(self.base_window_title)) - self.web_view.show_preparing_message() + self.loading_overlay(_('Loading book, please wait')) self.save_annotations() self.current_book_data = {} t = Thread(name='LoadBook', target=self._load_ebook_worker, args=(pathtoebook, open_at, reload_book)) @@ -276,7 +297,7 @@ class EbookViewer(MainWindow): def _load_ebook_worker(self, pathtoebook, open_at, reload_book): try: - ans = prepare_book(pathtoebook, force=reload_book) + ans = prepare_book(pathtoebook, force=reload_book, prepare_notify=self.prepare_notify) except WorkerError as e: self.book_prepared.emit(False, {'exception': e, 'tb': e.orig_tb, 'pathtoebook': pathtoebook}) except Exception as e: @@ -285,6 +306,9 @@ class EbookViewer(MainWindow): else: self.book_prepared.emit(True, {'base': ans, 'pathtoebook': pathtoebook, 'open_at': open_at}) + def prepare_notify(self): + self.book_preparation_started.emit() + def load_finished(self, ok, data): open_at, self.pending_open_at = self.pending_open_at, None if not ok: diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 20a91d2fad..70be45e97b 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -233,9 +233,10 @@ class ViewerBridge(Bridge): copy_image = from_js(object) change_background_image = from_js(object) overlay_visibility_changed = from_js(object) + show_loading_message = from_js(object) + show_error = from_js(object, object, object) create_view = to_js() - show_preparing_message = to_js() start_book_load = to_js() goto_toc_node = to_js() goto_cfi = to_js() @@ -295,10 +296,6 @@ class WebPage(QWebEnginePage): self.triggerAction(self.Copy) def javaScriptConsoleMessage(self, level, msg, linenumber, source_id): - if level >= QWebEnginePage.ErrorMessageLevel and source_id == 'userscript:viewer.js': - error_dialog(self.parent(), _('Unhandled error'), _( - 'There was an unhandled error: {} at line: {} of {}').format( - msg, linenumber, source_id.partition(':')[2]), show=True) prefix = {QWebEnginePage.InfoMessageLevel: 'INFO', QWebEnginePage.WarningMessageLevel: 'WARNING'}.get( level, 'ERROR') prints('%s: %s:%s: %s' % (prefix, source_id, linenumber, msg), file=sys.stderr) @@ -376,6 +373,8 @@ class WebView(RestartingWebEngineView): view_image = pyqtSignal(object) copy_image = pyqtSignal(object) overlay_visibility_changed = pyqtSignal(object) + show_loading_message = pyqtSignal(object) + show_error = pyqtSignal(object, object, object) def __init__(self, parent=None): self._host_widget = None @@ -403,6 +402,8 @@ class WebView(RestartingWebEngineView): self.bridge.view_image.connect(self.view_image) self.bridge.copy_image.connect(self.copy_image) self.bridge.overlay_visibility_changed.connect(self.overlay_visibility_changed) + self.bridge.show_loading_message.connect(self.show_loading_message) + self.bridge.show_error.connect(self.show_error) self.bridge.report_cfi.connect(self.call_callback) self.bridge.change_background_image.connect(self.change_background_image) self.pending_bridge_ready_actions = {} @@ -480,10 +481,6 @@ class WebView(RestartingWebEngineView): else: self.pending_bridge_ready_actions[action] = args - def show_preparing_message(self): - msg = _('Preparing book for first read, please wait') + '…' - self.execute_when_ready('show_preparing_message', msg) - def goto_toc_node(self, node_id): self.execute_when_ready('goto_toc_node', node_id) diff --git a/src/pyj/read_book/overlay.pyj b/src/pyj/read_book/overlay.pyj index a8f9320447..ef7764c9da 100644 --- a/src/pyj/read_book/overlay.pyj +++ b/src/pyj/read_book/overlay.pyj @@ -515,13 +515,19 @@ class Overlay: self.hide_current_panel() def show_loading_message(self, msg): - lm = LoadingMessage(msg, self.view.current_color_scheme) - self.panels.push(lm) - self.show_current_panel() + if ui_operations.show_loading_message: + ui_operations.show_loading_message(msg) + else: + lm = LoadingMessage(msg, self.view.current_color_scheme) + self.panels.push(lm) + self.show_current_panel() def hide_loading_message(self): - self.panels = [p for p in self.panels if not isinstance(p, LoadingMessage)] - self.show_current_panel() + if ui_operations.show_loading_message: + ui_operations.show_loading_message(None) + else: + self.panels = [p for p in self.panels if not isinstance(p, LoadingMessage)] + self.show_current_panel() def hide_current_panel(self): p = self.panels.pop() diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index 0dcfe61c64..4bbc835028 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -12,7 +12,7 @@ from book_list.globals import set_session_data from book_list.library_data import library_data from book_list.theme import get_color from dom import get_widget_css, set_css -from modals import create_modal_container, error_dialog +from modals import create_modal_container from qt import from_python, to_python from read_book.db import new_book from read_book.footnotes import main as footnotes_main @@ -133,7 +133,7 @@ def on_pop_state(): def show_error(title, msg, details): - error_dialog(title, msg, details) + to_python.show_error(title, msg, details) def manifest_received(key, initial_cfi, initial_toc_node, pathtoebook, end_type, xhr, ev): @@ -150,7 +150,7 @@ def manifest_received(key, initial_cfi, initial_toc_node, pathtoebook, end_type, v'delete book.manifest["last_read_positions"]' view.display_book(book, initial_cfi, initial_toc_node) else: - error_dialog(_('Could not open book'), _( + show_error(_('Could not open book'), _( 'Failed to load book manifest, click "Show details" for more info'), xhr.error_html or None) @@ -205,11 +205,6 @@ def show_home_page(): view.overlay.open_book(False) -@from_python -def show_preparing_message(msg): - view.show_loading_message(msg) - - @from_python def start_book_load(key, initial_cfi, initial_toc_node, pathtoebook): xhr = ajax('manifest', manifest_received.bind(None, key, initial_cfi, initial_toc_node, pathtoebook), ok_code=0) @@ -259,7 +254,7 @@ def onerror(msg, script_url, line_number, column_number, error_object): details = '' console.log(error_object) details = traceback.format_exception(error_object).join('') - error_dialog(_('Unhandled error'), msg, details) + show_error(_('Unhandled error'), msg, details) return True @@ -316,6 +311,8 @@ if window is window.top: to_python.quit() ui_operations.overlay_visibility_changed = def(visible): to_python.overlay_visibility_changed(visible) + ui_operations.show_loading_message = def(msg): + to_python.show_loading_message(msg) document.body.appendChild(E.div(id='view')) window.onerror = onerror