mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
53ffa30767
commit
88ed3546be
@ -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)
|
||||
|
86
src/calibre/gui2/viewer/overlay.py
Normal file
86
src/calibre/gui2/viewer/overlay.py
Normal 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()
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user