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