mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Refactor actions to be plugins
This commit is contained in:
commit
1b1e93b46d
@ -351,3 +351,12 @@ class CatalogPlugin(Plugin):
|
|||||||
# Default implementation does nothing
|
# Default implementation does nothing
|
||||||
raise NotImplementedError('CatalogPlugin.generate_catalog() default '
|
raise NotImplementedError('CatalogPlugin.generate_catalog() default '
|
||||||
'method, should be overridden in subclass')
|
'method, should be overridden in subclass')
|
||||||
|
|
||||||
|
class InterfaceActionBase(Plugin):
|
||||||
|
|
||||||
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
author = 'Kovid Goyal'
|
||||||
|
type = _('User Interface Action')
|
||||||
|
can_be_disabled = False
|
||||||
|
|
||||||
|
actual_plugin = None
|
||||||
|
@ -574,3 +574,92 @@ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
|||||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||||
x.__name__.endswith('MetadataWriter')]
|
x.__name__.endswith('MetadataWriter')]
|
||||||
plugins += input_profiles + output_profiles
|
plugins += input_profiles + output_profiles
|
||||||
|
|
||||||
|
from calibre.customize import InterfaceActionBase
|
||||||
|
|
||||||
|
class ActionAdd(InterfaceActionBase):
|
||||||
|
name = 'Add Books'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.add:AddAction'
|
||||||
|
|
||||||
|
class ActionFetchAnnotations(InterfaceActionBase):
|
||||||
|
name = 'Fetch Annotations'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.annotate:FetchAnnotationsAction'
|
||||||
|
|
||||||
|
class ActionGenerateCatalog(InterfaceActionBase):
|
||||||
|
name = 'Generate Catalog'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.catalog:GenerateCatalogAction'
|
||||||
|
|
||||||
|
class ActionConvert(InterfaceActionBase):
|
||||||
|
name = 'Convert Books'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.convert:ConvertAction'
|
||||||
|
|
||||||
|
class ActionDelete(InterfaceActionBase):
|
||||||
|
name = 'Remove Books'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.delete:DeleteAction'
|
||||||
|
|
||||||
|
class ActionEditMetadata(InterfaceActionBase):
|
||||||
|
name = 'Edit Metadata'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.edit_metadata:EditMetadataAction'
|
||||||
|
|
||||||
|
class ActionView(InterfaceActionBase):
|
||||||
|
name = 'View'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.view:ViewAction'
|
||||||
|
|
||||||
|
class ActionFetchNews(InterfaceActionBase):
|
||||||
|
name = 'Fetch News'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.fetch_news:FetchNewsAction'
|
||||||
|
|
||||||
|
class ActionSaveToDisk(InterfaceActionBase):
|
||||||
|
name = 'Save To Disk'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
|
||||||
|
|
||||||
|
class ActionShowBookDetails(InterfaceActionBase):
|
||||||
|
name = 'Show Book Details'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.show_book_details:ShowBookDetailsAction'
|
||||||
|
|
||||||
|
class ActionRestart(InterfaceActionBase):
|
||||||
|
name = 'Restart'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.restart:RestartAction'
|
||||||
|
|
||||||
|
class ActionOpenFolder(InterfaceActionBase):
|
||||||
|
name = 'Open Folder'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.open:OpenFolderAction'
|
||||||
|
|
||||||
|
class ActionSendToDevice(InterfaceActionBase):
|
||||||
|
name = 'Send To Device'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.device:SendToDeviceAction'
|
||||||
|
|
||||||
|
class ActionConnectShare(InterfaceActionBase):
|
||||||
|
name = 'Connect Share'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.device:ConnectShareAction'
|
||||||
|
|
||||||
|
class ActionHelp(InterfaceActionBase):
|
||||||
|
name = 'Help'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.help:HelpAction'
|
||||||
|
|
||||||
|
class ActionPreferences(InterfaceActionBase):
|
||||||
|
name = 'Preferences'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.preferences:PreferencesAction'
|
||||||
|
|
||||||
|
class ActionSimilarBooks(InterfaceActionBase):
|
||||||
|
name = 'Similar Books'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.similar_books:SimilarBooksAction'
|
||||||
|
|
||||||
|
class ActionChooseLibrary(InterfaceActionBase):
|
||||||
|
name = 'Choose Library'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.choose_library:ChooseLibraryAction'
|
||||||
|
|
||||||
|
class ActionAddToLibrary(InterfaceActionBase):
|
||||||
|
name = 'Add To Library'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.add_to_library:AddToLibraryAction'
|
||||||
|
|
||||||
|
class ActionEditCollections(InterfaceActionBase):
|
||||||
|
name = 'Edit Collections'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction'
|
||||||
|
|
||||||
|
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||||
|
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||||
|
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
|
||||||
|
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||||
|
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||||
|
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary]
|
||||||
|
@ -6,7 +6,8 @@ import os, shutil, traceback, functools, sys, re
|
|||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \
|
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \
|
||||||
MetadataReaderPlugin, MetadataWriterPlugin
|
MetadataReaderPlugin, MetadataWriterPlugin, \
|
||||||
|
InterfaceActionBase as InterfaceAction
|
||||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||||
from calibre.customize.builtins import plugins as builtin_plugins
|
from calibre.customize.builtins import plugins as builtin_plugins
|
||||||
@ -19,7 +20,6 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
|
|||||||
plugin_dir, OptionParser, prefs
|
plugin_dir, OptionParser, prefs
|
||||||
from calibre.ebooks.epub.fix import ePubFixer
|
from calibre.ebooks.epub.fix import ePubFixer
|
||||||
|
|
||||||
|
|
||||||
platform = 'linux'
|
platform = 'linux'
|
||||||
if iswindows:
|
if iswindows:
|
||||||
platform = 'windows'
|
platform = 'windows'
|
||||||
@ -246,6 +246,17 @@ def cover_sources():
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# Interface Actions # {{{
|
||||||
|
|
||||||
|
def interface_actions():
|
||||||
|
customization = config['plugin_customization']
|
||||||
|
for plugin in _initialized_plugins:
|
||||||
|
if isinstance(plugin, InterfaceAction):
|
||||||
|
if not is_disabled(plugin):
|
||||||
|
plugin.site_customization = customization.get(plugin.name, '')
|
||||||
|
yield plugin
|
||||||
|
# }}}
|
||||||
|
|
||||||
# Metadata read/write {{{
|
# Metadata read/write {{{
|
||||||
_metadata_readers = {}
|
_metadata_readers = {}
|
||||||
_metadata_writers = {}
|
_metadata_writers = {}
|
||||||
|
@ -244,14 +244,20 @@ def info_dialog(parent, title, msg, det_msg='', show=False):
|
|||||||
|
|
||||||
|
|
||||||
class Dispatcher(QObject):
|
class Dispatcher(QObject):
|
||||||
'''Convenience class to ensure that a function call always happens in the
|
'''
|
||||||
thread the receiver was created in.'''
|
Convenience class to use Qt signals with arbitrary python callables.
|
||||||
|
By default, ensures that a function call always happens in the
|
||||||
|
thread this Dispatcher was created in.
|
||||||
|
'''
|
||||||
dispatch_signal = pyqtSignal(object, object)
|
dispatch_signal = pyqtSignal(object, object)
|
||||||
|
|
||||||
def __init__(self, func):
|
def __init__(self, func, queued=True, parent=None):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self, parent)
|
||||||
self.func = func
|
self.func = func
|
||||||
self.dispatch_signal.connect(self.dispatch, type=Qt.QueuedConnection)
|
typ = Qt.QueuedConnection
|
||||||
|
if not queued:
|
||||||
|
typ = Qt.AutoConnection if queued is None else Qt.DirectConnection
|
||||||
|
self.dispatch_signal.connect(self.dispatch, type=typ)
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
self.dispatch_signal.emit(args, kwargs)
|
self.dispatch_signal.emit(args, kwargs)
|
||||||
|
File diff suppressed because it is too large
Load Diff
266
src/calibre/gui2/actions/add.py
Normal file
266
src/calibre/gui2/actions/add.py
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import QInputDialog, QPixmap, QMenu
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog, choose_files, \
|
||||||
|
choose_dir, warning_dialog, info_dialog
|
||||||
|
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
||||||
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
|
from calibre.utils.filenames import ascii_filename
|
||||||
|
from calibre.constants import preferred_encoding, filesystem_encoding
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class AddAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Add Books'
|
||||||
|
action_spec = (_('Add books'), 'add_book.svg', None, _('A'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
|
||||||
|
self.add_menu = QMenu()
|
||||||
|
self.add_menu.addAction(_('Add books from a single directory'),
|
||||||
|
self.add_books)
|
||||||
|
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_recursive_single)
|
||||||
|
self.add_menu.addAction(_('Add books from directories, including '
|
||||||
|
'sub directories (Multiple books per directory, assumes every '
|
||||||
|
'ebook file is a different book)'), self.add_recursive_multiple)
|
||||||
|
self.add_menu.addSeparator()
|
||||||
|
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
|
||||||
|
'formats)'), self.add_empty)
|
||||||
|
self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn)
|
||||||
|
self.qaction.setMenu(self.add_menu)
|
||||||
|
self.qaction.triggered.connect(self.add_books)
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
for action in list(self.add_menu.actions())[1:]:
|
||||||
|
action.setEnabled(enabled)
|
||||||
|
|
||||||
|
def add_recursive(self, single):
|
||||||
|
root = choose_dir(self.gui, 'recursive book import root dir dialog',
|
||||||
|
'Select root folder')
|
||||||
|
if not root:
|
||||||
|
return
|
||||||
|
from calibre.gui2.add import Adder
|
||||||
|
self._adder = Adder(self.gui,
|
||||||
|
self.gui.library_view.model().db,
|
||||||
|
self.Dispatcher(self._files_added), spare_server=self.gui.spare_server)
|
||||||
|
self._adder.add_recursive(root, single)
|
||||||
|
|
||||||
|
def add_recursive_single(self, *args):
|
||||||
|
'''
|
||||||
|
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, *args):
|
||||||
|
'''
|
||||||
|
Add books from the local filesystem to either the library or the device
|
||||||
|
recursively assuming multiple books per folder.
|
||||||
|
'''
|
||||||
|
self.add_recursive(False)
|
||||||
|
|
||||||
|
def add_empty(self, *args):
|
||||||
|
'''
|
||||||
|
Add an empty book item to the library. This does not import any formats
|
||||||
|
from a book file.
|
||||||
|
'''
|
||||||
|
num, ok = QInputDialog.getInt(self.gui, _('How many empty books?'),
|
||||||
|
_('How many empty books should be added?'), 1, 1, 100)
|
||||||
|
if ok:
|
||||||
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
for x in xrange(num):
|
||||||
|
self.gui.library_view.model().db.import_book(MetaInformation(None), [])
|
||||||
|
self.gui.library_view.model().books_added(num)
|
||||||
|
|
||||||
|
def add_isbns(self, isbns):
|
||||||
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
ids = set([])
|
||||||
|
for x in isbns:
|
||||||
|
mi = MetaInformation(None)
|
||||||
|
mi.isbn = x
|
||||||
|
ids.add(self.gui.library_view.model().db.import_book(mi, []))
|
||||||
|
self.gui.library_view.model().books_added(len(isbns))
|
||||||
|
self.gui.iactions['Edit Metadata'].do_download_metadata(ids)
|
||||||
|
|
||||||
|
|
||||||
|
def files_dropped(self, paths):
|
||||||
|
to_device = self.gui.stack.currentIndex() != 0
|
||||||
|
self._add_books(paths, to_device)
|
||||||
|
|
||||||
|
def files_dropped_on_book(self, event, paths):
|
||||||
|
accept = False
|
||||||
|
if self.gui.current_view() is not self.gui.library_view:
|
||||||
|
return
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
current_idx = self.gui.library_view.currentIndex()
|
||||||
|
if not current_idx.isValid(): return
|
||||||
|
cid = db.id(current_idx.row())
|
||||||
|
for path in paths:
|
||||||
|
ext = os.path.splitext(path)[1].lower()
|
||||||
|
if ext:
|
||||||
|
ext = ext[1:]
|
||||||
|
if ext in IMAGE_EXTENSIONS:
|
||||||
|
pmap = QPixmap()
|
||||||
|
pmap.load(path)
|
||||||
|
if not pmap.isNull():
|
||||||
|
accept = True
|
||||||
|
db.set_cover(cid, pmap)
|
||||||
|
elif ext in BOOK_EXTENSIONS:
|
||||||
|
db.add_format_with_hooks(cid, ext, path, index_is_id=True)
|
||||||
|
accept = True
|
||||||
|
if accept:
|
||||||
|
event.accept()
|
||||||
|
self.gui.library_view.model().current_changed(current_idx, current_idx)
|
||||||
|
|
||||||
|
def __add_filesystem_book(self, paths, allow_device=True):
|
||||||
|
if isinstance(paths, basestring):
|
||||||
|
paths = [paths]
|
||||||
|
books = [path for path in map(os.path.abspath, paths) if os.access(path,
|
||||||
|
os.R_OK)]
|
||||||
|
|
||||||
|
if books:
|
||||||
|
to_device = allow_device and self.gui.stack.currentIndex() != 0
|
||||||
|
self._add_books(books, to_device)
|
||||||
|
if to_device:
|
||||||
|
self.gui.status_bar.show_message(\
|
||||||
|
_('Uploading books to device.'), 2000)
|
||||||
|
|
||||||
|
|
||||||
|
def add_filesystem_book(self, paths, allow_device=True):
|
||||||
|
self._add_filesystem_book(paths, allow_device=allow_device)
|
||||||
|
|
||||||
|
def add_from_isbn(self, *args):
|
||||||
|
from calibre.gui2.dialogs.add_from_isbn import AddFromISBN
|
||||||
|
d = AddFromISBN(self.gui)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
self.add_isbns(d.isbns)
|
||||||
|
|
||||||
|
def add_books(self, *args):
|
||||||
|
'''
|
||||||
|
Add books from the local filesystem to either the library or the device.
|
||||||
|
'''
|
||||||
|
filters = [
|
||||||
|
(_('Books'), BOOK_EXTENSIONS),
|
||||||
|
(_('EPUB Books'), ['epub']),
|
||||||
|
(_('LRF Books'), ['lrf']),
|
||||||
|
(_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']),
|
||||||
|
(_('LIT Books'), ['lit']),
|
||||||
|
(_('MOBI Books'), ['mobi', 'prc', 'azw']),
|
||||||
|
(_('Topaz books'), ['tpz','azw1']),
|
||||||
|
(_('Text books'), ['txt', 'rtf']),
|
||||||
|
(_('PDF Books'), ['pdf']),
|
||||||
|
(_('Comics'), ['cbz', 'cbr', 'cbc']),
|
||||||
|
(_('Archives'), ['zip', 'rar']),
|
||||||
|
]
|
||||||
|
to_device = self.gui.stack.currentIndex() != 0
|
||||||
|
if to_device:
|
||||||
|
filters = [(_('Supported books'), self.gui.device_manager.device.FORMATS)]
|
||||||
|
|
||||||
|
books = choose_files(self.gui, 'add books dialog dir', 'Select books',
|
||||||
|
filters=filters)
|
||||||
|
if not books:
|
||||||
|
return
|
||||||
|
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.gui.stack.currentIndex() == 2 else \
|
||||||
|
'cardb' if self.gui.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.gui,
|
||||||
|
None if to_device else self.gui.library_view.model().db,
|
||||||
|
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
|
||||||
|
self._adder.add(paths)
|
||||||
|
|
||||||
|
def _files_added(self, paths=[], names=[], infos=[], on_card=None):
|
||||||
|
if paths:
|
||||||
|
self.gui.upload_books(paths,
|
||||||
|
list(map(ascii_filename, names)),
|
||||||
|
infos, on_card=on_card)
|
||||||
|
self.gui.status_bar.show_message(
|
||||||
|
_('Uploading books to device.'), 2000)
|
||||||
|
if getattr(self._adder, 'number_of_books_added', 0) > 0:
|
||||||
|
self.gui.library_view.model().books_added(self._adder.number_of_books_added)
|
||||||
|
if hasattr(self.gui, 'db_images'):
|
||||||
|
self.gui.db_images.reset()
|
||||||
|
if getattr(self._adder, 'merged_books', False):
|
||||||
|
books = u'\n'.join([x if isinstance(x, unicode) else
|
||||||
|
x.decode(preferred_encoding, 'replace') for x in
|
||||||
|
self._adder.merged_books])
|
||||||
|
info_dialog(self.gui, _('Merged some books'),
|
||||||
|
_('Some duplicates were found and merged into the '
|
||||||
|
'following existing books:'), det_msg=books, show=True)
|
||||||
|
if getattr(self._adder, 'critical', None):
|
||||||
|
det_msg = []
|
||||||
|
for name, log in self._adder.critical.items():
|
||||||
|
if isinstance(name, str):
|
||||||
|
name = name.decode(filesystem_encoding, 'replace')
|
||||||
|
det_msg.append(name+'\n'+log)
|
||||||
|
|
||||||
|
warning_dialog(self.gui, _('Failed to read metadata'),
|
||||||
|
_('Failed to read metadata from the following')+':',
|
||||||
|
det_msg='\n\n'.join(det_msg), show=True)
|
||||||
|
|
||||||
|
if hasattr(self._adder, 'cleanup'):
|
||||||
|
self._adder.cleanup()
|
||||||
|
self._adder = None
|
||||||
|
|
||||||
|
def _add_from_device_adder(self, paths=[], names=[], infos=[],
|
||||||
|
on_card=None, model=None):
|
||||||
|
self._files_added(paths, names, infos, on_card=on_card)
|
||||||
|
# set the in-library flags, and as a consequence send the library's
|
||||||
|
# metadata for this book to the device. This sets the uuid to the
|
||||||
|
# correct value.
|
||||||
|
self.gui.set_books_in_library(booklists=[model.db], reset=True)
|
||||||
|
model.reset()
|
||||||
|
|
||||||
|
def add_books_from_device(self, view):
|
||||||
|
rows = view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
paths = [p for p in view._model.paths(rows) if p is not None]
|
||||||
|
ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
|
||||||
|
def ext(x):
|
||||||
|
ans = os.path.splitext(x)[1]
|
||||||
|
ans = ans[1:] if len(ans) > 1 else ans
|
||||||
|
return ans.lower()
|
||||||
|
remove = set([p for p in paths if ext(p) in ve])
|
||||||
|
if remove:
|
||||||
|
paths = [p for p in paths if p not in remove]
|
||||||
|
info_dialog(self.gui, _('Not Implemented'),
|
||||||
|
_('The following books are virtual and cannot be added'
|
||||||
|
' to the calibre library:'), '\n'.join(remove),
|
||||||
|
show=True)
|
||||||
|
if not paths:
|
||||||
|
return
|
||||||
|
if not paths or len(paths) == 0:
|
||||||
|
d = error_dialog(self.gui, _('Add to library'), _('No book files found'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
from calibre.gui2.add import Adder
|
||||||
|
self.__adder_func = partial(self._add_from_device_adder, on_card=None,
|
||||||
|
model=view._model)
|
||||||
|
self._adder = Adder(self.gui, self.gui.library_view.model().db,
|
||||||
|
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
|
||||||
|
self._adder.add(paths)
|
||||||
|
|
||||||
|
|
22
src/calibre/gui2/actions/add_to_library.py
Normal file
22
src/calibre/gui2/actions/add_to_library.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class AddToLibraryAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Add To Library'
|
||||||
|
action_spec = (_('Add books to library'), 'add_book.svg', None, None)
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc != 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
self.qaction.triggered.connect(self.add_books_to_library)
|
||||||
|
|
||||||
|
def add_books_to_library(self, *args):
|
||||||
|
self.gui.iactions['Add Books'].add_books_from_device(
|
||||||
|
self.gui.current_view())
|
244
src/calibre/gui2/actions/annotate.py
Normal file
244
src/calibre/gui2/actions/annotate.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, datetime
|
||||||
|
|
||||||
|
from PyQt4.Qt import pyqtSignal, QModelIndex, QThread, Qt
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog, gprefs
|
||||||
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
||||||
|
from calibre import strftime
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class FetchAnnotationsAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Fetch Annotations'
|
||||||
|
action_spec = (_('Fetch annotations (experimental)'), None, None, None)
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fetch_annotations(self, *args):
|
||||||
|
# Generate a path_map from selected ids
|
||||||
|
def get_ids_from_selected_rows():
|
||||||
|
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) < 2:
|
||||||
|
rows = xrange(self.gui.library_view.model().rowCount(QModelIndex()))
|
||||||
|
ids = map(self.gui.library_view.model().id, rows)
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def get_formats(id):
|
||||||
|
formats = db.formats(id, index_is_id=True)
|
||||||
|
fmts = []
|
||||||
|
if formats:
|
||||||
|
for format in formats.split(','):
|
||||||
|
fmts.append(format.lower())
|
||||||
|
return fmts
|
||||||
|
|
||||||
|
def generate_annotation_paths(ids, db, device):
|
||||||
|
# Generate path templates
|
||||||
|
# Individual storage mount points scanned/resolved in driver.get_annotations()
|
||||||
|
path_map = {}
|
||||||
|
for id in ids:
|
||||||
|
mi = db.get_metadata(id, index_is_id=True)
|
||||||
|
a_path = device.create_upload_path(os.path.abspath('/<storage>'), mi, 'x.bookmark', create_dirs=False)
|
||||||
|
path_map[id] = dict(path=a_path, fmts=get_formats(id))
|
||||||
|
return path_map
|
||||||
|
|
||||||
|
device = self.gui.device_manager.device
|
||||||
|
|
||||||
|
if self.gui.current_view() is not self.gui.library_view:
|
||||||
|
return error_dialog(self.gui, _('Use library only'),
|
||||||
|
_('User annotations generated from main library only'),
|
||||||
|
show=True)
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
|
||||||
|
# Get the list of ids
|
||||||
|
ids = get_ids_from_selected_rows()
|
||||||
|
if not ids:
|
||||||
|
return error_dialog(self.gui, _('No books selected'),
|
||||||
|
_('No books selected to fetch annotations from'),
|
||||||
|
show=True)
|
||||||
|
|
||||||
|
# Map ids to paths
|
||||||
|
path_map = generate_annotation_paths(ids, db, device)
|
||||||
|
|
||||||
|
# Dispatch to devices.kindle.driver.get_annotations()
|
||||||
|
self.gui.device_manager.annotations(self.Dispatcher(self.annotations_fetched),
|
||||||
|
path_map)
|
||||||
|
|
||||||
|
def annotations_fetched(self, job):
|
||||||
|
from calibre.devices.usbms.device import Device
|
||||||
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||||
|
from calibre.library.cli import do_add_format
|
||||||
|
|
||||||
|
class Updater(QThread): # {{{
|
||||||
|
|
||||||
|
update_progress = pyqtSignal(int)
|
||||||
|
update_done = pyqtSignal()
|
||||||
|
FINISHED_READING_PCT_THRESHOLD = 96
|
||||||
|
|
||||||
|
def __init__(self, parent, db, annotation_map, done_callback):
|
||||||
|
QThread.__init__(self, parent)
|
||||||
|
self.db = db
|
||||||
|
self.pd = ProgressDialog(_('Merging user annotations into database'), '',
|
||||||
|
0, len(job.result), parent=parent)
|
||||||
|
|
||||||
|
self.am = annotation_map
|
||||||
|
self.done_callback = done_callback
|
||||||
|
self.pd.canceled_signal.connect(self.canceled)
|
||||||
|
self.pd.setModal(True)
|
||||||
|
self.pd.show()
|
||||||
|
self.update_progress.connect(self.pd.set_value,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
self.update_done.connect(self.pd.hide, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
def generate_annotation_html(self, bookmark):
|
||||||
|
# Returns <div class="user_annotations"> ... </div>
|
||||||
|
last_read_location = bookmark.last_read_location
|
||||||
|
timestamp = datetime.datetime.utcfromtimestamp(bookmark.timestamp)
|
||||||
|
percent_read = bookmark.percent_read
|
||||||
|
|
||||||
|
ka_soup = BeautifulSoup()
|
||||||
|
dtc = 0
|
||||||
|
divTag = Tag(ka_soup,'div')
|
||||||
|
divTag['class'] = 'user_annotations'
|
||||||
|
|
||||||
|
# Add the last-read location
|
||||||
|
spanTag = Tag(ka_soup, 'span')
|
||||||
|
spanTag['style'] = 'font-weight:bold'
|
||||||
|
if bookmark.book_format == 'pdf':
|
||||||
|
spanTag.insert(0,NavigableString(
|
||||||
|
_("%s<br />Last Page Read: %d (%d%%)") % \
|
||||||
|
(strftime(u'%x', timestamp.timetuple()),
|
||||||
|
last_read_location,
|
||||||
|
percent_read)))
|
||||||
|
else:
|
||||||
|
spanTag.insert(0,NavigableString(
|
||||||
|
_("%s<br />Last Page Read: Location %d (%d%%)") % \
|
||||||
|
(strftime(u'%x', timestamp.timetuple()),
|
||||||
|
last_read_location,
|
||||||
|
percent_read)))
|
||||||
|
|
||||||
|
divTag.insert(dtc, spanTag)
|
||||||
|
dtc += 1
|
||||||
|
divTag.insert(dtc, Tag(ka_soup,'br'))
|
||||||
|
dtc += 1
|
||||||
|
|
||||||
|
if bookmark.user_notes:
|
||||||
|
user_notes = bookmark.user_notes
|
||||||
|
annotations = []
|
||||||
|
|
||||||
|
# Add the annotations sorted by location
|
||||||
|
# Italicize highlighted text
|
||||||
|
for location in sorted(user_notes):
|
||||||
|
if user_notes[location]['text']:
|
||||||
|
annotations.append(
|
||||||
|
_('<b>Location %d • %s</b><br />%s<br />') % \
|
||||||
|
(user_notes[location]['displayed_location'],
|
||||||
|
user_notes[location]['type'],
|
||||||
|
user_notes[location]['text'] if \
|
||||||
|
user_notes[location]['type'] == 'Note' else \
|
||||||
|
'<i>%s</i>' % user_notes[location]['text']))
|
||||||
|
else:
|
||||||
|
if bookmark.book_format == 'pdf':
|
||||||
|
annotations.append(
|
||||||
|
_('<b>Page %d • %s</b><br />') % \
|
||||||
|
(user_notes[location]['displayed_location'],
|
||||||
|
user_notes[location]['type']))
|
||||||
|
else:
|
||||||
|
annotations.append(
|
||||||
|
_('<b>Location %d • %s</b><br />') % \
|
||||||
|
(user_notes[location]['displayed_location'],
|
||||||
|
user_notes[location]['type']))
|
||||||
|
|
||||||
|
for annotation in annotations:
|
||||||
|
divTag.insert(dtc, annotation)
|
||||||
|
dtc += 1
|
||||||
|
|
||||||
|
ka_soup.insert(0,divTag)
|
||||||
|
return ka_soup
|
||||||
|
|
||||||
|
def mark_book_as_read(self,id):
|
||||||
|
read_tag = gprefs.get('catalog_epub_mobi_read_tag')
|
||||||
|
if read_tag:
|
||||||
|
self.db.set_tags(id, [read_tag], append=True)
|
||||||
|
|
||||||
|
def canceled(self):
|
||||||
|
self.pd.hide()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
ignore_tags = set(['Catalog','Clippings'])
|
||||||
|
for (i, id) in enumerate(self.am):
|
||||||
|
bm = Device.UserAnnotation(self.am[id][0],self.am[id][1])
|
||||||
|
if bm.type == 'kindle_bookmark':
|
||||||
|
mi = self.db.get_metadata(id, index_is_id=True)
|
||||||
|
user_notes_soup = self.generate_annotation_html(bm.value)
|
||||||
|
if mi.comments:
|
||||||
|
a_offset = mi.comments.find('<div class="user_annotations">')
|
||||||
|
ad_offset = mi.comments.find('<hr class="annotations_divider" />')
|
||||||
|
|
||||||
|
if a_offset >= 0:
|
||||||
|
mi.comments = mi.comments[:a_offset]
|
||||||
|
if ad_offset >= 0:
|
||||||
|
mi.comments = mi.comments[:ad_offset]
|
||||||
|
if set(mi.tags).intersection(ignore_tags):
|
||||||
|
continue
|
||||||
|
if mi.comments:
|
||||||
|
hrTag = Tag(user_notes_soup,'hr')
|
||||||
|
hrTag['class'] = 'annotations_divider'
|
||||||
|
user_notes_soup.insert(0,hrTag)
|
||||||
|
|
||||||
|
mi.comments += user_notes_soup.prettify()
|
||||||
|
else:
|
||||||
|
mi.comments = unicode(user_notes_soup.prettify())
|
||||||
|
# Update library comments
|
||||||
|
self.db.set_comment(id, mi.comments)
|
||||||
|
|
||||||
|
# Update 'read' tag except for Catalogs/Clippings
|
||||||
|
if bm.value.percent_read >= self.FINISHED_READING_PCT_THRESHOLD:
|
||||||
|
if not set(mi.tags).intersection(ignore_tags):
|
||||||
|
self.mark_book_as_read(id)
|
||||||
|
|
||||||
|
# Add bookmark file to id
|
||||||
|
self.db.add_format_with_hooks(id, bm.value.bookmark_extension,
|
||||||
|
bm.value.path, index_is_id=True)
|
||||||
|
self.update_progress.emit(i)
|
||||||
|
elif bm.type == 'kindle_clippings':
|
||||||
|
# Find 'My Clippings' author=Kindle in database, or add
|
||||||
|
last_update = 'Last modified %s' % strftime(u'%x %X',bm.value['timestamp'].timetuple())
|
||||||
|
mc_id = list(db.data.parse('title:"My Clippings"'))
|
||||||
|
if mc_id:
|
||||||
|
do_add_format(self.db, mc_id[0], 'TXT', bm.value['path'])
|
||||||
|
mi = self.db.get_metadata(mc_id[0], index_is_id=True)
|
||||||
|
mi.comments = last_update
|
||||||
|
self.db.set_metadata(mc_id[0], mi)
|
||||||
|
else:
|
||||||
|
mi = MetaInformation('My Clippings', authors = ['Kindle'])
|
||||||
|
mi.tags = ['Clippings']
|
||||||
|
mi.comments = last_update
|
||||||
|
self.db.add_books([bm.value['path']], ['txt'], [mi])
|
||||||
|
|
||||||
|
self.update_done.emit()
|
||||||
|
self.done_callback(self.am.keys())
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
if not job.result: return
|
||||||
|
|
||||||
|
if self.gui.current_view() is not self.gui.library_view:
|
||||||
|
return error_dialog(self.gui, _('Use library only'),
|
||||||
|
_('User annotations generated from main library only'),
|
||||||
|
show=True)
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
|
||||||
|
self.__annotation_updater = Updater(self.gui, db, job.result,
|
||||||
|
self.Dispatcher(self.gui.library_view.model().refresh_ids))
|
||||||
|
self.__annotation_updater.start()
|
||||||
|
|
||||||
|
|
73
src/calibre/gui2/actions/catalog.py
Normal file
73
src/calibre/gui2/actions/catalog.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, shutil
|
||||||
|
|
||||||
|
from PyQt4.Qt import QModelIndex
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog, choose_dir
|
||||||
|
from calibre.gui2.tools import generate_catalog
|
||||||
|
from calibre.utils.config import dynamic
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class GenerateCatalogAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Generate Catalog'
|
||||||
|
action_spec = (_('Create catalog of books in your calibre library'), None, None, None)
|
||||||
|
|
||||||
|
def generate_catalog(self):
|
||||||
|
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) < 2:
|
||||||
|
rows = xrange(self.gui.library_view.model().rowCount(QModelIndex()))
|
||||||
|
ids = map(self.gui.library_view.model().id, rows)
|
||||||
|
|
||||||
|
dbspec = None
|
||||||
|
if not ids:
|
||||||
|
return error_dialog(self.gui, _('No books selected'),
|
||||||
|
_('No books selected to generate catalog for'),
|
||||||
|
show=True)
|
||||||
|
|
||||||
|
# Calling gui2.tools:generate_catalog()
|
||||||
|
ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager.device)
|
||||||
|
if ret is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
func, args, desc, out, sync, title = ret
|
||||||
|
|
||||||
|
fmt = os.path.splitext(out)[1][1:].upper()
|
||||||
|
job = self.gui.job_manager.run_job(
|
||||||
|
self.Dispatcher(self.catalog_generated), func, args=args,
|
||||||
|
description=desc)
|
||||||
|
job.catalog_file_path = out
|
||||||
|
job.fmt = fmt
|
||||||
|
job.catalog_sync, job.catalog_title = sync, title
|
||||||
|
self.gui.status_bar.show_message(_('Generating %s catalog...')%fmt)
|
||||||
|
|
||||||
|
def catalog_generated(self, job):
|
||||||
|
if job.result:
|
||||||
|
# Search terms nulled catalog results
|
||||||
|
return error_dialog(self.gui, _('No books found'),
|
||||||
|
_("No books to catalog\nCheck exclude tags"),
|
||||||
|
show=True)
|
||||||
|
if job.failed:
|
||||||
|
return self.gui.job_exception(job)
|
||||||
|
id = self.gui.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title)
|
||||||
|
self.gui.library_view.model().reset()
|
||||||
|
if job.catalog_sync:
|
||||||
|
sync = dynamic.get('catalogs_to_be_synced', set([]))
|
||||||
|
sync.add(id)
|
||||||
|
dynamic.set('catalogs_to_be_synced', sync)
|
||||||
|
self.gui.status_bar.show_message(_('Catalog generated.'), 3000)
|
||||||
|
self.gui.sync_catalogs()
|
||||||
|
if job.fmt not in ['EPUB','MOBI']:
|
||||||
|
export_dir = choose_dir(self.gui, _('Export Catalog Directory'),
|
||||||
|
_('Select destination for %s.%s') % (job.catalog_title, job.fmt.lower()))
|
||||||
|
if export_dir:
|
||||||
|
destination = os.path.join(export_dir, '%s.%s' % (job.catalog_title, job.fmt.lower()))
|
||||||
|
shutil.copyfile(job.catalog_file_path, destination)
|
||||||
|
|
||||||
|
|
39
src/calibre/gui2/actions/choose_library.py
Normal file
39
src/calibre/gui2/actions/choose_library.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class ChooseLibraryAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Choose Library'
|
||||||
|
action_spec = (_('%d books'), 'lt.png',
|
||||||
|
_('Choose calibre library to work with'), None)
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.count_changed(0)
|
||||||
|
self.qaction.triggered.connect(self.choose_library)
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
|
||||||
|
def count_changed(self, new_count):
|
||||||
|
text = self.action_spec[0]%new_count
|
||||||
|
a = self.qaction
|
||||||
|
a.setText(text)
|
||||||
|
tooltip = self.action_spec[2] + '\n\n' + text
|
||||||
|
a.setToolTip(tooltip)
|
||||||
|
a.setStatusTip(tooltip)
|
||||||
|
a.setWhatsThis(tooltip)
|
||||||
|
|
||||||
|
def choose_library(self, *args):
|
||||||
|
from calibre.gui2.dialogs.choose_library import ChooseLibrary
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
c = ChooseLibrary(db, self.gui.library_moved, self.gui)
|
||||||
|
c.exec_()
|
||||||
|
|
||||||
|
|
172
src/calibre/gui2/actions/convert.py
Normal file
172
src/calibre/gui2/actions/convert.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import QModelIndex, QMenu
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog, Dispatcher
|
||||||
|
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook
|
||||||
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class ConvertAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Convert Books'
|
||||||
|
action_spec = (_('Convert books'), 'convert.svg', None, _('C'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
cm = QMenu()
|
||||||
|
cm.addAction(_('Convert individually'), partial(self.convert_ebook,
|
||||||
|
False, bulk=False))
|
||||||
|
cm.addAction(_('Bulk convert'),
|
||||||
|
partial(self.convert_ebook, False, bulk=True))
|
||||||
|
cm.addSeparator()
|
||||||
|
ac = cm.addAction(
|
||||||
|
_('Create catalog of books in your calibre library'))
|
||||||
|
ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog)
|
||||||
|
self.qaction.setMenu(cm)
|
||||||
|
self.qaction.triggered.connect(self.convert_ebook)
|
||||||
|
self.convert_menu = cm
|
||||||
|
self.conversion_jobs = {}
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
|
||||||
|
def auto_convert(self, book_ids, on_card, format):
|
||||||
|
previous = self.gui.library_view.currentIndex()
|
||||||
|
rows = [x.row() for x in \
|
||||||
|
self.gui.library_view.selectionModel().selectedRows()]
|
||||||
|
jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, format)
|
||||||
|
if jobs == []: return
|
||||||
|
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
||||||
|
self.book_auto_converted, extra_job_args=[on_card])
|
||||||
|
|
||||||
|
def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format):
|
||||||
|
previous = self.gui.library_view.currentIndex()
|
||||||
|
rows = [x.row() for x in \
|
||||||
|
self.gui.library_view.selectionModel().selectedRows()]
|
||||||
|
jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, format)
|
||||||
|
if jobs == []: return
|
||||||
|
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
||||||
|
self.book_auto_converted_mail,
|
||||||
|
extra_job_args=[delete_from_library, to, fmts])
|
||||||
|
|
||||||
|
def auto_convert_news(self, book_ids, format):
|
||||||
|
previous = self.gui.library_view.currentIndex()
|
||||||
|
rows = [x.row() for x in \
|
||||||
|
self.gui.library_view.selectionModel().selectedRows()]
|
||||||
|
jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, format)
|
||||||
|
if jobs == []: return
|
||||||
|
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
||||||
|
self.book_auto_converted_news)
|
||||||
|
|
||||||
|
def auto_convert_catalogs(self, book_ids, format):
|
||||||
|
previous = self.gui.library_view.currentIndex()
|
||||||
|
rows = [x.row() for x in \
|
||||||
|
self.gui.library_view.selectionModel().selectedRows()]
|
||||||
|
jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, format)
|
||||||
|
if jobs == []: return
|
||||||
|
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
||||||
|
self.book_auto_converted_catalogs)
|
||||||
|
|
||||||
|
def get_books_for_conversion(self):
|
||||||
|
rows = [r.row() for r in \
|
||||||
|
self.gui.library_view.selectionModel().selectedRows()]
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self.gui, _('Cannot convert'),
|
||||||
|
_('No books selected'))
|
||||||
|
d.exec_()
|
||||||
|
return None
|
||||||
|
return [self.gui.library_view.model().db.id(r) for r in rows]
|
||||||
|
|
||||||
|
def convert_ebook(self, checked, bulk=None):
|
||||||
|
book_ids = self.get_books_for_conversion()
|
||||||
|
if book_ids is None: return
|
||||||
|
previous = self.gui.library_view.currentIndex()
|
||||||
|
rows = [x.row() for x in \
|
||||||
|
self.gui.library_view.selectionModel().selectedRows()]
|
||||||
|
num = 0
|
||||||
|
if bulk or (bulk is None and len(book_ids) > 1):
|
||||||
|
self.__bulk_queue = convert_bulk_ebook(self.gui, self.queue_convert_jobs,
|
||||||
|
self.gui.library_view.model().db, book_ids,
|
||||||
|
out_format=prefs['output_format'], args=(rows, previous,
|
||||||
|
self.book_converted))
|
||||||
|
if self.__bulk_queue is None:
|
||||||
|
return
|
||||||
|
num = len(self.__bulk_queue.book_ids)
|
||||||
|
else:
|
||||||
|
jobs, changed, bad = convert_single_ebook(self.gui,
|
||||||
|
self.gui.library_view.model().db, book_ids, out_format=prefs['output_format'])
|
||||||
|
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
||||||
|
self.book_converted)
|
||||||
|
num = len(jobs)
|
||||||
|
|
||||||
|
if num > 0:
|
||||||
|
self.gui.status_bar.show_message(_('Starting conversion of %d book(s)') %
|
||||||
|
num, 2000)
|
||||||
|
|
||||||
|
def queue_convert_jobs(self, jobs, changed, bad, rows, previous,
|
||||||
|
converted_func, extra_job_args=[]):
|
||||||
|
for func, args, desc, fmt, id, temp_files in jobs:
|
||||||
|
if id not in bad:
|
||||||
|
job = self.gui.job_manager.run_job(Dispatcher(converted_func),
|
||||||
|
func, args=args, description=desc)
|
||||||
|
args = [temp_files, fmt, id]+extra_job_args
|
||||||
|
self.conversion_jobs[job] = tuple(args)
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
self.gui.library_view.model().refresh_rows(rows)
|
||||||
|
current = self.gui.library_view.currentIndex()
|
||||||
|
self.gui.library_view.model().current_changed(current, previous)
|
||||||
|
|
||||||
|
def book_auto_converted(self, job):
|
||||||
|
temp_files, fmt, book_id, on_card = self.conversion_jobs[job]
|
||||||
|
self.book_converted(job)
|
||||||
|
self.gui.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[job]
|
||||||
|
self.book_converted(job)
|
||||||
|
self.gui.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[job]
|
||||||
|
self.book_converted(job)
|
||||||
|
self.gui.sync_news(send_ids=[book_id], do_auto_convert=False)
|
||||||
|
|
||||||
|
def book_auto_converted_catalogs(self, job):
|
||||||
|
temp_files, fmt, book_id = self.conversion_jobs[job]
|
||||||
|
self.book_converted(job)
|
||||||
|
self.gui.sync_catalogs(send_ids=[book_id], do_auto_convert=False)
|
||||||
|
|
||||||
|
def book_converted(self, job):
|
||||||
|
temp_files, fmt, book_id = self.conversion_jobs.pop(job)[:3]
|
||||||
|
try:
|
||||||
|
if job.failed:
|
||||||
|
self.gui.job_exception(job)
|
||||||
|
return
|
||||||
|
data = open(temp_files[-1].name, 'rb')
|
||||||
|
self.gui.library_view.model().db.add_format(book_id, \
|
||||||
|
fmt, data, index_is_id=True)
|
||||||
|
data.close()
|
||||||
|
self.gui.status_bar.show_message(job.description + \
|
||||||
|
(' completed'), 2000)
|
||||||
|
finally:
|
||||||
|
for f in temp_files:
|
||||||
|
try:
|
||||||
|
if os.path.exists(f.name):
|
||||||
|
os.remove(f.name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
if self.gui.current_view() is self.gui.library_view:
|
||||||
|
current = self.gui.library_view.currentIndex()
|
||||||
|
self.gui.library_view.model().current_changed(current, QModelIndex())
|
||||||
|
|
195
src/calibre/gui2/actions/delete.py
Normal file
195
src/calibre/gui2/actions/delete.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from PyQt4.Qt import QMenu
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
|
from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class DeleteAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Remove Books'
|
||||||
|
action_spec = (_('Remove books'), 'trash.svg', None, _('Del'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.delete_books)
|
||||||
|
self.delete_menu = QMenu()
|
||||||
|
self.delete_menu.addAction(_('Remove selected books'), self.delete_books)
|
||||||
|
self.delete_menu.addAction(
|
||||||
|
_('Remove files of a specific format from selected books..'),
|
||||||
|
self.delete_selected_formats)
|
||||||
|
self.delete_menu.addAction(
|
||||||
|
_('Remove all formats from selected books, except...'),
|
||||||
|
self.delete_all_but_selected_formats)
|
||||||
|
self.delete_menu.addAction(
|
||||||
|
_('Remove covers from selected books'), self.delete_covers)
|
||||||
|
self.delete_menu.addSeparator()
|
||||||
|
self.delete_menu.addAction(
|
||||||
|
_('Remove matching books from device'),
|
||||||
|
self.remove_matching_books_from_device)
|
||||||
|
self.qaction.setMenu(self.delete_menu)
|
||||||
|
self.delete_memory = {}
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
for action in list(self.delete_menu.actions())[1:]:
|
||||||
|
action.setEnabled(enabled)
|
||||||
|
|
||||||
|
def _get_selected_formats(self, msg):
|
||||||
|
from calibre.gui2.dialogs.select_formats import SelectFormats
|
||||||
|
fmts = self.gui.library_view.model().db.all_formats()
|
||||||
|
d = SelectFormats([x.lower() for x in fmts], msg, parent=self.gui)
|
||||||
|
if d.exec_() != d.Accepted:
|
||||||
|
return None
|
||||||
|
return d.selected_formats
|
||||||
|
|
||||||
|
def _get_selected_ids(self, err_title=_('Cannot delete')):
|
||||||
|
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self.gui, err_title, _('No book selected'))
|
||||||
|
d.exec_()
|
||||||
|
return set([])
|
||||||
|
return set(map(self.gui.library_view.model().id, rows))
|
||||||
|
|
||||||
|
def delete_selected_formats(self, *args):
|
||||||
|
ids = self._get_selected_ids()
|
||||||
|
if not ids:
|
||||||
|
return
|
||||||
|
fmts = self._get_selected_formats(
|
||||||
|
_('Choose formats to be deleted'))
|
||||||
|
if not fmts:
|
||||||
|
return
|
||||||
|
for id in ids:
|
||||||
|
for fmt in fmts:
|
||||||
|
self.gui.library_view.model().db.remove_format(id, fmt,
|
||||||
|
index_is_id=True, notify=False)
|
||||||
|
self.gui.library_view.model().refresh_ids(ids)
|
||||||
|
self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
|
||||||
|
self.gui.library_view.currentIndex())
|
||||||
|
if ids:
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
|
||||||
|
def delete_all_but_selected_formats(self, *args):
|
||||||
|
ids = self._get_selected_ids()
|
||||||
|
if not ids:
|
||||||
|
return
|
||||||
|
fmts = self._get_selected_formats(
|
||||||
|
'<p>'+_('Choose formats <b>not</b> to be deleted'))
|
||||||
|
if fmts is None:
|
||||||
|
return
|
||||||
|
for id in ids:
|
||||||
|
bfmts = self.gui.library_view.model().db.formats(id, index_is_id=True)
|
||||||
|
if bfmts is None:
|
||||||
|
continue
|
||||||
|
bfmts = set([x.lower() for x in bfmts.split(',')])
|
||||||
|
rfmts = bfmts - set(fmts)
|
||||||
|
for fmt in rfmts:
|
||||||
|
self.gui.library_view.model().db.remove_format(id, fmt,
|
||||||
|
index_is_id=True, notify=False)
|
||||||
|
self.gui.library_view.model().refresh_ids(ids)
|
||||||
|
self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
|
||||||
|
self.gui.library_view.currentIndex())
|
||||||
|
if ids:
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
|
||||||
|
def remove_matching_books_from_device(self, *args):
|
||||||
|
if not self.gui.device_manager.is_device_connected:
|
||||||
|
d = error_dialog(self.gui, _('Cannot delete books'),
|
||||||
|
_('No device is connected'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
ids = self._get_selected_ids()
|
||||||
|
if not ids:
|
||||||
|
#_get_selected_ids shows a dialog box if nothing is selected, so we
|
||||||
|
#do not need to show one here
|
||||||
|
return
|
||||||
|
to_delete = {}
|
||||||
|
some_to_delete = False
|
||||||
|
for model,name in ((self.gui.memory_view.model(), _('Main memory')),
|
||||||
|
(self.gui.card_a_view.model(), _('Storage Card A')),
|
||||||
|
(self.gui.card_b_view.model(), _('Storage Card B'))):
|
||||||
|
to_delete[name] = (model, model.paths_for_db_ids(ids))
|
||||||
|
if len(to_delete[name][1]) > 0:
|
||||||
|
some_to_delete = True
|
||||||
|
if not some_to_delete:
|
||||||
|
d = error_dialog(self.gui, _('No books to delete'),
|
||||||
|
_('None of the selected books are on the device'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
d = DeleteMatchingFromDeviceDialog(self.gui, to_delete)
|
||||||
|
if d.exec_():
|
||||||
|
paths = {}
|
||||||
|
ids = {}
|
||||||
|
for (model, id, path) in d.result:
|
||||||
|
if model not in paths:
|
||||||
|
paths[model] = []
|
||||||
|
ids[model] = []
|
||||||
|
paths[model].append(path)
|
||||||
|
ids[model].append(id)
|
||||||
|
for model in paths:
|
||||||
|
job = self.gui.remove_paths(paths[model])
|
||||||
|
self.delete_memory[job] = (paths[model], model)
|
||||||
|
model.mark_for_deletion(job, ids[model], rows_are_ids=True)
|
||||||
|
self.gui.status_bar.show_message(_('Deleting books from device.'), 1000)
|
||||||
|
|
||||||
|
def delete_covers(self, *args):
|
||||||
|
ids = self._get_selected_ids()
|
||||||
|
if not ids:
|
||||||
|
return
|
||||||
|
for id in ids:
|
||||||
|
self.gui.library_view.model().db.remove_cover(id)
|
||||||
|
self.gui.library_view.model().refresh_ids(ids)
|
||||||
|
self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
|
||||||
|
self.gui.library_view.currentIndex())
|
||||||
|
|
||||||
|
def delete_books(self, *args):
|
||||||
|
'''
|
||||||
|
Delete selected books from device or library.
|
||||||
|
'''
|
||||||
|
view = self.gui.current_view()
|
||||||
|
rows = view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
return
|
||||||
|
if self.gui.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.gui):
|
||||||
|
return
|
||||||
|
ci = view.currentIndex()
|
||||||
|
row = None
|
||||||
|
if ci.isValid():
|
||||||
|
row = ci.row()
|
||||||
|
ids_deleted = view.model().delete_books(rows)
|
||||||
|
for v in (self.gui.memory_view, self.gui.card_a_view, self.gui.card_b_view):
|
||||||
|
if v is None:
|
||||||
|
continue
|
||||||
|
v.model().clear_ondevice(ids_deleted)
|
||||||
|
if row is not None:
|
||||||
|
ci = view.model().index(row, 0)
|
||||||
|
if ci.isValid():
|
||||||
|
view.set_current_row(row)
|
||||||
|
else:
|
||||||
|
if not confirm('<p>'+_('The selected books will be '
|
||||||
|
'<b>permanently deleted</b> '
|
||||||
|
'from your device. Are you sure?')
|
||||||
|
+'</p>', 'device_delete_books', self.gui):
|
||||||
|
return
|
||||||
|
if self.gui.stack.currentIndex() == 1:
|
||||||
|
view = self.gui.memory_view
|
||||||
|
elif self.gui.stack.currentIndex() == 2:
|
||||||
|
view = self.gui.card_a_view
|
||||||
|
else:
|
||||||
|
view = self.gui.card_b_view
|
||||||
|
paths = view.model().paths(rows)
|
||||||
|
job = self.gui.remove_paths(paths)
|
||||||
|
self.delete_memory[job] = (paths, view.model())
|
||||||
|
view.model().mark_for_deletion(job, rows)
|
||||||
|
self.gui.status_bar.show_message(_('Deleting books from device.'), 1000)
|
||||||
|
|
146
src/calibre/gui2/actions/device.py
Normal file
146
src/calibre/gui2/actions/device.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import QToolButton, QMenu, pyqtSignal, QIcon
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.utils.smtp import config as email_config
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
|
||||||
|
class ShareConnMenu(QMenu): # {{{
|
||||||
|
|
||||||
|
connect_to_folder = pyqtSignal()
|
||||||
|
connect_to_itunes = pyqtSignal()
|
||||||
|
config_email = pyqtSignal()
|
||||||
|
toggle_server = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QMenu.__init__(self, parent)
|
||||||
|
mitem = self.addAction(QIcon(I('devices/folder.svg')), _('Connect to folder'))
|
||||||
|
mitem.setEnabled(True)
|
||||||
|
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
|
||||||
|
self.connect_to_folder_action = mitem
|
||||||
|
mitem = self.addAction(QIcon(I('devices/itunes.png')),
|
||||||
|
_('Connect to iTunes'))
|
||||||
|
mitem.setEnabled(True)
|
||||||
|
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
|
||||||
|
self.connect_to_itunes_action = mitem
|
||||||
|
if not (iswindows or isosx):
|
||||||
|
mitem.setVisible(False)
|
||||||
|
self.addSeparator()
|
||||||
|
self.toggle_server_action = \
|
||||||
|
self.addAction(QIcon(I('network-server.svg')),
|
||||||
|
_('Start Content Server'))
|
||||||
|
self.toggle_server_action.triggered.connect(lambda x:
|
||||||
|
self.toggle_server.emit())
|
||||||
|
self.addSeparator()
|
||||||
|
|
||||||
|
self.email_actions = []
|
||||||
|
|
||||||
|
def server_state_changed(self, running):
|
||||||
|
text = _('Start Content Server')
|
||||||
|
if running:
|
||||||
|
text = _('Stop Content Server')
|
||||||
|
self.toggle_server_action.setText(text)
|
||||||
|
|
||||||
|
def build_email_entries(self, sync_menu):
|
||||||
|
from calibre.gui2.device import DeviceAction
|
||||||
|
for ac in self.email_actions:
|
||||||
|
self.removeAction(ac)
|
||||||
|
self.email_actions = []
|
||||||
|
self.memory = []
|
||||||
|
opts = email_config().parse()
|
||||||
|
if opts.accounts:
|
||||||
|
self.email_to_menu = QMenu(_('Email to')+'...', self)
|
||||||
|
keys = sorted(opts.accounts.keys())
|
||||||
|
for account in keys:
|
||||||
|
formats, auto, default = opts.accounts[account]
|
||||||
|
dest = 'mail:'+account+';'+formats
|
||||||
|
action1 = DeviceAction(dest, False, False, I('mail.svg'),
|
||||||
|
_('Email to')+' '+account)
|
||||||
|
action2 = DeviceAction(dest, True, False, I('mail.svg'),
|
||||||
|
_('Email to')+' '+account+ _(' and delete from library'))
|
||||||
|
map(self.email_to_menu.addAction, (action1, action2))
|
||||||
|
map(self.memory.append, (action1, action2))
|
||||||
|
if default:
|
||||||
|
map(self.addAction, (action1, action2))
|
||||||
|
map(self.email_actions.append, (action1, action2))
|
||||||
|
self.email_to_menu.addSeparator()
|
||||||
|
action1.a_s.connect(sync_menu.action_triggered)
|
||||||
|
action2.a_s.connect(sync_menu.action_triggered)
|
||||||
|
ac = self.addMenu(self.email_to_menu)
|
||||||
|
self.email_actions.append(ac)
|
||||||
|
else:
|
||||||
|
ac = self.addAction(_('Setup email based sharing of books'))
|
||||||
|
self.email_actions.append(ac)
|
||||||
|
ac.triggered.connect(self.setup_email)
|
||||||
|
|
||||||
|
def setup_email(self, *args):
|
||||||
|
self.config_email.emit()
|
||||||
|
|
||||||
|
def set_state(self, device_connected):
|
||||||
|
self.connect_to_folder_action.setEnabled(not device_connected)
|
||||||
|
self.connect_to_itunes_action.setEnabled(not device_connected)
|
||||||
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class SendToDeviceAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Send To Device'
|
||||||
|
action_spec = (_('Send to device'), 'sync.svg', None, _('D'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.do_sync)
|
||||||
|
self.gui.create_device_menu()
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
|
||||||
|
def do_sync(self, *args):
|
||||||
|
self.gui._sync_action_triggered()
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectShareAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Connect Share'
|
||||||
|
action_spec = (_('Connect/share'), 'connect_share.svg', None, None)
|
||||||
|
popup_type = QToolButton.InstantPopup
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.share_conn_menu = ShareConnMenu(self.gui)
|
||||||
|
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
|
||||||
|
self.share_conn_menu.config_email.connect(partial(
|
||||||
|
self.gui.iactions['Preferences'].do_config,
|
||||||
|
initial_category='email'))
|
||||||
|
self.qaction.setMenu(self.share_conn_menu)
|
||||||
|
self.share_conn_menu.connect_to_folder.connect(self.gui.connect_to_folder)
|
||||||
|
self.share_conn_menu.connect_to_itunes.connect(self.gui.connect_to_itunes)
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
|
||||||
|
def set_state(self, device_connected):
|
||||||
|
self.share_conn_menu.set_state(device_connected)
|
||||||
|
|
||||||
|
def build_email_entries(self):
|
||||||
|
m = self.gui.iactions['Send To Device'].qaction.menu()
|
||||||
|
self.share_conn_menu.build_email_entries(m)
|
||||||
|
|
||||||
|
def content_server_state_changed(self, running):
|
||||||
|
self.share_conn_menu.server_state_changed(running)
|
||||||
|
|
||||||
|
def toggle_content_server(self):
|
||||||
|
if self.gui.content_server is None:
|
||||||
|
self.gui.start_content_server()
|
||||||
|
else:
|
||||||
|
self.gui.content_server.exit()
|
||||||
|
self.gui.content_server = None
|
29
src/calibre/gui2/actions/edit_collections.py
Normal file
29
src/calibre/gui2/actions/edit_collections.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class EditCollectionsAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Edit Collections'
|
||||||
|
action_spec = (_('Manage collections'), None, None, None)
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc != 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
self.qaction.triggered.connect(self.edit_collections)
|
||||||
|
|
||||||
|
def edit_collections(self, *args):
|
||||||
|
oncard = None
|
||||||
|
cv = self.gui.current_view()
|
||||||
|
if cv is self.gui.card_a_view:
|
||||||
|
oncard = 'carda'
|
||||||
|
if cv is self.gui.card_b_view:
|
||||||
|
oncard = 'cardb'
|
||||||
|
self.gui.iactions['Edit Metadata'].edit_device_collections(cv,
|
||||||
|
oncard=oncard)
|
||||||
|
|
359
src/calibre/gui2/actions/edit_metadata.py
Normal file
359
src/calibre/gui2/actions/edit_metadata.py
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import Qt, QTimer, QMenu
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog, config, warning_dialog
|
||||||
|
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
|
||||||
|
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class EditMetadataAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Edit Metadata'
|
||||||
|
action_spec = (_('Edit metadata'), 'edit_input.svg', None, _('E'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.create_action(spec=(_('Merge book records'), 'merge_books.svg',
|
||||||
|
None, _('M')), attr='action_merge')
|
||||||
|
md = QMenu()
|
||||||
|
md.addAction(_('Edit metadata individually'),
|
||||||
|
partial(self.edit_metadata, False, bulk=False))
|
||||||
|
md.addSeparator()
|
||||||
|
md.addAction(_('Edit metadata in bulk'),
|
||||||
|
partial(self.edit_metadata, False, bulk=True))
|
||||||
|
md.addSeparator()
|
||||||
|
md.addAction(_('Download metadata and covers'),
|
||||||
|
partial(self.download_metadata, False, covers=True),
|
||||||
|
Qt.ControlModifier+Qt.Key_D)
|
||||||
|
md.addAction(_('Download only metadata'),
|
||||||
|
partial(self.download_metadata, False, covers=False))
|
||||||
|
md.addAction(_('Download only covers'),
|
||||||
|
partial(self.download_metadata, False, covers=True,
|
||||||
|
set_metadata=False, set_social_metadata=False))
|
||||||
|
md.addAction(_('Download only social metadata'),
|
||||||
|
partial(self.download_metadata, False, covers=False,
|
||||||
|
set_metadata=False, set_social_metadata=True))
|
||||||
|
self.metadata_menu = md
|
||||||
|
|
||||||
|
mb = QMenu()
|
||||||
|
mb.addAction(_('Merge into first selected book - delete others'),
|
||||||
|
self.merge_books)
|
||||||
|
mb.addSeparator()
|
||||||
|
mb.addAction(_('Merge into first selected book - keep others'),
|
||||||
|
partial(self.merge_books, safe_merge=True))
|
||||||
|
self.merge_menu = mb
|
||||||
|
self.action_merge.setMenu(mb)
|
||||||
|
md.addSeparator()
|
||||||
|
md.addAction(self.action_merge)
|
||||||
|
|
||||||
|
self.qaction.triggered.connect(self.edit_metadata)
|
||||||
|
self.qaction.setMenu(md)
|
||||||
|
self.action_merge.triggered.connect(self.merge_books)
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
self.action_merge.setEnabled(enabled)
|
||||||
|
|
||||||
|
def download_metadata(self, checked, covers=True, set_metadata=True,
|
||||||
|
set_social_metadata=None):
|
||||||
|
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self.gui, _('Cannot download metadata'),
|
||||||
|
_('No books selected'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
ids = [db.id(row.row()) for row in rows]
|
||||||
|
self.do_download_metadata(ids, covers=covers,
|
||||||
|
set_metadata=set_metadata,
|
||||||
|
set_social_metadata=set_social_metadata)
|
||||||
|
|
||||||
|
def do_download_metadata(self, ids, covers=True, set_metadata=True,
|
||||||
|
set_social_metadata=None):
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
if set_social_metadata is None:
|
||||||
|
get_social_metadata = config['get_social_metadata']
|
||||||
|
else:
|
||||||
|
get_social_metadata = set_social_metadata
|
||||||
|
from calibre.gui2.metadata import DownloadMetadata
|
||||||
|
self._download_book_metadata = DownloadMetadata(db, ids,
|
||||||
|
get_covers=covers, set_metadata=set_metadata,
|
||||||
|
get_social_metadata=get_social_metadata)
|
||||||
|
self._download_book_metadata.start()
|
||||||
|
if set_social_metadata is not None and set_social_metadata:
|
||||||
|
x = _('social metadata')
|
||||||
|
else:
|
||||||
|
x = _('covers') if covers and not set_metadata else _('metadata')
|
||||||
|
self.gui.progress_indicator.start(
|
||||||
|
_('Downloading %s for %d book(s)')%(x, len(ids)))
|
||||||
|
self._book_metadata_download_check = QTimer(self.gui)
|
||||||
|
self._book_metadata_download_check.timeout.connect(self.book_metadata_download_check,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
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.gui.progress_indicator.stop()
|
||||||
|
cr = self.gui.library_view.currentIndex().row()
|
||||||
|
x = self._download_book_metadata
|
||||||
|
self._download_book_metadata = None
|
||||||
|
if x.exception is None:
|
||||||
|
self.gui.library_view.model().refresh_ids(
|
||||||
|
x.updated, cr)
|
||||||
|
if self.gui.cover_flow:
|
||||||
|
self.gui.cover_flow.dataChanged()
|
||||||
|
if x.failures:
|
||||||
|
details = ['%s: %s'%(title, reason) for title,
|
||||||
|
reason in x.failures.values()]
|
||||||
|
details = '%s\n'%('\n'.join(details))
|
||||||
|
warning_dialog(self.gui, _('Failed to download some metadata'),
|
||||||
|
_('Failed to download metadata for the following:'),
|
||||||
|
det_msg=details).exec_()
|
||||||
|
else:
|
||||||
|
err = _('Failed to download metadata:')
|
||||||
|
error_dialog(self.gui, _('Error'), err, det_msg=x.tb).exec_()
|
||||||
|
|
||||||
|
|
||||||
|
def edit_metadata(self, checked, bulk=None):
|
||||||
|
'''
|
||||||
|
Edit metadata of selected books in library.
|
||||||
|
'''
|
||||||
|
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||||
|
previous = self.gui.library_view.currentIndex()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self.gui, _('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.gui.library_view.model().refresh_ids([id])
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
self.gui.iactions['View'].metadata_view_id = self.gui.library_view.model().db.id(row.row())
|
||||||
|
d = MetadataSingleDialog(self.gui, row.row(),
|
||||||
|
self.gui.library_view.model().db,
|
||||||
|
accepted_callback=accepted,
|
||||||
|
cancel_all=rows.index(row) < len(rows)-1)
|
||||||
|
d.view_format.connect(self.gui.iactions['View'].metadata_view_format)
|
||||||
|
d.exec_()
|
||||||
|
if d.cancel_all:
|
||||||
|
break
|
||||||
|
if rows:
|
||||||
|
current = self.gui.library_view.currentIndex()
|
||||||
|
m = self.gui.library_view.model()
|
||||||
|
if self.gui.cover_flow:
|
||||||
|
self.gui.cover_flow.dataChanged()
|
||||||
|
m.current_changed(current, previous)
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
|
||||||
|
def edit_bulk_metadata(self, checked):
|
||||||
|
'''
|
||||||
|
Edit metadata of selected books in library in bulk.
|
||||||
|
'''
|
||||||
|
rows = [r.row() for r in \
|
||||||
|
self.gui.library_view.selectionModel().selectedRows()]
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self.gui, _('Cannot edit metadata'),
|
||||||
|
_('No books selected'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
if MetadataBulkDialog(self.gui, rows,
|
||||||
|
self.gui.library_view.model().db).changed:
|
||||||
|
self.gui.library_view.model().resort(reset=False)
|
||||||
|
self.gui.library_view.model().research()
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
if self.gui.cover_flow:
|
||||||
|
self.gui.cover_flow.dataChanged()
|
||||||
|
|
||||||
|
# Merge books {{{
|
||||||
|
def merge_books(self, safe_merge=False):
|
||||||
|
'''
|
||||||
|
Merge selected books in library.
|
||||||
|
'''
|
||||||
|
if self.gui.stack.currentIndex() != 0:
|
||||||
|
return
|
||||||
|
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
return error_dialog(self.gui, _('Cannot merge books'),
|
||||||
|
_('No books selected'), show=True)
|
||||||
|
if len(rows) < 2:
|
||||||
|
return error_dialog(self.gui, _('Cannot merge books'),
|
||||||
|
_('At least two books must be selected for merging'),
|
||||||
|
show=True)
|
||||||
|
dest_id, src_books, src_ids = self.books_to_merge(rows)
|
||||||
|
if safe_merge:
|
||||||
|
if not confirm('<p>'+_(
|
||||||
|
'All book formats and metadata from the selected books '
|
||||||
|
'will be added to the <b>first selected book.</b><br><br> '
|
||||||
|
'The second and subsequently selected books will not '
|
||||||
|
'be deleted or changed.<br><br>'
|
||||||
|
'Please confirm you want to proceed.')
|
||||||
|
+'</p>', 'merge_books_safe', self.gui):
|
||||||
|
return
|
||||||
|
self.add_formats(dest_id, src_books)
|
||||||
|
self.merge_metadata(dest_id, src_ids)
|
||||||
|
else:
|
||||||
|
if not confirm('<p>'+_(
|
||||||
|
'All book formats and metadata from the selected books will be merged '
|
||||||
|
'into the <b>first selected book</b>.<br><br>'
|
||||||
|
'After merger the second and '
|
||||||
|
'subsequently selected books will be <b>deleted</b>. <br><br>'
|
||||||
|
'All book formats of the first selected book will be kept '
|
||||||
|
'and any duplicate formats in the second and subsequently selected books '
|
||||||
|
'will be permanently <b>deleted</b> from your computer.<br><br> '
|
||||||
|
'Are you <b>sure</b> you want to proceed?')
|
||||||
|
+'</p>', 'merge_books', self.gui):
|
||||||
|
return
|
||||||
|
if len(rows)>5:
|
||||||
|
if not confirm('<p>'+_('You are about to merge more than 5 books. '
|
||||||
|
'Are you <b>sure</b> you want to proceed?')
|
||||||
|
+'</p>', 'merge_too_many_books', self.gui):
|
||||||
|
return
|
||||||
|
self.add_formats(dest_id, src_books)
|
||||||
|
self.merge_metadata(dest_id, src_ids)
|
||||||
|
self.delete_books_after_merge(src_ids)
|
||||||
|
# leave the selection highlight on first selected book
|
||||||
|
dest_row = rows[0].row()
|
||||||
|
for row in rows:
|
||||||
|
if row.row() < rows[0].row():
|
||||||
|
dest_row -= 1
|
||||||
|
ci = self.gui.library_view.model().index(dest_row, 0)
|
||||||
|
if ci.isValid():
|
||||||
|
self.gui.library_view.setCurrentIndex(ci)
|
||||||
|
|
||||||
|
def add_formats(self, dest_id, src_books, replace=False):
|
||||||
|
for src_book in src_books:
|
||||||
|
if src_book:
|
||||||
|
fmt = os.path.splitext(src_book)[-1].replace('.', '').upper()
|
||||||
|
with open(src_book, 'rb') as f:
|
||||||
|
self.gui.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True,
|
||||||
|
notify=False, replace=replace)
|
||||||
|
|
||||||
|
def books_to_merge(self, rows):
|
||||||
|
src_books = []
|
||||||
|
src_ids = []
|
||||||
|
m = self.gui.library_view.model()
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
id_ = m.id(row)
|
||||||
|
if i == 0:
|
||||||
|
dest_id = id_
|
||||||
|
else:
|
||||||
|
src_ids.append(id_)
|
||||||
|
dbfmts = m.db.formats(id_, index_is_id=True)
|
||||||
|
if dbfmts:
|
||||||
|
for fmt in dbfmts.split(','):
|
||||||
|
src_books.append(m.db.format_abspath(id_, fmt,
|
||||||
|
index_is_id=True))
|
||||||
|
return [dest_id, src_books, src_ids]
|
||||||
|
|
||||||
|
def delete_books_after_merge(self, ids_to_delete):
|
||||||
|
self.gui.library_view.model().delete_books_by_id(ids_to_delete)
|
||||||
|
|
||||||
|
def merge_metadata(self, dest_id, src_ids):
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
dest_mi = db.get_metadata(dest_id, index_is_id=True, get_cover=True)
|
||||||
|
orig_dest_comments = dest_mi.comments
|
||||||
|
for src_id in src_ids:
|
||||||
|
src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True)
|
||||||
|
if src_mi.comments and orig_dest_comments != src_mi.comments:
|
||||||
|
if not dest_mi.comments:
|
||||||
|
dest_mi.comments = src_mi.comments
|
||||||
|
else:
|
||||||
|
dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments)
|
||||||
|
if src_mi.title and (not dest_mi.title or
|
||||||
|
dest_mi.title == _('Unknown')):
|
||||||
|
dest_mi.title = src_mi.title
|
||||||
|
if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] ==
|
||||||
|
_('Unknown')):
|
||||||
|
dest_mi.authors = src_mi.authors
|
||||||
|
dest_mi.author_sort = src_mi.author_sort
|
||||||
|
if src_mi.tags:
|
||||||
|
if not dest_mi.tags:
|
||||||
|
dest_mi.tags = src_mi.tags
|
||||||
|
else:
|
||||||
|
dest_mi.tags.extend(src_mi.tags)
|
||||||
|
if src_mi.cover and not dest_mi.cover:
|
||||||
|
dest_mi.cover = src_mi.cover
|
||||||
|
if not dest_mi.publisher:
|
||||||
|
dest_mi.publisher = src_mi.publisher
|
||||||
|
if not dest_mi.rating:
|
||||||
|
dest_mi.rating = src_mi.rating
|
||||||
|
if not dest_mi.series:
|
||||||
|
dest_mi.series = src_mi.series
|
||||||
|
dest_mi.series_index = src_mi.series_index
|
||||||
|
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
|
||||||
|
|
||||||
|
for key in db.field_metadata: #loop thru all defined fields
|
||||||
|
if db.field_metadata[key]['is_custom']:
|
||||||
|
colnum = db.field_metadata[key]['colnum']
|
||||||
|
# Get orig_dest_comments before it gets changed
|
||||||
|
if db.field_metadata[key]['datatype'] == 'comments':
|
||||||
|
orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
|
||||||
|
for src_id in src_ids:
|
||||||
|
dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
|
||||||
|
src_value = db.get_custom(src_id, num=colnum, index_is_id=True)
|
||||||
|
if db.field_metadata[key]['datatype'] == 'comments':
|
||||||
|
if src_value and src_value != orig_dest_value:
|
||||||
|
if not dest_value:
|
||||||
|
db.set_custom(dest_id, src_value, num=colnum)
|
||||||
|
else:
|
||||||
|
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
|
||||||
|
db.set_custom(dest_id, dest_value, num=colnum)
|
||||||
|
if db.field_metadata[key]['datatype'] in \
|
||||||
|
('bool', 'int', 'float', 'rating', 'datetime') \
|
||||||
|
and not dest_value:
|
||||||
|
db.set_custom(dest_id, src_value, num=colnum)
|
||||||
|
if db.field_metadata[key]['datatype'] == 'series' \
|
||||||
|
and not dest_value:
|
||||||
|
if src_value:
|
||||||
|
src_index = db.get_custom_extra(src_id, num=colnum, index_is_id=True)
|
||||||
|
db.set_custom(dest_id, src_value, num=colnum, extra=src_index)
|
||||||
|
if db.field_metadata[key]['datatype'] == 'text' \
|
||||||
|
and not db.field_metadata[key]['is_multiple'] \
|
||||||
|
and not dest_value:
|
||||||
|
db.set_custom(dest_id, src_value, num=colnum)
|
||||||
|
if db.field_metadata[key]['datatype'] == 'text' \
|
||||||
|
and db.field_metadata[key]['is_multiple']:
|
||||||
|
if src_value:
|
||||||
|
if not dest_value:
|
||||||
|
dest_value = src_value
|
||||||
|
else:
|
||||||
|
dest_value.extend(src_value)
|
||||||
|
db.set_custom(dest_id, dest_value, num=colnum)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def edit_device_collections(self, view, oncard=None):
|
||||||
|
model = view.model()
|
||||||
|
result = model.get_collections_with_ids()
|
||||||
|
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||||
|
d = TagListEditor(self.gui, tag_to_match=None, data=result, compare=compare)
|
||||||
|
d.exec_()
|
||||||
|
if d.result() == d.Accepted:
|
||||||
|
to_rename = d.to_rename # dict of new text to old ids
|
||||||
|
to_delete = d.to_delete # list of ids
|
||||||
|
for text in to_rename:
|
||||||
|
for old_id in to_rename[text]:
|
||||||
|
model.rename_collection(old_id, new_name=unicode(text))
|
||||||
|
for item in to_delete:
|
||||||
|
model.delete_collection_using_id(item)
|
||||||
|
self.gui.upload_collections(model.db, view=view, oncard=oncard)
|
||||||
|
view.reset()
|
||||||
|
|
||||||
|
|
66
src/calibre/gui2/actions/fetch_news.py
Normal file
66
src/calibre/gui2/actions/fetch_news.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from PyQt4.Qt import Qt
|
||||||
|
|
||||||
|
from calibre.gui2 import Dispatcher
|
||||||
|
from calibre.gui2.tools import fetch_scheduled_recipe
|
||||||
|
from calibre.utils.config import dynamic
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class FetchNewsAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Fetch News'
|
||||||
|
action_spec = (_('Fetch news'), 'news.svg', None, _('F'))
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.conversion_jobs = {}
|
||||||
|
|
||||||
|
def init_scheduler(self, db):
|
||||||
|
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||||
|
self.scheduler = Scheduler(self.gui, db)
|
||||||
|
self.scheduler.start_recipe_fetch.connect(self.download_scheduled_recipe, type=Qt.QueuedConnection)
|
||||||
|
self.qaction.setMenu(self.scheduler.news_menu)
|
||||||
|
self.qaction.triggered.connect(
|
||||||
|
self.scheduler.show_dialog)
|
||||||
|
self.database_changed = self.scheduler.database_changed
|
||||||
|
|
||||||
|
def connect_scheduler(self):
|
||||||
|
self.scheduler.delete_old_news.connect(
|
||||||
|
self.gui.library_view.model().delete_books_by_id,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
def download_scheduled_recipe(self, arg):
|
||||||
|
func, args, desc, fmt, temp_files = \
|
||||||
|
fetch_scheduled_recipe(arg)
|
||||||
|
job = self.gui.job_manager.run_job(
|
||||||
|
Dispatcher(self.scheduled_recipe_fetched), func, args=args,
|
||||||
|
description=desc)
|
||||||
|
self.conversion_jobs[job] = (temp_files, fmt, arg)
|
||||||
|
self.gui.status_bar.show_message(_('Fetching news from ')+arg['title'], 2000)
|
||||||
|
|
||||||
|
def scheduled_recipe_fetched(self, job):
|
||||||
|
temp_files, fmt, arg = self.conversion_jobs.pop(job)
|
||||||
|
pt = temp_files[0]
|
||||||
|
if job.failed:
|
||||||
|
self.scheduler.recipe_download_failed(arg)
|
||||||
|
return self.gui.job_exception(job)
|
||||||
|
id = self.gui.library_view.model().add_news(pt.name, arg)
|
||||||
|
self.gui.library_view.model().reset()
|
||||||
|
sync = dynamic.get('news_to_be_synced', set([]))
|
||||||
|
sync.add(id)
|
||||||
|
dynamic.set('news_to_be_synced', sync)
|
||||||
|
self.scheduler.recipe_downloaded(arg)
|
||||||
|
self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000)
|
||||||
|
self.gui.email_news(id)
|
||||||
|
self.gui.sync_news()
|
||||||
|
|
||||||
|
|
25
src/calibre/gui2/actions/help.py
Normal file
25
src/calibre/gui2/actions/help.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from PyQt4.Qt import QUrl
|
||||||
|
|
||||||
|
from calibre.gui2 import open_url
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class HelpAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Help'
|
||||||
|
action_spec = (_('Help'), 'help.svg', _('Browse the calibre User Manual'), _('F1'),)
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.show_help)
|
||||||
|
|
||||||
|
def show_help(self, *args):
|
||||||
|
open_url(QUrl('http://calibre-ebook.com/user_manual'))
|
||||||
|
|
||||||
|
|
||||||
|
|
24
src/calibre/gui2/actions/open.py
Normal file
24
src/calibre/gui2/actions/open.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class OpenFolderAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Open Folder'
|
||||||
|
action_spec = (_('Open containing folder'), 'document_open.svg', None,
|
||||||
|
_('O'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.gui.iactions['View'].view_folder)
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
self.qaction.setEnabled(enabled)
|
||||||
|
|
||||||
|
|
61
src/calibre/gui2/actions/preferences.py
Normal file
61
src/calibre/gui2/actions/preferences.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from PyQt4.Qt import QIcon, QMenu
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.gui2.dialogs.config import ConfigDialog
|
||||||
|
from calibre.gui2 import error_dialog, config
|
||||||
|
|
||||||
|
class PreferencesAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Preferences'
|
||||||
|
action_spec = (_('Preferences'), 'config.svg', None, _('Ctrl+P'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
pm = QMenu()
|
||||||
|
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
|
||||||
|
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
|
||||||
|
self.gui.run_wizard)
|
||||||
|
self.qaction.setMenu(pm)
|
||||||
|
self.preferences_menu = pm
|
||||||
|
for x in (self.gui.preferences_action, self.qaction):
|
||||||
|
x.triggered.connect(self.do_config)
|
||||||
|
|
||||||
|
|
||||||
|
def do_config(self, checked=False, initial_category='general'):
|
||||||
|
if self.gui.job_manager.has_jobs():
|
||||||
|
d = error_dialog(self.gui, _('Cannot configure'),
|
||||||
|
_('Cannot configure while there are running jobs.'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
if self.gui.must_restart_before_config:
|
||||||
|
d = error_dialog(self.gui, _('Cannot configure'),
|
||||||
|
_('Cannot configure before calibre is restarted.'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
d = ConfigDialog(self.gui, self.gui.library_view,
|
||||||
|
server=self.gui.content_server, initial_category=initial_category)
|
||||||
|
|
||||||
|
d.exec_()
|
||||||
|
self.gui.content_server = d.server
|
||||||
|
if self.gui.content_server is not None:
|
||||||
|
self.gui.content_server.state_callback = \
|
||||||
|
self.Dispatcher(self.gui.iactions['Connect Share'].content_server_state_changed)
|
||||||
|
self.gui.content_server.state_callback(self.gui.content_server.is_running)
|
||||||
|
|
||||||
|
if d.result() == d.Accepted:
|
||||||
|
self.gui.search.search_as_you_type(config['search_as_you_type'])
|
||||||
|
self.gui.tags_view.set_new_model() # in case columns changed
|
||||||
|
self.gui.iactions['Save To Disk'].reread_prefs()
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
self.gui.create_device_menu()
|
||||||
|
self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
|
||||||
|
self.gui.tool_bar.apply_settings()
|
||||||
|
|
||||||
|
|
||||||
|
|
22
src/calibre/gui2/actions/restart.py
Normal file
22
src/calibre/gui2/actions/restart.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class RestartAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Restart'
|
||||||
|
action_spec = (_('&Restart'), None, None, _('Ctrl+R'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.restart)
|
||||||
|
|
||||||
|
def restart(self, *args):
|
||||||
|
self.gui.quit(restart=True)
|
||||||
|
|
||||||
|
|
153
src/calibre/gui2/actions/save_to_disk.py
Normal file
153
src/calibre/gui2/actions/save_to_disk.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import QMenu, pyqtSignal
|
||||||
|
|
||||||
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.gui2 import error_dialog, Dispatcher, \
|
||||||
|
choose_dir, warning_dialog, open_local_file
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
|
|
||||||
|
class SaveMenu(QMenu): # {{{
|
||||||
|
|
||||||
|
save_fmt = pyqtSignal(object)
|
||||||
|
|
||||||
|
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))
|
||||||
|
action.triggered.connect(
|
||||||
|
getattr(self, 'do_'+ext))
|
||||||
|
|
||||||
|
def do(self, ext, *args):
|
||||||
|
self.save_fmt.emit(ext)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
class SaveToDiskAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = "Save To Disk"
|
||||||
|
action_spec = (_('Save to disk'), 'save.svg', None, _('S'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.save_to_disk)
|
||||||
|
self.save_menu = QMenu()
|
||||||
|
self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk,
|
||||||
|
False))
|
||||||
|
self.save_menu.addAction(_('Save to disk in a single directory'),
|
||||||
|
partial(self.save_to_single_dir, False))
|
||||||
|
self.save_menu.addAction(_('Save only %s format to disk')%
|
||||||
|
prefs['output_format'].upper(),
|
||||||
|
partial(self.save_single_format_to_disk, False))
|
||||||
|
self.save_menu.addAction(
|
||||||
|
_('Save only %s format to disk in a single directory')%
|
||||||
|
prefs['output_format'].upper(),
|
||||||
|
partial(self.save_single_fmt_to_single_dir, False))
|
||||||
|
self.save_sub_menu = SaveMenu(self.gui)
|
||||||
|
self.save_sub_menu_action = self.save_menu.addMenu(self.save_sub_menu)
|
||||||
|
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
|
||||||
|
self.qaction.setMenu(self.save_menu)
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
for action in list(self.save_menu.actions())[1:]:
|
||||||
|
action.setEnabled(enabled)
|
||||||
|
|
||||||
|
def reread_prefs(self):
|
||||||
|
self.save_menu.actions()[2].setText(
|
||||||
|
_('Save only %s format to disk')%
|
||||||
|
prefs['output_format'].upper())
|
||||||
|
self.save_menu.actions()[3].setText(
|
||||||
|
_('Save only %s format to disk in a single directory')%
|
||||||
|
prefs['output_format'].upper())
|
||||||
|
|
||||||
|
def save_single_format_to_disk(self, checked):
|
||||||
|
self.save_to_disk(checked, False, prefs['output_format'])
|
||||||
|
|
||||||
|
def save_specific_format_disk(self, fmt):
|
||||||
|
self.save_to_disk(False, False, fmt)
|
||||||
|
|
||||||
|
def save_to_single_dir(self, checked):
|
||||||
|
self.save_to_disk(checked, True)
|
||||||
|
|
||||||
|
def save_single_fmt_to_single_dir(self, *args):
|
||||||
|
self.save_to_disk(False, single_dir=True,
|
||||||
|
single_format=prefs['output_format'])
|
||||||
|
|
||||||
|
def save_to_disk(self, checked, single_dir=False, single_format=None):
|
||||||
|
rows = self.gui.current_view().selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
return error_dialog(self.gui, _('Cannot save to disk'),
|
||||||
|
_('No books selected'), show=True)
|
||||||
|
path = choose_dir(self.gui, 'save to disk dialog',
|
||||||
|
_('Choose destination directory'))
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
dpath = os.path.abspath(path).replace('/', os.sep)+os.sep
|
||||||
|
lpath = self.gui.library_view.model().db.library_path.replace('/',
|
||||||
|
os.sep)+os.sep
|
||||||
|
if dpath.startswith(lpath):
|
||||||
|
return error_dialog(self.gui, _('Not allowed'),
|
||||||
|
_('You are trying to save files into the calibre '
|
||||||
|
'library. This can cause corruption of your '
|
||||||
|
'library. Save to disk is meant to export '
|
||||||
|
'files from your calibre library elsewhere.'), show=True)
|
||||||
|
|
||||||
|
if self.gui.current_view() is self.gui.library_view:
|
||||||
|
from calibre.gui2.add import Saver
|
||||||
|
from calibre.library.save_to_disk import config
|
||||||
|
opts = config().parse()
|
||||||
|
if single_format is not None:
|
||||||
|
opts.formats = single_format
|
||||||
|
# Special case for Kindle annotation files
|
||||||
|
if single_format.lower() in ['mbp','pdr','tan']:
|
||||||
|
opts.to_lowercase = False
|
||||||
|
opts.save_cover = False
|
||||||
|
opts.write_opf = False
|
||||||
|
opts.template = opts.send_template
|
||||||
|
if single_dir:
|
||||||
|
opts.template = opts.template.split('/')[-1].strip()
|
||||||
|
if not opts.template:
|
||||||
|
opts.template = '{title} - {authors}'
|
||||||
|
self._saver = Saver(self.gui, self.gui.library_view.model().db,
|
||||||
|
Dispatcher(self._books_saved), rows, path, opts,
|
||||||
|
spare_server=self.gui.spare_server)
|
||||||
|
|
||||||
|
else:
|
||||||
|
paths = self.gui.current_view().model().paths(rows)
|
||||||
|
self.gui.device_manager.save_books(
|
||||||
|
Dispatcher(self.books_saved), paths, path)
|
||||||
|
|
||||||
|
|
||||||
|
def _books_saved(self, path, failures, error):
|
||||||
|
self._saver = None
|
||||||
|
if error:
|
||||||
|
return error_dialog(self.gui, _('Error while saving'),
|
||||||
|
_('There was an error while saving.'),
|
||||||
|
error, show=True)
|
||||||
|
if failures:
|
||||||
|
failures = [u'%s\n\t%s'%
|
||||||
|
(title, '\n\t'.join(err.splitlines())) for title, err in
|
||||||
|
failures]
|
||||||
|
|
||||||
|
warning_dialog(self.gui, _('Could not save some books'),
|
||||||
|
_('Could not save some books') + ', ' +
|
||||||
|
_('Click the show details button to see which ones.'),
|
||||||
|
u'\n\n'.join(failures), show=True)
|
||||||
|
open_local_file(path)
|
||||||
|
|
||||||
|
def books_saved(self, job):
|
||||||
|
if job.failed:
|
||||||
|
return self.gui.device_job_exception(job)
|
||||||
|
|
||||||
|
|
31
src/calibre/gui2/actions/show_book_details.py
Normal file
31
src/calibre/gui2/actions/show_book_details.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.gui2.dialogs.book_info import BookInfo
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
|
|
||||||
|
class ShowBookDetailsAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Show Book Details'
|
||||||
|
action_spec = (_('Show book details'), 'dialog_information.svg', None,
|
||||||
|
_('I'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.show_book_info)
|
||||||
|
|
||||||
|
def show_book_info(self, *args):
|
||||||
|
if self.gui.current_view() is not self.gui.library_view:
|
||||||
|
error_dialog(self.gui, _('No detailed info available'),
|
||||||
|
_('No detailed information is available for books '
|
||||||
|
'on the device.')).exec_()
|
||||||
|
return
|
||||||
|
index = self.gui.library_view.currentIndex()
|
||||||
|
if index.isValid():
|
||||||
|
BookInfo(self.gui, self.gui.library_view, index).show()
|
||||||
|
|
60
src/calibre/gui2/actions/similar_books.py
Normal file
60
src/calibre/gui2/actions/similar_books.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import QMenu
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class SimilarBooksAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Similar Books'
|
||||||
|
action_spec = (_('Similar books...'), None, None, None)
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
m = QMenu(self.gui)
|
||||||
|
for text, icon, target, shortcut in [
|
||||||
|
(_('Books by same author'), 'user_profile.svg', 'authors', _('Alt+A')),
|
||||||
|
(_('Books in this series'), 'books_in_series.svg', 'series', _('Alt+S')),
|
||||||
|
(_('Books by this publisher'), 'publisher.png', 'publisher', _('Alt+P')),
|
||||||
|
(_('Books with the same tags'), 'tags.svg', 'tag', _('Alt+T')),]:
|
||||||
|
ac = self.create_action(spec=(text, icon, None, shortcut),
|
||||||
|
attr=target)
|
||||||
|
m.addAction(ac)
|
||||||
|
m.triggered.connect(partial(self.show_similar_books, target))
|
||||||
|
self.qaction.setMenu(m)
|
||||||
|
self.similar_menu = m
|
||||||
|
|
||||||
|
def show_similar_books(self, type, *args):
|
||||||
|
search, join = [], ' '
|
||||||
|
idx = self.gui.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 in ('author', 'authors'):
|
||||||
|
authors = idx.model().db.authors(row)
|
||||||
|
if authors:
|
||||||
|
search = ['author:"='+a.strip().replace('|', ',')+'"' \
|
||||||
|
for a in authors.split(',')]
|
||||||
|
join = ' or '
|
||||||
|
if search:
|
||||||
|
self.gui.search.set_search_string(join.join(search))
|
||||||
|
|
||||||
|
|
181
src/calibre/gui2/actions/view.py
Normal file
181
src/calibre/gui2/actions/view.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, time
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import Qt, QMenu
|
||||||
|
|
||||||
|
from calibre.constants import isosx
|
||||||
|
from calibre.gui2 import error_dialog, Dispatcher, question_dialog, config, \
|
||||||
|
open_local_file
|
||||||
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
|
class ViewAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'View'
|
||||||
|
action_spec = (_('View'), 'view.svg', None, _('V'))
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.persistent_files = []
|
||||||
|
self.metadata_view_id = None
|
||||||
|
self.qaction.triggered.connect(self.view_book)
|
||||||
|
self.view_menu = QMenu()
|
||||||
|
self.view_menu.addAction(_('View'), partial(self.view_book, False))
|
||||||
|
ac = self.view_menu.addAction(_('View specific format'))
|
||||||
|
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
|
||||||
|
self.qaction.setMenu(self.view_menu)
|
||||||
|
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
|
||||||
|
def location_selected(self, loc):
|
||||||
|
enabled = loc == 'library'
|
||||||
|
for action in list(self.view_menu.actions())[1:]:
|
||||||
|
action.setEnabled(enabled)
|
||||||
|
|
||||||
|
def view_format(self, row, format):
|
||||||
|
fmt_path = self.gui.library_view.model().db.format_abspath(row, format)
|
||||||
|
if fmt_path:
|
||||||
|
self._view_file(fmt_path)
|
||||||
|
|
||||||
|
def view_format_by_id(self, id_, format):
|
||||||
|
fmt_path = self.gui.library_view.model().db.format_abspath(id_, format,
|
||||||
|
index_is_id=True)
|
||||||
|
if fmt_path:
|
||||||
|
self._view_file(fmt_path)
|
||||||
|
|
||||||
|
def metadata_view_format(self, fmt):
|
||||||
|
fmt_path = self.gui.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.gui.device_job_exception(job)
|
||||||
|
return
|
||||||
|
self._view_file(job.result)
|
||||||
|
|
||||||
|
def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True):
|
||||||
|
self.gui.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.gui.job_manager.launch_gui_app(viewer,
|
||||||
|
kwargs=dict(args=args))
|
||||||
|
else:
|
||||||
|
open_local_file(name)
|
||||||
|
time.sleep(2) # User feedback
|
||||||
|
finally:
|
||||||
|
self.gui.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.gui.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self.gui, _('Cannot view'), _('No book selected'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
|
||||||
|
row = rows[0].row()
|
||||||
|
formats = self.gui.library_view.model().db.formats(row).upper().split(',')
|
||||||
|
d = ChooseFormatDialog(self.gui, _('Choose the format to view'), formats)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
format = d.format()
|
||||||
|
self.view_format(row, format)
|
||||||
|
|
||||||
|
def _view_check(self, num, max_=3):
|
||||||
|
if num <= max_:
|
||||||
|
return True
|
||||||
|
return question_dialog(self.gui, _('Multiple Books Selected'),
|
||||||
|
_('You are attempting to open %d books. Opening too many '
|
||||||
|
'books at once can be slow and have a negative effect on the '
|
||||||
|
'responsiveness of your computer. Once started the process '
|
||||||
|
'cannot be stopped until complete. Do you wish to continue?'
|
||||||
|
) % num)
|
||||||
|
|
||||||
|
def view_folder(self, *args):
|
||||||
|
rows = self.gui.current_view().selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self.gui, _('Cannot open folder'),
|
||||||
|
_('No book selected'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
if not self._view_check(len(rows)):
|
||||||
|
return
|
||||||
|
for row in rows:
|
||||||
|
path = self.gui.library_view.model().db.abspath(row.row())
|
||||||
|
open_local_file(path)
|
||||||
|
|
||||||
|
def view_folder_for_id(self, id_):
|
||||||
|
path = self.gui.library_view.model().db.abspath(id_, index_is_id=True)
|
||||||
|
open_local_file(path)
|
||||||
|
|
||||||
|
def view_book(self, triggered):
|
||||||
|
rows = self.gui.current_view().selectionModel().selectedRows()
|
||||||
|
self._view_books(rows)
|
||||||
|
|
||||||
|
def view_specific_book(self, index):
|
||||||
|
self._view_books([index])
|
||||||
|
|
||||||
|
def _view_books(self, rows):
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
self._launch_viewer()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._view_check(len(rows)):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.gui.current_view() is self.gui.library_view:
|
||||||
|
for row in rows:
|
||||||
|
if hasattr(row, 'row'):
|
||||||
|
row = row.row()
|
||||||
|
|
||||||
|
formats = self.gui.library_view.model().db.formats(row)
|
||||||
|
title = self.gui.library_view.model().db.title(row)
|
||||||
|
if not formats:
|
||||||
|
error_dialog(self.gui, _('Cannot view'),
|
||||||
|
_('%s has no available formats.')%(title,), show=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
formats = formats.upper().split(',')
|
||||||
|
|
||||||
|
|
||||||
|
in_prefs = False
|
||||||
|
for format in prefs['input_format_order']:
|
||||||
|
if format in formats:
|
||||||
|
in_prefs = True
|
||||||
|
self.view_format(row, format)
|
||||||
|
break
|
||||||
|
if not in_prefs:
|
||||||
|
self.view_format(row, formats[0])
|
||||||
|
else:
|
||||||
|
paths = self.gui.current_view().model().paths(rows)
|
||||||
|
for path in paths:
|
||||||
|
pt = PersistentTemporaryFile('_viewer_'+\
|
||||||
|
os.path.splitext(path)[1])
|
||||||
|
self.persistent_files.append(pt)
|
||||||
|
pt.close()
|
||||||
|
self.gui.device_manager.view_book(\
|
||||||
|
Dispatcher(self.book_downloaded_for_viewing),
|
||||||
|
path, pt.name)
|
||||||
|
|
||||||
|
|
@ -443,7 +443,7 @@ class Saver(QObject):
|
|||||||
from calibre.ebooks.metadata.worker import SaveWorker
|
from calibre.ebooks.metadata.worker import SaveWorker
|
||||||
self.worker = SaveWorker(self.rq, db, self.ids, path, self.opts,
|
self.worker = SaveWorker(self.rq, db, self.ids, path, self.opts,
|
||||||
spare_server=self.spare_server)
|
spare_server=self.spare_server)
|
||||||
self.connect(self.pd, SIGNAL('canceled()'), self.canceled)
|
self.pd.canceled_signal.connect(self.canceled)
|
||||||
self.timer = QTimer(self)
|
self.timer = QTimer(self)
|
||||||
self.connect(self.timer, SIGNAL('timeout()'), self.update)
|
self.connect(self.timer, SIGNAL('timeout()'), self.update)
|
||||||
self.timer.start(200)
|
self.timer.start(200)
|
||||||
|
@ -144,7 +144,7 @@ class CoverFlowMixin(object):
|
|||||||
self.sync_cf_to_listview)
|
self.sync_cf_to_listview)
|
||||||
self.db_images = DatabaseImages(self.library_view.model())
|
self.db_images = DatabaseImages(self.library_view.model())
|
||||||
self.cover_flow.setImages(self.db_images)
|
self.cover_flow.setImages(self.db_images)
|
||||||
self.cover_flow.itemActivated.connect(self.view_specific_book)
|
self.cover_flow.itemActivated.connect(self.iactions['View'].view_specific_book)
|
||||||
else:
|
else:
|
||||||
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
|
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
|
||||||
+'<br>'+pictureflowerror)
|
+'<br>'+pictureflowerror)
|
||||||
|
@ -608,8 +608,6 @@ class DeviceMixin(object): # {{{
|
|||||||
self.device_error_dialog = error_dialog(self, _('Error'),
|
self.device_error_dialog = error_dialog(self, _('Error'),
|
||||||
_('Error communicating with device'), ' ')
|
_('Error communicating with device'), ' ')
|
||||||
self.device_error_dialog.setModal(Qt.NonModal)
|
self.device_error_dialog.setModal(Qt.NonModal)
|
||||||
self.share_conn_menu.connect_to_folder.connect(self.connect_to_folder)
|
|
||||||
self.share_conn_menu.connect_to_itunes.connect(self.connect_to_itunes)
|
|
||||||
self.emailer = Emailer()
|
self.emailer = Emailer()
|
||||||
self.emailer.start()
|
self.emailer.start()
|
||||||
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
||||||
@ -647,20 +645,18 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
def create_device_menu(self):
|
def create_device_menu(self):
|
||||||
self._sync_menu = DeviceMenu(self)
|
self._sync_menu = DeviceMenu(self)
|
||||||
self.share_conn_menu.build_email_entries(self._sync_menu)
|
self.iactions['Send To Device'].qaction.setMenu(self._sync_menu)
|
||||||
self.action_sync.setMenu(self._sync_menu)
|
self.iactions['Connect Share'].build_email_entries()
|
||||||
self.connect(self._sync_menu,
|
self.connect(self._sync_menu,
|
||||||
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self.dispatch_sync_event)
|
self.dispatch_sync_event)
|
||||||
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
|
self._sync_menu.fetch_annotations.connect(
|
||||||
|
self.iactions['Fetch Annotations'].fetch_annotations)
|
||||||
self._sync_menu.disconnect_mounted_device.connect(self.disconnect_mounted_device)
|
self._sync_menu.disconnect_mounted_device.connect(self.disconnect_mounted_device)
|
||||||
|
self.iactions['Connect Share'].set_state(self.device_connected)
|
||||||
if self.device_connected:
|
if self.device_connected:
|
||||||
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
|
|
||||||
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
|
|
||||||
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
|
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
|
||||||
else:
|
else:
|
||||||
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
|
|
||||||
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
|
|
||||||
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
|
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
|
||||||
|
|
||||||
def device_job_exception(self, job):
|
def device_job_exception(self, job):
|
||||||
@ -696,17 +692,14 @@ class DeviceMixin(object): # {{{
|
|||||||
# Device connected {{{
|
# Device connected {{{
|
||||||
|
|
||||||
def set_device_menu_items_state(self, connected):
|
def set_device_menu_items_state(self, connected):
|
||||||
|
self.iactions['Connect Share'].set_state(connected)
|
||||||
if connected:
|
if connected:
|
||||||
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
|
|
||||||
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
|
|
||||||
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
|
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
|
||||||
self._sync_menu.enable_device_actions(True,
|
self._sync_menu.enable_device_actions(True,
|
||||||
self.device_manager.device.card_prefix(),
|
self.device_manager.device.card_prefix(),
|
||||||
self.device_manager.device)
|
self.device_manager.device)
|
||||||
self.eject_action.setEnabled(True)
|
self.eject_action.setEnabled(True)
|
||||||
else:
|
else:
|
||||||
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
|
|
||||||
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
|
|
||||||
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
|
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
|
||||||
self._sync_menu.enable_device_actions(False)
|
self._sync_menu.enable_device_actions(False)
|
||||||
self.eject_action.setEnabled(False)
|
self.eject_action.setEnabled(False)
|
||||||
@ -791,8 +784,9 @@ class DeviceMixin(object): # {{{
|
|||||||
self.device_job_exception(job)
|
self.device_job_exception(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.delete_memory.has_key(job):
|
dm = self.iactions['Remove Books'].delete_memory
|
||||||
paths, model = self.delete_memory.pop(job)
|
if dm.has_key(job):
|
||||||
|
paths, model = dm.pop(job)
|
||||||
self.device_manager.remove_books_from_metadata(paths,
|
self.device_manager.remove_books_from_metadata(paths,
|
||||||
self.booklists())
|
self.booklists())
|
||||||
model.paths_deleted(paths)
|
model.paths_deleted(paths)
|
||||||
@ -924,7 +918,7 @@ class DeviceMixin(object): # {{{
|
|||||||
_('Auto convert the following books before sending via '
|
_('Auto convert the following books before sending via '
|
||||||
'email?'), det_msg=autos,
|
'email?'), det_msg=autos,
|
||||||
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
||||||
self.auto_convert_mail(to, fmts, delete_from_library, auto, format)
|
self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format)
|
||||||
|
|
||||||
if bad:
|
if bad:
|
||||||
bad = '\n'.join('%s'%(i,) for i in bad)
|
bad = '\n'.join('%s'%(i,) for i in bad)
|
||||||
@ -1026,7 +1020,7 @@ class DeviceMixin(object): # {{{
|
|||||||
_('Auto convert the following books before uploading to '
|
_('Auto convert the following books before uploading to '
|
||||||
'the device?'), det_msg=autos,
|
'the device?'), det_msg=autos,
|
||||||
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
||||||
self.auto_convert_catalogs(auto, format)
|
self.iactions['Convert Books'].auto_convert_catalogs(auto, format)
|
||||||
files = [f for f in files if f is not None]
|
files = [f for f in files if f is not None]
|
||||||
if not files:
|
if not files:
|
||||||
dynamic.set('catalogs_to_be_synced', set([]))
|
dynamic.set('catalogs_to_be_synced', set([]))
|
||||||
@ -1088,7 +1082,7 @@ class DeviceMixin(object): # {{{
|
|||||||
_('Auto convert the following books before uploading to '
|
_('Auto convert the following books before uploading to '
|
||||||
'the device?'), det_msg=autos,
|
'the device?'), det_msg=autos,
|
||||||
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
||||||
self.auto_convert_news(auto, format)
|
self.iactions['Convert Books'].auto_convert_news(auto, format)
|
||||||
files = [f for f in files if f is not None]
|
files = [f for f in files if f is not None]
|
||||||
for f in files:
|
for f in files:
|
||||||
f.deleted_after_upload = del_on_upload
|
f.deleted_after_upload = del_on_upload
|
||||||
@ -1207,7 +1201,7 @@ class DeviceMixin(object): # {{{
|
|||||||
_('Auto convert the following books before uploading to '
|
_('Auto convert the following books before uploading to '
|
||||||
'the device?'), det_msg=autos,
|
'the device?'), det_msg=autos,
|
||||||
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
||||||
self.auto_convert(auto, on_card, format)
|
self.iactions['Convert Books'].auto_convert(auto, on_card, format)
|
||||||
|
|
||||||
if bad:
|
if bad:
|
||||||
bad = '\n'.join('%s'%(i,) for i in bad)
|
bad = '\n'.join('%s'%(i,) for i in bad)
|
||||||
|
@ -12,7 +12,7 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \
|
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \
|
||||||
QPixmap, QListWidgetItem, QDialog
|
QPixmap, QListWidgetItem, QDialog, pyqtSignal
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
|
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
|
||||||
choose_files, choose_images, ResizableDialog, \
|
choose_files, choose_images, ResizableDialog, \
|
||||||
@ -99,6 +99,7 @@ class Format(QListWidgetItem):
|
|||||||
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||||
|
|
||||||
COVER_FETCH_TIMEOUT = 240 # seconds
|
COVER_FETCH_TIMEOUT = 240 # seconds
|
||||||
|
view_format = pyqtSignal(object)
|
||||||
|
|
||||||
def do_reset_cover(self, *args):
|
def do_reset_cover(self, *args):
|
||||||
pix = QPixmap(I('default_cover.svg'))
|
pix = QPixmap(I('default_cover.svg'))
|
||||||
@ -474,7 +475,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
|
|
||||||
def show_format(self, item, *args):
|
def show_format(self, item, *args):
|
||||||
fmt = item.ext
|
fmt = item.ext
|
||||||
self.emit(SIGNAL('view_format(PyQt_PyObject)'), fmt)
|
self.view_format.emit(fmt)
|
||||||
|
|
||||||
def deduce_author_sort(self):
|
def deduce_author_sort(self):
|
||||||
au = unicode(self.authors.text())
|
au = unicode(self.authors.text())
|
||||||
|
@ -5,12 +5,14 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
''''''
|
''''''
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, SIGNAL, Qt
|
from PyQt4.Qt import QDialog, pyqtSignal, Qt
|
||||||
|
|
||||||
from calibre.gui2.dialogs.progress_ui import Ui_Dialog
|
from calibre.gui2.dialogs.progress_ui import Ui_Dialog
|
||||||
|
|
||||||
class ProgressDialog(QDialog, Ui_Dialog):
|
class ProgressDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
|
canceled_signal = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, title, msg='', min=0, max=99, parent=None):
|
def __init__(self, title, msg='', min=0, max=99, parent=None):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
@ -23,7 +25,7 @@ class ProgressDialog(QDialog, Ui_Dialog):
|
|||||||
self.bar.setValue(min)
|
self.bar.setValue(min)
|
||||||
self.canceled = False
|
self.canceled = False
|
||||||
|
|
||||||
self.connect(self.button_box, SIGNAL('rejected()'), self._canceled)
|
self.button_box.rejected.connect(self._canceled)
|
||||||
|
|
||||||
def set_msg(self, msg=''):
|
def set_msg(self, msg=''):
|
||||||
self.message.setText(msg)
|
self.message.setText(msg)
|
||||||
@ -50,7 +52,7 @@ class ProgressDialog(QDialog, Ui_Dialog):
|
|||||||
self.canceled = True
|
self.canceled = True
|
||||||
self.button_box.setDisabled(True)
|
self.button_box.setDisabled(True)
|
||||||
self.title.setText(_('Aborting...'))
|
self.title.setText(_('Aborting...'))
|
||||||
self.emit(SIGNAL('canceled()'))
|
self.canceled_signal.emit()
|
||||||
|
|
||||||
def keyPressEvent(self, ev):
|
def keyPressEvent(self, ev):
|
||||||
if ev.key() == Qt.Key_Escape:
|
if ev.key() == Qt.Key_Escape:
|
||||||
|
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import functools, sys, os
|
import functools, sys, os
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, Qt, QStackedWidget, \
|
from PyQt4.Qt import Qt, QStackedWidget, QMenu, \
|
||||||
QSize, QSizePolicy, QStatusBar, QLabel, QFont
|
QSize, QSizePolicy, QStatusBar, QLabel, QFont
|
||||||
|
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
@ -27,69 +27,35 @@ def partial(*args, **kwargs):
|
|||||||
_keep_refs.append(ans)
|
_keep_refs.append(ans)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
LIBRARY_CONTEXT_MENU = (
|
||||||
|
'Edit Metadata', 'Send To Device', 'Save To Disk', 'Connect Share', None,
|
||||||
|
'Convert Books', 'View', 'Open Folder', 'Show Book Details', None,
|
||||||
|
'Remove Books',
|
||||||
|
)
|
||||||
|
|
||||||
|
DEVICE_CONTEXT_MENU = ('View', 'Save To Disk', None, 'Remove Books', None,
|
||||||
|
'Add To Library', 'Edit Collections',
|
||||||
|
)
|
||||||
|
|
||||||
class LibraryViewMixin(object): # {{{
|
class LibraryViewMixin(object): # {{{
|
||||||
|
|
||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
similar_menu = QMenu(_('Similar books...'))
|
lm = QMenu(self)
|
||||||
similar_menu.addAction(self.action_books_by_same_author)
|
def populate_menu(m, items):
|
||||||
similar_menu.addAction(self.action_books_in_this_series)
|
for what in items:
|
||||||
similar_menu.addAction(self.action_books_with_the_same_tags)
|
if what is None:
|
||||||
similar_menu.addAction(self.action_books_by_this_publisher)
|
m.addSeparator()
|
||||||
self.action_books_by_same_author.setShortcut(Qt.ALT + Qt.Key_A)
|
elif what in self.iactions:
|
||||||
self.action_books_in_this_series.setShortcut(Qt.ALT + Qt.Key_S)
|
m.addAction(self.iactions[what].qaction)
|
||||||
self.action_books_by_this_publisher.setShortcut(Qt.ALT + Qt.Key_P)
|
populate_menu(lm, LIBRARY_CONTEXT_MENU)
|
||||||
self.action_books_with_the_same_tags.setShortcut(Qt.ALT+Qt.Key_T)
|
dm = QMenu(self)
|
||||||
self.addAction(self.action_books_by_same_author)
|
populate_menu(dm, DEVICE_CONTEXT_MENU)
|
||||||
self.addAction(self.action_books_by_this_publisher)
|
ec = self.iactions['Edit Collections'].qaction
|
||||||
self.addAction(self.action_books_in_this_series)
|
self.library_view.set_context_menu(lm, ec)
|
||||||
self.addAction(self.action_books_with_the_same_tags)
|
for v in (self.memory_view, self.card_a_view, self.card_b_view):
|
||||||
self.similar_menu = similar_menu
|
v.set_context_menu(dm, ec)
|
||||||
self.action_books_by_same_author.triggered.connect(
|
|
||||||
partial(self.show_similar_books, 'authors'))
|
|
||||||
self.action_books_in_this_series.triggered.connect(
|
|
||||||
partial(self.show_similar_books, 'series'))
|
|
||||||
self.action_books_with_the_same_tags.triggered.connect(
|
|
||||||
partial(self.show_similar_books, 'tag'))
|
|
||||||
self.action_books_by_this_publisher.triggered.connect(
|
|
||||||
partial(self.show_similar_books, 'publisher'))
|
|
||||||
|
|
||||||
self.library_view.set_context_menu(self.action_edit, self.action_sync,
|
self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection)
|
||||||
self.action_convert, self.action_view,
|
|
||||||
self.action_save,
|
|
||||||
self.action_open_containing_folder,
|
|
||||||
self.action_show_book_details,
|
|
||||||
self.action_del,
|
|
||||||
self.action_conn_share,
|
|
||||||
add_to_library = None,
|
|
||||||
edit_device_collections=None,
|
|
||||||
similar_menu=similar_menu)
|
|
||||||
add_to_library = (_('Add books to library'), self.add_books_from_device)
|
|
||||||
|
|
||||||
edit_device_collections = (_('Manage collections'),
|
|
||||||
partial(self.edit_device_collections, oncard=None))
|
|
||||||
self.memory_view.set_context_menu(None, None, None,
|
|
||||||
self.action_view, self.action_save, None, None,
|
|
||||||
self.action_del, None,
|
|
||||||
add_to_library=add_to_library,
|
|
||||||
edit_device_collections=edit_device_collections)
|
|
||||||
|
|
||||||
edit_device_collections = (_('Manage collections'),
|
|
||||||
partial(self.edit_device_collections, oncard='carda'))
|
|
||||||
self.card_a_view.set_context_menu(None, None, None,
|
|
||||||
self.action_view, self.action_save, None, None,
|
|
||||||
self.action_del, None,
|
|
||||||
add_to_library=add_to_library,
|
|
||||||
edit_device_collections=edit_device_collections)
|
|
||||||
|
|
||||||
edit_device_collections = (_('Manage collections'),
|
|
||||||
partial(self.edit_device_collections, oncard='cardb'))
|
|
||||||
self.card_b_view.set_context_menu(None, None, None,
|
|
||||||
self.action_view, self.action_save, None, None,
|
|
||||||
self.action_del, None,
|
|
||||||
add_to_library=add_to_library,
|
|
||||||
edit_device_collections=edit_device_collections)
|
|
||||||
|
|
||||||
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
|
|
||||||
for func, args in [
|
for func, args in [
|
||||||
('connect_to_search_box', (self.search,
|
('connect_to_search_box', (self.search,
|
||||||
self.search_done)),
|
self.search_done)),
|
||||||
@ -116,37 +82,10 @@ class LibraryViewMixin(object): # {{{
|
|||||||
|
|
||||||
for view in ('library', 'memory', 'card_a', 'card_b'):
|
for view in ('library', 'memory', 'card_a', 'card_b'):
|
||||||
view = getattr(self, view+'_view')
|
view = getattr(self, view+'_view')
|
||||||
view.verticalHeader().sectionDoubleClicked.connect(self.view_specific_book)
|
view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def show_similar_books(self, type, *args):
|
|
||||||
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 in ('author', 'authors'):
|
|
||||||
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 search_done(self, view, ok):
|
def search_done(self, view, ok):
|
||||||
if view is self.current_view():
|
if view is self.current_view():
|
||||||
self.search.search_done(ok)
|
self.search.search_done(ok)
|
||||||
@ -308,10 +247,10 @@ class LayoutMixin(object): # {{{
|
|||||||
|
|
||||||
def finalize_layout(self):
|
def finalize_layout(self):
|
||||||
self.status_bar.initialize(self.system_tray_icon)
|
self.status_bar.initialize(self.system_tray_icon)
|
||||||
self.book_details.show_book_info.connect(self.show_book_info)
|
self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info)
|
||||||
self.book_details.files_dropped.connect(self.files_dropped_on_book)
|
self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book)
|
||||||
self.book_details.open_containing_folder.connect(self.view_folder_for_id)
|
self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id)
|
||||||
self.book_details.view_specific_format.connect(self.view_format_by_id)
|
self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)
|
||||||
|
|
||||||
m = self.library_view.model()
|
m = self.library_view.model()
|
||||||
if m.rowCount(None) > 0:
|
if m.rowCount(None) > 0:
|
||||||
|
@ -5,42 +5,32 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from operator import attrgetter
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \
|
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
|
||||||
pyqtSignal, QToolButton, \
|
pyqtSignal, QToolButton, \
|
||||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \
|
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \
|
||||||
QMenu, QUrl
|
QMenu
|
||||||
|
|
||||||
from calibre.constants import __appname__, isosx
|
from calibre.constants import __appname__
|
||||||
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
|
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
|
||||||
from calibre.gui2.throbber import ThrobbingButton
|
from calibre.gui2.throbber import ThrobbingButton
|
||||||
from calibre.gui2 import config, open_url, gprefs
|
from calibre.gui2 import config, gprefs
|
||||||
from calibre.gui2.widgets import ComboBoxWithHelp
|
from calibre.gui2.widgets import ComboBoxWithHelp
|
||||||
from calibre import human_readable
|
from calibre import human_readable
|
||||||
from calibre.utils.config import prefs
|
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
|
||||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
|
||||||
from calibre.utils.smtp import config as email_config
|
|
||||||
|
|
||||||
|
TOOLBAR_NO_DEVICE = (
|
||||||
|
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
|
||||||
|
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
|
||||||
|
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
|
||||||
|
)
|
||||||
|
|
||||||
class SaveMenu(QMenu): # {{{
|
TOOLBAR_DEVICE = (
|
||||||
|
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View',
|
||||||
save_fmt = pyqtSignal(object)
|
'Send To Device', None, None, 'Location Manager', None, None,
|
||||||
|
'Fetch News', 'Save To Disk', 'Connect Share', None,
|
||||||
def __init__(self, parent):
|
'Remove Books', None, 'Help', 'Preferences',
|
||||||
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))
|
|
||||||
action.triggered.connect(
|
|
||||||
getattr(self, 'do_'+ext))
|
|
||||||
|
|
||||||
def do(self, ext, *args):
|
|
||||||
self.save_fmt.emit(ext)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class LocationManager(QObject): # {{{
|
class LocationManager(QObject): # {{{
|
||||||
|
|
||||||
@ -221,8 +211,9 @@ class SearchBar(QWidget): # {{{
|
|||||||
|
|
||||||
class ToolBar(QToolBar): # {{{
|
class ToolBar(QToolBar): # {{{
|
||||||
|
|
||||||
def __init__(self, actions, donate, location_manager, parent=None):
|
def __init__(self, donate, location_manager, parent):
|
||||||
QToolBar.__init__(self, parent)
|
QToolBar.__init__(self, parent)
|
||||||
|
self.gui = parent
|
||||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||||
self.setMovable(False)
|
self.setMovable(False)
|
||||||
self.setFloatable(False)
|
self.setFloatable(False)
|
||||||
@ -232,7 +223,6 @@ class ToolBar(QToolBar): # {{{
|
|||||||
self.donate = donate
|
self.donate = donate
|
||||||
self.apply_settings()
|
self.apply_settings()
|
||||||
|
|
||||||
self.all_actions = actions
|
|
||||||
self.location_manager = location_manager
|
self.location_manager = location_manager
|
||||||
self.location_manager.locations_changed.connect(self.build_bar)
|
self.location_manager.locations_changed.connect(self.build_bar)
|
||||||
self.d_widget = QWidget()
|
self.d_widget = QWidget()
|
||||||
@ -258,51 +248,30 @@ class ToolBar(QToolBar): # {{{
|
|||||||
|
|
||||||
def build_bar(self):
|
def build_bar(self):
|
||||||
showing_device = self.location_manager.has_device
|
showing_device = self.location_manager.has_device
|
||||||
order_field = 'device' if showing_device else 'normal'
|
actions = TOOLBAR_DEVICE if showing_device else TOOLBAR_NO_DEVICE
|
||||||
o = attrgetter(order_field+'_order')
|
|
||||||
sepvals = [2] if showing_device else [1]
|
|
||||||
sepvals += [3]
|
|
||||||
actions = [x for x in self.all_actions if o(x) > -1]
|
|
||||||
actions.sort(cmp=lambda x,y : cmp(o(x), o(y)))
|
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
|
for what in actions:
|
||||||
|
if what is None:
|
||||||
|
self.addSeparator()
|
||||||
|
elif what == 'Location Manager':
|
||||||
|
for ac in self.location_manager.available_actions:
|
||||||
|
self.addAction(ac)
|
||||||
|
self.setup_tool_button(ac, QToolButton.MenuButtonPopup)
|
||||||
|
elif what == 'Donate' and config['show_donate_button']:
|
||||||
|
self.addWidget(self.d_widget)
|
||||||
|
elif what in self.gui.iactions:
|
||||||
|
action = self.gui.iactions[what]
|
||||||
|
self.addAction(action.qaction)
|
||||||
|
self.setup_tool_button(action.qaction, action.popup_type)
|
||||||
|
|
||||||
def setup_tool_button(ac):
|
def setup_tool_button(self, ac, menu_mode=None):
|
||||||
ch = self.widgetForAction(ac)
|
ch = self.widgetForAction(ac)
|
||||||
ch.setCursor(Qt.PointingHandCursor)
|
ch.setCursor(Qt.PointingHandCursor)
|
||||||
ch.setAutoRaise(True)
|
ch.setAutoRaise(True)
|
||||||
if ac.menu() is not None:
|
if ac.menu() is not None and menu_mode is not None:
|
||||||
name = getattr(ac, 'action_name', None)
|
ch.setPopupMode(menu_mode)
|
||||||
ch.setPopupMode(ch.InstantPopup if name == 'conn_share'
|
|
||||||
else ch.MenuButtonPopup)
|
|
||||||
|
|
||||||
for x in actions:
|
|
||||||
self.addAction(x)
|
|
||||||
setup_tool_button(x)
|
|
||||||
|
|
||||||
if x.action_name == 'choose_library':
|
|
||||||
self.choose_action = x
|
|
||||||
if showing_device:
|
|
||||||
self.addSeparator()
|
|
||||||
for ac in self.location_manager.available_actions:
|
|
||||||
self.addAction(ac)
|
|
||||||
setup_tool_button(ac)
|
|
||||||
self.addSeparator()
|
|
||||||
self.location_manager.location_library.trigger()
|
|
||||||
elif config['show_donate_button']:
|
|
||||||
self.addWidget(self.d_widget)
|
|
||||||
|
|
||||||
for x in actions:
|
|
||||||
if x.separator_before in sepvals:
|
|
||||||
self.insertSeparator(x)
|
|
||||||
|
|
||||||
self.choose_action.setVisible(not showing_device)
|
|
||||||
|
|
||||||
def count_changed(self, new_count):
|
|
||||||
text = _('%d books')%new_count
|
|
||||||
a = self.choose_action
|
|
||||||
a.setText(text)
|
|
||||||
a.setToolTip(_('Choose calibre library to work with') + '\n\n' + text)
|
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
QToolBar.resizeEvent(self, ev)
|
QToolBar.resizeEvent(self, ev)
|
||||||
@ -321,84 +290,9 @@ class ToolBar(QToolBar): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class Action(QAction):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ShareConnMenu(QMenu): # {{{
|
|
||||||
|
|
||||||
connect_to_folder = pyqtSignal()
|
|
||||||
connect_to_itunes = pyqtSignal()
|
|
||||||
config_email = pyqtSignal()
|
|
||||||
toggle_server = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
QMenu.__init__(self, parent)
|
|
||||||
mitem = self.addAction(QIcon(I('devices/folder.svg')), _('Connect to folder'))
|
|
||||||
mitem.setEnabled(True)
|
|
||||||
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
|
|
||||||
self.connect_to_folder_action = mitem
|
|
||||||
mitem = self.addAction(QIcon(I('devices/itunes.png')),
|
|
||||||
_('Connect to iTunes'))
|
|
||||||
mitem.setEnabled(True)
|
|
||||||
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
|
|
||||||
self.connect_to_itunes_action = mitem
|
|
||||||
self.addSeparator()
|
|
||||||
self.toggle_server_action = \
|
|
||||||
self.addAction(QIcon(I('network-server.svg')),
|
|
||||||
_('Start Content Server'))
|
|
||||||
self.toggle_server_action.triggered.connect(lambda x:
|
|
||||||
self.toggle_server.emit())
|
|
||||||
self.addSeparator()
|
|
||||||
|
|
||||||
self.email_actions = []
|
|
||||||
|
|
||||||
def server_state_changed(self, running):
|
|
||||||
text = _('Start Content Server')
|
|
||||||
if running:
|
|
||||||
text = _('Stop Content Server')
|
|
||||||
self.toggle_server_action.setText(text)
|
|
||||||
|
|
||||||
def build_email_entries(self, sync_menu):
|
|
||||||
from calibre.gui2.device import DeviceAction
|
|
||||||
for ac in self.email_actions:
|
|
||||||
self.removeAction(ac)
|
|
||||||
self.email_actions = []
|
|
||||||
self.memory = []
|
|
||||||
opts = email_config().parse()
|
|
||||||
if opts.accounts:
|
|
||||||
self.email_to_menu = QMenu(_('Email to')+'...', self)
|
|
||||||
keys = sorted(opts.accounts.keys())
|
|
||||||
for account in keys:
|
|
||||||
formats, auto, default = opts.accounts[account]
|
|
||||||
dest = 'mail:'+account+';'+formats
|
|
||||||
action1 = DeviceAction(dest, False, False, I('mail.svg'),
|
|
||||||
_('Email to')+' '+account)
|
|
||||||
action2 = DeviceAction(dest, True, False, I('mail.svg'),
|
|
||||||
_('Email to')+' '+account+ _(' and delete from library'))
|
|
||||||
map(self.email_to_menu.addAction, (action1, action2))
|
|
||||||
map(self.memory.append, (action1, action2))
|
|
||||||
if default:
|
|
||||||
map(self.addAction, (action1, action2))
|
|
||||||
map(self.email_actions.append, (action1, action2))
|
|
||||||
self.email_to_menu.addSeparator()
|
|
||||||
action1.a_s.connect(sync_menu.action_triggered)
|
|
||||||
action2.a_s.connect(sync_menu.action_triggered)
|
|
||||||
ac = self.addMenu(self.email_to_menu)
|
|
||||||
self.email_actions.append(ac)
|
|
||||||
else:
|
|
||||||
ac = self.addAction(_('Setup email based sharing of books'))
|
|
||||||
self.email_actions.append(ac)
|
|
||||||
ac.triggered.connect(self.setup_email)
|
|
||||||
|
|
||||||
def setup_email(self, *args):
|
|
||||||
self.config_email.emit()
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class MainWindowMixin(object):
|
class MainWindowMixin(object):
|
||||||
|
|
||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
self.device_connected = None
|
|
||||||
self.setObjectName('MainWindow')
|
self.setObjectName('MainWindow')
|
||||||
self.setWindowIcon(QIcon(I('library.png')))
|
self.setWindowIcon(QIcon(I('library.png')))
|
||||||
self.setWindowTitle(__appname__)
|
self.setWindowTitle(__appname__)
|
||||||
@ -412,230 +306,18 @@ class MainWindowMixin(object):
|
|||||||
self.donate_button = ThrobbingButton(self.centralwidget)
|
self.donate_button = ThrobbingButton(self.centralwidget)
|
||||||
self.location_manager = LocationManager(self)
|
self.location_manager = LocationManager(self)
|
||||||
|
|
||||||
self.init_scheduler(db)
|
self.iactions['Fetch News'].init_scheduler(db)
|
||||||
all_actions = self.setup_actions()
|
|
||||||
|
|
||||||
self.search_bar = SearchBar(self)
|
self.search_bar = SearchBar(self)
|
||||||
self.tool_bar = ToolBar(all_actions, self.donate_button,
|
self.tool_bar = ToolBar(self.donate_button,
|
||||||
self.location_manager, self)
|
self.location_manager, self)
|
||||||
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
||||||
self.tool_bar.choose_action.triggered.connect(self.choose_library)
|
|
||||||
|
|
||||||
l = self.centralwidget.layout()
|
l = self.centralwidget.layout()
|
||||||
l.addWidget(self.search_bar)
|
l.addWidget(self.search_bar)
|
||||||
|
|
||||||
def init_scheduler(self, db):
|
|
||||||
self.scheduler = Scheduler(self, db)
|
|
||||||
self.scheduler.start_recipe_fetch.connect(
|
|
||||||
self.download_scheduled_recipe, type=Qt.QueuedConnection)
|
|
||||||
|
|
||||||
def read_toolbar_settings(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def choose_library(self, *args):
|
|
||||||
from calibre.gui2.dialogs.choose_library import ChooseLibrary
|
|
||||||
db = self.library_view.model().db
|
|
||||||
c = ChooseLibrary(db, self.library_moved, self)
|
|
||||||
c.exec_()
|
|
||||||
|
|
||||||
def setup_actions(self): # {{{
|
|
||||||
all_actions = []
|
|
||||||
|
|
||||||
def ac(normal_order, device_order, separator_before,
|
|
||||||
name, text, icon, shortcut=None, tooltip=None):
|
|
||||||
action = Action(QIcon(I(icon)), text, self)
|
|
||||||
action.normal_order = normal_order
|
|
||||||
action.device_order = device_order
|
|
||||||
action.separator_before = separator_before
|
|
||||||
action.action_name = name
|
|
||||||
text = tooltip if tooltip else text
|
|
||||||
action.setToolTip(text)
|
|
||||||
action.setStatusTip(text)
|
|
||||||
action.setWhatsThis(text)
|
|
||||||
action.setAutoRepeat(False)
|
|
||||||
action.setObjectName('action_'+name)
|
|
||||||
if shortcut:
|
|
||||||
action.setShortcut(shortcut)
|
|
||||||
setattr(self, 'action_'+name, action)
|
|
||||||
all_actions.append(action)
|
|
||||||
|
|
||||||
ac(0, 0, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
|
|
||||||
ac(1, 1, 0, 'edit', _('Edit metadata'), 'edit_input.svg', _('E'))
|
|
||||||
ac(2, 2, 3, 'convert', _('Convert books'), 'convert.svg', _('C'))
|
|
||||||
ac(3, 3, 0, 'view', _('View'), 'view.svg', _('V'))
|
|
||||||
ac(-1, 4, 0, 'sync', _('Send to device'), 'sync.svg')
|
|
||||||
ac(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png',
|
|
||||||
tooltip=_('Choose calibre library to work with'))
|
|
||||||
ac(6, 6, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
|
|
||||||
ac(7, 7, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
|
|
||||||
ac(8, 8, 0, 'conn_share', _('Connect/share'), 'connect_share.svg')
|
|
||||||
ac(9, 9, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
|
|
||||||
ac(10, 10, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
|
|
||||||
ac(11, 11, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
|
|
||||||
|
|
||||||
ac(-1, -1, 0, 'merge', _('Merge book records'), 'merge_books.svg', _('M'))
|
|
||||||
ac(-1, -1, 0, 'open_containing_folder', _('Open containing folder'),
|
|
||||||
'document_open.svg')
|
|
||||||
ac(-1, -1, 0, 'show_book_details', _('Show book details'),
|
|
||||||
'dialog_information.svg')
|
|
||||||
ac(-1, -1, 0, 'books_by_same_author', _('Books by same author'),
|
|
||||||
'user_profile.svg')
|
|
||||||
ac(-1, -1, 0, 'books_in_this_series', _('Books in this series'),
|
|
||||||
'books_in_series.svg')
|
|
||||||
ac(-1, -1, 0, 'books_by_this_publisher', _('Books by this publisher'),
|
|
||||||
'publisher.png')
|
|
||||||
ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
|
|
||||||
'tags.svg')
|
|
||||||
|
|
||||||
self.action_news.setMenu(self.scheduler.news_menu)
|
|
||||||
self.action_news.triggered.connect(
|
|
||||||
self.scheduler.show_dialog)
|
|
||||||
self.share_conn_menu = ShareConnMenu(self)
|
|
||||||
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
|
|
||||||
self.share_conn_menu.config_email.connect(partial(self.do_config,
|
|
||||||
initial_category='email'))
|
|
||||||
self.action_conn_share.setMenu(self.share_conn_menu)
|
|
||||||
|
|
||||||
self.action_help.triggered.connect(self.show_help)
|
|
||||||
md = QMenu()
|
|
||||||
md.addAction(_('Edit metadata individually'),
|
|
||||||
partial(self.edit_metadata, False, bulk=False))
|
|
||||||
md.addSeparator()
|
|
||||||
md.addAction(_('Edit metadata in bulk'),
|
|
||||||
partial(self.edit_metadata, False, bulk=True))
|
|
||||||
md.addSeparator()
|
|
||||||
md.addAction(_('Download metadata and covers'),
|
|
||||||
partial(self.download_metadata, False, covers=True),
|
|
||||||
Qt.ControlModifier+Qt.Key_D)
|
|
||||||
md.addAction(_('Download only metadata'),
|
|
||||||
partial(self.download_metadata, False, covers=False))
|
|
||||||
md.addAction(_('Download only covers'),
|
|
||||||
partial(self.download_metadata, False, covers=True,
|
|
||||||
set_metadata=False, set_social_metadata=False))
|
|
||||||
md.addAction(_('Download only social metadata'),
|
|
||||||
partial(self.download_metadata, False, covers=False,
|
|
||||||
set_metadata=False, set_social_metadata=True))
|
|
||||||
self.metadata_menu = md
|
|
||||||
|
|
||||||
mb = QMenu()
|
|
||||||
mb.addAction(_('Merge into first selected book - delete others'),
|
|
||||||
self.merge_books)
|
|
||||||
mb.addSeparator()
|
|
||||||
mb.addAction(_('Merge into first selected book - keep others'),
|
|
||||||
partial(self.merge_books, safe_merge=True))
|
|
||||||
self.merge_menu = mb
|
|
||||||
self.action_merge.setMenu(mb)
|
|
||||||
md.addSeparator()
|
|
||||||
md.addAction(self.action_merge)
|
|
||||||
|
|
||||||
self.add_menu = QMenu()
|
|
||||||
self.add_menu.addAction(_('Add books from a single directory'),
|
|
||||||
self.add_books)
|
|
||||||
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_recursive_single)
|
|
||||||
self.add_menu.addAction(_('Add books from directories, including '
|
|
||||||
'sub directories (Multiple books per directory, assumes every '
|
|
||||||
'ebook file is a different book)'), self.add_recursive_multiple)
|
|
||||||
self.add_menu.addSeparator()
|
|
||||||
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
|
|
||||||
'formats)'), self.add_empty)
|
|
||||||
self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn)
|
|
||||||
self.action_add.setMenu(self.add_menu)
|
|
||||||
self.action_add.triggered.connect(self.add_books)
|
|
||||||
self.action_del.triggered.connect(self.delete_books)
|
|
||||||
self.action_edit.triggered.connect(self.edit_metadata)
|
|
||||||
self.action_merge.triggered.connect(self.merge_books)
|
|
||||||
|
|
||||||
self.action_save.triggered.connect(self.save_to_disk)
|
|
||||||
self.save_menu = QMenu()
|
|
||||||
self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk,
|
|
||||||
False))
|
|
||||||
self.save_menu.addAction(_('Save to disk in a single directory'),
|
|
||||||
partial(self.save_to_single_dir, False))
|
|
||||||
self.save_menu.addAction(_('Save only %s format to disk')%
|
|
||||||
prefs['output_format'].upper(),
|
|
||||||
partial(self.save_single_format_to_disk, False))
|
|
||||||
self.save_menu.addAction(
|
|
||||||
_('Save only %s format to disk in a single directory')%
|
|
||||||
prefs['output_format'].upper(),
|
|
||||||
partial(self.save_single_fmt_to_single_dir, False))
|
|
||||||
self.save_sub_menu = SaveMenu(self)
|
|
||||||
self.save_menu.addMenu(self.save_sub_menu)
|
|
||||||
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
|
|
||||||
|
|
||||||
self.action_view.triggered.connect(self.view_book)
|
|
||||||
self.view_menu = QMenu()
|
|
||||||
self.view_menu.addAction(_('View'), partial(self.view_book, False))
|
|
||||||
ac = self.view_menu.addAction(_('View specific format'))
|
|
||||||
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
|
|
||||||
self.action_view.setMenu(self.view_menu)
|
|
||||||
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
|
|
||||||
|
|
||||||
self.delete_menu = QMenu()
|
|
||||||
self.delete_menu.addAction(_('Remove selected books'), self.delete_books)
|
|
||||||
self.delete_menu.addAction(
|
|
||||||
_('Remove files of a specific format from selected books..'),
|
|
||||||
self.delete_selected_formats)
|
|
||||||
self.delete_menu.addAction(
|
|
||||||
_('Remove all formats from selected books, except...'),
|
|
||||||
self.delete_all_but_selected_formats)
|
|
||||||
self.delete_menu.addAction(
|
|
||||||
_('Remove covers from selected books'), self.delete_covers)
|
|
||||||
self.delete_menu.addSeparator()
|
|
||||||
self.delete_menu.addAction(
|
|
||||||
_('Remove matching books from device'),
|
|
||||||
self.remove_matching_books_from_device)
|
|
||||||
self.action_del.setMenu(self.delete_menu)
|
|
||||||
|
|
||||||
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
|
||||||
self.addAction(self.action_open_containing_folder)
|
|
||||||
self.action_open_containing_folder.triggered.connect(self.view_folder)
|
|
||||||
self.action_sync.setShortcut(Qt.Key_D)
|
|
||||||
self.action_sync.setEnabled(True)
|
|
||||||
self.create_device_menu()
|
|
||||||
self.action_sync.triggered.connect(
|
|
||||||
self._sync_action_triggered)
|
|
||||||
|
|
||||||
self.action_edit.setMenu(md)
|
|
||||||
self.action_save.setMenu(self.save_menu)
|
|
||||||
|
|
||||||
cm = QMenu()
|
|
||||||
cm.addAction(_('Convert individually'), partial(self.convert_ebook,
|
|
||||||
False, bulk=False))
|
|
||||||
cm.addAction(_('Bulk convert'),
|
|
||||||
partial(self.convert_ebook, False, bulk=True))
|
|
||||||
cm.addSeparator()
|
|
||||||
ac = cm.addAction(
|
|
||||||
_('Create catalog of books in your calibre library'))
|
|
||||||
ac.triggered.connect(self.generate_catalog)
|
|
||||||
self.action_convert.setMenu(cm)
|
|
||||||
self.action_convert.triggered.connect(self.convert_ebook)
|
|
||||||
self.convert_menu = cm
|
|
||||||
|
|
||||||
pm = QMenu()
|
|
||||||
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
|
|
||||||
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
|
|
||||||
self.run_wizard)
|
|
||||||
self.action_preferences.setMenu(pm)
|
|
||||||
self.preferences_menu = pm
|
|
||||||
for x in (self.preferences_action, self.action_preferences):
|
|
||||||
x.triggered.connect(self.do_config)
|
|
||||||
|
|
||||||
|
|
||||||
return all_actions
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
def show_help(self, *args):
|
|
||||||
open_url(QUrl('http://calibre-ebook.com/user_manual'))
|
|
||||||
|
|
||||||
def content_server_state_changed(self, running):
|
|
||||||
self.share_conn_menu.server_state_changed(running)
|
|
||||||
|
|
||||||
def toggle_content_server(self):
|
|
||||||
if self.content_server is None:
|
|
||||||
self.start_content_server()
|
|
||||||
else:
|
|
||||||
self.content_server.exit()
|
|
||||||
self.content_server = None
|
|
||||||
|
@ -389,37 +389,10 @@ class BooksView(QTableView): # {{{
|
|||||||
#}}}
|
#}}}
|
||||||
|
|
||||||
# Context Menu {{{
|
# Context Menu {{{
|
||||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
def set_context_menu(self, menu, edit_collections_action):
|
||||||
save, open_folder, book_details, delete, conn_share,
|
|
||||||
similar_menu=None, add_to_library=None,
|
|
||||||
edit_device_collections=None):
|
|
||||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||||
self.context_menu = QMenu(self)
|
self.context_menu = menu
|
||||||
if edit_metadata is not None:
|
self.edit_collections_action = edit_collections_action
|
||||||
self.context_menu.addAction(edit_metadata)
|
|
||||||
if send_to_device is not None:
|
|
||||||
self.context_menu.addAction(send_to_device)
|
|
||||||
if convert is not None:
|
|
||||||
self.context_menu.addAction(convert)
|
|
||||||
if conn_share is not None:
|
|
||||||
self.context_menu.addAction(conn_share)
|
|
||||||
self.context_menu.addAction(view)
|
|
||||||
self.context_menu.addAction(save)
|
|
||||||
if open_folder is not None:
|
|
||||||
self.context_menu.addAction(open_folder)
|
|
||||||
if delete is not None:
|
|
||||||
self.context_menu.addAction(delete)
|
|
||||||
if book_details is not None:
|
|
||||||
self.context_menu.addAction(book_details)
|
|
||||||
if similar_menu is not None:
|
|
||||||
self.context_menu.addMenu(similar_menu)
|
|
||||||
if add_to_library is not None:
|
|
||||||
func = partial(add_to_library[1], view=self)
|
|
||||||
self.context_menu.addAction(add_to_library[0], func)
|
|
||||||
if edit_device_collections is not None:
|
|
||||||
func = partial(edit_device_collections[1], view=self)
|
|
||||||
self.edit_collections_menu = \
|
|
||||||
self.context_menu.addAction(edit_device_collections[0], func)
|
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.context_menu.popup(event.globalPos())
|
self.context_menu.popup(event.globalPos())
|
||||||
@ -528,10 +501,11 @@ class DeviceBooksView(BooksView): # {{{
|
|||||||
self.setAcceptDrops(False)
|
self.setAcceptDrops(False)
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.edit_collections_menu.setVisible(
|
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
|
||||||
callable(getattr(self._model.db, 'supports_collections', None)) and \
|
|
||||||
self._model.db.supports_collections() and \
|
self._model.db.supports_collections() and \
|
||||||
prefs['manage_device_metadata'] == 'manual')
|
prefs['manage_device_metadata'] == 'manual'
|
||||||
|
|
||||||
|
self.edit_collections_action.setVisible(edit_collections)
|
||||||
self.context_menu.popup(event.globalPos())
|
self.context_menu.popup(event.globalPos())
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ class GuiRunner(QObject):
|
|||||||
main.initialize(self.library_path, self.db, self.listener, self.actions)
|
main.initialize(self.library_path, self.db, self.listener, self.actions)
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Started up in', time.time() - self.startup_time)
|
prints('Started up in', time.time() - self.startup_time)
|
||||||
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
|
add_filesystem_book = partial(main.iactions['Add Books'].add_filesystem_book, allow_device=False)
|
||||||
sys.excepthook = main.unhandled_exception
|
sys.excepthook = main.unhandled_exception
|
||||||
if len(self.args) > 1:
|
if len(self.args) > 1:
|
||||||
p = os.path.abspath(self.args[1])
|
p = os.path.abspath(self.args[1])
|
||||||
|
@ -15,7 +15,7 @@ from threading import Thread
|
|||||||
from PyQt4.Qt import Qt, SIGNAL, QTimer, \
|
from PyQt4.Qt import Qt, SIGNAL, QTimer, \
|
||||||
QPixmap, QMenu, QIcon, pyqtSignal, \
|
QPixmap, QMenu, QIcon, pyqtSignal, \
|
||||||
QDialog, \
|
QDialog, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
QSystemTrayIcon, QApplication, QKeySequence, \
|
||||||
QMessageBox, QHelpEvent
|
QMessageBox, QHelpEvent
|
||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
@ -23,6 +23,8 @@ from calibre.constants import __appname__, isosx
|
|||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
|
from calibre.customize.ui import interface_actions
|
||||||
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
|
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
|
||||||
gprefs, max_available_height, config, info_dialog, Dispatcher
|
gprefs, max_available_height, config, info_dialog, Dispatcher
|
||||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||||
@ -32,17 +34,10 @@ from calibre.gui2.main_window import MainWindow
|
|||||||
from calibre.gui2.layout import MainWindowMixin
|
from calibre.gui2.layout import MainWindowMixin
|
||||||
from calibre.gui2.device import DeviceMixin
|
from calibre.gui2.device import DeviceMixin
|
||||||
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
||||||
from calibre.gui2.dialogs.config import ConfigDialog
|
|
||||||
|
|
||||||
from calibre.gui2.dialogs.book_info import BookInfo
|
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
|
||||||
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
||||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||||
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||||
from calibre.gui2.tag_view import TagBrowserMixin
|
from calibre.gui2.tag_view import TagBrowserMixin
|
||||||
from calibre.gui2.actions import AnnotationsAction, AddAction, DeleteAction, \
|
|
||||||
EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, \
|
|
||||||
ConvertAction, ViewAction
|
|
||||||
|
|
||||||
|
|
||||||
class Listener(Thread): # {{{
|
class Listener(Thread): # {{{
|
||||||
@ -91,16 +86,27 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
|||||||
|
|
||||||
class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
|
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin
|
||||||
AnnotationsAction, AddAction, DeleteAction,
|
):
|
||||||
EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction,
|
|
||||||
ConvertAction, ViewAction):
|
|
||||||
'The main GUI'
|
'The main GUI'
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, opts, parent=None):
|
def __init__(self, opts, parent=None):
|
||||||
MainWindow.__init__(self, opts, parent)
|
MainWindow.__init__(self, opts, parent)
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
|
self.device_connected = None
|
||||||
|
acmap = {}
|
||||||
|
for action in interface_actions():
|
||||||
|
mod, cls = action.actual_plugin.split(':')
|
||||||
|
ac = getattr(__import__(mod, fromlist=['1'], level=0), cls)(self,
|
||||||
|
action.site_customization)
|
||||||
|
if ac.name in acmap:
|
||||||
|
if ac.priority >= acmap[ac.name].priority:
|
||||||
|
acmap[ac.name] = ac
|
||||||
|
else:
|
||||||
|
acmap[ac.name] = ac
|
||||||
|
|
||||||
|
self.iactions = acmap
|
||||||
|
|
||||||
def initialize(self, library_path, db, listener, actions):
|
def initialize(self, library_path, db, listener, actions):
|
||||||
opts = self.opts
|
opts = self.opts
|
||||||
@ -119,6 +125,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
self.another_instance_wants_to_talk)
|
self.another_instance_wants_to_talk)
|
||||||
self.check_messages_timer.start(1000)
|
self.check_messages_timer.start(1000)
|
||||||
|
|
||||||
|
for ac in self.iactions.values():
|
||||||
|
ac.do_genesis()
|
||||||
MainWindowMixin.__init__(self, db)
|
MainWindowMixin.__init__(self, db)
|
||||||
|
|
||||||
# Jobs Button {{{
|
# Jobs Button {{{
|
||||||
@ -140,9 +148,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
self.verbose = opts.verbose
|
self.verbose = opts.verbose
|
||||||
self.get_metadata = GetMetadata()
|
self.get_metadata = GetMetadata()
|
||||||
self.upload_memory = {}
|
self.upload_memory = {}
|
||||||
self.delete_memory = {}
|
|
||||||
self.conversion_jobs = {}
|
|
||||||
self.persistent_files = []
|
|
||||||
self.metadata_dialogs = []
|
self.metadata_dialogs = []
|
||||||
self.default_thumbnail = None
|
self.default_thumbnail = None
|
||||||
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
||||||
@ -166,22 +171,13 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
QIcon(I('eject.svg')), _('&Eject connected device'))
|
QIcon(I('eject.svg')), _('&Eject connected device'))
|
||||||
self.eject_action.setEnabled(False)
|
self.eject_action.setEnabled(False)
|
||||||
self.addAction(self.quit_action)
|
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.system_tray_menu.addAction(self.quit_action)
|
||||||
self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q))
|
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.system_tray_icon.setContextMenu(self.system_tray_menu)
|
||||||
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
|
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
|
||||||
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
|
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
|
||||||
self.connect(self.restore_action, SIGNAL('triggered()'),
|
self.connect(self.restore_action, SIGNAL('triggered()'),
|
||||||
self.show_windows)
|
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,
|
self.connect(self.system_tray_icon,
|
||||||
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
||||||
self.system_tray_icon_activated)
|
self.system_tray_icon_activated)
|
||||||
@ -209,9 +205,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
|
|
||||||
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||||
self.hide_windows()
|
self.hide_windows()
|
||||||
for t in (self.tool_bar, ):
|
self.library_view.model().count_changed_signal.connect(
|
||||||
self.library_view.model().count_changed_signal.connect \
|
self.iactions['Choose Library'].count_changed)
|
||||||
(t.count_changed)
|
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||||
@ -249,22 +244,21 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
self.start_content_server()
|
self.start_content_server()
|
||||||
|
|
||||||
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
||||||
AddAction.__init__(self)
|
|
||||||
|
|
||||||
self.read_settings()
|
self.read_settings()
|
||||||
self.finalize_layout()
|
self.finalize_layout()
|
||||||
self.donate_button.start_animation()
|
self.donate_button.start_animation()
|
||||||
|
|
||||||
self.scheduler.delete_old_news.connect(
|
self.iactions['Fetch News'].connect_scheduler()
|
||||||
self.library_view.model().delete_books_by_id,
|
|
||||||
type=Qt.QueuedConnection)
|
|
||||||
|
|
||||||
def start_content_server(self):
|
def start_content_server(self):
|
||||||
from calibre.library.server.main import start_threaded_server
|
from calibre.library.server.main import start_threaded_server
|
||||||
from calibre.library.server import server_config
|
from calibre.library.server import server_config
|
||||||
self.content_server = start_threaded_server(
|
self.content_server = start_threaded_server(
|
||||||
self.library_view.model().db, server_config().parse())
|
self.library_view.model().db, server_config().parse())
|
||||||
self.content_server.state_callback = Dispatcher(self.content_server_state_changed)
|
self.content_server.state_callback = Dispatcher(
|
||||||
|
self.iactions['Connect Share'].content_server_state_changed)
|
||||||
self.content_server.state_callback(True)
|
self.content_server.state_callback(True)
|
||||||
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
||||||
|
|
||||||
@ -329,7 +323,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
if len(argv) > 1:
|
if len(argv) > 1:
|
||||||
path = os.path.abspath(argv[1])
|
path = os.path.abspath(argv[1])
|
||||||
if os.access(path, os.R_OK):
|
if os.access(path, os.R_OK):
|
||||||
self.add_filesystem_book(path)
|
self.iactions['Add Books'].add_filesystem_book(path)
|
||||||
self.setWindowState(self.windowState() & \
|
self.setWindowState(self.windowState() & \
|
||||||
~Qt.WindowMinimized|Qt.WindowActive)
|
~Qt.WindowMinimized|Qt.WindowActive)
|
||||||
self.show_windows()
|
self.show_windows()
|
||||||
@ -357,41 +351,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
||||||
|
|
||||||
|
|
||||||
def do_config(self, checked=False, initial_category='general'):
|
|
||||||
if self.job_manager.has_jobs():
|
|
||||||
d = error_dialog(self, _('Cannot configure'),
|
|
||||||
_('Cannot configure while there are running jobs.'))
|
|
||||||
d.exec_()
|
|
||||||
return
|
|
||||||
if self.must_restart_before_config:
|
|
||||||
d = error_dialog(self, _('Cannot configure'),
|
|
||||||
_('Cannot configure before calibre is restarted.'))
|
|
||||||
d.exec_()
|
|
||||||
return
|
|
||||||
d = ConfigDialog(self, self.library_view,
|
|
||||||
server=self.content_server, initial_category=initial_category)
|
|
||||||
|
|
||||||
d.exec_()
|
|
||||||
self.content_server = d.server
|
|
||||||
if self.content_server is not None:
|
|
||||||
self.content_server.state_callback = \
|
|
||||||
Dispatcher(self.content_server_state_changed)
|
|
||||||
self.content_server.state_callback(self.content_server.is_running)
|
|
||||||
|
|
||||||
if d.result() == d.Accepted:
|
|
||||||
self.read_toolbar_settings()
|
|
||||||
self.search.search_as_you_type(config['search_as_you_type'])
|
|
||||||
self.save_menu.actions()[2].setText(
|
|
||||||
_('Save only %s format to disk')%
|
|
||||||
prefs['output_format'].upper())
|
|
||||||
self.save_menu.actions()[3].setText(
|
|
||||||
_('Save only %s format to disk in a single directory')%
|
|
||||||
prefs['output_format'].upper())
|
|
||||||
self.tags_view.set_new_model() # in case columns changed
|
|
||||||
self.tags_view.recount()
|
|
||||||
self.create_device_menu()
|
|
||||||
self.set_device_menu_items_state(bool(self.device_connected))
|
|
||||||
self.tool_bar.apply_settings()
|
|
||||||
|
|
||||||
def library_moved(self, newloc):
|
def library_moved(self, newloc):
|
||||||
if newloc is None: return
|
if newloc is None: return
|
||||||
@ -407,18 +366,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
self.saved_search.clear_to_help()
|
self.saved_search.clear_to_help()
|
||||||
self.book_details.reset_info()
|
self.book_details.reset_info()
|
||||||
self.library_view.model().count_changed()
|
self.library_view.model().count_changed()
|
||||||
self.scheduler.database_changed(db)
|
self.iactions['Fetch News'].database_changed(db)
|
||||||
prefs['library_path'] = self.library_path
|
prefs['library_path'] = self.library_path
|
||||||
|
|
||||||
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):
|
def location_selected(self, location):
|
||||||
'''
|
'''
|
||||||
@ -430,26 +380,12 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
for x in ('tb', 'cb'):
|
for x in ('tb', 'cb'):
|
||||||
splitter = getattr(self, x+'_splitter')
|
splitter = getattr(self, x+'_splitter')
|
||||||
splitter.button.setEnabled(location == 'library')
|
splitter.button.setEnabled(location == 'library')
|
||||||
|
for action in self.iactions.values():
|
||||||
|
action.location_selected(location)
|
||||||
if location == 'library':
|
if location == 'library':
|
||||||
self.action_edit.setEnabled(True)
|
|
||||||
self.action_merge.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)
|
|
||||||
self.search_restriction.setEnabled(True)
|
self.search_restriction.setEnabled(True)
|
||||||
for action in list(self.delete_menu.actions())[1:]:
|
|
||||||
action.setEnabled(True)
|
|
||||||
else:
|
else:
|
||||||
self.action_edit.setEnabled(False)
|
|
||||||
self.action_merge.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)
|
|
||||||
self.search_restriction.setEnabled(False)
|
self.search_restriction.setEnabled(False)
|
||||||
for action in list(self.delete_menu.actions())[1:]:
|
|
||||||
action.setEnabled(False)
|
|
||||||
# Reset the view in case something changed while it was invisible
|
# Reset the view in case something changed while it was invisible
|
||||||
self.current_view().reset()
|
self.current_view().reset()
|
||||||
self.set_number_of_books_shown()
|
self.set_number_of_books_shown()
|
||||||
@ -504,7 +440,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
geometry = config['main_window_geometry']
|
geometry = config['main_window_geometry']
|
||||||
if geometry is not None:
|
if geometry is not None:
|
||||||
self.restoreGeometry(geometry)
|
self.restoreGeometry(geometry)
|
||||||
self.read_toolbar_settings()
|
|
||||||
self.read_layout_settings()
|
self.read_layout_settings()
|
||||||
|
|
||||||
def write_settings(self):
|
def write_settings(self):
|
||||||
@ -512,9 +447,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
dynamic.set('sort_history', self.library_view.model().sort_history)
|
dynamic.set('sort_history', self.library_view.model().sort_history)
|
||||||
self.save_layout_state()
|
self.save_layout_state()
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
self.quit(restart=True)
|
|
||||||
|
|
||||||
def quit(self, checked=True, restart=False):
|
def quit(self, checked=True, restart=False):
|
||||||
if not self.confirm_quit():
|
if not self.confirm_quit():
|
||||||
return
|
return
|
||||||
|
@ -157,4 +157,11 @@ The base class for such devices is :class:`calibre.devices.usbms.driver.USBMS`.
|
|||||||
:members:
|
:members:
|
||||||
:member-order: bysource
|
:member-order: bysource
|
||||||
|
|
||||||
|
User Interface Actions
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. autoclass:: calibre.gui2.actions.InterfaceAction
|
||||||
|
:show-inheritance:
|
||||||
|
:members:
|
||||||
|
:member-order: bysource
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user