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.
This commit is contained in:
Kovid Goyal 2014-08-16 15:30:50 +05:30
parent 70a813281a
commit c190748d1e
3 changed files with 188 additions and 23 deletions

View File

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

165
src/calibre/gui2/splash.py Normal file
View File

@ -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 <kovid at kovidgoyal.net>'
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)

View File

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