mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
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:
parent
70a813281a
commit
c190748d1e
@ -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
165
src/calibre/gui2/splash.py
Normal 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)
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user