mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Do not use a separate process to draw the splash screen
I've now had a couple of reports of calibre hanging randomly on windows during startup with the splash screen enabled. And in any case with Qt 5.4.1 there is a simpler workaround for the splash screen not rendering issue
This commit is contained in:
parent
7f73fe505e
commit
d4d0ccd970
@ -5,7 +5,9 @@ import sys, os, time, socket, traceback, re
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import apsw
|
import apsw
|
||||||
from PyQt5.Qt import (QCoreApplication, QIcon, QObject, QTimer)
|
from PyQt5.Qt import (
|
||||||
|
QCoreApplication, QIcon, QObject, QTimer, Qt, QSplashScreen, QBrush,
|
||||||
|
QColor, QPixmap)
|
||||||
|
|
||||||
from calibre import prints, plugins, force_unicode
|
from calibre import prints, plugins, force_unicode
|
||||||
from calibre.constants import (iswindows, __appname__, isosx, DEBUG, islinux,
|
from calibre.constants import (iswindows, __appname__, isosx, DEBUG, islinux,
|
||||||
@ -15,7 +17,6 @@ from calibre.gui2 import (
|
|||||||
ORG_NAME, APP_UID, initialize_file_icon_provider, Application, choose_dir,
|
ORG_NAME, APP_UID, initialize_file_icon_provider, Application, choose_dir,
|
||||||
error_dialog, question_dialog, gprefs, setup_gui_option_parser)
|
error_dialog, question_dialog, gprefs, setup_gui_option_parser)
|
||||||
from calibre.gui2.main_window import option_parser as _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
|
from calibre.utils.config import prefs, dynamic
|
||||||
|
|
||||||
if iswindows:
|
if iswindows:
|
||||||
@ -157,6 +158,34 @@ class EventAccumulator(object):
|
|||||||
def __call__(self, ev):
|
def __call__(self, ev):
|
||||||
self.events.append(ev)
|
self.events.append(ev)
|
||||||
|
|
||||||
|
class SplashScreen(QSplashScreen):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.drawn_once = False
|
||||||
|
QSplashScreen.__init__(self, QPixmap(I('library.png')))
|
||||||
|
|
||||||
|
def drawContents(self, painter):
|
||||||
|
self.drawn_once = True
|
||||||
|
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 show_message(self, msg):
|
||||||
|
self.showMessage(msg)
|
||||||
|
self.wait_for_draw()
|
||||||
|
|
||||||
|
def wait_for_draw(self):
|
||||||
|
# Without this the splash screen is not painted on linux and windows
|
||||||
|
self.drawn_once = False
|
||||||
|
st = time.time()
|
||||||
|
while not self.drawn_once and (time.time() - st < 0.1):
|
||||||
|
Application.instance().processEvents()
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
QSplashScreen.show(self)
|
||||||
|
|
||||||
class GuiRunner(QObject):
|
class GuiRunner(QObject):
|
||||||
'''Make sure an event loop is running before starting the main work of
|
'''Make sure an event loop is running before starting the main work of
|
||||||
initialization'''
|
initialization'''
|
||||||
@ -178,8 +207,9 @@ class GuiRunner(QObject):
|
|||||||
main = self.main = Main(self.opts, gui_debug=self.gui_debug)
|
main = self.main = Main(self.opts, gui_debug=self.gui_debug)
|
||||||
if self.splash_screen is not None:
|
if self.splash_screen is not None:
|
||||||
self.splash_screen.show_message(_('Initializing user interface...'))
|
self.splash_screen.show_message(_('Initializing user interface...'))
|
||||||
|
self.splash_screen.finish(main)
|
||||||
with gprefs: # Only write gui.json after initialization is complete
|
with gprefs: # Only write gui.json after initialization is complete
|
||||||
main.initialize(self.library_path, db, self.listener, self.actions, splash_screen=self.splash_screen)
|
main.initialize(self.library_path, db, self.listener, self.actions)
|
||||||
self.splash_screen = None
|
self.splash_screen = None
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Started up in %.2f seconds'%(time.time() -
|
prints('Started up in %.2f seconds'%(time.time() -
|
||||||
@ -197,22 +227,14 @@ class GuiRunner(QObject):
|
|||||||
add_filesystem_book(event)
|
add_filesystem_book(event)
|
||||||
self.app.file_event_hook = add_filesystem_book
|
self.app.file_event_hook = add_filesystem_book
|
||||||
|
|
||||||
def hide_splash_screen(self):
|
|
||||||
if self.splash_screen is not None:
|
|
||||||
with self.app: # Disable quit on last window closed
|
|
||||||
self.splash_screen.hide()
|
|
||||||
self.splash_screen = None
|
|
||||||
|
|
||||||
def choose_dir(self, initial_dir):
|
def choose_dir(self, initial_dir):
|
||||||
self.hide_splash_screen()
|
return choose_dir(self.splash_screen, 'choose calibre library',
|
||||||
return choose_dir(None, 'choose calibre library',
|
|
||||||
_('Choose a location for your new calibre e-book library'),
|
_('Choose a location for your new calibre e-book library'),
|
||||||
default_dir=initial_dir)
|
default_dir=initial_dir)
|
||||||
|
|
||||||
def show_error(self, title, msg, det_msg=''):
|
def show_error(self, title, msg, det_msg=''):
|
||||||
self.hide_splash_screen()
|
|
||||||
with self.app:
|
with self.app:
|
||||||
error_dialog(self.main, title, msg, det_msg=det_msg, show=True)
|
error_dialog(self.splash_screen, title, msg, det_msg=det_msg, show=True)
|
||||||
|
|
||||||
def initialization_failed(self):
|
def initialization_failed(self):
|
||||||
print 'Catastrophic failure initializing GUI, bailing out...'
|
print 'Catastrophic failure initializing GUI, bailing out...'
|
||||||
@ -255,9 +277,8 @@ class GuiRunner(QObject):
|
|||||||
try:
|
try:
|
||||||
db = LibraryDatabase(self.library_path)
|
db = LibraryDatabase(self.library_path)
|
||||||
except apsw.Error:
|
except apsw.Error:
|
||||||
self.hide_splash_screen()
|
|
||||||
with self.app:
|
with self.app:
|
||||||
repair = question_dialog(None, _('Corrupted database'),
|
repair = question_dialog(self.splash_screen, _('Corrupted database'),
|
||||||
_('The library database at %s appears to be corrupted. Do '
|
_('The library database at %s appears to be corrupted. Do '
|
||||||
'you want calibre to try and rebuild it automatically? '
|
'you want calibre to try and rebuild it automatically? '
|
||||||
'The rebuild may not be completely successful. '
|
'The rebuild may not be completely successful. '
|
||||||
@ -277,13 +298,13 @@ class GuiRunner(QObject):
|
|||||||
self.initialize_db_stage2(db, None)
|
self.initialize_db_stage2(db, None)
|
||||||
|
|
||||||
def show_splash_screen(self):
|
def show_splash_screen(self):
|
||||||
self.splash_screen = SplashScreen(get_debug_executable())
|
self.splash_screen = SplashScreen()
|
||||||
|
self.splash_screen.show()
|
||||||
self.splash_screen.show_message(_('Starting %s: Loading books...') % __appname__)
|
self.splash_screen.show_message(_('Starting %s: Loading books...') % __appname__)
|
||||||
|
|
||||||
def initialize(self, *args):
|
def initialize(self, *args):
|
||||||
if gprefs['show_splash_screen']:
|
if gprefs['show_splash_screen']:
|
||||||
self.show_splash_screen()
|
self.show_splash_screen()
|
||||||
|
|
||||||
self.library_path = get_library_path(self)
|
self.library_path = get_library_path(self)
|
||||||
if not self.library_path:
|
if not self.library_path:
|
||||||
self.initialization_failed()
|
self.initialization_failed()
|
||||||
|
@ -1,165 +0,0 @@
|
|||||||
#!/usr/bin/env python2
|
|
||||||
# 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 isosx, DEBUG
|
|
||||||
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):
|
|
||||||
from calibre.utils.ipc.simple_worker import start_pipe_worker
|
|
||||||
if DEBUG:
|
|
||||||
args = {'stdout':None, 'stderr': None}
|
|
||||||
else:
|
|
||||||
args = {'stdout':open(os.devnull, 'wb'), 'stderr':subprocess.STDOUT}
|
|
||||||
self.process = start_pipe_worker('from calibre.gui2.splash import main; main()', **args)
|
|
||||||
|
|
||||||
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)
|
|
||||||
app = QApplication([])
|
|
||||||
s = CalibreSplashScreen()
|
|
||||||
s.show_message.connect(s.showMessage, type=Qt.QueuedConnection)
|
|
||||||
s.shutdown.connect(app.quit, type=Qt.QueuedConnection)
|
|
||||||
s.show()
|
|
||||||
t = Thread(target=run_loop, args=(s,))
|
|
||||||
t.daemon = True
|
|
||||||
t.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)
|
|
@ -209,7 +209,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
else:
|
else:
|
||||||
stmap[st.name] = st
|
stmap[st.name] = st
|
||||||
|
|
||||||
def initialize(self, library_path, db, listener, actions, show_gui=True, splash_screen=None):
|
def initialize(self, library_path, db, listener, actions, show_gui=True):
|
||||||
opts = self.opts
|
opts = self.opts
|
||||||
self.preferences_action, self.quit_action = actions
|
self.preferences_action, self.quit_action = actions
|
||||||
self.library_path = library_path
|
self.library_path = library_path
|
||||||
@ -335,8 +335,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
|
|
||||||
if show_gui:
|
if show_gui:
|
||||||
self.show()
|
self.show()
|
||||||
if splash_screen is not None:
|
|
||||||
splash_screen.hide()
|
|
||||||
|
|
||||||
if self.system_tray_icon is not None and self.system_tray_icon.isVisible() and opts.start_in_tray:
|
if self.system_tray_icon is not None and self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||||
self.hide_windows()
|
self.hide_windows()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user