mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-08 06:05:04 -05:00
831 lines
32 KiB
Python
831 lines
32 KiB
Python
#!/usr/bin/env python
|
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
|
from __future__ import with_statement
|
|
|
|
__license__ = 'GPL v3'
|
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
|
__docformat__ = 'restructuredtext en'
|
|
|
|
|
|
'''The main GUI'''
|
|
|
|
import collections, os, sys, textwrap, time, gc
|
|
from Queue import Queue, Empty
|
|
from threading import Thread
|
|
from collections import OrderedDict
|
|
|
|
from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction,
|
|
QMenu, QIcon, pyqtSignal, QUrl,
|
|
QDialog, QSystemTrayIcon, QApplication)
|
|
|
|
from calibre import prints, force_unicode
|
|
from calibre.constants import __appname__, isosx, filesystem_encoding
|
|
from calibre.utils.config import prefs, dynamic
|
|
from calibre.utils.ipc.server import Server
|
|
from calibre.library.database2 import LibraryDatabase2
|
|
from calibre.customize.ui import interface_actions, available_store_plugins
|
|
from calibre.gui2 import (error_dialog, GetMetadata, open_url,
|
|
gprefs, max_available_height, config, info_dialog, Dispatcher,
|
|
question_dialog, warning_dialog)
|
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
|
from calibre.gui2.widgets import ProgressIndicator
|
|
from calibre.gui2.update import UpdateMixin
|
|
from calibre.gui2.main_window import MainWindow
|
|
from calibre.gui2.layout import MainWindowMixin
|
|
from calibre.gui2.device import DeviceMixin
|
|
from calibre.gui2.email import EmailMixin
|
|
from calibre.gui2.ebook_download import EbookDownloadMixin
|
|
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
|
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
|
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
|
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
|
from calibre.gui2.tag_browser.ui import TagBrowserMixin
|
|
from calibre.gui2.keyboard import Manager
|
|
from calibre.gui2.auto_add import AutoAdder
|
|
from calibre.library.sqlite import sqlite, DatabaseException
|
|
from calibre.gui2.proceed import ProceedQuestion
|
|
from calibre.gui2.dialogs.message_box import JobError
|
|
|
|
class Listener(Thread): # {{{
|
|
|
|
def __init__(self, listener):
|
|
Thread.__init__(self)
|
|
self.daemon = True
|
|
self.listener, self.queue = listener, Queue()
|
|
self._run = True
|
|
self.start()
|
|
|
|
def run(self):
|
|
if self.listener is None:
|
|
return
|
|
while self._run:
|
|
try:
|
|
conn = self.listener.accept()
|
|
msg = conn.recv()
|
|
self.queue.put(msg)
|
|
except:
|
|
continue
|
|
|
|
def close(self):
|
|
self._run = False
|
|
try:
|
|
self.listener.close()
|
|
except:
|
|
pass
|
|
|
|
# }}}
|
|
|
|
class SystemTrayIcon(QSystemTrayIcon): # {{{
|
|
|
|
tooltip_requested = pyqtSignal(object)
|
|
|
|
def __init__(self, icon, parent):
|
|
QSystemTrayIcon.__init__(self, icon, parent)
|
|
|
|
def event(self, ev):
|
|
if ev.type() == ev.ToolTip:
|
|
evh = QHelpEvent(ev)
|
|
self.tooltip_requested.emit(
|
|
(self, evh.globalPos()))
|
|
return True
|
|
return QSystemTrayIcon.event(self, ev)
|
|
|
|
# }}}
|
|
|
|
_gui = None
|
|
|
|
def get_gui():
|
|
return _gui
|
|
|
|
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
|
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
|
|
EbookDownloadMixin
|
|
):
|
|
'The main GUI'
|
|
|
|
proceed_requested = pyqtSignal(object, object)
|
|
|
|
def __init__(self, opts, parent=None, gui_debug=None):
|
|
global _gui
|
|
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
|
|
self.proceed_requested.connect(self.do_proceed,
|
|
type=Qt.QueuedConnection)
|
|
self.proceed_question = ProceedQuestion(self)
|
|
self.job_error_dialog = JobError(self)
|
|
self.keyboard = Manager(self)
|
|
_gui = self
|
|
self.opts = opts
|
|
self.device_connected = None
|
|
self.gui_debug = gui_debug
|
|
self.iactions = OrderedDict()
|
|
# Actions
|
|
for action in interface_actions():
|
|
if opts.ignore_plugins and action.plugin_path is not None:
|
|
continue
|
|
try:
|
|
ac = self.init_iaction(action)
|
|
except:
|
|
# Ignore errors in loading user supplied plugins
|
|
import traceback
|
|
traceback.print_exc()
|
|
if action.plugin_path is None:
|
|
raise
|
|
continue
|
|
ac.plugin_path = action.plugin_path
|
|
ac.interface_action_base_plugin = action
|
|
self.add_iaction(ac)
|
|
self.load_store_plugins()
|
|
|
|
def init_iaction(self, action):
|
|
ac = action.load_actual_plugin(self)
|
|
ac.plugin_path = action.plugin_path
|
|
ac.interface_action_base_plugin = action
|
|
action.actual_iaction_plugin_loaded = True
|
|
return ac
|
|
|
|
def add_iaction(self, ac):
|
|
acmap = self.iactions
|
|
if ac.name in acmap:
|
|
if ac.priority >= acmap[ac.name].priority:
|
|
acmap[ac.name] = ac
|
|
else:
|
|
acmap[ac.name] = ac
|
|
|
|
def load_store_plugins(self):
|
|
self.istores = OrderedDict()
|
|
for store in available_store_plugins():
|
|
if self.opts.ignore_plugins and store.plugin_path is not None:
|
|
continue
|
|
try:
|
|
st = self.init_istore(store)
|
|
self.add_istore(st)
|
|
except:
|
|
# Ignore errors in loading user supplied plugins
|
|
import traceback
|
|
traceback.print_exc()
|
|
if store.plugin_path is None:
|
|
raise
|
|
continue
|
|
|
|
def init_istore(self, store):
|
|
st = store.load_actual_plugin(self)
|
|
st.plugin_path = store.plugin_path
|
|
st.base_plugin = store
|
|
store.actual_istore_plugin_loaded = True
|
|
return st
|
|
|
|
def add_istore(self, st):
|
|
stmap = self.istores
|
|
if st.name in stmap:
|
|
if st.priority >= stmap[st.name].priority:
|
|
stmap[st.name] = st
|
|
else:
|
|
stmap[st.name] = st
|
|
|
|
|
|
def initialize(self, library_path, db, listener, actions, show_gui=True):
|
|
opts = self.opts
|
|
self.preferences_action, self.quit_action = actions
|
|
self.library_path = library_path
|
|
self.content_server = None
|
|
self.spare_servers = []
|
|
self.must_restart_before_config = False
|
|
self.listener = Listener(listener)
|
|
self.check_messages_timer = QTimer()
|
|
self.connect(self.check_messages_timer, SIGNAL('timeout()'),
|
|
self.another_instance_wants_to_talk)
|
|
self.check_messages_timer.start(1000)
|
|
|
|
for ac in self.iactions.values():
|
|
ac.do_genesis()
|
|
self.donate_action = QAction(QIcon(I('donate.png')),
|
|
_('&Donate to support calibre'), self)
|
|
for st in self.istores.values():
|
|
st.do_genesis()
|
|
MainWindowMixin.__init__(self, db)
|
|
|
|
# Jobs Button {{{
|
|
self.job_manager = JobManager()
|
|
self.jobs_dialog = JobsDialog(self, self.job_manager)
|
|
self.jobs_button = JobsButton(horizontal=True, parent=self)
|
|
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
|
|
# }}}
|
|
|
|
LayoutMixin.__init__(self)
|
|
EmailMixin.__init__(self)
|
|
EbookDownloadMixin.__init__(self)
|
|
DeviceMixin.__init__(self)
|
|
|
|
self.progress_indicator = ProgressIndicator(self)
|
|
self.progress_indicator.pos = (0, 20)
|
|
self.verbose = opts.verbose
|
|
self.get_metadata = GetMetadata()
|
|
self.upload_memory = {}
|
|
self.metadata_dialogs = []
|
|
self.default_thumbnail = None
|
|
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
|
self.viewers = collections.deque()
|
|
self.system_tray_icon = SystemTrayIcon(QIcon(I('lt.png')), self)
|
|
self.system_tray_icon.setToolTip('calibre')
|
|
self.system_tray_icon.tooltip_requested.connect(
|
|
self.job_manager.show_tooltip)
|
|
if not config['systray_icon']:
|
|
self.system_tray_icon.hide()
|
|
else:
|
|
self.system_tray_icon.show()
|
|
self.system_tray_menu = QMenu(self)
|
|
self.restore_action = self.system_tray_menu.addAction(
|
|
QIcon(I('page.png')), _('&Restore'))
|
|
self.system_tray_menu.addAction(self.donate_action)
|
|
self.donate_button.setDefaultAction(self.donate_action)
|
|
self.donate_button.setStatusTip(self.donate_button.toolTip())
|
|
self.eject_action = self.system_tray_menu.addAction(
|
|
QIcon(I('eject.png')), _('&Eject connected device'))
|
|
self.eject_action.setEnabled(False)
|
|
self.addAction(self.quit_action)
|
|
self.system_tray_menu.addAction(self.quit_action)
|
|
self.keyboard.register_shortcut('quit calibre', _('Quit calibre'),
|
|
default_keys=('Ctrl+Q',), action=self.quit_action)
|
|
self.system_tray_icon.setContextMenu(self.system_tray_menu)
|
|
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
|
|
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
|
|
self.connect(self.restore_action, SIGNAL('triggered()'),
|
|
self.show_windows)
|
|
self.system_tray_icon.activated.connect(
|
|
self.system_tray_icon_activated)
|
|
|
|
self.esc_action = QAction(self)
|
|
self.addAction(self.esc_action)
|
|
self.keyboard.register_shortcut('clear current search',
|
|
_('Clear the current search'), default_keys=('Esc',),
|
|
action=self.esc_action)
|
|
self.esc_action.triggered.connect(self.esc)
|
|
|
|
####################### Start spare job server ########################
|
|
QTimer.singleShot(1000, self.add_spare_server)
|
|
|
|
####################### Location Manager ########################
|
|
self.location_manager.location_selected.connect(self.location_selected)
|
|
self.location_manager.unmount_device.connect(self.device_manager.umount_device)
|
|
self.location_manager.configure_device.connect(self.configure_connected_device)
|
|
self.eject_action.triggered.connect(self.device_manager.umount_device)
|
|
|
|
#################### Update notification ###################
|
|
UpdateMixin.__init__(self, opts)
|
|
|
|
####################### Search boxes ########################
|
|
SavedSearchBoxMixin.__init__(self)
|
|
SearchBoxMixin.__init__(self)
|
|
|
|
####################### Library view ########################
|
|
LibraryViewMixin.__init__(self, db)
|
|
|
|
if show_gui:
|
|
self.show()
|
|
|
|
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
|
self.hide_windows()
|
|
self.library_view.model().count_changed_signal.connect(
|
|
self.iactions['Choose Library'].count_changed)
|
|
if not gprefs.get('quick_start_guide_added', False):
|
|
from calibre.ebooks.metadata.meta import get_metadata
|
|
mi = get_metadata(open(P('quick_start.epub'), 'rb'), 'epub')
|
|
self.library_view.model().add_books([P('quick_start.epub')], ['epub'],
|
|
[mi])
|
|
gprefs['quick_start_guide_added'] = True
|
|
self.library_view.model().books_added(1)
|
|
if hasattr(self, 'db_images'):
|
|
self.db_images.reset()
|
|
if self.library_view.model().rowCount(None) < 3:
|
|
self.library_view.resizeColumnsToContents()
|
|
|
|
self.library_view.model().count_changed()
|
|
self.bars_manager.database_changed(self.library_view.model().db)
|
|
self.library_view.model().database_changed.connect(self.bars_manager.database_changed,
|
|
type=Qt.QueuedConnection)
|
|
|
|
########################### Tags Browser ##############################
|
|
TagBrowserMixin.__init__(self, db)
|
|
|
|
######################### Search Restriction ##########################
|
|
SearchRestrictionMixin.__init__(self)
|
|
if db.prefs['gui_restriction']:
|
|
self.apply_named_search_restriction(db.prefs['gui_restriction'])
|
|
|
|
########################### Cover Flow ################################
|
|
|
|
CoverFlowMixin.__init__(self)
|
|
|
|
self._calculated_available_height = min(max_available_height()-15,
|
|
self.height())
|
|
self.resize(self.width(), self._calculated_available_height)
|
|
|
|
self.build_context_menus()
|
|
|
|
for ac in self.iactions.values():
|
|
try:
|
|
ac.gui_layout_complete()
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
if ac.plugin_path is None:
|
|
raise
|
|
|
|
if config['autolaunch_server']:
|
|
self.start_content_server()
|
|
|
|
|
|
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
|
|
|
self.read_settings()
|
|
self.finalize_layout()
|
|
if self.bars_manager.showing_donate:
|
|
self.donate_button.start_animation()
|
|
self.set_window_title()
|
|
|
|
for ac in self.iactions.values():
|
|
try:
|
|
ac.initialization_complete()
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
if ac.plugin_path is None:
|
|
raise
|
|
self.device_manager.set_current_library_uuid(db.library_id)
|
|
|
|
self.keyboard.finalize()
|
|
self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)
|
|
|
|
self.save_layout_state()
|
|
|
|
# Collect cycles now
|
|
gc.collect()
|
|
|
|
if show_gui and self.gui_debug is not None:
|
|
info_dialog(self, _('Debug mode'), '<p>' +
|
|
_('You have started calibre in debug mode. After you '
|
|
'quit calibre, the debug log will be available in '
|
|
'the file: %s<p>The '
|
|
'log will be displayed automatically.')%self.gui_debug, show=True)
|
|
|
|
self.iactions['Connect Share'].check_smartdevice_menus()
|
|
QTimer.singleShot(1, self.start_smartdevice)
|
|
|
|
def esc(self, *args):
|
|
self.clear_button.click()
|
|
|
|
def start_smartdevice(self):
|
|
message = None
|
|
if self.device_manager.get_option('smartdevice', 'autostart'):
|
|
try:
|
|
message = self.device_manager.start_plugin('smartdevice')
|
|
except:
|
|
message = 'start smartdevice unknown exception'
|
|
prints(message)
|
|
import traceback
|
|
traceback.print_exc()
|
|
if message:
|
|
if not self.device_manager.is_running('Wireless Devices'):
|
|
error_dialog(self, _('Problem starting the wireless device'),
|
|
_('The wireless device driver did not start. '
|
|
'It said "%s"')%message, show=True)
|
|
self.iactions['Connect Share'].set_smartdevice_action_state()
|
|
|
|
def start_content_server(self, check_started=True):
|
|
from calibre.library.server.main import start_threaded_server
|
|
from calibre.library.server import server_config
|
|
self.content_server = start_threaded_server(
|
|
self.library_view.model().db, server_config().parse())
|
|
self.content_server.state_callback = Dispatcher(
|
|
self.iactions['Connect Share'].content_server_state_changed)
|
|
if check_started:
|
|
self.content_server.start_failure_callback = \
|
|
Dispatcher(self.content_server_start_failed)
|
|
|
|
def content_server_start_failed(self, msg):
|
|
error_dialog(self, _('Failed to start Content Server'),
|
|
_('Could not start the content server. Error:\n\n%s')%msg,
|
|
show=True)
|
|
|
|
def resizeEvent(self, ev):
|
|
MainWindow.resizeEvent(self, ev)
|
|
self.search.setMaximumWidth(self.width()-150)
|
|
|
|
def add_spare_server(self, *args):
|
|
self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0)))
|
|
|
|
@property
|
|
def spare_server(self):
|
|
# Because of the use of the property decorator, we're called one
|
|
# extra time. Ignore.
|
|
if not hasattr(self, '__spare_server_property_limiter'):
|
|
self.__spare_server_property_limiter = True
|
|
return None
|
|
try:
|
|
QTimer.singleShot(1000, self.add_spare_server)
|
|
return self.spare_servers.pop()
|
|
except:
|
|
pass
|
|
|
|
def do_proceed(self, func, payload):
|
|
if callable(func):
|
|
func(payload)
|
|
|
|
def no_op(self, *args):
|
|
pass
|
|
|
|
def system_tray_icon_activated(self, r):
|
|
if r == QSystemTrayIcon.Trigger:
|
|
if self.isVisible():
|
|
self.hide_windows()
|
|
else:
|
|
self.show_windows()
|
|
|
|
@property
|
|
def is_minimized_to_tray(self):
|
|
return getattr(self, '__systray_minimized', False)
|
|
|
|
def ask_a_yes_no_question(self, title, msg, det_msg='',
|
|
show_copy_button=False, ans_when_user_unavailable=True,
|
|
skip_dialog_name=None, skipped_value=True):
|
|
if self.is_minimized_to_tray:
|
|
return ans_when_user_unavailable
|
|
return question_dialog(self, title, msg, det_msg=det_msg,
|
|
show_copy_button=show_copy_button,
|
|
skip_dialog_name=skip_dialog_name,
|
|
skip_dialog_skipped_value=skipped_value)
|
|
|
|
def hide_windows(self):
|
|
for window in QApplication.topLevelWidgets():
|
|
if isinstance(window, (MainWindow, QDialog)) and \
|
|
window.isVisible():
|
|
window.hide()
|
|
setattr(window, '__systray_minimized', True)
|
|
|
|
def show_windows(self):
|
|
for window in QApplication.topLevelWidgets():
|
|
if getattr(window, '__systray_minimized', False):
|
|
window.show()
|
|
setattr(window, '__systray_minimized', False)
|
|
|
|
def test_server(self, *args):
|
|
if self.content_server is not None and \
|
|
self.content_server.exception is not None:
|
|
error_dialog(self, _('Failed to start content server'),
|
|
unicode(self.content_server.exception)).exec_()
|
|
|
|
@property
|
|
def current_db(self):
|
|
return self.library_view.model().db
|
|
|
|
def another_instance_wants_to_talk(self):
|
|
try:
|
|
msg = self.listener.queue.get_nowait()
|
|
except Empty:
|
|
return
|
|
if msg.startswith('launched:'):
|
|
argv = eval(msg[len('launched:'):])
|
|
if len(argv) > 1:
|
|
path = os.path.abspath(argv[1])
|
|
if os.access(path, os.R_OK):
|
|
self.iactions['Add Books'].add_filesystem_book(path)
|
|
self.setWindowState(self.windowState() & \
|
|
~Qt.WindowMinimized|Qt.WindowActive)
|
|
self.show_windows()
|
|
self.raise_()
|
|
self.activateWindow()
|
|
elif msg.startswith('refreshdb:'):
|
|
self.library_view.model().refresh()
|
|
self.library_view.model().research()
|
|
self.tags_view.recount()
|
|
self.library_view.model().db.refresh_format_cache()
|
|
elif msg.startswith('shutdown:'):
|
|
self.quit(confirm_quit=False)
|
|
else:
|
|
print msg
|
|
|
|
def current_view(self):
|
|
'''Convenience method that returns the currently visible view '''
|
|
idx = self.stack.currentIndex()
|
|
if idx == 0:
|
|
return self.library_view
|
|
if idx == 1:
|
|
return self.memory_view
|
|
if idx == 2:
|
|
return self.card_a_view
|
|
if idx == 3:
|
|
return self.card_b_view
|
|
|
|
def booklists(self):
|
|
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
|
|
|
def library_moved(self, newloc, copy_structure=False, call_close=True,
|
|
allow_rebuild=False):
|
|
if newloc is None: return
|
|
default_prefs = None
|
|
try:
|
|
olddb = self.library_view.model().db
|
|
if copy_structure:
|
|
default_prefs = olddb.prefs
|
|
except:
|
|
olddb = None
|
|
try:
|
|
db = LibraryDatabase2(newloc, default_prefs=default_prefs)
|
|
except (DatabaseException, sqlite.Error):
|
|
if not allow_rebuild: raise
|
|
import traceback
|
|
repair = question_dialog(self, _('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.')
|
|
% force_unicode(newloc, filesystem_encoding),
|
|
det_msg=traceback.format_exc()
|
|
)
|
|
if repair:
|
|
from calibre.gui2.dialogs.restore_library import repair_library_at
|
|
if repair_library_at(newloc, parent=self):
|
|
db = LibraryDatabase2(newloc, default_prefs=default_prefs)
|
|
else:
|
|
return
|
|
else:
|
|
return
|
|
if self.content_server is not None:
|
|
self.content_server.set_database(db)
|
|
self.library_path = newloc
|
|
prefs['library_path'] = self.library_path
|
|
self.book_on_device(None, reset=True)
|
|
db.set_book_on_device_func(self.book_on_device)
|
|
self.library_view.set_database(db)
|
|
self.tags_view.set_database(db, self.alter_tb)
|
|
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
|
self.status_bar.clear_message()
|
|
self.search.clear()
|
|
self.saved_search.clear()
|
|
self.book_details.reset_info()
|
|
#self.library_view.model().count_changed()
|
|
db = self.library_view.model().db
|
|
self.iactions['Choose Library'].count_changed(db.count())
|
|
self.set_window_title()
|
|
self.apply_named_search_restriction('') # reset restriction to null
|
|
self.saved_searches_changed(recount=False) # reload the search restrictions combo box
|
|
self.apply_named_search_restriction(db.prefs['gui_restriction'])
|
|
for action in self.iactions.values():
|
|
action.library_changed(db)
|
|
if olddb is not None:
|
|
try:
|
|
if call_close:
|
|
olddb.conn.close()
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
olddb.break_cycles()
|
|
if self.device_connected:
|
|
self.set_books_in_library(self.booklists(), reset=True)
|
|
self.refresh_ondevice()
|
|
self.memory_view.reset()
|
|
self.card_a_view.reset()
|
|
self.card_b_view.reset()
|
|
self.device_manager.set_current_library_uuid(db.library_id)
|
|
self.library_view.set_current_row(0)
|
|
# Run a garbage collection now so that it does not freeze the
|
|
# interface later
|
|
gc.collect()
|
|
|
|
|
|
def set_window_title(self):
|
|
self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name())
|
|
|
|
def location_selected(self, location):
|
|
'''
|
|
Called when a location icon is clicked (e.g. Library)
|
|
'''
|
|
page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
|
|
self.stack.setCurrentIndex(page)
|
|
self.book_details.reset_info()
|
|
for x in ('tb', 'cb'):
|
|
splitter = getattr(self, x+'_splitter')
|
|
splitter.button.setEnabled(location == 'library')
|
|
for action in self.iactions.values():
|
|
action.location_selected(location)
|
|
if location == 'library':
|
|
self.search_restriction.setEnabled(True)
|
|
self.highlight_only_button.setEnabled(True)
|
|
else:
|
|
self.search_restriction.setEnabled(False)
|
|
self.highlight_only_button.setEnabled(False)
|
|
# Reset the view in case something changed while it was invisible
|
|
self.current_view().reset()
|
|
self.set_number_of_books_shown()
|
|
|
|
|
|
|
|
def job_exception(self, job, dialog_title=_('Conversion Error')):
|
|
if not hasattr(self, '_modeless_dialogs'):
|
|
self._modeless_dialogs = []
|
|
minz = self.is_minimized_to_tray
|
|
if self.isVisible():
|
|
for x in list(self._modeless_dialogs):
|
|
if not x.isVisible():
|
|
self._modeless_dialogs.remove(x)
|
|
try:
|
|
if 'calibre.ebooks.DRMError' in job.details:
|
|
if not minz:
|
|
from calibre.gui2.dialogs.drm_error import DRMErrorMessage
|
|
d = DRMErrorMessage(self, _('Cannot convert') + ' ' +
|
|
job.description.split(':')[-1].partition('(')[-1][:-1])
|
|
d.setModal(False)
|
|
d.show()
|
|
self._modeless_dialogs.append(d)
|
|
return
|
|
|
|
if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details:
|
|
title = job.description.split(':')[-1].partition('(')[-1][:-1]
|
|
msg = _('<p><b>Failed to convert: %s')%title
|
|
msg += '<p>'+_('''
|
|
Many older ebook reader devices are incapable of displaying
|
|
EPUB files that have internal components over a certain size.
|
|
Therefore, when converting to EPUB, calibre automatically tries
|
|
to split up the EPUB into smaller sized pieces. For some
|
|
files that are large undifferentiated blocks of text, this
|
|
splitting fails.
|
|
<p>You can <b>work around the problem</b> by either increasing the
|
|
maximum split size under EPUB Output in the conversion dialog,
|
|
or by turning on Heuristic Processing, also in the conversion
|
|
dialog. Note that if you make the maximum split size too large,
|
|
your ebook reader may have trouble with the EPUB.
|
|
''')
|
|
if not minz:
|
|
d = error_dialog(self, _('Conversion Failed'), msg,
|
|
det_msg=job.details)
|
|
d.setModal(False)
|
|
d.show()
|
|
self._modeless_dialogs.append(d)
|
|
return
|
|
|
|
if 'calibre.web.feeds.input.RecipeDisabled' in job.details:
|
|
if not minz:
|
|
msg = job.details
|
|
msg = msg[msg.find('calibre.web.feeds.input.RecipeDisabled:'):]
|
|
msg = msg.partition(':')[-1]
|
|
d = error_dialog(self, _('Recipe Disabled'),
|
|
'<p>%s</p>'%msg)
|
|
d.setModal(False)
|
|
d.show()
|
|
self._modeless_dialogs.append(d)
|
|
return
|
|
|
|
if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details:
|
|
if not minz:
|
|
import json
|
|
payload = job.details.rpartition(
|
|
'calibre.ebooks.conversion.ConversionUserFeedBack:')[-1]
|
|
payload = json.loads('{' + payload.partition('{')[-1])
|
|
d = {'info':info_dialog, 'warn':warning_dialog,
|
|
'error':error_dialog}.get(payload['level'],
|
|
error_dialog)
|
|
d = d(self, payload['title'],
|
|
'<p>%s</p>'%payload['msg'],
|
|
det_msg=payload['det_msg'])
|
|
d.setModal(False)
|
|
d.show()
|
|
self._modeless_dialogs.append(d)
|
|
return
|
|
except:
|
|
pass
|
|
if job.killed:
|
|
return
|
|
try:
|
|
prints(job.details, file=sys.stderr)
|
|
except:
|
|
pass
|
|
if not minz:
|
|
self.job_error_dialog.show_error(dialog_title,
|
|
_('<b>Failed</b>')+': '+unicode(job.description),
|
|
det_msg=job.details)
|
|
|
|
def read_settings(self):
|
|
geometry = config['main_window_geometry']
|
|
if geometry is not None:
|
|
self.restoreGeometry(geometry)
|
|
self.read_layout_settings()
|
|
|
|
def write_settings(self):
|
|
with gprefs: # Only write to gprefs once
|
|
config.set('main_window_geometry', self.saveGeometry())
|
|
dynamic.set('sort_history', self.library_view.model().sort_history)
|
|
self.save_layout_state()
|
|
|
|
def quit(self, checked=True, restart=False, debug_on_restart=False,
|
|
confirm_quit=True):
|
|
if confirm_quit and not self.confirm_quit():
|
|
return
|
|
try:
|
|
self.shutdown()
|
|
except:
|
|
pass
|
|
self.restart_after_quit = restart
|
|
self.debug_on_restart = debug_on_restart
|
|
QApplication.instance().quit()
|
|
|
|
def donate(self, *args):
|
|
open_url(QUrl('http://calibre-ebook.com/donate'))
|
|
|
|
def confirm_quit(self):
|
|
if self.job_manager.has_jobs():
|
|
msg = _('There are active jobs. Are you sure you want to quit?')
|
|
if self.job_manager.has_device_jobs():
|
|
msg = '<p>'+__appname__ + \
|
|
_(''' is communicating with the device!<br>
|
|
Quitting may cause corruption on the device.<br>
|
|
Are you sure you want to quit?''')+'</p>'
|
|
|
|
if not question_dialog(self, _('Active jobs'), msg):
|
|
return False
|
|
return True
|
|
|
|
|
|
def shutdown(self, write_settings=True):
|
|
try:
|
|
db = self.library_view.model().db
|
|
cf = db.clean
|
|
except:
|
|
pass
|
|
else:
|
|
cf()
|
|
# Save the current field_metadata for applications like calibre2opds
|
|
# Goes here, because if cf is valid, db is valid.
|
|
db.prefs['field_metadata'] = db.field_metadata.all_metadata()
|
|
db.commit_dirty_cache()
|
|
db.prefs.write_serialized(prefs['library_path'])
|
|
for action in self.iactions.values():
|
|
if not action.shutting_down():
|
|
return
|
|
if write_settings:
|
|
self.write_settings()
|
|
self.check_messages_timer.stop()
|
|
self.update_checker.terminate()
|
|
self.listener.close()
|
|
self.job_manager.server.close()
|
|
self.job_manager.threaded_server.close()
|
|
while self.spare_servers:
|
|
self.spare_servers.pop().close()
|
|
self.device_manager.keep_going = False
|
|
self.auto_adder.stop()
|
|
mb = self.library_view.model().metadata_backup
|
|
if mb is not None:
|
|
mb.stop()
|
|
|
|
self.hide_windows()
|
|
try:
|
|
try:
|
|
if self.content_server is not None:
|
|
s = self.content_server
|
|
self.content_server = None
|
|
s.exit()
|
|
except:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
pass
|
|
time.sleep(2)
|
|
self.hide_windows()
|
|
# Do not report any errors that happen after the shutdown
|
|
sys.excepthook = sys.__excepthook__
|
|
return True
|
|
|
|
def run_wizard(self, *args):
|
|
if self.confirm_quit():
|
|
self.run_wizard_b4_shutdown = True
|
|
self.restart_after_quit = True
|
|
try:
|
|
self.shutdown(write_settings=False)
|
|
except:
|
|
pass
|
|
QApplication.instance().quit()
|
|
|
|
|
|
|
|
def closeEvent(self, e):
|
|
self.write_settings()
|
|
if self.system_tray_icon.isVisible():
|
|
if not dynamic['systray_msg'] and not isosx:
|
|
info_dialog(self, 'calibre', 'calibre '+ \
|
|
_('will keep running in the system tray. To close it, '
|
|
'choose <b>Quit</b> in the context menu of the '
|
|
'system tray.'), show_copy_button=False).exec_()
|
|
dynamic['systray_msg'] = True
|
|
self.hide_windows()
|
|
e.ignore()
|
|
else:
|
|
if self.confirm_quit():
|
|
try:
|
|
self.shutdown(write_settings=False)
|
|
except:
|
|
pass
|
|
e.accept()
|
|
else:
|
|
e.ignore()
|
|
|
|
# }}}
|
|
|