From c190748d1e9f3152d0e8fc6a14e3c992e56ba2b2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Aug 2014 15:30:50 +0530 Subject: [PATCH] Various improvements to the splash screen On windows and linux draw the splash screen in a worker process so that the splash screen is updated properly despite the UI thread being blocked while calibre is starting up. Fixes #1357553 [Splash screen is messed up in calibre 2.05 beta](https://bugs.launchpad.net/calibre/+bug/1357553) Also, draw the messages in the splash screen on an opaque beackground so that they can always be read. Drawing the splash screen in a separate process does not work on OS X so we continue to use the old technique there. --- src/calibre/gui2/main.py | 42 +++++----- src/calibre/gui2/splash.py | 165 +++++++++++++++++++++++++++++++++++++ src/calibre/gui2/ui.py | 4 +- 3 files changed, 188 insertions(+), 23 deletions(-) create mode 100644 src/calibre/gui2/splash.py diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 7c9bae50ab..a491baae22 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -5,8 +5,7 @@ import sys, os, time, socket, traceback from functools import partial import apsw -from PyQt5.Qt import (QCoreApplication, QIcon, QObject, QTimer, - QPixmap, QSplashScreen, QApplication) +from PyQt5.Qt import (QCoreApplication, QIcon, QObject, QTimer) from calibre import prints, plugins, force_unicode from calibre.constants import (iswindows, __appname__, isosx, DEBUG, islinux, @@ -16,6 +15,7 @@ from calibre.gui2 import ( ORG_NAME, APP_UID, initialize_file_icon_provider, Application, choose_dir, error_dialog, question_dialog, gprefs, detach_gui, setup_gui_option_parser) from calibre.gui2.main_window import option_parser as _option_parser +from calibre.gui2.splash import SplashScreen from calibre.utils.config import prefs, dynamic if iswindows: @@ -183,11 +183,10 @@ class GuiRunner(QObject): from calibre.gui2.ui import Main main = self.main = Main(self.opts, gui_debug=self.gui_debug) if self.splash_screen is not None: - self.splash_screen.showMessage(_('Initializing user interface...')) + self.splash_screen.show_message(_('Initializing user interface...')) with gprefs: # Only write gui.json after initialization is complete - main.initialize(self.library_path, db, self.listener, self.actions) - if self.splash_screen is not None: - self.splash_screen.finish(main) + main.initialize(self.library_path, db, self.listener, self.actions, splash_screen=self.splash_screen) + self.splash_screen = None if DEBUG: prints('Started up in %.2f seconds'%(time.time() - self.startup_time), 'with', len(db.data), 'books') @@ -214,12 +213,12 @@ class GuiRunner(QObject): if db is None and tb is not None: # DB Repair failed - error_dialog(self.splash_screen, _('Repairing failed'), + error_dialog(None, _('Repairing failed'), _('The database repair failed. Starting with ' 'a new empty library.'), det_msg=tb, show=True) if db is None: - candidate = choose_dir(self.splash_screen, 'choose calibre library', + candidate = choose_dir(None, 'choose calibre library', _('Choose a location for your new calibre e-book library'), default_dir=get_default_library_path()) if not candidate: @@ -229,7 +228,7 @@ class GuiRunner(QObject): self.library_path = candidate db = LibraryDatabase(candidate) except: - error_dialog(self.splash_screen, _('Bad database location'), + error_dialog(None, _('Bad database location'), _('Bad database location %r. calibre will now quit.' )%self.library_path, det_msg=traceback.format_exc(), show=True) @@ -249,7 +248,7 @@ class GuiRunner(QObject): try: db = LibraryDatabase(self.library_path) except apsw.Error: - repair = question_dialog(self.splash_screen, _('Corrupted database'), + repair = question_dialog(None, _('Corrupted database'), _('The library database at %s appears to be corrupted. Do ' 'you want calibre to try and rebuild it automatically? ' 'The rebuild may not be completely successful. ' @@ -261,7 +260,7 @@ class GuiRunner(QObject): if repair_library(self.library_path): db = LibraryDatabase(self.library_path) except: - error_dialog(self.splash_screen, _('Bad database location'), + error_dialog(None, _('Bad database location'), _('Bad database location %r. Will start with ' ' a new, empty calibre library')%self.library_path, det_msg=traceback.format_exc(), show=True) @@ -269,19 +268,14 @@ class GuiRunner(QObject): self.initialize_db_stage2(db, None) def show_splash_screen(self): - self.splash_pixmap = QPixmap() - self.splash_pixmap.load(I('library.png')) - self.splash_screen = QSplashScreen(self.splash_pixmap) - self.splash_screen.showMessage(_('Starting %s: Loading books...') % - __appname__) - self.splash_screen.show() - QApplication.instance().processEvents() + self.splash_screen = SplashScreen(get_debug_executable()) + self.splash_screen.show_message(_('Starting %s: Loading books...') % __appname__) def initialize(self, *args): if gprefs['show_splash_screen']: self.show_splash_screen() - self.library_path = get_library_path(parent=self.splash_screen) + self.library_path = get_library_path(parent=None) if not self.library_path: self.initialization_failed() @@ -294,10 +288,14 @@ def get_debug_executable(): if 'console.app' not in base: base = os.path.join(base, 'console.app', 'Contents') exe = os.path.basename(e) - exe = os.path.join(base, 'MacOS', exe+'-debug') + if '-debug' not in exe: + exe += '-debug' + exe = os.path.join(base, 'MacOS', exe) else: - base, ext = os.path.splitext(e) - exe = base + '-debug' + ext + exe = e + if '-debug' not in exe: + base, ext = os.path.splitext(e) + exe = base + '-debug' + ext return exe def run_in_debug_mode(logpath=None): diff --git a/src/calibre/gui2/splash.py b/src/calibre/gui2/splash.py new file mode 100644 index 0000000000..04c111ed62 --- /dev/null +++ b/src/calibre/gui2/splash.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2014, Kovid Goyal ' + +import os, subprocess, time, struct, sys, errno +from threading import Thread +from Queue import Queue +from functools import partial + +from PyQt5.Qt import QApplication, QSplashScreen, pyqtSignal, QBrush, QColor, Qt, QPixmap + +from calibre.constants import iswindows, isosx +from calibre.utils.ipc import eintr_retry_call + +class SplashScreen(Thread): + + daemon = True + + def __init__(self, debug_executable): + Thread.__init__(self) + self.queue = Queue() + try: + self.launch_process(debug_executable) + except Exception: + import traceback + traceback.print_exc() + self.process = None + self.keep_going = True + if self.process is not None: + self.start() + self.show_message = partial(self._rpc, 'show_message') + self.hide = partial(self._rpc, 'hide') + + def launch_process(self, debug_executable): + kwargs = {'stdin':subprocess.PIPE} + if iswindows: + import win32process + kwargs['creationflags'] = win32process.CREATE_NO_WINDOW + kwargs['stdout'] = open(os.devnull, 'wb') + kwargs['stderr'] = subprocess.STDOUT + self.process = subprocess.Popen([debug_executable, '-c', 'from calibre.gui2.splash import main; main()'], **kwargs) + + def _rpc(self, name, *args): + self.queue.put(('_' + name, args)) + + def run(self): + while self.keep_going: + try: + func, args = self.queue.get() + if func == '_hide': + self.keep_going = False + getattr(self, func)(*args) + except Exception: + import traceback + traceback.print_exc() + self.terminate_worker() + + def terminate_worker(self): + if self.process is None: + return + self.process.stdin.close() + # Give the worker two seconds to exit naturally + c = 20 + while self.process.poll() is None and c > 0: + time.sleep(0.1) + c -= 1 + if self.process.poll() is None: + try: + self.process.terminate() + except EnvironmentError as e: + if getattr(e, 'errno', None) != errno.ESRCH: # ESRCH ==> process does not exist anymore + import traceback + traceback.print_exc() + self.process.wait() + + def send(self, msg): + if self.process is not None and not self.process.stdin.closed: + if not isinstance(msg, bytes): + msg = msg.encode('utf-8') + eintr_retry_call(self.process.stdin.write, struct.pack(b'>L', len(msg)) + msg) + + def _show_message(self, msg): + self.send(msg) + + def _hide(self): + if self.process is not None: + self.process.stdin.close() + +def read(amount): + ans = b'' + left = amount + while left > 0: + raw = eintr_retry_call(sys.stdin.read, left) + if len(raw) == 0: + raise EOFError('') + left -= len(raw) + ans += raw + return ans + +def run_loop(splash_screen): + shutdown = splash_screen.shutdown.emit + try: + while True: + raw = read(4) + mlen = struct.unpack(b'>L', raw)[0] + msg = read(mlen).decode('utf-8') + if not msg: + return shutdown() + splash_screen.show_message.emit(msg) + except EOFError: + pass + except: + import traceback + traceback.print_exc() + return shutdown() + +class CalibreSplashScreen(QSplashScreen): + + shutdown = pyqtSignal() + show_message = pyqtSignal(object) + + def __init__(self): + QSplashScreen.__init__(self, QPixmap(I('library.png'))) + + def drawContents(self, painter): + painter.setBackgroundMode(Qt.OpaqueMode) + painter.setBackground(QBrush(QColor(0xee, 0xee, 0xee))) + painter.setPen(Qt.black) + painter.setRenderHint(painter.TextAntialiasing, True) + painter.drawText(self.rect().adjusted(5, 5, -5, -5), Qt.AlignLeft, self.message()) + + +def main(): + os.closerange(3, 256) + from calibre.gui2 import Application + app = Application([]) + s = CalibreSplashScreen() + s.show_message.connect(s.showMessage, type=Qt.QueuedConnection) + s.shutdown.connect(app.quit, type=Qt.QueuedConnection) + s.show() + Thread(target=run_loop, args=(s,)).start() + app.exec_() + +if isosx: + # Showing the splash screen in a separate process doesn't work on OS X and + # I can't be bothered to figure out why + del SplashScreen + + class SplashScreen(CalibreSplashScreen): + + def __init__(self, *args): + CalibreSplashScreen.__init__(self) + self.show() + + def show(self): + CalibreSplashScreen.show(self) + QApplication.instance().processEvents() + QApplication.instance().flush() + + def show_message(self, msg): + CalibreSplashScreen.showMessage(self, msg) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index b762bf7b11..e159af32a2 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -221,7 +221,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ else: stmap[st.name] = st - def initialize(self, library_path, db, listener, actions, show_gui=True): + def initialize(self, library_path, db, listener, actions, show_gui=True, splash_screen=None): opts = self.opts self.preferences_action, self.quit_action = actions self.library_path = library_path @@ -346,6 +346,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if show_gui: self.show() + if splash_screen is not None: + splash_screen.hide() if self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows()