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
|
||||
raise NotImplementedError('CatalogPlugin.generate_catalog() default '
|
||||
'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 \
|
||||
x.__name__.endswith('MetadataWriter')]
|
||||
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 calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \
|
||||
MetadataReaderPlugin, MetadataWriterPlugin
|
||||
MetadataReaderPlugin, MetadataWriterPlugin, \
|
||||
InterfaceActionBase as InterfaceAction
|
||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||
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
|
||||
from calibre.ebooks.epub.fix import ePubFixer
|
||||
|
||||
|
||||
platform = 'linux'
|
||||
if iswindows:
|
||||
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_readers = {}
|
||||
_metadata_writers = {}
|
||||
|
@ -244,14 +244,20 @@ def info_dialog(parent, title, msg, det_msg='', show=False):
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def __init__(self, func):
|
||||
QObject.__init__(self)
|
||||
def __init__(self, func, queued=True, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
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):
|
||||
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
|
||||
self.worker = SaveWorker(self.rq, db, self.ids, path, self.opts,
|
||||
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.connect(self.timer, SIGNAL('timeout()'), self.update)
|
||||
self.timer.start(200)
|
||||
|
@ -144,7 +144,7 @@ class CoverFlowMixin(object):
|
||||
self.sync_cf_to_listview)
|
||||
self.db_images = DatabaseImages(self.library_view.model())
|
||||
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:
|
||||
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
|
||||
+'<br>'+pictureflowerror)
|
||||
|
@ -608,8 +608,6 @@ class DeviceMixin(object): # {{{
|
||||
self.device_error_dialog = error_dialog(self, _('Error'),
|
||||
_('Error communicating with device'), ' ')
|
||||
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.start()
|
||||
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
||||
@ -647,20 +645,18 @@ class DeviceMixin(object): # {{{
|
||||
|
||||
def create_device_menu(self):
|
||||
self._sync_menu = DeviceMenu(self)
|
||||
self.share_conn_menu.build_email_entries(self._sync_menu)
|
||||
self.action_sync.setMenu(self._sync_menu)
|
||||
self.iactions['Send To Device'].qaction.setMenu(self._sync_menu)
|
||||
self.iactions['Connect Share'].build_email_entries()
|
||||
self.connect(self._sync_menu,
|
||||
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||
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.iactions['Connect Share'].set_state(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)
|
||||
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)
|
||||
|
||||
def device_job_exception(self, job):
|
||||
@ -696,17 +692,14 @@ class DeviceMixin(object): # {{{
|
||||
# Device connected {{{
|
||||
|
||||
def set_device_menu_items_state(self, connected):
|
||||
self.iactions['Connect Share'].set_state(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.enable_device_actions(True,
|
||||
self.device_manager.device.card_prefix(),
|
||||
self.device_manager.device)
|
||||
self.eject_action.setEnabled(True)
|
||||
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.enable_device_actions(False)
|
||||
self.eject_action.setEnabled(False)
|
||||
@ -791,8 +784,9 @@ class DeviceMixin(object): # {{{
|
||||
self.device_job_exception(job)
|
||||
return
|
||||
|
||||
if self.delete_memory.has_key(job):
|
||||
paths, model = self.delete_memory.pop(job)
|
||||
dm = self.iactions['Remove Books'].delete_memory
|
||||
if dm.has_key(job):
|
||||
paths, model = dm.pop(job)
|
||||
self.device_manager.remove_books_from_metadata(paths,
|
||||
self.booklists())
|
||||
model.paths_deleted(paths)
|
||||
@ -924,7 +918,7 @@ class DeviceMixin(object): # {{{
|
||||
_('Auto convert the following books before sending via '
|
||||
'email?'), det_msg=autos,
|
||||
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:
|
||||
bad = '\n'.join('%s'%(i,) for i in bad)
|
||||
@ -1026,7 +1020,7 @@ class DeviceMixin(object): # {{{
|
||||
_('Auto convert the following books before uploading to '
|
||||
'the device?'), det_msg=autos,
|
||||
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]
|
||||
if not files:
|
||||
dynamic.set('catalogs_to_be_synced', set([]))
|
||||
@ -1088,7 +1082,7 @@ class DeviceMixin(object): # {{{
|
||||
_('Auto convert the following books before uploading to '
|
||||
'the device?'), det_msg=autos,
|
||||
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]
|
||||
for f in files:
|
||||
f.deleted_after_upload = del_on_upload
|
||||
@ -1207,7 +1201,7 @@ class DeviceMixin(object): # {{{
|
||||
_('Auto convert the following books before uploading to '
|
||||
'the device?'), det_msg=autos,
|
||||
buttons=QMessageBox.Yes|QMessageBox.Cancel):
|
||||
self.auto_convert(auto, on_card, format)
|
||||
self.iactions['Convert Books'].auto_convert(auto, on_card, format)
|
||||
|
||||
if bad:
|
||||
bad = '\n'.join('%s'%(i,) for i in bad)
|
||||
|
@ -12,7 +12,7 @@ import time
|
||||
import traceback
|
||||
|
||||
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, \
|
||||
choose_files, choose_images, ResizableDialog, \
|
||||
@ -99,6 +99,7 @@ class Format(QListWidgetItem):
|
||||
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
|
||||
COVER_FETCH_TIMEOUT = 240 # seconds
|
||||
view_format = pyqtSignal(object)
|
||||
|
||||
def do_reset_cover(self, *args):
|
||||
pix = QPixmap(I('default_cover.svg'))
|
||||
@ -474,7 +475,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
|
||||
def show_format(self, item, *args):
|
||||
fmt = item.ext
|
||||
self.emit(SIGNAL('view_format(PyQt_PyObject)'), fmt)
|
||||
self.view_format.emit(fmt)
|
||||
|
||||
def deduce_author_sort(self):
|
||||
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
|
||||
|
||||
class ProgressDialog(QDialog, Ui_Dialog):
|
||||
|
||||
canceled_signal = pyqtSignal()
|
||||
|
||||
def __init__(self, title, msg='', min=0, max=99, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
@ -23,7 +25,7 @@ class ProgressDialog(QDialog, Ui_Dialog):
|
||||
self.bar.setValue(min)
|
||||
self.canceled = False
|
||||
|
||||
self.connect(self.button_box, SIGNAL('rejected()'), self._canceled)
|
||||
self.button_box.rejected.connect(self._canceled)
|
||||
|
||||
def set_msg(self, msg=''):
|
||||
self.message.setText(msg)
|
||||
@ -50,7 +52,7 @@ class ProgressDialog(QDialog, Ui_Dialog):
|
||||
self.canceled = True
|
||||
self.button_box.setDisabled(True)
|
||||
self.title.setText(_('Aborting...'))
|
||||
self.emit(SIGNAL('canceled()'))
|
||||
self.canceled_signal.emit()
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() == Qt.Key_Escape:
|
||||
|
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import functools, sys, os
|
||||
|
||||
from PyQt4.Qt import QMenu, Qt, QStackedWidget, \
|
||||
from PyQt4.Qt import Qt, QStackedWidget, QMenu, \
|
||||
QSize, QSizePolicy, QStatusBar, QLabel, QFont
|
||||
|
||||
from calibre.utils.config import prefs
|
||||
@ -27,69 +27,35 @@ def partial(*args, **kwargs):
|
||||
_keep_refs.append(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): # {{{
|
||||
|
||||
def __init__(self, db):
|
||||
similar_menu = QMenu(_('Similar books...'))
|
||||
similar_menu.addAction(self.action_books_by_same_author)
|
||||
similar_menu.addAction(self.action_books_in_this_series)
|
||||
similar_menu.addAction(self.action_books_with_the_same_tags)
|
||||
similar_menu.addAction(self.action_books_by_this_publisher)
|
||||
self.action_books_by_same_author.setShortcut(Qt.ALT + Qt.Key_A)
|
||||
self.action_books_in_this_series.setShortcut(Qt.ALT + Qt.Key_S)
|
||||
self.action_books_by_this_publisher.setShortcut(Qt.ALT + Qt.Key_P)
|
||||
self.action_books_with_the_same_tags.setShortcut(Qt.ALT+Qt.Key_T)
|
||||
self.addAction(self.action_books_by_same_author)
|
||||
self.addAction(self.action_books_by_this_publisher)
|
||||
self.addAction(self.action_books_in_this_series)
|
||||
self.addAction(self.action_books_with_the_same_tags)
|
||||
self.similar_menu = similar_menu
|
||||
self.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'))
|
||||
lm = QMenu(self)
|
||||
def populate_menu(m, items):
|
||||
for what in items:
|
||||
if what is None:
|
||||
m.addSeparator()
|
||||
elif what in self.iactions:
|
||||
m.addAction(self.iactions[what].qaction)
|
||||
populate_menu(lm, LIBRARY_CONTEXT_MENU)
|
||||
dm = QMenu(self)
|
||||
populate_menu(dm, DEVICE_CONTEXT_MENU)
|
||||
ec = self.iactions['Edit Collections'].qaction
|
||||
self.library_view.set_context_menu(lm, ec)
|
||||
for v in (self.memory_view, self.card_a_view, self.card_b_view):
|
||||
v.set_context_menu(dm, ec)
|
||||
|
||||
self.library_view.set_context_menu(self.action_edit, self.action_sync,
|
||||
self.action_convert, self.action_view,
|
||||
self.action_save,
|
||||
self.action_open_containing_folder,
|
||||
self.action_show_book_details,
|
||||
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)
|
||||
self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection)
|
||||
for func, args in [
|
||||
('connect_to_search_box', (self.search,
|
||||
self.search_done)),
|
||||
@ -116,37 +82,10 @@ class LibraryViewMixin(object): # {{{
|
||||
|
||||
for view in ('library', 'memory', 'card_a', 'card_b'):
|
||||
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):
|
||||
if view is self.current_view():
|
||||
self.search.search_done(ok)
|
||||
@ -308,10 +247,10 @@ class LayoutMixin(object): # {{{
|
||||
|
||||
def finalize_layout(self):
|
||||
self.status_bar.initialize(self.system_tray_icon)
|
||||
self.book_details.show_book_info.connect(self.show_book_info)
|
||||
self.book_details.files_dropped.connect(self.files_dropped_on_book)
|
||||
self.book_details.open_containing_folder.connect(self.view_folder_for_id)
|
||||
self.book_details.view_specific_format.connect(self.view_format_by_id)
|
||||
self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info)
|
||||
self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book)
|
||||
self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id)
|
||||
self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)
|
||||
|
||||
m = self.library_view.model()
|
||||
if m.rowCount(None) > 0:
|
||||
|
@ -5,42 +5,32 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from operator import attrgetter
|
||||
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, \
|
||||
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.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 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): # {{{
|
||||
|
||||
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)
|
||||
|
||||
# }}}
|
||||
TOOLBAR_DEVICE = (
|
||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View',
|
||||
'Send To Device', None, None, 'Location Manager', None, None,
|
||||
'Fetch News', 'Save To Disk', 'Connect Share', None,
|
||||
'Remove Books', None, 'Help', 'Preferences',
|
||||
)
|
||||
|
||||
class LocationManager(QObject): # {{{
|
||||
|
||||
@ -221,8 +211,9 @@ class SearchBar(QWidget): # {{{
|
||||
|
||||
class ToolBar(QToolBar): # {{{
|
||||
|
||||
def __init__(self, actions, donate, location_manager, parent=None):
|
||||
def __init__(self, donate, location_manager, parent):
|
||||
QToolBar.__init__(self, parent)
|
||||
self.gui = parent
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
self.setMovable(False)
|
||||
self.setFloatable(False)
|
||||
@ -232,7 +223,6 @@ class ToolBar(QToolBar): # {{{
|
||||
self.donate = donate
|
||||
self.apply_settings()
|
||||
|
||||
self.all_actions = actions
|
||||
self.location_manager = location_manager
|
||||
self.location_manager.locations_changed.connect(self.build_bar)
|
||||
self.d_widget = QWidget()
|
||||
@ -258,51 +248,30 @@ class ToolBar(QToolBar): # {{{
|
||||
|
||||
def build_bar(self):
|
||||
showing_device = self.location_manager.has_device
|
||||
order_field = 'device' if showing_device else 'normal'
|
||||
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)))
|
||||
actions = TOOLBAR_DEVICE if showing_device else TOOLBAR_NO_DEVICE
|
||||
|
||||
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):
|
||||
ch = self.widgetForAction(ac)
|
||||
ch.setCursor(Qt.PointingHandCursor)
|
||||
ch.setAutoRaise(True)
|
||||
if ac.menu() is not None:
|
||||
name = getattr(ac, 'action_name', None)
|
||||
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 setup_tool_button(self, ac, menu_mode=None):
|
||||
ch = self.widgetForAction(ac)
|
||||
ch.setCursor(Qt.PointingHandCursor)
|
||||
ch.setAutoRaise(True)
|
||||
if ac.menu() is not None and menu_mode is not None:
|
||||
ch.setPopupMode(menu_mode)
|
||||
|
||||
def 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):
|
||||
|
||||
def __init__(self, db):
|
||||
self.device_connected = None
|
||||
self.setObjectName('MainWindow')
|
||||
self.setWindowIcon(QIcon(I('library.png')))
|
||||
self.setWindowTitle(__appname__)
|
||||
@ -412,230 +306,18 @@ class MainWindowMixin(object):
|
||||
self.donate_button = ThrobbingButton(self.centralwidget)
|
||||
self.location_manager = LocationManager(self)
|
||||
|
||||
self.init_scheduler(db)
|
||||
all_actions = self.setup_actions()
|
||||
self.iactions['Fetch News'].init_scheduler(db)
|
||||
|
||||
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.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
||||
self.tool_bar.choose_action.triggered.connect(self.choose_library)
|
||||
|
||||
l = self.centralwidget.layout()
|
||||
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 {{{
|
||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
||||
save, open_folder, book_details, delete, conn_share,
|
||||
similar_menu=None, add_to_library=None,
|
||||
edit_device_collections=None):
|
||||
def set_context_menu(self, menu, edit_collections_action):
|
||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||
self.context_menu = QMenu(self)
|
||||
if edit_metadata is not None:
|
||||
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)
|
||||
self.context_menu = menu
|
||||
self.edit_collections_action = edit_collections_action
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
self.context_menu.popup(event.globalPos())
|
||||
@ -528,10 +501,11 @@ class DeviceBooksView(BooksView): # {{{
|
||||
self.setAcceptDrops(False)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
self.edit_collections_menu.setVisible(
|
||||
callable(getattr(self._model.db, 'supports_collections', None)) and \
|
||||
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) 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())
|
||||
event.accept()
|
||||
|
||||
|
@ -155,7 +155,7 @@ class GuiRunner(QObject):
|
||||
main.initialize(self.library_path, self.db, self.listener, self.actions)
|
||||
if DEBUG:
|
||||
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
|
||||
if len(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, \
|
||||
QPixmap, QMenu, QIcon, pyqtSignal, \
|
||||
QDialog, \
|
||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||
QSystemTrayIcon, QApplication, QKeySequence, \
|
||||
QMessageBox, QHelpEvent
|
||||
|
||||
from calibre import prints
|
||||
@ -23,6 +23,8 @@ from calibre.constants import __appname__, isosx
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.config import prefs, dynamic
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
from calibre.customize.ui import interface_actions
|
||||
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
|
||||
gprefs, max_available_height, config, info_dialog, Dispatcher
|
||||
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.device import DeviceMixin
|
||||
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.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||
from calibre.gui2.tag_view import TagBrowserMixin
|
||||
from calibre.gui2.actions import AnnotationsAction, AddAction, DeleteAction, \
|
||||
EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, \
|
||||
ConvertAction, ViewAction
|
||||
|
||||
|
||||
class Listener(Thread): # {{{
|
||||
@ -91,16 +86,27 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
||||
|
||||
class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
|
||||
AnnotationsAction, AddAction, DeleteAction,
|
||||
EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction,
|
||||
ConvertAction, ViewAction):
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin
|
||||
):
|
||||
'The main GUI'
|
||||
|
||||
|
||||
def __init__(self, opts, parent=None):
|
||||
MainWindow.__init__(self, opts, parent)
|
||||
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):
|
||||
opts = self.opts
|
||||
@ -119,6 +125,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
self.another_instance_wants_to_talk)
|
||||
self.check_messages_timer.start(1000)
|
||||
|
||||
for ac in self.iactions.values():
|
||||
ac.do_genesis()
|
||||
MainWindowMixin.__init__(self, db)
|
||||
|
||||
# Jobs Button {{{
|
||||
@ -140,9 +148,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
self.verbose = opts.verbose
|
||||
self.get_metadata = GetMetadata()
|
||||
self.upload_memory = {}
|
||||
self.delete_memory = {}
|
||||
self.conversion_jobs = {}
|
||||
self.persistent_files = []
|
||||
self.metadata_dialogs = []
|
||||
self.default_thumbnail = None
|
||||
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
||||
@ -166,22 +171,13 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
QIcon(I('eject.svg')), _('&Eject connected device'))
|
||||
self.eject_action.setEnabled(False)
|
||||
self.addAction(self.quit_action)
|
||||
self.action_restart = QAction(_('&Restart'), self)
|
||||
self.addAction(self.action_restart)
|
||||
self.system_tray_menu.addAction(self.quit_action)
|
||||
self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q))
|
||||
self.action_restart.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
|
||||
self.action_show_book_details.setShortcut(QKeySequence(Qt.Key_I))
|
||||
self.addAction(self.action_show_book_details)
|
||||
self.system_tray_icon.setContextMenu(self.system_tray_menu)
|
||||
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
|
||||
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
|
||||
self.connect(self.restore_action, SIGNAL('triggered()'),
|
||||
self.show_windows)
|
||||
self.connect(self.action_show_book_details,
|
||||
SIGNAL('triggered(bool)'), self.show_book_info)
|
||||
self.connect(self.action_restart, SIGNAL('triggered()'),
|
||||
self.restart)
|
||||
self.connect(self.system_tray_icon,
|
||||
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
||||
self.system_tray_icon_activated)
|
||||
@ -209,9 +205,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
|
||||
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||
self.hide_windows()
|
||||
for t in (self.tool_bar, ):
|
||||
self.library_view.model().count_changed_signal.connect \
|
||||
(t.count_changed)
|
||||
self.library_view.model().count_changed_signal.connect(
|
||||
self.iactions['Choose Library'].count_changed)
|
||||
if not gprefs.get('quick_start_guide_added', False):
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||
@ -249,22 +244,21 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
self.start_content_server()
|
||||
|
||||
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
||||
AddAction.__init__(self)
|
||||
|
||||
|
||||
self.read_settings()
|
||||
self.finalize_layout()
|
||||
self.donate_button.start_animation()
|
||||
|
||||
self.scheduler.delete_old_news.connect(
|
||||
self.library_view.model().delete_books_by_id,
|
||||
type=Qt.QueuedConnection)
|
||||
self.iactions['Fetch News'].connect_scheduler()
|
||||
|
||||
def start_content_server(self):
|
||||
from calibre.library.server.main import start_threaded_server
|
||||
from calibre.library.server import server_config
|
||||
self.content_server = start_threaded_server(
|
||||
self.library_view.model().db, server_config().parse())
|
||||
self.content_server.state_callback = Dispatcher(self.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.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
||||
|
||||
@ -329,7 +323,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
if len(argv) > 1:
|
||||
path = os.path.abspath(argv[1])
|
||||
if os.access(path, os.R_OK):
|
||||
self.add_filesystem_book(path)
|
||||
self.iactions['Add Books'].add_filesystem_book(path)
|
||||
self.setWindowState(self.windowState() & \
|
||||
~Qt.WindowMinimized|Qt.WindowActive)
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
if newloc is None: return
|
||||
@ -407,18 +366,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
self.saved_search.clear_to_help()
|
||||
self.book_details.reset_info()
|
||||
self.library_view.model().count_changed()
|
||||
self.scheduler.database_changed(db)
|
||||
self.iactions['Fetch News'].database_changed(db)
|
||||
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):
|
||||
'''
|
||||
@ -430,26 +380,12 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
for x in ('tb', 'cb'):
|
||||
splitter = getattr(self, x+'_splitter')
|
||||
splitter.button.setEnabled(location == 'library')
|
||||
for action in self.iactions.values():
|
||||
action.location_selected(location)
|
||||
if location == 'library':
|
||||
self.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)
|
||||
for action in list(self.delete_menu.actions())[1:]:
|
||||
action.setEnabled(True)
|
||||
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)
|
||||
for action in list(self.delete_menu.actions())[1:]:
|
||||
action.setEnabled(False)
|
||||
# Reset the view in case something changed while it was invisible
|
||||
self.current_view().reset()
|
||||
self.set_number_of_books_shown()
|
||||
@ -504,7 +440,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
geometry = config['main_window_geometry']
|
||||
if geometry is not None:
|
||||
self.restoreGeometry(geometry)
|
||||
self.read_toolbar_settings()
|
||||
self.read_layout_settings()
|
||||
|
||||
def write_settings(self):
|
||||
@ -512,9 +447,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
dynamic.set('sort_history', self.library_view.model().sort_history)
|
||||
self.save_layout_state()
|
||||
|
||||
def restart(self):
|
||||
self.quit(restart=True)
|
||||
|
||||
def quit(self, checked=True, restart=False):
|
||||
if not self.confirm_quit():
|
||||
return
|
||||
|
@ -157,4 +157,11 @@ The base class for such devices is :class:`calibre.devices.usbms.driver.USBMS`.
|
||||
:members:
|
||||
: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