mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
1889 lines
79 KiB
Python
1889 lines
79 KiB
Python
from __future__ import with_statement
|
|
__license__ = 'GPL v3'
|
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
'''The main GUI'''
|
|
import os, sys, textwrap, collections, traceback, time, socket
|
|
from xml.parsers.expat import ExpatError
|
|
from Queue import Queue, Empty
|
|
from threading import Thread
|
|
from functools import partial
|
|
from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
|
|
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
|
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
|
QMessageBox, QStackedLayout
|
|
from PyQt4.QtSvg import QSvgRenderer
|
|
|
|
from calibre import __version__, __appname__, sanitize_file_name, \
|
|
iswindows, isosx, prints, patheq
|
|
from calibre.ptempfile import PersistentTemporaryFile
|
|
from calibre.utils.config import prefs, dynamic
|
|
from calibre.utils.ipc.server import Server
|
|
from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
|
|
initialize_file_icon_provider, question_dialog,\
|
|
pixmap_to_data, choose_dir, ORG_NAME, \
|
|
set_sidebar_directories, Dispatcher, \
|
|
Application, available_height, \
|
|
max_available_height, config, info_dialog, \
|
|
available_width, GetMetadata
|
|
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
|
|
from calibre.gui2.widgets import ProgressIndicator
|
|
from calibre.gui2.wizard import move_library
|
|
from calibre.gui2.dialogs.scheduler import Scheduler
|
|
from calibre.gui2.update import CheckForUpdates
|
|
from calibre.gui2.main_window import MainWindow, option_parser as _option_parser
|
|
from calibre.gui2.main_ui import Ui_MainWindow
|
|
from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceGUI, Emailer
|
|
from calibre.gui2.status import StatusBar
|
|
from calibre.gui2.jobs import JobManager, JobsDialog
|
|
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
|
|
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
|
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
|
|
fetch_scheduled_recipe
|
|
from calibre.gui2.dialogs.config import ConfigDialog
|
|
from calibre.gui2.dialogs.search import SearchDialog
|
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
|
from calibre.gui2.dialogs.book_info import BookInfo
|
|
from calibre.ebooks import BOOK_EXTENSIONS
|
|
from calibre.library.database2 import LibraryDatabase2, CoverCache
|
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
|
|
|
ADDRESS = r'\\.\pipe\CalibreGUI' if iswindows else \
|
|
os.path.expanduser('~/.calibre-gui.socket')
|
|
|
|
class SaveMenu(QMenu):
|
|
|
|
def __init__(self, parent):
|
|
QMenu.__init__(self, _('Save single format to disk...'), parent)
|
|
for ext in sorted(BOOK_EXTENSIONS):
|
|
action = self.addAction(ext.upper())
|
|
setattr(self, 'do_'+ext, partial(self.do, ext))
|
|
self.connect(action, SIGNAL('triggered(bool)'),
|
|
getattr(self, 'do_'+ext))
|
|
|
|
def do(self, ext, *args):
|
|
self.emit(SIGNAL('save_fmt(PyQt_PyObject)'), ext)
|
|
|
|
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):
|
|
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 Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|
'The main GUI'
|
|
|
|
def set_default_thumbnail(self, height):
|
|
r = QSvgRenderer(':/images/book.svg')
|
|
pixmap = QPixmap(height, height)
|
|
pixmap.fill(QColor(255,255,255))
|
|
p = QPainter(pixmap)
|
|
r.render(p)
|
|
p.end()
|
|
self.default_thumbnail = (pixmap.width(), pixmap.height(),
|
|
pixmap_to_data(pixmap))
|
|
|
|
def __init__(self, listener, opts, actions, parent=None):
|
|
self.preferences_action, self.quit_action = actions
|
|
self.spare_servers = []
|
|
MainWindow.__init__(self, opts, parent)
|
|
# Initialize fontconfig in a separate thread as this can be a lengthy
|
|
# process if run for the first time on this machine
|
|
from calibre.utils.fonts import fontconfig
|
|
self.fc = fontconfig
|
|
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)
|
|
|
|
Ui_MainWindow.__init__(self)
|
|
self.setupUi(self)
|
|
self.setWindowTitle(__appname__)
|
|
self.progress_indicator = ProgressIndicator(self)
|
|
self.verbose = opts.verbose
|
|
self.get_metadata = GetMetadata()
|
|
self.read_settings()
|
|
self.job_manager = JobManager()
|
|
self.emailer = Emailer()
|
|
self.emailer.start()
|
|
self.jobs_dialog = JobsDialog(self, self.job_manager)
|
|
self.upload_memory = {}
|
|
self.delete_memory = {}
|
|
self.conversion_jobs = {}
|
|
self.persistent_files = []
|
|
self.metadata_dialogs = []
|
|
self.default_thumbnail = None
|
|
self.device_error_dialog = error_dialog(self, _('Error'),
|
|
_('Error communicating with device'), ' ')
|
|
self.device_error_dialog.setModal(Qt.NonModal)
|
|
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
|
self.device_connected = False
|
|
self.viewers = collections.deque()
|
|
self.content_server = None
|
|
self.system_tray_icon = QSystemTrayIcon(QIcon(':/library'), self)
|
|
self.system_tray_icon.setToolTip('calibre')
|
|
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(':/images/page.svg'), _('&Restore'))
|
|
self.donate_action = self.system_tray_menu.addAction(
|
|
QIcon(':/images/donate.svg'), _('&Donate to support calibre'))
|
|
self.donate_button.setDefaultAction(self.donate_action)
|
|
if not config['show_donate_button']:
|
|
self.donate_button.setVisible(False)
|
|
self.addAction(self.quit_action)
|
|
self.action_restart = QAction(_('&Restart'), self)
|
|
self.addAction(self.action_restart)
|
|
self.system_tray_menu.addAction(self.quit_action)
|
|
self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q))
|
|
self.action_restart.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
|
|
self.action_show_book_details.setShortcut(QKeySequence(Qt.Key_I))
|
|
self.addAction(self.action_show_book_details)
|
|
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.connect(self.action_show_book_details,
|
|
SIGNAL('triggered(bool)'), self.show_book_info)
|
|
self.connect(self.action_restart, SIGNAL('triggered()'),
|
|
self.restart)
|
|
self.connect(self.system_tray_icon,
|
|
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
|
self.system_tray_icon_activated)
|
|
self.tool_bar.contextMenuEvent = self.no_op
|
|
|
|
####################### Start spare job server ########################
|
|
QTimer.singleShot(1000, self.add_spare_server)
|
|
|
|
####################### Setup device detection ########################
|
|
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
|
self.job_manager)
|
|
self.device_manager.start()
|
|
|
|
|
|
####################### Location View ########################
|
|
QObject.connect(self.location_view,
|
|
SIGNAL('location_selected(PyQt_PyObject)'),
|
|
self.location_selected)
|
|
QObject.connect(self.location_view,
|
|
SIGNAL('umount_device()'),
|
|
self.device_manager.umount_device)
|
|
|
|
####################### Vanity ########################
|
|
self.vanity_template = _('<p>For help visit <a href="http://%s.'
|
|
'kovidgoyal.net/user_manual">%s.kovidgoyal.net</a>'
|
|
'<br>')%(__appname__, __appname__)
|
|
self.vanity_template += _('<b>%s</b>: %s by <b>Kovid Goyal '
|
|
'%%(version)s</b><br>%%(device)s</p>')%(__appname__, __version__)
|
|
self.latest_version = ' '
|
|
self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
|
|
self.device_info = ' '
|
|
self.update_checker = CheckForUpdates()
|
|
QObject.connect(self.update_checker,
|
|
SIGNAL('update_found(PyQt_PyObject)'), self.update_found)
|
|
self.update_checker.start()
|
|
####################### Status Bar #####################
|
|
self.status_bar = StatusBar(self.jobs_dialog, self.system_tray_icon)
|
|
self.setStatusBar(self.status_bar)
|
|
QObject.connect(self.job_manager, SIGNAL('job_added(int)'),
|
|
self.status_bar.job_added, Qt.QueuedConnection)
|
|
QObject.connect(self.job_manager, SIGNAL('job_done(int)'),
|
|
self.status_bar.job_done, Qt.QueuedConnection)
|
|
QObject.connect(self.status_bar, SIGNAL('show_book_info()'),
|
|
self.show_book_info)
|
|
####################### Setup Toolbar #####################
|
|
md = QMenu()
|
|
md.addAction(_('Edit metadata individually'))
|
|
md.addSeparator()
|
|
md.addAction(_('Edit metadata in bulk'))
|
|
md.addSeparator()
|
|
md.addAction(_('Download metadata and covers'))
|
|
md.addAction(_('Download only metadata'))
|
|
md.addAction(_('Download only covers'))
|
|
self.metadata_menu = md
|
|
self.add_menu = QMenu()
|
|
self.add_menu.addAction(_('Add books from a single directory'))
|
|
self.add_menu.addAction(_('Add books from directories, including '
|
|
'sub-directories (One book per directory, assumes every ebook '
|
|
'file is the same book in a different format)'))
|
|
self.add_menu.addAction(_('Add books from directories, including '
|
|
'sub directories (Multiple books per directory, assumes every '
|
|
'ebook file is a different book)'))
|
|
self.action_add.setMenu(self.add_menu)
|
|
QObject.connect(self.action_add, SIGNAL("triggered(bool)"),
|
|
self.add_books)
|
|
QObject.connect(self.add_menu.actions()[0], SIGNAL("triggered(bool)"),
|
|
self.add_books)
|
|
QObject.connect(self.add_menu.actions()[1], SIGNAL("triggered(bool)"),
|
|
self.add_recursive_single)
|
|
QObject.connect(self.add_menu.actions()[2], SIGNAL("triggered(bool)"),
|
|
self.add_recursive_multiple)
|
|
QObject.connect(self.action_del, SIGNAL("triggered(bool)"),
|
|
self.delete_books)
|
|
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"),
|
|
self.edit_metadata)
|
|
self.__em1__ = partial(self.edit_metadata, bulk=False)
|
|
QObject.connect(md.actions()[0], SIGNAL('triggered(bool)'),
|
|
self.__em1__)
|
|
self.__em2__ = partial(self.edit_metadata, bulk=True)
|
|
QObject.connect(md.actions()[2], SIGNAL('triggered(bool)'),
|
|
self.__em2__)
|
|
self.__em3__ = partial(self.download_metadata, covers=True)
|
|
QObject.connect(md.actions()[4], SIGNAL('triggered(bool)'),
|
|
self.__em3__)
|
|
self.__em4__ = partial(self.download_metadata, covers=False)
|
|
QObject.connect(md.actions()[5], SIGNAL('triggered(bool)'),
|
|
self.__em4__)
|
|
self.__em5__ = partial(self.download_metadata, covers=True,
|
|
set_metadata=False)
|
|
QObject.connect(md.actions()[6], SIGNAL('triggered(bool)'),
|
|
self.__em5__)
|
|
|
|
|
|
|
|
self.save_menu = QMenu()
|
|
self.save_menu.addAction(_('Save to disk'))
|
|
self.save_menu.addAction(_('Save to disk in a single directory'))
|
|
self.save_menu.addAction(_('Save only %s format to disk')%
|
|
prefs['output_format'].upper())
|
|
self.save_sub_menu = SaveMenu(self)
|
|
self.save_menu.addMenu(self.save_sub_menu)
|
|
self.connect(self.save_sub_menu, SIGNAL('save_fmt(PyQt_PyObject)'),
|
|
self.save_specific_format_disk)
|
|
|
|
self.view_menu = QMenu()
|
|
self.view_menu.addAction(_('View'))
|
|
self.view_menu.addAction(_('View specific format'))
|
|
self.action_view.setMenu(self.view_menu)
|
|
QObject.connect(self.action_save, SIGNAL("triggered(bool)"),
|
|
self.save_to_disk)
|
|
QObject.connect(self.save_menu.actions()[0], SIGNAL("triggered(bool)"),
|
|
self.save_to_disk)
|
|
QObject.connect(self.save_menu.actions()[1], SIGNAL("triggered(bool)"),
|
|
self.save_to_single_dir)
|
|
QObject.connect(self.save_menu.actions()[2], SIGNAL("triggered(bool)"),
|
|
self.save_single_format_to_disk)
|
|
QObject.connect(self.action_view, SIGNAL("triggered(bool)"),
|
|
self.view_book)
|
|
QObject.connect(self.view_menu.actions()[0],
|
|
SIGNAL("triggered(bool)"), self.view_book)
|
|
QObject.connect(self.view_menu.actions()[1],
|
|
SIGNAL("triggered(bool)"), self.view_specific_format)
|
|
self.connect(self.action_open_containing_folder,
|
|
SIGNAL('triggered(bool)'), self.view_folder)
|
|
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
|
self.addAction(self.action_open_containing_folder)
|
|
self.action_sync.setShortcut(Qt.Key_D)
|
|
self.action_sync.setEnabled(True)
|
|
self.create_device_menu()
|
|
self.action_edit.setMenu(md)
|
|
self.action_save.setMenu(self.save_menu)
|
|
cm = QMenu()
|
|
cm.addAction(_('Convert individually'))
|
|
cm.addAction(_('Bulk convert'))
|
|
self.action_convert.setMenu(cm)
|
|
QObject.connect(cm.actions()[0],
|
|
SIGNAL('triggered(bool)'), self.convert_single)
|
|
QObject.connect(cm.actions()[1],
|
|
SIGNAL('triggered(bool)'), self.convert_bulk)
|
|
QObject.connect(self.action_convert,
|
|
SIGNAL('triggered(bool)'), self.convert_single)
|
|
self.convert_menu = cm
|
|
pm = QMenu()
|
|
ap = self.action_preferences
|
|
pm.addAction(ap.icon(), ap.text())
|
|
pm.addAction(self.preferences_action)
|
|
pm.addAction(_('Run welcome wizard'))
|
|
self.connect(pm.actions()[1], SIGNAL('triggered(bool)'),
|
|
self.run_wizard)
|
|
self.connect(pm.actions()[0], SIGNAL('triggered(bool)'),
|
|
self.do_config)
|
|
|
|
self.action_preferences.setMenu(pm)
|
|
self.preferences_menu = pm
|
|
|
|
self.tool_bar.widgetForAction(self.action_news).\
|
|
setPopupMode(QToolButton.MenuButtonPopup)
|
|
self.tool_bar.widgetForAction(self.action_edit).\
|
|
setPopupMode(QToolButton.MenuButtonPopup)
|
|
self.tool_bar.widgetForAction(self.action_sync).\
|
|
setPopupMode(QToolButton.MenuButtonPopup)
|
|
self.tool_bar.widgetForAction(self.action_convert).\
|
|
setPopupMode(QToolButton.MenuButtonPopup)
|
|
self.tool_bar.widgetForAction(self.action_save).\
|
|
setPopupMode(QToolButton.MenuButtonPopup)
|
|
self.tool_bar.widgetForAction(self.action_add).\
|
|
setPopupMode(QToolButton.MenuButtonPopup)
|
|
self.tool_bar.widgetForAction(self.action_view).\
|
|
setPopupMode(QToolButton.MenuButtonPopup)
|
|
self.tool_bar.widgetForAction(self.action_preferences).\
|
|
setPopupMode(QToolButton.MenuButtonPopup)
|
|
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
|
|
|
|
self.connect(self.preferences_action, SIGNAL('triggered(bool)'),
|
|
self.do_config)
|
|
self.connect(self.action_preferences, SIGNAL('triggered(bool)'),
|
|
self.do_config)
|
|
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
|
|
self.do_advanced_search)
|
|
|
|
####################### Library view ########################
|
|
similar_menu = QMenu(_('Similar books...'))
|
|
similar_menu.addAction(self.action_books_by_same_author)
|
|
similar_menu.addAction(self.action_books_in_this_series)
|
|
similar_menu.addAction(self.action_books_with_the_same_tags)
|
|
similar_menu.addAction(self.action_books_by_this_publisher)
|
|
self.action_books_by_same_author.setShortcut(Qt.ALT + Qt.Key_A)
|
|
self.action_books_in_this_series.setShortcut(Qt.ALT + Qt.Key_S)
|
|
self.action_books_by_this_publisher.setShortcut(Qt.ALT + Qt.Key_P)
|
|
self.action_books_with_the_same_tags.setShortcut(Qt.ALT+Qt.Key_T)
|
|
self.addAction(self.action_books_by_same_author)
|
|
self.addAction(self.action_books_by_this_publisher)
|
|
self.addAction(self.action_books_in_this_series)
|
|
self.addAction(self.action_books_with_the_same_tags)
|
|
self.similar_menu = similar_menu
|
|
self.connect(self.action_books_by_same_author, SIGNAL('triggered()'),
|
|
lambda : self.show_similar_books('author'))
|
|
self.connect(self.action_books_in_this_series, SIGNAL('triggered()'),
|
|
lambda : self.show_similar_books('series'))
|
|
self.connect(self.action_books_with_the_same_tags,
|
|
SIGNAL('triggered()'),
|
|
lambda : self.show_similar_books('tag'))
|
|
self.connect(self.action_books_by_this_publisher, SIGNAL('triggered()'),
|
|
lambda : self.show_similar_books('publisher'))
|
|
self.library_view.set_context_menu(self.action_edit, self.action_sync,
|
|
self.action_convert, self.action_view,
|
|
self.action_save,
|
|
self.action_open_containing_folder,
|
|
self.action_show_book_details,
|
|
similar_menu=similar_menu)
|
|
self.memory_view.set_context_menu(None, None, None,
|
|
self.action_view, self.action_save, None, None)
|
|
self.card_a_view.set_context_menu(None, None, None,
|
|
self.action_view, self.action_save, None, None)
|
|
self.card_b_view.set_context_menu(None, None, None,
|
|
self.action_view, self.action_save, None, None)
|
|
QObject.connect(self.library_view,
|
|
SIGNAL('files_dropped(PyQt_PyObject)'),
|
|
self.files_dropped, Qt.QueuedConnection)
|
|
for func, target in [
|
|
('connect_to_search_box', self.search),
|
|
('connect_to_book_display',
|
|
self.status_bar.book_info.show_data),
|
|
]:
|
|
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
|
|
getattr(view, func)(target)
|
|
|
|
self.memory_view.connect_dirtied_signal(self.upload_booklists)
|
|
self.card_a_view.connect_dirtied_signal(self.upload_booklists)
|
|
self.card_b_view.connect_dirtied_signal(self.upload_booklists)
|
|
|
|
self.show_windows()
|
|
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
|
self.hide_windows()
|
|
self.stack.setCurrentIndex(0)
|
|
try:
|
|
db = LibraryDatabase2(self.library_path)
|
|
except Exception, err:
|
|
error_dialog(self, _('Bad database location'),
|
|
unicode(err)).exec_()
|
|
dir = unicode(QFileDialog.getExistingDirectory(self,
|
|
_('Choose a location for your ebook library.'),
|
|
os.path.expanduser('~')))
|
|
if not dir:
|
|
QCoreApplication.exit(1)
|
|
else:
|
|
self.library_path = dir
|
|
db = LibraryDatabase2(self.library_path)
|
|
self.library_view.set_database(db)
|
|
prefs['library_path'] = self.library_path
|
|
self.library_view.sortByColumn(*dynamic.get('sort_column',
|
|
('timestamp', Qt.DescendingOrder)))
|
|
if not self.library_view.restore_column_widths():
|
|
self.library_view.resizeColumnsToContents()
|
|
self.library_view.resizeRowsToContents()
|
|
self.search.setFocus(Qt.OtherFocusReason)
|
|
self.cover_cache = CoverCache(self.library_path)
|
|
self.cover_cache.start()
|
|
self.library_view.model().cover_cache = self.cover_cache
|
|
self.tags_view.setVisible(False)
|
|
self.match_all.setVisible(False)
|
|
self.match_any.setVisible(False)
|
|
self.popularity.setVisible(False)
|
|
self.tags_view.set_database(db, self.match_all, self.popularity)
|
|
self.connect(self.tags_view,
|
|
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
|
self.search.search_from_tags)
|
|
self.connect(self.status_bar.tag_view_button,
|
|
SIGNAL('toggled(bool)'), self.toggle_tags_view)
|
|
self.connect(self.search,
|
|
SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
|
|
self.tags_view.model().reinit)
|
|
self.connect(self.library_view.model(),
|
|
SIGNAL('count_changed(int)'), self.location_view.count_changed)
|
|
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
|
|
self.tags_view.recount)
|
|
self.library_view.model().count_changed()
|
|
########################### Cover Flow ################################
|
|
self.cover_flow = None
|
|
if CoverFlow is not None:
|
|
text_height = 40 if config['separate_cover_flow'] else 25
|
|
ah = available_height()
|
|
cfh = ah-100
|
|
cfh = 3./5 * cfh - text_height
|
|
if not config['separate_cover_flow']:
|
|
cfh = 220 if ah > 950 else 170 if ah > 850 else 140
|
|
self.cover_flow = CoverFlow(height=cfh, text_height=text_height)
|
|
self.cover_flow.setVisible(False)
|
|
if not config['separate_cover_flow']:
|
|
self.library.layout().addWidget(self.cover_flow)
|
|
self.connect(self.cover_flow, SIGNAL('currentChanged(int)'),
|
|
self.sync_cf_to_listview)
|
|
self.connect(self.cover_flow, SIGNAL('itemActivated(int)'),
|
|
self.show_book_info)
|
|
self.connect(self.status_bar.cover_flow_button,
|
|
SIGNAL('toggled(bool)'), self.toggle_cover_flow)
|
|
self.connect(self.cover_flow, SIGNAL('stop()'),
|
|
self.status_bar.cover_flow_button.toggle)
|
|
QObject.connect(self.library_view.selectionModel(),
|
|
SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
|
self.sync_cf_to_listview)
|
|
self.db_images = DatabaseImages(self.library_view.model())
|
|
self.cover_flow.setImages(self.db_images)
|
|
else:
|
|
self.status_bar.cover_flow_button.disable(pictureflowerror)
|
|
|
|
|
|
self.setMaximumHeight(max_available_height())
|
|
|
|
|
|
if config['autolaunch_server']:
|
|
from calibre.library.server import start_threaded_server
|
|
from calibre.library import server_config
|
|
self.content_server = start_threaded_server(
|
|
db, server_config().parse())
|
|
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
|
|
|
|
|
self.scheduler = Scheduler(self)
|
|
self.action_news.setMenu(self.scheduler.news_menu)
|
|
self.connect(self.action_news, SIGNAL('triggered(bool)'),
|
|
self.scheduler.show_dialog)
|
|
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
|
|
|
def create_device_menu(self):
|
|
self._sync_menu = DeviceMenu(self)
|
|
self.action_sync.setMenu(self._sync_menu)
|
|
self.connect(self._sync_menu,
|
|
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
|
self.dispatch_sync_event)
|
|
self.connect(self.action_sync, SIGNAL('triggered(bool)'),
|
|
self._sync_menu.trigger_default)
|
|
|
|
def add_spare_server(self, *args):
|
|
self.spare_servers.append(Server())
|
|
|
|
@property
|
|
def spare_server(self):
|
|
try:
|
|
QTimer.singleShot(1000, self.add_spare_server)
|
|
return self.spare_servers.pop()
|
|
except:
|
|
pass
|
|
|
|
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()
|
|
|
|
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.exception is not None:
|
|
error_dialog(self, _('Failed to start content server'),
|
|
unicode(self.content_server.exception)).exec_()
|
|
|
|
def show_similar_books(self, type):
|
|
search, join = [], ' '
|
|
idx = self.library_view.currentIndex()
|
|
if not idx.isValid():
|
|
return
|
|
row = idx.row()
|
|
if type == 'series':
|
|
series = idx.model().db.series(row)
|
|
if series:
|
|
search = ['series:'+series]
|
|
elif type == 'publisher':
|
|
publisher = idx.model().db.publisher(row)
|
|
if publisher:
|
|
search = ['publisher:'+publisher]
|
|
elif type == 'tag':
|
|
tags = idx.model().db.tags(row)
|
|
if tags:
|
|
search = ['tag:'+t for t in tags.split(',')]
|
|
elif type == 'author':
|
|
authors = idx.model().db.authors(row)
|
|
if authors:
|
|
search = ['author:'+a.strip().replace('|', ',') \
|
|
for a in authors.split(',')]
|
|
join = ' or '
|
|
if search:
|
|
self.search.set_search_string(join.join(search))
|
|
|
|
|
|
|
|
def uncheck_cover_button(self, *args):
|
|
self.status_bar.cover_flow_button.setChecked(False)
|
|
|
|
def toggle_cover_flow(self, show):
|
|
if config['separate_cover_flow']:
|
|
if show:
|
|
d = QDialog(self)
|
|
ah, aw = available_height(), available_width()
|
|
d.resize(int(aw/2.), ah-60)
|
|
d._layout = QStackedLayout()
|
|
d.setLayout(d._layout)
|
|
d.setWindowTitle(_('Browse by covers'))
|
|
d.layout().addWidget(self.cover_flow)
|
|
self.cover_flow.setVisible(True)
|
|
self.cover_flow.setFocus(Qt.OtherFocusReason)
|
|
self.library_view.scrollTo(self.library_view.currentIndex())
|
|
d.show()
|
|
self.connect(d, SIGNAL('finished(int)'),
|
|
self.uncheck_cover_button)
|
|
self.cf_dialog = d
|
|
else:
|
|
cfd = getattr(self, 'cf_dialog', None)
|
|
if cfd is not None:
|
|
self.cover_flow.setVisible(False)
|
|
cfd.hide()
|
|
self.cf_dialog = None
|
|
else:
|
|
if show:
|
|
self.library_view.setCurrentIndex(
|
|
self.library_view.currentIndex())
|
|
self.cover_flow.setVisible(True)
|
|
self.cover_flow.setFocus(Qt.OtherFocusReason)
|
|
#self.status_bar.book_info.book_data.setMaximumHeight(100)
|
|
#self.status_bar.setMaximumHeight(120)
|
|
self.library_view.scrollTo(self.library_view.currentIndex())
|
|
else:
|
|
self.cover_flow.setVisible(False)
|
|
#self.status_bar.book_info.book_data.setMaximumHeight(1000)
|
|
self.setMaximumHeight(available_height())
|
|
|
|
def toggle_tags_view(self, show):
|
|
if show:
|
|
self.tags_view.setVisible(True)
|
|
self.match_all.setVisible(True)
|
|
self.match_any.setVisible(True)
|
|
self.popularity.setVisible(True)
|
|
self.tags_view.setFocus(Qt.OtherFocusReason)
|
|
else:
|
|
self.tags_view.setVisible(False)
|
|
self.match_all.setVisible(False)
|
|
self.match_any.setVisible(False)
|
|
self.popularity.setVisible(False)
|
|
|
|
def sync_cf_to_listview(self, index, *args):
|
|
if not hasattr(index, 'row') and \
|
|
self.library_view.currentIndex().row() != index:
|
|
index = self.library_view.model().index(index, 0)
|
|
self.library_view.setCurrentIndex(index)
|
|
if hasattr(index, 'row') and self.cover_flow.isVisible() and \
|
|
self.cover_flow.currentSlide() != index.row():
|
|
self.cover_flow.setCurrentSlide(index.row())
|
|
|
|
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.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()
|
|
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
|
|
|
|
|
|
|
|
########################## Connect to device ##############################
|
|
|
|
def device_detected(self, connected):
|
|
'''
|
|
Called when a device is connected to the computer.
|
|
'''
|
|
if connected:
|
|
self.device_manager.get_device_information(\
|
|
Dispatcher(self.info_read))
|
|
self.set_default_thumbnail(\
|
|
self.device_manager.device.THUMBNAIL_HEIGHT)
|
|
self.status_bar.showMessage(_('Device: ')+\
|
|
self.device_manager.device.__class__.__name__+\
|
|
_(' detected.'), 3000)
|
|
self.device_connected = True
|
|
self._sync_menu.enable_device_actions(True, self.device_manager.device.card_prefix())
|
|
else:
|
|
self.device_connected = False
|
|
self._sync_menu.enable_device_actions(False)
|
|
self.location_view.model().update_devices()
|
|
self.vanity.setText(self.vanity_template%\
|
|
dict(version=self.latest_version, device=' '))
|
|
self.device_info = ' '
|
|
if self.current_view() != self.library_view:
|
|
self.status_bar.reset_info()
|
|
self.location_selected('library')
|
|
|
|
def info_read(self, job):
|
|
'''
|
|
Called once device information has been read.
|
|
'''
|
|
if job.failed:
|
|
return self.device_job_exception(job)
|
|
info, cp, fs = job.result
|
|
self.location_view.model().update_devices(cp, fs)
|
|
self.device_info = _('Connected ')+info[0]
|
|
self.vanity.setText(self.vanity_template%\
|
|
dict(version=self.latest_version, device=self.device_info))
|
|
|
|
self.device_manager.books(Dispatcher(self.metadata_downloaded))
|
|
|
|
def metadata_downloaded(self, job):
|
|
'''
|
|
Called once metadata has been read for all books on the device.
|
|
'''
|
|
if job.failed:
|
|
if isinstance(job.exception, ExpatError):
|
|
error_dialog(self, _('Device database corrupted'),
|
|
_('''
|
|
<p>The database of books on the reader is corrupted. Try the following:
|
|
<ol>
|
|
<li>Unplug the reader. Wait for it to finish regenerating the database (i.e. wait till it is ready to be used). Plug it back in. Now it should work with %(app)s. If not try the next step.</li>
|
|
<li>Quit %(app)s. Find the file media.xml in the reader's main memory. Delete it. Unplug the reader. Wait for it to regenerate the file. Re-connect it and start %(app)s.</li>
|
|
</ol>
|
|
''')%dict(app=__appname__)).exec_()
|
|
else:
|
|
self.device_job_exception(job)
|
|
return
|
|
mainlist, cardalist, cardblist = job.result
|
|
self.memory_view.set_database(mainlist)
|
|
self.memory_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
|
|
self.card_a_view.set_database(cardalist)
|
|
self.card_a_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
|
|
self.card_b_view.set_database(cardblist)
|
|
self.card_b_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
|
|
for view in (self.memory_view, self.card_a_view, self.card_b_view):
|
|
view.sortByColumn(3, Qt.DescendingOrder)
|
|
if not view.restore_column_widths():
|
|
view.resizeColumnsToContents()
|
|
view.resizeRowsToContents()
|
|
view.resize_on_select = not view.isVisible()
|
|
self.sync_news()
|
|
############################################################################
|
|
|
|
|
|
|
|
################################# Add books ################################
|
|
|
|
def add_recursive(self, single):
|
|
root = choose_dir(self, 'recursive book import root dir dialog',
|
|
'Select root folder')
|
|
if not root:
|
|
return
|
|
from calibre.gui2.add import Adder
|
|
self._adder = Adder(self,
|
|
self.library_view.model().db,
|
|
Dispatcher(self._files_added), spare_server=self.spare_server)
|
|
self._adder.add_recursive(root, single)
|
|
|
|
def add_recursive_single(self, checked):
|
|
'''
|
|
Add books from the local filesystem to either the library or the device
|
|
recursively assuming one book per folder.
|
|
'''
|
|
self.add_recursive(True)
|
|
|
|
def add_recursive_multiple(self, checked):
|
|
'''
|
|
Add books from the local filesystem to either the library or the device
|
|
recursively assuming multiple books per folder.
|
|
'''
|
|
self.add_recursive(False)
|
|
|
|
def files_dropped(self, paths):
|
|
to_device = self.stack.currentIndex() != 0
|
|
self._add_books(paths, to_device)
|
|
|
|
|
|
def add_filesystem_book(self, path):
|
|
if os.access(path, os.R_OK):
|
|
books = [os.path.abspath(path)]
|
|
to_device = self.stack.currentIndex() != 0
|
|
self._add_books(books, to_device)
|
|
if to_device:
|
|
self.status_bar.showMessage(\
|
|
_('Uploading books to device.'), 2000)
|
|
|
|
def add_books(self, checked):
|
|
'''
|
|
Add books from the local filesystem to either the library or the device.
|
|
'''
|
|
books = choose_files(self, 'add books dialog dir', 'Select books',
|
|
filters=[
|
|
(_('Books'), BOOK_EXTENSIONS),
|
|
(_('EPUB Books'), ['epub']),
|
|
(_('LRF Books'), ['lrf']),
|
|
(_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']),
|
|
(_('LIT Books'), ['lit']),
|
|
(_('MOBI Books'), ['mobi', 'prc', 'azw']),
|
|
(_('Text books'), ['txt', 'rtf']),
|
|
(_('PDF Books'), ['pdf']),
|
|
(_('Comics'), ['cbz', 'cbr', 'cbc']),
|
|
(_('Archives'), ['zip', 'rar']),
|
|
])
|
|
if not books:
|
|
return
|
|
to_device = self.stack.currentIndex() != 0
|
|
self._add_books(books, to_device)
|
|
|
|
|
|
def _add_books(self, paths, to_device, on_card=None):
|
|
if on_card is None:
|
|
on_card = 'carda' if self.stack.currentIndex() == 2 else 'cardb' if self.stack.currentIndex() == 3 else None
|
|
if not paths:
|
|
return
|
|
from calibre.gui2.add import Adder
|
|
self.__adder_func = partial(self._files_added, on_card=on_card)
|
|
self._adder = Adder(self,
|
|
None if to_device else self.library_view.model().db,
|
|
Dispatcher(self.__adder_func), spare_server=self.spare_server)
|
|
self._adder.add(paths)
|
|
|
|
def _files_added(self, paths=[], names=[], infos=[], on_card=None):
|
|
if paths:
|
|
self.upload_books(paths,
|
|
list(map(sanitize_file_name, names)),
|
|
infos, on_card=on_card)
|
|
self.status_bar.showMessage(
|
|
_('Uploading books to device.'), 2000)
|
|
if self._adder.number_of_books_added > 0:
|
|
self.library_view.model().books_added(self._adder.number_of_books_added)
|
|
if hasattr(self, 'db_images'):
|
|
self.db_images.reset()
|
|
|
|
self._adder = None
|
|
|
|
|
|
############################################################################
|
|
|
|
############################### Delete books ###############################
|
|
def delete_books(self, checked):
|
|
'''
|
|
Delete selected books from device or library.
|
|
'''
|
|
view = self.current_view()
|
|
rows = view.selectionModel().selectedRows()
|
|
if not rows or len(rows) == 0:
|
|
return
|
|
if self.stack.currentIndex() == 0:
|
|
if not confirm('<p>'+_('The selected books will be '
|
|
'<b>permanently deleted</b> and the files '
|
|
'removed from your computer. Are you sure?')
|
|
+'</p>', 'library_delete_books', self):
|
|
return
|
|
view.model().delete_books(rows)
|
|
else:
|
|
if self.stack.currentIndex() == 1:
|
|
view = self.memory_view
|
|
elif self.stack.currentIndex() == 2:
|
|
view = self.card_a_view
|
|
else:
|
|
view = self.card_b_view
|
|
paths = view.model().paths(rows)
|
|
job = self.remove_paths(paths)
|
|
self.delete_memory[job] = (paths, view.model())
|
|
view.model().mark_for_deletion(job, rows)
|
|
self.status_bar.showMessage(_('Deleting books from device.'), 1000)
|
|
|
|
def remove_paths(self, paths):
|
|
return self.device_manager.delete_books(\
|
|
Dispatcher(self.books_deleted), paths)
|
|
|
|
def books_deleted(self, job):
|
|
'''
|
|
Called once deletion is done on the device
|
|
'''
|
|
for view in (self.memory_view, self.card_a_view, self.card_b_view):
|
|
view.model().deletion_done(job, job.failed)
|
|
if job.failed:
|
|
self.device_job_exception(job)
|
|
return
|
|
|
|
if self.delete_memory.has_key(job):
|
|
paths, model = self.delete_memory.pop(job)
|
|
self.device_manager.remove_books_from_metadata(paths,
|
|
self.booklists())
|
|
model.paths_deleted(paths)
|
|
self.upload_booklists()
|
|
|
|
############################################################################
|
|
|
|
############################### Edit metadata ##############################
|
|
|
|
def download_metadata(self, checked, covers=True, set_metadata=True):
|
|
rows = self.library_view.selectionModel().selectedRows()
|
|
previous = self.library_view.currentIndex()
|
|
if not rows or len(rows) == 0:
|
|
d = error_dialog(self, _('Cannot download metadata'),
|
|
_('No books selected'))
|
|
d.exec_()
|
|
return
|
|
db = self.library_view.model().db
|
|
ids = [db.id(row.row()) for row in rows]
|
|
from calibre.gui2.metadata import DownloadMetadata
|
|
self._download_book_metadata = DownloadMetadata(db, ids,
|
|
get_covers=covers, set_metadata=set_metadata)
|
|
self._download_book_metadata.start()
|
|
x = _('covers') if covers and not set_metadata else _('metadata')
|
|
self.progress_indicator.start(
|
|
_('Downloading %s for %d book(s)')%(x, len(ids)))
|
|
self._book_metadata_download_check = QTimer(self)
|
|
self.connect(self._book_metadata_download_check,
|
|
SIGNAL('timeout()'), self.book_metadata_download_check)
|
|
self._book_metadata_download_check.start(100)
|
|
|
|
def book_metadata_download_check(self):
|
|
if self._download_book_metadata.is_alive():
|
|
return
|
|
self._book_metadata_download_check.stop()
|
|
self.progress_indicator.stop()
|
|
cr = self.library_view.currentIndex().row()
|
|
x = self._download_book_metadata
|
|
self._download_book_metadata = None
|
|
if x.exception is None:
|
|
db = self.library_view.model().refresh_ids(
|
|
x.updated, cr)
|
|
if x.failures:
|
|
details = ['%s: %s'%(title, reason) for title,
|
|
reason in x.failures.values()]
|
|
details = '%s\n'%('\n'.join(details))
|
|
warning_dialog(_('Failed to download some metadata'),
|
|
_('Failed to download metadata for the following:'),
|
|
details, self).exec_()
|
|
else:
|
|
err = _('Failed to download metadata:')
|
|
error_dialog(self, _('Error'), err, det_msg=x.tb).exec_()
|
|
|
|
|
|
|
|
|
|
def edit_metadata(self, checked, bulk=None):
|
|
'''
|
|
Edit metadata of selected books in library.
|
|
'''
|
|
rows = self.library_view.selectionModel().selectedRows()
|
|
previous = self.library_view.currentIndex()
|
|
if not rows or len(rows) == 0:
|
|
d = error_dialog(self, _('Cannot edit metadata'),
|
|
_('No books selected'))
|
|
d.exec_()
|
|
return
|
|
|
|
if bulk or (bulk is None and len(rows) > 1):
|
|
return self.edit_bulk_metadata(checked)
|
|
|
|
def accepted(id):
|
|
self.library_view.model().refresh_ids([id])
|
|
|
|
for row in rows:
|
|
self._metadata_view_id = self.library_view.model().db.id(row.row())
|
|
d = MetadataSingleDialog(self, row.row(),
|
|
self.library_view.model().db,
|
|
accepted_callback=accepted)
|
|
self.connect(d, SIGNAL('view_format(PyQt_PyObject)'),
|
|
self.metadata_view_format)
|
|
d.exec_()
|
|
if rows:
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, previous)
|
|
|
|
def edit_bulk_metadata(self, checked):
|
|
'''
|
|
Edit metadata of selected books in library in bulk.
|
|
'''
|
|
rows = [r.row() for r in \
|
|
self.library_view.selectionModel().selectedRows()]
|
|
if not rows or len(rows) == 0:
|
|
d = error_dialog(self, _('Cannot edit metadata'),
|
|
_('No books selected'))
|
|
d.exec_()
|
|
return
|
|
if MetadataBulkDialog(self, rows,
|
|
self.library_view.model().db).changed:
|
|
self.library_view.model().resort(reset=False)
|
|
self.library_view.model().research()
|
|
|
|
############################################################################
|
|
|
|
|
|
############################## Save to disk ################################
|
|
def save_single_format_to_disk(self, checked):
|
|
self.save_to_disk(checked, True, prefs['output_format'])
|
|
|
|
def save_specific_format_disk(self, fmt):
|
|
self.save_to_disk(False, True, fmt)
|
|
|
|
def save_to_single_dir(self, checked):
|
|
self.save_to_disk(checked, True)
|
|
|
|
def save_to_disk(self, checked, single_dir=False, single_format=None):
|
|
rows = self.current_view().selectionModel().selectedRows()
|
|
if not rows or len(rows) == 0:
|
|
return error_dialog(self, _('Cannot save to disk'),
|
|
_('No books selected'), show=True)
|
|
path = choose_dir(self, 'save to disk dialog',
|
|
_('Choose destination directory'))
|
|
if not path:
|
|
return
|
|
|
|
if self.current_view() is self.library_view:
|
|
from calibre.gui2.add import Saver
|
|
self._saver = Saver(self, self.library_view.model().db,
|
|
Dispatcher(self._books_saved), rows, path,
|
|
by_author=self.library_view.model().by_author,
|
|
single_dir=single_dir,
|
|
single_format=single_format,
|
|
spare_server=self.spare_server)
|
|
|
|
else:
|
|
paths = self.current_view().model().paths(rows)
|
|
self.device_manager.save_books(
|
|
Dispatcher(self.books_saved), paths, path)
|
|
|
|
|
|
def _books_saved(self, path, failures, error):
|
|
single_format = self._saver.worker.single_format
|
|
self._saver = None
|
|
if error:
|
|
return error_dialog(self, _('Error while saving'),
|
|
_('There was an error while saving.'),
|
|
error, show=True)
|
|
if failures and single_format:
|
|
single_format = single_format.upper()
|
|
warning_dialog(self, _('Could not save some books'),
|
|
_('Could not save some books') + ', ' +
|
|
(_('as the %s format is not available for them.')%single_format) +
|
|
_('Click the show details button to see which ones.'),
|
|
'\n'.join(failures), show=True)
|
|
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
|
|
|
def books_saved(self, job):
|
|
if job.failed:
|
|
return self.device_job_exception(job)
|
|
|
|
############################################################################
|
|
|
|
############################### Fetch news #################################
|
|
|
|
def download_scheduled_recipe(self, recipe, script, callback):
|
|
func, args, desc, fmt, temp_files = \
|
|
fetch_scheduled_recipe(recipe, script)
|
|
job = self.job_manager.run_job(
|
|
Dispatcher(self.scheduled_recipe_fetched), func, args=args,
|
|
description=desc)
|
|
self.conversion_jobs[job] = (temp_files, fmt, recipe, callback)
|
|
self.status_bar.showMessage(_('Fetching news from ')+recipe.title, 2000)
|
|
|
|
def scheduled_recipe_fetched(self, job):
|
|
temp_files, fmt, recipe, callback = self.conversion_jobs.pop(job)
|
|
pt = temp_files[0]
|
|
if job.failed:
|
|
return self.job_exception(job)
|
|
id = self.library_view.model().add_news(pt.name, recipe)
|
|
self.library_view.model().reset()
|
|
sync = dynamic.get('news_to_be_synced', set([]))
|
|
sync.add(id)
|
|
dynamic.set('news_to_be_synced', sync)
|
|
callback(recipe)
|
|
self.status_bar.showMessage(recipe.title + _(' fetched.'), 3000)
|
|
self.email_news(id)
|
|
self.sync_news()
|
|
|
|
############################################################################
|
|
|
|
############################### Convert ####################################
|
|
|
|
def auto_convert(self, book_ids, on_card, format):
|
|
previous = self.library_view.currentIndex()
|
|
rows = [x.row() for x in \
|
|
self.library_view.selectionModel().selectedRows()]
|
|
jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
|
|
if jobs == []: return
|
|
for func, args, desc, fmt, id, temp_files in jobs:
|
|
if id not in bad:
|
|
job = self.job_manager.run_job(Dispatcher(self.book_auto_converted),
|
|
func, args=args, description=desc)
|
|
self.conversion_jobs[job] = (temp_files, fmt, id, on_card)
|
|
|
|
if changed:
|
|
self.library_view.model().refresh_rows(rows)
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, previous)
|
|
|
|
def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format):
|
|
previous = self.library_view.currentIndex()
|
|
rows = [x.row() for x in \
|
|
self.library_view.selectionModel().selectedRows()]
|
|
jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
|
|
if jobs == []: return
|
|
for func, args, desc, fmt, id, temp_files in jobs:
|
|
if id not in bad:
|
|
job = self.job_manager.run_job(Dispatcher(self.book_auto_converted_mail),
|
|
func, args=args, description=desc)
|
|
self.conversion_jobs[job] = (temp_files, fmt, id,
|
|
delete_from_library, to, fmts)
|
|
|
|
if changed:
|
|
self.library_view.model().refresh_rows(rows)
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, previous)
|
|
|
|
def auto_convert_news(self, book_ids, format):
|
|
previous = self.library_view.currentIndex()
|
|
rows = [x.row() for x in \
|
|
self.library_view.selectionModel().selectedRows()]
|
|
jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
|
|
if jobs == []: return
|
|
for func, args, desc, fmt, id, temp_files in jobs:
|
|
if id not in bad:
|
|
job = self.job_manager.run_job(Dispatcher(self.book_auto_converted_news),
|
|
func, args=args, description=desc)
|
|
self.conversion_jobs[job] = (temp_files, fmt, id)
|
|
|
|
if changed:
|
|
self.library_view.model().refresh_rows(rows)
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, previous)
|
|
|
|
|
|
def get_books_for_conversion(self):
|
|
rows = [r.row() for r in \
|
|
self.library_view.selectionModel().selectedRows()]
|
|
if not rows or len(rows) == 0:
|
|
d = error_dialog(self, _('Cannot convert'),
|
|
_('No books selected'))
|
|
d.exec_()
|
|
return None
|
|
return [self.library_view.model().db.id(r) for r in rows]
|
|
|
|
def convert_bulk(self, checked):
|
|
book_ids = self.get_books_for_conversion()
|
|
if book_ids is None: return
|
|
previous = self.library_view.currentIndex()
|
|
rows = [x.row() for x in \
|
|
self.library_view.selectionModel().selectedRows()]
|
|
jobs, changed, bad = convert_bulk_ebook(self,
|
|
self.library_view.model().db, book_ids, out_format=prefs['output_format'])
|
|
for func, args, desc, fmt, id, temp_files in jobs:
|
|
if id not in bad:
|
|
job = self.job_manager.run_job(Dispatcher(self.book_converted),
|
|
func, args=args, description=desc)
|
|
self.conversion_jobs[job] = (temp_files, fmt, id)
|
|
|
|
if changed:
|
|
self.library_view.model().refresh_rows(rows)
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, previous)
|
|
|
|
def convert_single(self, checked):
|
|
book_ids = self.get_books_for_conversion()
|
|
if book_ids is None: return
|
|
previous = self.library_view.currentIndex()
|
|
rows = [x.row() for x in \
|
|
self.library_view.selectionModel().selectedRows()]
|
|
jobs, changed, bad = convert_single_ebook(self,
|
|
self.library_view.model().db, book_ids, out_format=prefs['output_format'])
|
|
for func, args, desc, fmt, id, temp_files in jobs:
|
|
if id not in bad:
|
|
job = self.job_manager.run_job(Dispatcher(self.book_converted),
|
|
func, args=args, description=desc)
|
|
self.conversion_jobs[job] = (temp_files, fmt, id)
|
|
|
|
if changed:
|
|
self.library_view.model().refresh_rows(rows)
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, previous)
|
|
|
|
def book_auto_converted(self, job):
|
|
temp_files, fmt, book_id, on_card = self.conversion_jobs.pop(job)
|
|
try:
|
|
if job.failed:
|
|
return self.job_exception(job)
|
|
data = open(temp_files[0].name, 'rb')
|
|
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
|
|
data.close()
|
|
self.status_bar.showMessage(job.description + (' completed'), 2000)
|
|
finally:
|
|
for f in temp_files:
|
|
try:
|
|
if os.path.exists(f.name):
|
|
os.remove(f.name)
|
|
except:
|
|
pass
|
|
self.tags_view.recount()
|
|
if self.current_view() is self.library_view:
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, QModelIndex())
|
|
|
|
self.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
|
|
|
|
def book_auto_converted_mail(self, job):
|
|
temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs.pop(job)
|
|
try:
|
|
if job.failed:
|
|
self.job_exception(job)
|
|
return
|
|
data = open(temp_files[0].name, 'rb')
|
|
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
|
|
data.close()
|
|
self.status_bar.showMessage(job.description + (' completed'), 2000)
|
|
finally:
|
|
for f in temp_files:
|
|
try:
|
|
if os.path.exists(f.name):
|
|
os.remove(f.name)
|
|
except:
|
|
pass
|
|
self.tags_view.recount()
|
|
if self.current_view() is self.library_view:
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, QModelIndex())
|
|
|
|
self.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
|
|
|
|
def book_auto_converted_news(self, job):
|
|
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
|
|
try:
|
|
if job.failed:
|
|
return self.job_exception(job)
|
|
data = open(temp_files[0].name, 'rb')
|
|
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
|
|
data.close()
|
|
self.status_bar.showMessage(job.description + (' completed'), 2000)
|
|
finally:
|
|
for f in temp_files:
|
|
try:
|
|
if os.path.exists(f.name):
|
|
os.remove(f.name)
|
|
except:
|
|
pass
|
|
self.tags_view.recount()
|
|
if self.current_view() is self.library_view:
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, QModelIndex())
|
|
|
|
self.sync_news(send_ids=[book_id], do_auto_convert=False)
|
|
|
|
def book_converted(self, job):
|
|
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
|
|
try:
|
|
if job.failed:
|
|
self.job_exception(job)
|
|
return
|
|
data = open(temp_files[-1].name, 'rb')
|
|
self.library_view.model().db.add_format(book_id, \
|
|
fmt, data, index_is_id=True)
|
|
data.close()
|
|
self.status_bar.showMessage(job.description + \
|
|
(' completed'), 2000)
|
|
finally:
|
|
for f in temp_files:
|
|
try:
|
|
if os.path.exists(f.name):
|
|
os.remove(f.name)
|
|
except:
|
|
pass
|
|
self.tags_view.recount()
|
|
if self.current_view() is self.library_view:
|
|
current = self.library_view.currentIndex()
|
|
self.library_view.model().current_changed(current, QModelIndex())
|
|
|
|
#############################View book######################################
|
|
|
|
def view_format(self, row, format):
|
|
fmt_path = self.library_view.model().db.format_abspath(row, format)
|
|
if fmt_path:
|
|
self._view_file(fmt_path)
|
|
|
|
def metadata_view_format(self, fmt):
|
|
fmt_path = self.library_view.model().db.\
|
|
format_abspath(self._metadata_view_id,
|
|
fmt, index_is_id=True)
|
|
if fmt_path:
|
|
self._view_file(fmt_path)
|
|
|
|
|
|
def book_downloaded_for_viewing(self, job):
|
|
if job.failed:
|
|
self.device_job_exception(job)
|
|
return
|
|
self._view_file(job.result)
|
|
|
|
def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True):
|
|
self.setCursor(Qt.BusyCursor)
|
|
try:
|
|
if internal:
|
|
args = [viewer]
|
|
if isosx and 'ebook' in viewer:
|
|
args.append('--raise-window')
|
|
if name is not None:
|
|
args.append(name)
|
|
self.job_manager.launch_gui_app(viewer,
|
|
kwargs=dict(args=args))
|
|
else:
|
|
QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name)
|
|
time.sleep(2) # User feedback
|
|
finally:
|
|
self.unsetCursor()
|
|
|
|
def _view_file(self, name):
|
|
ext = os.path.splitext(name)[1].upper().replace('.', '')
|
|
viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer'
|
|
internal = ext in config['internally_viewed_formats']
|
|
self._launch_viewer(name, viewer, internal)
|
|
|
|
def view_specific_format(self, triggered):
|
|
rows = self.library_view.selectionModel().selectedRows()
|
|
if not rows or len(rows) == 0:
|
|
d = error_dialog(self, _('Cannot view'), _('No book selected'))
|
|
d.exec_()
|
|
return
|
|
|
|
row = rows[0].row()
|
|
formats = self.library_view.model().db.formats(row).upper().split(',')
|
|
d = ChooseFormatDialog(self, _('Choose the format to view'), formats)
|
|
d.exec_()
|
|
if d.result() == QDialog.Accepted:
|
|
format = d.format()
|
|
self.view_format(row, format)
|
|
else:
|
|
return
|
|
|
|
def view_folder(self, *args):
|
|
rows = self.current_view().selectionModel().selectedRows()
|
|
if self.current_view() is self.library_view:
|
|
if not rows or len(rows) == 0:
|
|
d = error_dialog(self, _('Cannot open folder'),
|
|
_('No book selected'))
|
|
d.exec_()
|
|
return
|
|
for row in rows:
|
|
path = self.library_view.model().db.abspath(row.row())
|
|
QDesktopServices.openUrl(QUrl('file:'+path))
|
|
|
|
|
|
def view_book(self, triggered):
|
|
rows = self.current_view().selectionModel().selectedRows()
|
|
if self.current_view() is self.library_view:
|
|
if not rows or len(rows) == 0:
|
|
self._launch_viewer()
|
|
return
|
|
|
|
row = rows[0].row()
|
|
formats = self.library_view.model().db.formats(row).upper()
|
|
formats = formats.split(',')
|
|
title = self.library_view.model().db.title(row)
|
|
id = self.library_view.model().db.id(row)
|
|
format = None
|
|
if len(formats) == 1:
|
|
format = formats[0]
|
|
if 'LRF' in formats:
|
|
format = 'LRF'
|
|
if 'EPUB' in formats:
|
|
format = 'EPUB'
|
|
if 'MOBI' in formats:
|
|
format = 'MOBI'
|
|
if not formats:
|
|
d = error_dialog(self, _('Cannot view'),
|
|
_('%s has no available formats.')%(title,))
|
|
d.exec_()
|
|
return
|
|
if format is None:
|
|
d = ChooseFormatDialog(self, _('Choose the format to view'),
|
|
formats)
|
|
d.exec_()
|
|
if d.result() == QDialog.Accepted:
|
|
format = d.format()
|
|
else:
|
|
return
|
|
|
|
self.view_format(row, format)
|
|
else:
|
|
paths = self.current_view().model().paths(rows)
|
|
if paths:
|
|
pt = PersistentTemporaryFile('_viewer_'+\
|
|
os.path.splitext(paths[0])[1])
|
|
self.persistent_files.append(pt)
|
|
pt.close()
|
|
self.device_manager.view_book(\
|
|
Dispatcher(self.book_downloaded_for_viewing),
|
|
paths[0], pt.name)
|
|
|
|
|
|
|
|
############################################################################
|
|
|
|
########################### Do advanced search #############################
|
|
|
|
def do_advanced_search(self, *args):
|
|
d = SearchDialog(self)
|
|
if d.exec_() == QDialog.Accepted:
|
|
self.search.set_search_string(d.search_string())
|
|
|
|
############################################################################
|
|
|
|
############################### Do config ##################################
|
|
|
|
def do_config(self, *args):
|
|
if self.job_manager.has_jobs():
|
|
d = error_dialog(self, _('Cannot configure'),
|
|
_('Cannot configure while there are running jobs.'))
|
|
d.exec_()
|
|
return
|
|
d = ConfigDialog(self, self.library_view.model().db,
|
|
server=self.content_server)
|
|
d.exec_()
|
|
self.content_server = d.server
|
|
if d.result() == d.Accepted:
|
|
self.tool_bar.setIconSize(config['toolbar_icon_size'])
|
|
self.tool_bar.setToolButtonStyle(
|
|
Qt.ToolButtonTextUnderIcon if \
|
|
config['show_text_in_toolbar'] else \
|
|
Qt.ToolButtonIconOnly)
|
|
self.save_menu.actions()[2].setText(
|
|
_('Save only %s format to disk')%
|
|
prefs['output_format'].upper())
|
|
if hasattr(d, 'directories'):
|
|
set_sidebar_directories(d.directories)
|
|
self.library_view.model().read_config()
|
|
self.create_device_menu()
|
|
|
|
|
|
if not patheq(self.library_path, d.database_location):
|
|
newloc = d.database_location
|
|
move_library(self.library_path, newloc, self,
|
|
self.library_moved)
|
|
|
|
|
|
def library_moved(self, newloc):
|
|
if newloc is None: return
|
|
db = LibraryDatabase2(newloc)
|
|
self.library_view.set_database(db)
|
|
self.status_bar.clearMessage()
|
|
self.search.clear_to_help()
|
|
self.status_bar.reset_info()
|
|
self.library_view.sortByColumn(3, Qt.DescendingOrder)
|
|
|
|
############################################################################
|
|
|
|
################################ Book info #################################
|
|
|
|
def show_book_info(self, *args):
|
|
if self.current_view() is not self.library_view:
|
|
error_dialog(self, _('No detailed info available'),
|
|
_('No detailed information is available for books '
|
|
'on the device.')).exec_()
|
|
return
|
|
index = self.library_view.currentIndex()
|
|
if index.isValid():
|
|
BookInfo(self, self.library_view, index).show()
|
|
|
|
############################################################################
|
|
|
|
############################################################################
|
|
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)
|
|
view = self.memory_view if page == 1 else \
|
|
self.card_a_view if page == 2 else \
|
|
self.card_b_view if page == 3 else None
|
|
if view:
|
|
if view.resize_on_select:
|
|
view.resizeRowsToContents()
|
|
if not view.restore_column_widths():
|
|
view.resizeColumnsToContents()
|
|
view.resize_on_select = False
|
|
self.status_bar.reset_info()
|
|
self.current_view().clearSelection()
|
|
if location == 'library':
|
|
self.action_edit.setEnabled(True)
|
|
self.action_convert.setEnabled(True)
|
|
self.view_menu.actions()[1].setEnabled(True)
|
|
self.action_open_containing_folder.setEnabled(True)
|
|
self.action_sync.setEnabled(True)
|
|
else:
|
|
self.action_edit.setEnabled(False)
|
|
self.action_convert.setEnabled(False)
|
|
self.view_menu.actions()[1].setEnabled(False)
|
|
self.action_open_containing_folder.setEnabled(False)
|
|
self.action_sync.setEnabled(False)
|
|
|
|
|
|
def device_job_exception(self, job):
|
|
'''
|
|
Handle exceptions in threaded device jobs.
|
|
'''
|
|
try:
|
|
if 'Could not read 32 bytes on the control bus.' in \
|
|
unicode(job.details):
|
|
error_dialog(self, _('Error talking to device'),
|
|
_('There was a temporary error talking to the '
|
|
'device. Please unplug and reconnect the device '
|
|
'and or reboot.')).show()
|
|
return
|
|
except:
|
|
pass
|
|
try:
|
|
prints(job.details, file=sys.stderr)
|
|
except:
|
|
pass
|
|
if not self.device_error_dialog.isVisible():
|
|
self.device_error_dialog.setDetailedText(job.details)
|
|
self.device_error_dialog.show()
|
|
|
|
def job_exception(self, job):
|
|
try:
|
|
if 'calibre.ebooks.DRMError' in job.details:
|
|
error_dialog(self, _('Conversion Error'),
|
|
_('<p>Could not convert: %s<p>It is a '
|
|
'<a href="%s">DRM</a>ed book. You must first remove the '
|
|
'DRM using 3rd party tools.')%\
|
|
(job.description.split(':')[-1],
|
|
'http://wiki.mobileread.com/wiki/DRM')).exec_()
|
|
return
|
|
except:
|
|
pass
|
|
if job.killed:
|
|
return
|
|
try:
|
|
prints(job.details, file=sys.stderr)
|
|
except:
|
|
pass
|
|
error_dialog(self, _('Conversion Error'),
|
|
_('<b>Failed</b>')+': '+unicode(job.description),
|
|
det_msg=job.details).exec_()
|
|
|
|
|
|
def initialize_database(self):
|
|
self.library_path = prefs['library_path']
|
|
if self.library_path is None: # Need to migrate to new database layout
|
|
base = os.path.expanduser('~')
|
|
if iswindows:
|
|
from calibre import plugins
|
|
from PyQt4.Qt import QDir
|
|
base = plugins['winutil'][0].special_folder_path(
|
|
plugins['winutil'][0].CSIDL_PERSONAL)
|
|
if not base or not os.path.exists(base):
|
|
base = unicode(QDir.homePath()).replace('/', os.sep)
|
|
dir = unicode(QFileDialog.getExistingDirectory(self,
|
|
_('Choose a location for your ebook library.'), base))
|
|
if not dir:
|
|
dir = os.path.expanduser('~/Library')
|
|
self.library_path = os.path.abspath(dir)
|
|
if not os.path.exists(self.library_path):
|
|
try:
|
|
os.makedirs(self.library_path)
|
|
except:
|
|
self.library_path = os.path.expanduser('~/Library')
|
|
error_dialog(self, _('Invalid library location'),
|
|
_('Could not access %s. Using %s as the library.')%
|
|
(repr(self.library_path), repr(self.library_path))
|
|
).exec_()
|
|
os.makedirs(self.library_path)
|
|
|
|
|
|
def read_settings(self):
|
|
self.initialize_database()
|
|
geometry = config['main_window_geometry']
|
|
if geometry is not None:
|
|
self.restoreGeometry(geometry)
|
|
set_sidebar_directories(None)
|
|
self.tool_bar.setIconSize(config['toolbar_icon_size'])
|
|
self.tool_bar.setToolButtonStyle(
|
|
Qt.ToolButtonTextUnderIcon if \
|
|
config['show_text_in_toolbar'] else \
|
|
Qt.ToolButtonIconOnly)
|
|
|
|
|
|
def write_settings(self):
|
|
config.set('main_window_geometry', self.saveGeometry())
|
|
dynamic.set('sort_column', self.library_view.model().sorted_on)
|
|
self.library_view.write_settings()
|
|
if self.device_connected:
|
|
self.memory_view.write_settings()
|
|
|
|
def restart(self):
|
|
self.quit(restart=True)
|
|
|
|
def quit(self, checked=True, restart=False):
|
|
if not self.confirm_quit():
|
|
return
|
|
try:
|
|
self.shutdown()
|
|
except:
|
|
pass
|
|
self.restart_after_quit = restart
|
|
QApplication.instance().quit()
|
|
|
|
def donate(self, *args):
|
|
BUTTON = '''
|
|
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
|
|
<input type="hidden" name="cmd" value="_s-xclick" />
|
|
<input type="hidden" name="hosted_button_id" value="3029467" />
|
|
<input type="image" src="https://www.paypal.com/en_US/i/btn/btn_donateCC_LG.gif" border="0" name="submit" alt="Donate to support calibre development" />
|
|
<img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1" />
|
|
</form>
|
|
'''
|
|
MSG = _('is the result of the efforts of many volunteers from all '
|
|
'over the world. If you find it useful, please consider '
|
|
'donating to support its development.')
|
|
HTML = u'''
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
|
<title>Donate to support calibre</title>
|
|
</head>
|
|
<body style="background:white">
|
|
<div><a href="http://calibre.kovidgoyal.net"><img style="border:0px" src="http://calibre.kovidgoyal.net/chrome/site/calibre_banner.png" alt="calibre" /></a></div>
|
|
<p>Calibre %s</p>
|
|
%s
|
|
</body>
|
|
</html>
|
|
'''%(MSG, BUTTON)
|
|
pt = PersistentTemporaryFile('_donate.htm')
|
|
pt.write(HTML.encode('utf-8'))
|
|
pt.close()
|
|
QDesktopServices.openUrl(QUrl.fromLocalFile(pt.name))
|
|
|
|
|
|
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>'
|
|
|
|
d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg,
|
|
QMessageBox.Yes|QMessageBox.No, self)
|
|
d.setIconPixmap(QPixmap(':/images/dialog_warning.svg'))
|
|
d.setDefaultButton(QMessageBox.No)
|
|
if d.exec_() != QMessageBox.Yes:
|
|
return False
|
|
return True
|
|
|
|
|
|
def shutdown(self, write_settings=True):
|
|
if write_settings:
|
|
self.write_settings()
|
|
self.check_messages_timer.stop()
|
|
self.listener.close()
|
|
self.job_manager.server.close()
|
|
while self.spare_servers:
|
|
self.spare_servers.pop().close()
|
|
self.device_manager.keep_going = False
|
|
self.cover_cache.stop()
|
|
self.hide_windows()
|
|
self.cover_cache.terminate()
|
|
self.emailer.stop()
|
|
try:
|
|
try:
|
|
if self.content_server is not None:
|
|
self.content_server.exit()
|
|
except:
|
|
pass
|
|
time.sleep(2)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
self.hide_windows()
|
|
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.')).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()
|
|
|
|
def update_found(self, version):
|
|
os = 'windows' if iswindows else 'osx' if isosx else 'linux'
|
|
url = 'http://%s.kovidgoyal.net/download_%s'%(__appname__, os)
|
|
self.latest_version = '<br>' + _('<span style="color:red; font-weight:bold">'
|
|
'Latest version: <a href="%s">%s</a></span>')%(url, version)
|
|
self.vanity.setText(self.vanity_template%\
|
|
(dict(version=self.latest_version,
|
|
device=self.device_info)))
|
|
self.vanity.update()
|
|
if config.get('new_version_notification') and \
|
|
dynamic.get('update to version %s'%version, True):
|
|
if question_dialog(self, _('Update available'),
|
|
_('%s has been updated to version %s. '
|
|
'See the <a href="http://calibre.kovidgoyal.net/wiki/'
|
|
'Changelog">new features</a>. Visit the download pa'
|
|
'ge?')%(__appname__, version)):
|
|
url = 'http://calibre.kovidgoyal.net/download_'+\
|
|
('windows' if iswindows else 'osx' if isosx else 'linux')
|
|
QDesktopServices.openUrl(QUrl(url))
|
|
dynamic.set('update to version %s'%version, False)
|
|
|
|
|
|
def option_parser():
|
|
parser = _option_parser('''\
|
|
%prog [opts] [path_to_ebook]
|
|
|
|
Launch the main calibre Graphical User Interface and optionally add the ebook at
|
|
path_to_ebook to the database.
|
|
''')
|
|
parser.add_option('--with-library', default=None, action='store',
|
|
help=_('Use the library located at the specified path.'))
|
|
parser.add_option('--start-in-tray', default=False, action='store_true',
|
|
help=_('Start minimized to system tray.'))
|
|
parser.add_option('-v', '--verbose', default=0, action='count',
|
|
help=_('Log debugging information to console'))
|
|
return parser
|
|
|
|
def init_qt(args):
|
|
parser = option_parser()
|
|
opts, args = parser.parse_args(args)
|
|
if opts.with_library is not None and os.path.isdir(opts.with_library):
|
|
prefs.set('library_path', opts.with_library)
|
|
print 'Using library at', prefs['library_path']
|
|
app = Application(args)
|
|
actions = tuple(Main.create_application_menubar())
|
|
app.setWindowIcon(QIcon(':/library'))
|
|
QCoreApplication.setOrganizationName(ORG_NAME)
|
|
QCoreApplication.setApplicationName(APP_UID)
|
|
return app, opts, args, actions
|
|
|
|
def run_gui(opts, args, actions, listener, app):
|
|
initialize_file_icon_provider()
|
|
if not dynamic.get('welcome_wizard_was_run', False):
|
|
from calibre.gui2.wizard import wizard
|
|
wizard().exec_()
|
|
dynamic.set('welcome_wizard_was_run', True)
|
|
main = Main(listener, opts, actions)
|
|
sys.excepthook = main.unhandled_exception
|
|
if len(args) > 1:
|
|
args[1] = os.path.abspath(args[1])
|
|
main.add_filesystem_book(args[1])
|
|
ret = app.exec_()
|
|
if getattr(main, 'run_wizard_b4_shutdown', False):
|
|
from calibre.gui2.wizard import wizard
|
|
wizard().exec_()
|
|
if getattr(main, 'restart_after_quit', False):
|
|
e = sys.executable if getattr(sys, 'froze', False) else sys.argv[0]
|
|
print 'Restarting with:', e, sys.argv
|
|
if hasattr(sys, 'frameworks_dir'):
|
|
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
|
|
import subprocess
|
|
subprocess.Popen('sleep 3s; open '+app, shell=True)
|
|
else:
|
|
os.execvp(e, sys.argv)
|
|
else:
|
|
if iswindows:
|
|
try:
|
|
main.system_tray_icon.hide()
|
|
except:
|
|
pass
|
|
return ret
|
|
|
|
def cant_start(msg=_('If you are sure it is not running')+', ',
|
|
what=None):
|
|
d = QMessageBox(QMessageBox.Critical, _('Cannot Start ')+__appname__,
|
|
'<p>'+(_('%s is already running.')%__appname__)+'</p>',
|
|
QMessageBox.Ok)
|
|
base = '<p>%s</p><p>%s %s'
|
|
where = __appname__ + ' '+_('may be running in the system tray, in the')+' '
|
|
if isosx:
|
|
where += _('upper right region of the screen.')
|
|
else:
|
|
where += _('lower right region of the screen.')
|
|
if what is None:
|
|
if iswindows:
|
|
what = _('try rebooting your computer.')
|
|
else:
|
|
what = _('try deleting the file')+': '+ADDRESS
|
|
|
|
d.setInformativeText(base%(where, msg, what))
|
|
d.exec_()
|
|
raise SystemExit(1)
|
|
|
|
class RC(Thread):
|
|
|
|
def run(self):
|
|
from multiprocessing.connection import Client
|
|
self.done = False
|
|
self.conn = Client(ADDRESS)
|
|
self.done = True
|
|
|
|
def communicate(args):
|
|
t = RC()
|
|
t.start()
|
|
time.sleep(3)
|
|
if not t.done:
|
|
f = os.path.expanduser('~/.calibre_calibre GUI.lock')
|
|
cant_start(what=_('try deleting the file')+': '+f)
|
|
raise SystemExit(1)
|
|
|
|
if len(args) > 1:
|
|
args[1] = os.path.abspath(args[1])
|
|
t.conn.send('launched:'+repr(args))
|
|
t.conn.close()
|
|
raise SystemExit(0)
|
|
|
|
|
|
def main(args=sys.argv):
|
|
app, opts, args, actions = init_qt(args)
|
|
from calibre.utils.lock import singleinstance
|
|
from multiprocessing.connection import Listener
|
|
si = singleinstance('calibre GUI')
|
|
if si:
|
|
try:
|
|
listener = Listener(address=ADDRESS)
|
|
except socket.error:
|
|
if iswindows:
|
|
cant_start()
|
|
os.remove(ADDRESS)
|
|
try:
|
|
listener = Listener(address=ADDRESS)
|
|
except socket.error:
|
|
cant_start()
|
|
else:
|
|
return run_gui(opts, args, actions, listener, app)
|
|
else:
|
|
return run_gui(opts, args, actions, listener, app)
|
|
try:
|
|
listener = Listener(address=ADDRESS)
|
|
except socket.error: # Good si is correct
|
|
communicate(args)
|
|
else:
|
|
return run_gui(opts, args, actions, listener, app)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
sys.exit(main())
|
|
except Exception, err:
|
|
if not iswindows: raise
|
|
tb = traceback.format_exc()
|
|
from PyQt4.QtGui import QErrorMessage
|
|
logfile = os.path.join(os.path.expanduser('~'), 'calibre.log')
|
|
if os.path.exists(logfile):
|
|
log = open(logfile).read().decode('utf-8', 'ignore')
|
|
d = QErrorMessage()
|
|
d.showMessage(('<b>Error:</b>%s<br><b>Traceback:</b><br>'
|
|
'%s<b>Log:</b><br>%s')%(unicode(err),
|
|
unicode(tb).replace('\n', '<br>'),
|
|
log.replace('\n', '<br>')))
|
|
|