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.
This commit is contained in:
Kovid Goyal 2019-10-08 15:42:14 +05:30
parent 53ffa30767
commit 88ed3546be
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 138 additions and 26 deletions

View File

@ -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)

View File

@ -0,0 +1,86 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
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('<i>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()

View File

@ -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:

View File

@ -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)

View File

@ -515,11 +515,17 @@ class Overlay:
self.hide_current_panel()
def show_loading_message(self, msg):
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):
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()

View File

@ -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