mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Use a separate process for the ToC Editor from the main GUI
This is needed because the main GUI does not use web engine
This commit is contained in:
parent
a304b67ce3
commit
27798beaf6
@ -44,6 +44,7 @@ VIEWER_APP_UID = 'com.calibre-ebook.viewer'
|
|||||||
EDITOR_APP_UID = 'com.calibre-ebook.edit-book'
|
EDITOR_APP_UID = 'com.calibre-ebook.edit-book'
|
||||||
MAIN_APP_UID = 'com.calibre-ebook.main-gui'
|
MAIN_APP_UID = 'com.calibre-ebook.main-gui'
|
||||||
STORE_DIALOG_APP_UID = 'com.calibre-ebook.store-dialog'
|
STORE_DIALOG_APP_UID = 'com.calibre-ebook.store-dialog'
|
||||||
|
TOC_DIALOG_APP_UID = 'com.calibre-ebook.toc-editor'
|
||||||
try:
|
try:
|
||||||
preferred_encoding = locale.getpreferredencoding()
|
preferred_encoding = locale.getpreferredencoding()
|
||||||
codecs.lookup(preferred_encoding)
|
codecs.lookup(preferred_encoding)
|
||||||
|
@ -6,6 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from PyQt5.Qt import (QTimer, QDialog, QGridLayout, QCheckBox, QLabel,
|
from PyQt5.Qt import (QTimer, QDialog, QGridLayout, QCheckBox, QLabel,
|
||||||
@ -13,6 +14,7 @@ from PyQt5.Qt import (QTimer, QDialog, QGridLayout, QCheckBox, QLabel,
|
|||||||
|
|
||||||
from calibre.gui2 import error_dialog, gprefs
|
from calibre.gui2 import error_dialog, gprefs
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.utils.monotonic import monotonic
|
||||||
from polyglot.builtins import iteritems, unicode_type
|
from polyglot.builtins import iteritems, unicode_type
|
||||||
|
|
||||||
SUPPORTED = {'EPUB', 'AZW3'}
|
SUPPORTED = {'EPUB', 'AZW3'}
|
||||||
@ -100,6 +102,7 @@ class ToCEditAction(InterfaceAction):
|
|||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.edit_books)
|
self.qaction.triggered.connect(self.edit_books)
|
||||||
|
self.jobs = []
|
||||||
|
|
||||||
def get_supported_books(self, book_ids):
|
def get_supported_books(self, book_ids):
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
@ -138,15 +141,50 @@ class ToCEditAction(InterfaceAction):
|
|||||||
self.do_one(book_id, fmt)
|
self.do_one(book_id, fmt)
|
||||||
|
|
||||||
def do_one(self, book_id, fmt):
|
def do_one(self, book_id, fmt):
|
||||||
from calibre.gui2.toc.main import TOCEditor
|
|
||||||
db = self.gui.current_db
|
db = self.gui.current_db
|
||||||
path = db.format(book_id, fmt, index_is_id=True, as_path=True)
|
path = db.format(book_id, fmt, index_is_id=True, as_path=True)
|
||||||
title = db.title(book_id, index_is_id=True) + ' [%s]'%fmt
|
title = db.title(book_id, index_is_id=True) + ' [%s]'%fmt
|
||||||
d = TOCEditor(path, title=title, parent=self.gui)
|
data = {'path': path, 'title': title}
|
||||||
d.start()
|
self.gui.job_manager.launch_gui_app('toc-dialog', kwargs=data)
|
||||||
if d.exec_() == d.Accepted:
|
job = data.copy()
|
||||||
with open(path, 'rb') as f:
|
job.update({'book_id': book_id, 'fmt': fmt, 'library_id': db.new_api.library_id, 'started': False, 'start_time': monotonic()})
|
||||||
db.add_format(book_id, fmt, f, index_is_id=True)
|
self.jobs.append(job)
|
||||||
|
self.check_for_completions()
|
||||||
|
|
||||||
|
def check_for_completions(self):
|
||||||
|
from calibre.utils.lock import lock_file
|
||||||
|
for job in tuple(self.jobs):
|
||||||
|
lock_path = job['path'] + '.lock'
|
||||||
|
if job['started']:
|
||||||
|
if not os.path.exists(lock_path):
|
||||||
|
self.jobs.remove(job)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
lf = lock_file(lock_path, timeout=0.01, sleep_time=0.005)
|
||||||
|
except EnvironmentError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.jobs.remove(job)
|
||||||
|
ret = int(lf.read().decode('ascii'))
|
||||||
|
lf.close()
|
||||||
|
os.remove(lock_path)
|
||||||
|
if ret == 0:
|
||||||
|
db = self.gui.current_db
|
||||||
|
if db.new_api.library_id != job['library_id']:
|
||||||
|
error_dialog(self.gui, _('Library changed'), _(
|
||||||
|
'Cannot save changes made to {0} by the ToC editor as'
|
||||||
|
' the calibre library has changed.').format(job['title']), show=True)
|
||||||
|
else:
|
||||||
|
db.new_api.add_format(job['book_id'], job['fmt'], job['path'], run_hooks=False)
|
||||||
|
os.remove(job['path'])
|
||||||
|
else:
|
||||||
|
if monotonic() - job['start_time'] > 10:
|
||||||
|
self.jobs.remove(job)
|
||||||
|
continue
|
||||||
|
if os.path.exists(lock_path):
|
||||||
|
job['started'] = True
|
||||||
|
if self.jobs:
|
||||||
|
QTimer.singleShot(100, self.check_for_completions)
|
||||||
|
|
||||||
def edit_books(self):
|
def edit_books(self):
|
||||||
book_id_map = self.get_books_for_editing()
|
book_id_map = self.get_books_for_editing()
|
||||||
|
@ -281,9 +281,9 @@ class JobManager(QAbstractTableModel, AdaptSQP): # {{{
|
|||||||
self.add_job(job)
|
self.add_job(job)
|
||||||
self.threaded_server.add_job(job)
|
self.threaded_server.add_job(job)
|
||||||
|
|
||||||
def launch_gui_app(self, name, args=[], kwargs={}, description=''):
|
def launch_gui_app(self, name, args=(), kwargs=None, description=''):
|
||||||
job = ParallelJob(name, description, lambda x: x,
|
job = ParallelJob(name, description, lambda x: x,
|
||||||
args=args, kwargs=kwargs)
|
args=list(args), kwargs=kwargs or {})
|
||||||
self.server.run_job(job, gui=True, redirect_output=False)
|
self.server.run_job(job, gui=True, redirect_output=False)
|
||||||
|
|
||||||
def _kill_job(self, job):
|
def _kill_job(self, job):
|
||||||
|
@ -1,29 +1,37 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python2
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||||
|
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
import os
|
||||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
import sys
|
||||||
__docformat__ = 'restructuredtext en'
|
import textwrap
|
||||||
|
|
||||||
import sys, os, textwrap
|
|
||||||
from threading import Thread
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from PyQt5.Qt import (QPushButton, QFrame, QMenu, QInputDialog, QCheckBox,
|
from PyQt5.Qt import (
|
||||||
QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget,
|
QCheckBox, QCursor, QDialog, QDialogButtonBox, QFrame, QGridLayout, QIcon,
|
||||||
QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem,
|
QInputDialog, QItemSelectionModel, QKeySequence, QLabel, QMenu, QPushButton,
|
||||||
QToolButton, QItemSelectionModel, QCursor, QKeySequence, QSizePolicy)
|
QSize, QSizePolicy, QStackedWidget, Qt, QToolButton, QTreeWidget,
|
||||||
|
QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
|
||||||
|
)
|
||||||
|
|
||||||
from calibre.ebooks.oeb.polish.container import get_container, AZW3Container
|
from calibre.constants import TOC_DIALOG_APP_UID, islinux, iswindows
|
||||||
|
from calibre.ebooks.oeb.polish.container import AZW3Container, get_container
|
||||||
from calibre.ebooks.oeb.polish.toc import (
|
from calibre.ebooks.oeb.polish.toc import (
|
||||||
get_toc, add_id, TOC, commit_toc, from_xpaths, from_links, from_files)
|
TOC, add_id, commit_toc, from_files, from_links, from_xpaths, get_toc
|
||||||
from calibre.gui2 import Application, error_dialog, gprefs, info_dialog, question_dialog
|
)
|
||||||
|
from calibre.gui2 import (
|
||||||
|
Application, error_dialog, gprefs, info_dialog, question_dialog, set_app_uid
|
||||||
|
)
|
||||||
|
from calibre.gui2.convert.xpath_wizard import XPathEdit
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.gui2.toc.location import ItemEdit
|
from calibre.gui2.toc.location import ItemEdit
|
||||||
from calibre.gui2.convert.xpath_wizard import XPathEdit
|
from calibre.ptempfile import reset_base_dir
|
||||||
|
from calibre.utils.lock import ExclusiveFile
|
||||||
from calibre.utils.logging import GUILog
|
from calibre.utils.logging import GUILog
|
||||||
from polyglot.builtins import map, unicode_type, range
|
from polyglot.builtins import map, range, unicode_type
|
||||||
|
|
||||||
ICON_SIZE = 24
|
ICON_SIZE = 24
|
||||||
|
|
||||||
@ -1117,10 +1125,30 @@ class TOCEditor(QDialog): # {{{
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def main(path=None, title=None):
|
||||||
app = Application([], force_calibre_style=True)
|
# Ensure we can continue to function if GUI is closed
|
||||||
app
|
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
|
||||||
d = TOCEditor(sys.argv[-1])
|
reset_base_dir()
|
||||||
|
if iswindows:
|
||||||
|
# Ensure that all instances are grouped together in the task bar. This
|
||||||
|
# prevents them from being grouped with viewer/editor process when
|
||||||
|
# launched from within calibre, as both use calibre-parallel.exe
|
||||||
|
set_app_uid(TOC_DIALOG_APP_UID)
|
||||||
|
|
||||||
|
with ExclusiveFile(path + '.lock') as wf:
|
||||||
|
override = 'calibre-gui' if islinux else None
|
||||||
|
app = Application([], override_program_name=override)
|
||||||
|
d = TOCEditor(path, title=title)
|
||||||
d.start()
|
d.start()
|
||||||
d.exec_()
|
ret = 1
|
||||||
del d # Needed to prevent sigsegv in exit cleanup
|
if d.exec_() == d.Accepted:
|
||||||
|
ret = 0
|
||||||
|
wf.write('{}'.format(ret).encode('ascii'))
|
||||||
|
del d
|
||||||
|
del app
|
||||||
|
raise SystemExit(ret)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(path=sys.argv[-1], title='test')
|
||||||
|
os.remove(sys.argv[-1] + '.lock')
|
||||||
|
@ -88,6 +88,13 @@ def store_dialog(args=sys.argv):
|
|||||||
main(args)
|
main(args)
|
||||||
|
|
||||||
|
|
||||||
|
def toc_dialog(**kw):
|
||||||
|
detach_gui()
|
||||||
|
init_dbus()
|
||||||
|
from calibre.gui2.toc.main import main
|
||||||
|
main(**kw)
|
||||||
|
|
||||||
|
|
||||||
def gui_ebook_edit(path=None, notify=None):
|
def gui_ebook_edit(path=None, notify=None):
|
||||||
' For launching the editor from inside calibre '
|
' For launching the editor from inside calibre '
|
||||||
init_dbus()
|
init_dbus()
|
||||||
|
@ -32,6 +32,9 @@ PARALLEL_FUNCS = {
|
|||||||
'store-dialog' :
|
'store-dialog' :
|
||||||
('calibre.gui_launch', 'store_dialog', None),
|
('calibre.gui_launch', 'store_dialog', None),
|
||||||
|
|
||||||
|
'toc-dialog' :
|
||||||
|
('calibre.gui_launch', 'toc_dialog', None),
|
||||||
|
|
||||||
'render_pages' :
|
'render_pages' :
|
||||||
('calibre.ebooks.comic.input', 'render_pages', 'notification'),
|
('calibre.ebooks.comic.input', 'render_pages', 'notification'),
|
||||||
|
|
||||||
|
@ -85,6 +85,19 @@ def retry_for_a_time(timeout, sleep_time, func, error_retry, *args):
|
|||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
|
||||||
|
def lock_file(path, timeout=15, sleep_time=0.2):
|
||||||
|
if iswindows:
|
||||||
|
return retry_for_a_time(
|
||||||
|
timeout, sleep_time, windows_open, windows_retry, path
|
||||||
|
)
|
||||||
|
f = unix_open(path)
|
||||||
|
retry_for_a_time(
|
||||||
|
timeout, sleep_time, fcntl.flock, unix_retry,
|
||||||
|
f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
||||||
|
)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
class ExclusiveFile(object):
|
class ExclusiveFile(object):
|
||||||
|
|
||||||
def __init__(self, path, timeout=15, sleep_time=0.2):
|
def __init__(self, path, timeout=15, sleep_time=0.2):
|
||||||
@ -95,17 +108,7 @@ class ExclusiveFile(object):
|
|||||||
self.sleep_time = sleep_time
|
self.sleep_time = sleep_time
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
if iswindows:
|
self.file = lock_file(self.path, self.timeout, self.sleep_time)
|
||||||
self.file = retry_for_a_time(
|
|
||||||
self.timeout, self.sleep_time, windows_open, windows_retry, self.path
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
f = unix_open(self.path)
|
|
||||||
retry_for_a_time(
|
|
||||||
self.timeout, self.sleep_time, fcntl.flock, unix_retry,
|
|
||||||
f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
|
||||||
)
|
|
||||||
self.file = f
|
|
||||||
return self.file
|
return self.file
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(self, type, value, traceback):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user