Refactor actions to be plugins

This commit is contained in:
Kovid Goyal 2010-08-13 21:41:59 -06:00
commit 1b1e93b46d
35 changed files with 2504 additions and 1952 deletions

View File

@ -351,3 +351,12 @@ class CatalogPlugin(Plugin):
# Default implementation does nothing # Default implementation does nothing
raise NotImplementedError('CatalogPlugin.generate_catalog() default ' raise NotImplementedError('CatalogPlugin.generate_catalog() default '
'method, should be overridden in subclass') 'method, should be overridden in subclass')
class InterfaceActionBase(Plugin):
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('User Interface Action')
can_be_disabled = False
actual_plugin = None

View File

@ -574,3 +574,92 @@ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
x.__name__.endswith('MetadataWriter')] x.__name__.endswith('MetadataWriter')]
plugins += input_profiles + output_profiles plugins += input_profiles + output_profiles
from calibre.customize import InterfaceActionBase
class ActionAdd(InterfaceActionBase):
name = 'Add Books'
actual_plugin = 'calibre.gui2.actions.add:AddAction'
class ActionFetchAnnotations(InterfaceActionBase):
name = 'Fetch Annotations'
actual_plugin = 'calibre.gui2.actions.annotate:FetchAnnotationsAction'
class ActionGenerateCatalog(InterfaceActionBase):
name = 'Generate Catalog'
actual_plugin = 'calibre.gui2.actions.catalog:GenerateCatalogAction'
class ActionConvert(InterfaceActionBase):
name = 'Convert Books'
actual_plugin = 'calibre.gui2.actions.convert:ConvertAction'
class ActionDelete(InterfaceActionBase):
name = 'Remove Books'
actual_plugin = 'calibre.gui2.actions.delete:DeleteAction'
class ActionEditMetadata(InterfaceActionBase):
name = 'Edit Metadata'
actual_plugin = 'calibre.gui2.actions.edit_metadata:EditMetadataAction'
class ActionView(InterfaceActionBase):
name = 'View'
actual_plugin = 'calibre.gui2.actions.view:ViewAction'
class ActionFetchNews(InterfaceActionBase):
name = 'Fetch News'
actual_plugin = 'calibre.gui2.actions.fetch_news:FetchNewsAction'
class ActionSaveToDisk(InterfaceActionBase):
name = 'Save To Disk'
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
class ActionShowBookDetails(InterfaceActionBase):
name = 'Show Book Details'
actual_plugin = 'calibre.gui2.actions.show_book_details:ShowBookDetailsAction'
class ActionRestart(InterfaceActionBase):
name = 'Restart'
actual_plugin = 'calibre.gui2.actions.restart:RestartAction'
class ActionOpenFolder(InterfaceActionBase):
name = 'Open Folder'
actual_plugin = 'calibre.gui2.actions.open:OpenFolderAction'
class ActionSendToDevice(InterfaceActionBase):
name = 'Send To Device'
actual_plugin = 'calibre.gui2.actions.device:SendToDeviceAction'
class ActionConnectShare(InterfaceActionBase):
name = 'Connect Share'
actual_plugin = 'calibre.gui2.actions.device:ConnectShareAction'
class ActionHelp(InterfaceActionBase):
name = 'Help'
actual_plugin = 'calibre.gui2.actions.help:HelpAction'
class ActionPreferences(InterfaceActionBase):
name = 'Preferences'
actual_plugin = 'calibre.gui2.actions.preferences:PreferencesAction'
class ActionSimilarBooks(InterfaceActionBase):
name = 'Similar Books'
actual_plugin = 'calibre.gui2.actions.similar_books:SimilarBooksAction'
class ActionChooseLibrary(InterfaceActionBase):
name = 'Choose Library'
actual_plugin = 'calibre.gui2.actions.choose_library:ChooseLibraryAction'
class ActionAddToLibrary(InterfaceActionBase):
name = 'Add To Library'
actual_plugin = 'calibre.gui2.actions.add_to_library:AddToLibraryAction'
class ActionEditCollections(InterfaceActionBase):
name = 'Edit Collections'
actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction'
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary]

View File

@ -6,7 +6,8 @@ import os, shutil, traceback, functools, sys, re
from contextlib import closing from contextlib import closing
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \ from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \
MetadataReaderPlugin, MetadataWriterPlugin MetadataReaderPlugin, MetadataWriterPlugin, \
InterfaceActionBase as InterfaceAction
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
from calibre.customize.profiles import InputProfile, OutputProfile from calibre.customize.profiles import InputProfile, OutputProfile
from calibre.customize.builtins import plugins as builtin_plugins from calibre.customize.builtins import plugins as builtin_plugins
@ -19,7 +20,6 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
plugin_dir, OptionParser, prefs plugin_dir, OptionParser, prefs
from calibre.ebooks.epub.fix import ePubFixer from calibre.ebooks.epub.fix import ePubFixer
platform = 'linux' platform = 'linux'
if iswindows: if iswindows:
platform = 'windows' platform = 'windows'
@ -246,6 +246,17 @@ def cover_sources():
# }}} # }}}
# Interface Actions # {{{
def interface_actions():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, InterfaceAction):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
# }}}
# Metadata read/write {{{ # Metadata read/write {{{
_metadata_readers = {} _metadata_readers = {}
_metadata_writers = {} _metadata_writers = {}

View File

@ -244,14 +244,20 @@ def info_dialog(parent, title, msg, det_msg='', show=False):
class Dispatcher(QObject): class Dispatcher(QObject):
'''Convenience class to ensure that a function call always happens in the '''
thread the receiver was created in.''' Convenience class to use Qt signals with arbitrary python callables.
By default, ensures that a function call always happens in the
thread this Dispatcher was created in.
'''
dispatch_signal = pyqtSignal(object, object) dispatch_signal = pyqtSignal(object, object)
def __init__(self, func): def __init__(self, func, queued=True, parent=None):
QObject.__init__(self) QObject.__init__(self, parent)
self.func = func self.func = func
self.dispatch_signal.connect(self.dispatch, type=Qt.QueuedConnection) typ = Qt.QueuedConnection
if not queued:
typ = Qt.AutoConnection if queued is None else Qt.DirectConnection
self.dispatch_signal.connect(self.dispatch, type=typ)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
self.dispatch_signal.emit(args, kwargs) self.dispatch_signal.emit(args, kwargs)

File diff suppressed because it is too large Load Diff

View 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)

View 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())

View 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 &bull; %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 &bull; %s</b><br />') % \
(user_notes[location]['displayed_location'],
user_notes[location]['type']))
else:
annotations.append(
_('<b>Location %d &bull; %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()

View 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)

View 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_()

View 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())

View 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)

View 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

View 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)

View 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()

View 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()

View 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'))

View 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)

View 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()

View 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)

View 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)

View 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()

View 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))

View 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)

View File

@ -443,7 +443,7 @@ class Saver(QObject):
from calibre.ebooks.metadata.worker import SaveWorker from calibre.ebooks.metadata.worker import SaveWorker
self.worker = SaveWorker(self.rq, db, self.ids, path, self.opts, self.worker = SaveWorker(self.rq, db, self.ids, path, self.opts,
spare_server=self.spare_server) spare_server=self.spare_server)
self.connect(self.pd, SIGNAL('canceled()'), self.canceled) self.pd.canceled_signal.connect(self.canceled)
self.timer = QTimer(self) self.timer = QTimer(self)
self.connect(self.timer, SIGNAL('timeout()'), self.update) self.connect(self.timer, SIGNAL('timeout()'), self.update)
self.timer.start(200) self.timer.start(200)

View File

@ -144,7 +144,7 @@ class CoverFlowMixin(object):
self.sync_cf_to_listview) self.sync_cf_to_listview)
self.db_images = DatabaseImages(self.library_view.model()) self.db_images = DatabaseImages(self.library_view.model())
self.cover_flow.setImages(self.db_images) self.cover_flow.setImages(self.db_images)
self.cover_flow.itemActivated.connect(self.view_specific_book) self.cover_flow.itemActivated.connect(self.iactions['View'].view_specific_book)
else: else:
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded') self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
+'<br>'+pictureflowerror) +'<br>'+pictureflowerror)

View File

@ -608,8 +608,6 @@ class DeviceMixin(object): # {{{
self.device_error_dialog = error_dialog(self, _('Error'), self.device_error_dialog = error_dialog(self, _('Error'),
_('Error communicating with device'), ' ') _('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal) self.device_error_dialog.setModal(Qt.NonModal)
self.share_conn_menu.connect_to_folder.connect(self.connect_to_folder)
self.share_conn_menu.connect_to_itunes.connect(self.connect_to_itunes)
self.emailer = Emailer() self.emailer = Emailer()
self.emailer.start() self.emailer.start()
self.device_manager = DeviceManager(Dispatcher(self.device_detected), self.device_manager = DeviceManager(Dispatcher(self.device_detected),
@ -647,20 +645,18 @@ class DeviceMixin(object): # {{{
def create_device_menu(self): def create_device_menu(self):
self._sync_menu = DeviceMenu(self) self._sync_menu = DeviceMenu(self)
self.share_conn_menu.build_email_entries(self._sync_menu) self.iactions['Send To Device'].qaction.setMenu(self._sync_menu)
self.action_sync.setMenu(self._sync_menu) self.iactions['Connect Share'].build_email_entries()
self.connect(self._sync_menu, self.connect(self._sync_menu,
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.dispatch_sync_event) self.dispatch_sync_event)
self._sync_menu.fetch_annotations.connect(self.fetch_annotations) self._sync_menu.fetch_annotations.connect(
self.iactions['Fetch Annotations'].fetch_annotations)
self._sync_menu.disconnect_mounted_device.connect(self.disconnect_mounted_device) self._sync_menu.disconnect_mounted_device.connect(self.disconnect_mounted_device)
self.iactions['Connect Share'].set_state(self.device_connected)
if self.device_connected: if self.device_connected:
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
self._sync_menu.disconnect_mounted_device_action.setEnabled(True) self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
else: else:
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
self._sync_menu.disconnect_mounted_device_action.setEnabled(False) self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
def device_job_exception(self, job): def device_job_exception(self, job):
@ -696,17 +692,14 @@ class DeviceMixin(object): # {{{
# Device connected {{{ # Device connected {{{
def set_device_menu_items_state(self, connected): def set_device_menu_items_state(self, connected):
self.iactions['Connect Share'].set_state(connected)
if connected: if connected:
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
self._sync_menu.disconnect_mounted_device_action.setEnabled(True) self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
self._sync_menu.enable_device_actions(True, self._sync_menu.enable_device_actions(True,
self.device_manager.device.card_prefix(), self.device_manager.device.card_prefix(),
self.device_manager.device) self.device_manager.device)
self.eject_action.setEnabled(True) self.eject_action.setEnabled(True)
else: else:
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
self._sync_menu.disconnect_mounted_device_action.setEnabled(False) self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
self._sync_menu.enable_device_actions(False) self._sync_menu.enable_device_actions(False)
self.eject_action.setEnabled(False) self.eject_action.setEnabled(False)
@ -791,8 +784,9 @@ class DeviceMixin(object): # {{{
self.device_job_exception(job) self.device_job_exception(job)
return return
if self.delete_memory.has_key(job): dm = self.iactions['Remove Books'].delete_memory
paths, model = self.delete_memory.pop(job) if dm.has_key(job):
paths, model = dm.pop(job)
self.device_manager.remove_books_from_metadata(paths, self.device_manager.remove_books_from_metadata(paths,
self.booklists()) self.booklists())
model.paths_deleted(paths) model.paths_deleted(paths)
@ -924,7 +918,7 @@ class DeviceMixin(object): # {{{
_('Auto convert the following books before sending via ' _('Auto convert the following books before sending via '
'email?'), det_msg=autos, 'email?'), det_msg=autos,
buttons=QMessageBox.Yes|QMessageBox.Cancel): buttons=QMessageBox.Yes|QMessageBox.Cancel):
self.auto_convert_mail(to, fmts, delete_from_library, auto, format) self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format)
if bad: if bad:
bad = '\n'.join('%s'%(i,) for i in bad) bad = '\n'.join('%s'%(i,) for i in bad)
@ -1026,7 +1020,7 @@ class DeviceMixin(object): # {{{
_('Auto convert the following books before uploading to ' _('Auto convert the following books before uploading to '
'the device?'), det_msg=autos, 'the device?'), det_msg=autos,
buttons=QMessageBox.Yes|QMessageBox.Cancel): buttons=QMessageBox.Yes|QMessageBox.Cancel):
self.auto_convert_catalogs(auto, format) self.iactions['Convert Books'].auto_convert_catalogs(auto, format)
files = [f for f in files if f is not None] files = [f for f in files if f is not None]
if not files: if not files:
dynamic.set('catalogs_to_be_synced', set([])) dynamic.set('catalogs_to_be_synced', set([]))
@ -1088,7 +1082,7 @@ class DeviceMixin(object): # {{{
_('Auto convert the following books before uploading to ' _('Auto convert the following books before uploading to '
'the device?'), det_msg=autos, 'the device?'), det_msg=autos,
buttons=QMessageBox.Yes|QMessageBox.Cancel): buttons=QMessageBox.Yes|QMessageBox.Cancel):
self.auto_convert_news(auto, format) self.iactions['Convert Books'].auto_convert_news(auto, format)
files = [f for f in files if f is not None] files = [f for f in files if f is not None]
for f in files: for f in files:
f.deleted_after_upload = del_on_upload f.deleted_after_upload = del_on_upload
@ -1207,7 +1201,7 @@ class DeviceMixin(object): # {{{
_('Auto convert the following books before uploading to ' _('Auto convert the following books before uploading to '
'the device?'), det_msg=autos, 'the device?'), det_msg=autos,
buttons=QMessageBox.Yes|QMessageBox.Cancel): buttons=QMessageBox.Yes|QMessageBox.Cancel):
self.auto_convert(auto, on_card, format) self.iactions['Convert Books'].auto_convert(auto, on_card, format)
if bad: if bad:
bad = '\n'.join('%s'%(i,) for i in bad) bad = '\n'.join('%s'%(i,) for i in bad)

View File

@ -12,7 +12,7 @@ import time
import traceback import traceback
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \ from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \
QPixmap, QListWidgetItem, QDialog QPixmap, QListWidgetItem, QDialog, pyqtSignal
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \ from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
choose_files, choose_images, ResizableDialog, \ choose_files, choose_images, ResizableDialog, \
@ -99,6 +99,7 @@ class Format(QListWidgetItem):
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
COVER_FETCH_TIMEOUT = 240 # seconds COVER_FETCH_TIMEOUT = 240 # seconds
view_format = pyqtSignal(object)
def do_reset_cover(self, *args): def do_reset_cover(self, *args):
pix = QPixmap(I('default_cover.svg')) pix = QPixmap(I('default_cover.svg'))
@ -474,7 +475,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def show_format(self, item, *args): def show_format(self, item, *args):
fmt = item.ext fmt = item.ext
self.emit(SIGNAL('view_format(PyQt_PyObject)'), fmt) self.view_format.emit(fmt)
def deduce_author_sort(self): def deduce_author_sort(self):
au = unicode(self.authors.text()) au = unicode(self.authors.text())

View File

@ -5,12 +5,14 @@ __docformat__ = 'restructuredtext en'
'''''' ''''''
from PyQt4.Qt import QDialog, SIGNAL, Qt from PyQt4.Qt import QDialog, pyqtSignal, Qt
from calibre.gui2.dialogs.progress_ui import Ui_Dialog from calibre.gui2.dialogs.progress_ui import Ui_Dialog
class ProgressDialog(QDialog, Ui_Dialog): class ProgressDialog(QDialog, Ui_Dialog):
canceled_signal = pyqtSignal()
def __init__(self, title, msg='', min=0, max=99, parent=None): def __init__(self, title, msg='', min=0, max=99, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setupUi(self) self.setupUi(self)
@ -23,7 +25,7 @@ class ProgressDialog(QDialog, Ui_Dialog):
self.bar.setValue(min) self.bar.setValue(min)
self.canceled = False self.canceled = False
self.connect(self.button_box, SIGNAL('rejected()'), self._canceled) self.button_box.rejected.connect(self._canceled)
def set_msg(self, msg=''): def set_msg(self, msg=''):
self.message.setText(msg) self.message.setText(msg)
@ -50,7 +52,7 @@ class ProgressDialog(QDialog, Ui_Dialog):
self.canceled = True self.canceled = True
self.button_box.setDisabled(True) self.button_box.setDisabled(True)
self.title.setText(_('Aborting...')) self.title.setText(_('Aborting...'))
self.emit(SIGNAL('canceled()')) self.canceled_signal.emit()
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Escape: if ev.key() == Qt.Key_Escape:

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
import functools, sys, os import functools, sys, os
from PyQt4.Qt import QMenu, Qt, QStackedWidget, \ from PyQt4.Qt import Qt, QStackedWidget, QMenu, \
QSize, QSizePolicy, QStatusBar, QLabel, QFont QSize, QSizePolicy, QStatusBar, QLabel, QFont
from calibre.utils.config import prefs from calibre.utils.config import prefs
@ -27,69 +27,35 @@ def partial(*args, **kwargs):
_keep_refs.append(ans) _keep_refs.append(ans)
return ans return ans
LIBRARY_CONTEXT_MENU = (
'Edit Metadata', 'Send To Device', 'Save To Disk', 'Connect Share', None,
'Convert Books', 'View', 'Open Folder', 'Show Book Details', None,
'Remove Books',
)
DEVICE_CONTEXT_MENU = ('View', 'Save To Disk', None, 'Remove Books', None,
'Add To Library', 'Edit Collections',
)
class LibraryViewMixin(object): # {{{ class LibraryViewMixin(object): # {{{
def __init__(self, db): def __init__(self, db):
similar_menu = QMenu(_('Similar books...')) lm = QMenu(self)
similar_menu.addAction(self.action_books_by_same_author) def populate_menu(m, items):
similar_menu.addAction(self.action_books_in_this_series) for what in items:
similar_menu.addAction(self.action_books_with_the_same_tags) if what is None:
similar_menu.addAction(self.action_books_by_this_publisher) m.addSeparator()
self.action_books_by_same_author.setShortcut(Qt.ALT + Qt.Key_A) elif what in self.iactions:
self.action_books_in_this_series.setShortcut(Qt.ALT + Qt.Key_S) m.addAction(self.iactions[what].qaction)
self.action_books_by_this_publisher.setShortcut(Qt.ALT + Qt.Key_P) populate_menu(lm, LIBRARY_CONTEXT_MENU)
self.action_books_with_the_same_tags.setShortcut(Qt.ALT+Qt.Key_T) dm = QMenu(self)
self.addAction(self.action_books_by_same_author) populate_menu(dm, DEVICE_CONTEXT_MENU)
self.addAction(self.action_books_by_this_publisher) ec = self.iactions['Edit Collections'].qaction
self.addAction(self.action_books_in_this_series) self.library_view.set_context_menu(lm, ec)
self.addAction(self.action_books_with_the_same_tags) for v in (self.memory_view, self.card_a_view, self.card_b_view):
self.similar_menu = similar_menu v.set_context_menu(dm, ec)
self.action_books_by_same_author.triggered.connect(
partial(self.show_similar_books, 'authors'))
self.action_books_in_this_series.triggered.connect(
partial(self.show_similar_books, 'series'))
self.action_books_with_the_same_tags.triggered.connect(
partial(self.show_similar_books, 'tag'))
self.action_books_by_this_publisher.triggered.connect(
partial(self.show_similar_books, 'publisher'))
self.library_view.set_context_menu(self.action_edit, self.action_sync, self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection)
self.action_convert, self.action_view,
self.action_save,
self.action_open_containing_folder,
self.action_show_book_details,
self.action_del,
self.action_conn_share,
add_to_library = None,
edit_device_collections=None,
similar_menu=similar_menu)
add_to_library = (_('Add books to library'), self.add_books_from_device)
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard=None))
self.memory_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard='carda'))
self.card_a_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard='cardb'))
self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
for func, args in [ for func, args in [
('connect_to_search_box', (self.search, ('connect_to_search_box', (self.search,
self.search_done)), self.search_done)),
@ -116,37 +82,10 @@ class LibraryViewMixin(object): # {{{
for view in ('library', 'memory', 'card_a', 'card_b'): for view in ('library', 'memory', 'card_a', 'card_b'):
view = getattr(self, view+'_view') view = getattr(self, view+'_view')
view.verticalHeader().sectionDoubleClicked.connect(self.view_specific_book) view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book)
def show_similar_books(self, type, *args):
search, join = [], ' '
idx = self.library_view.currentIndex()
if not idx.isValid():
return
row = idx.row()
if type == 'series':
series = idx.model().db.series(row)
if series:
search = ['series:"'+series+'"']
elif type == 'publisher':
publisher = idx.model().db.publisher(row)
if publisher:
search = ['publisher:"'+publisher+'"']
elif type == 'tag':
tags = idx.model().db.tags(row)
if tags:
search = ['tag:"='+t+'"' for t in tags.split(',')]
elif type in ('author', 'authors'):
authors = idx.model().db.authors(row)
if authors:
search = ['author:"='+a.strip().replace('|', ',')+'"' \
for a in authors.split(',')]
join = ' or '
if search:
self.search.set_search_string(join.join(search))
def search_done(self, view, ok): def search_done(self, view, ok):
if view is self.current_view(): if view is self.current_view():
self.search.search_done(ok) self.search.search_done(ok)
@ -308,10 +247,10 @@ class LayoutMixin(object): # {{{
def finalize_layout(self): def finalize_layout(self):
self.status_bar.initialize(self.system_tray_icon) self.status_bar.initialize(self.system_tray_icon)
self.book_details.show_book_info.connect(self.show_book_info) self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info)
self.book_details.files_dropped.connect(self.files_dropped_on_book) self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book)
self.book_details.open_containing_folder.connect(self.view_folder_for_id) self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id)
self.book_details.view_specific_format.connect(self.view_format_by_id) self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)
m = self.library_view.model() m = self.library_view.model()
if m.rowCount(None) > 0: if m.rowCount(None) > 0:

View File

@ -5,42 +5,32 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from operator import attrgetter
from functools import partial from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \ from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
pyqtSignal, QToolButton, \ pyqtSignal, QToolButton, \
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \ QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \
QMenu, QUrl QMenu
from calibre.constants import __appname__, isosx from calibre.constants import __appname__
from calibre.gui2.search_box import SearchBox2, SavedSearchBox from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2 import config, open_url, gprefs from calibre.gui2 import config, gprefs
from calibre.gui2.widgets import ComboBoxWithHelp from calibre.gui2.widgets import ComboBoxWithHelp
from calibre import human_readable from calibre import human_readable
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2.dialogs.scheduler import Scheduler
from calibre.utils.smtp import config as email_config
TOOLBAR_NO_DEVICE = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
)
class SaveMenu(QMenu): # {{{ TOOLBAR_DEVICE = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View',
save_fmt = pyqtSignal(object) 'Send To Device', None, None, 'Location Manager', None, None,
'Fetch News', 'Save To Disk', 'Connect Share', None,
def __init__(self, parent): 'Remove Books', None, 'Help', 'Preferences',
QMenu.__init__(self, _('Save single format to disk...'), parent) )
for ext in sorted(BOOK_EXTENSIONS):
action = self.addAction(ext.upper())
setattr(self, 'do_'+ext, partial(self.do, ext))
action.triggered.connect(
getattr(self, 'do_'+ext))
def do(self, ext, *args):
self.save_fmt.emit(ext)
# }}}
class LocationManager(QObject): # {{{ class LocationManager(QObject): # {{{
@ -221,8 +211,9 @@ class SearchBar(QWidget): # {{{
class ToolBar(QToolBar): # {{{ class ToolBar(QToolBar): # {{{
def __init__(self, actions, donate, location_manager, parent=None): def __init__(self, donate, location_manager, parent):
QToolBar.__init__(self, parent) QToolBar.__init__(self, parent)
self.gui = parent
self.setContextMenuPolicy(Qt.PreventContextMenu) self.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False) self.setMovable(False)
self.setFloatable(False) self.setFloatable(False)
@ -232,7 +223,6 @@ class ToolBar(QToolBar): # {{{
self.donate = donate self.donate = donate
self.apply_settings() self.apply_settings()
self.all_actions = actions
self.location_manager = location_manager self.location_manager = location_manager
self.location_manager.locations_changed.connect(self.build_bar) self.location_manager.locations_changed.connect(self.build_bar)
self.d_widget = QWidget() self.d_widget = QWidget()
@ -258,51 +248,30 @@ class ToolBar(QToolBar): # {{{
def build_bar(self): def build_bar(self):
showing_device = self.location_manager.has_device showing_device = self.location_manager.has_device
order_field = 'device' if showing_device else 'normal' actions = TOOLBAR_DEVICE if showing_device else TOOLBAR_NO_DEVICE
o = attrgetter(order_field+'_order')
sepvals = [2] if showing_device else [1]
sepvals += [3]
actions = [x for x in self.all_actions if o(x) > -1]
actions.sort(cmp=lambda x,y : cmp(o(x), o(y)))
self.clear() self.clear()
for what in actions:
if what is None:
self.addSeparator()
elif what == 'Location Manager':
for ac in self.location_manager.available_actions:
self.addAction(ac)
self.setup_tool_button(ac, QToolButton.MenuButtonPopup)
elif what == 'Donate' and config['show_donate_button']:
self.addWidget(self.d_widget)
elif what in self.gui.iactions:
action = self.gui.iactions[what]
self.addAction(action.qaction)
self.setup_tool_button(action.qaction, action.popup_type)
def setup_tool_button(ac): def setup_tool_button(self, ac, menu_mode=None):
ch = self.widgetForAction(ac) ch = self.widgetForAction(ac)
ch.setCursor(Qt.PointingHandCursor) ch.setCursor(Qt.PointingHandCursor)
ch.setAutoRaise(True) ch.setAutoRaise(True)
if ac.menu() is not None: if ac.menu() is not None and menu_mode is not None:
name = getattr(ac, 'action_name', None) ch.setPopupMode(menu_mode)
ch.setPopupMode(ch.InstantPopup if name == 'conn_share'
else ch.MenuButtonPopup)
for x in actions:
self.addAction(x)
setup_tool_button(x)
if x.action_name == 'choose_library':
self.choose_action = x
if showing_device:
self.addSeparator()
for ac in self.location_manager.available_actions:
self.addAction(ac)
setup_tool_button(ac)
self.addSeparator()
self.location_manager.location_library.trigger()
elif config['show_donate_button']:
self.addWidget(self.d_widget)
for x in actions:
if x.separator_before in sepvals:
self.insertSeparator(x)
self.choose_action.setVisible(not showing_device)
def count_changed(self, new_count):
text = _('%d books')%new_count
a = self.choose_action
a.setText(text)
a.setToolTip(_('Choose calibre library to work with') + '\n\n' + text)
def resizeEvent(self, ev): def resizeEvent(self, ev):
QToolBar.resizeEvent(self, ev) QToolBar.resizeEvent(self, ev)
@ -321,84 +290,9 @@ class ToolBar(QToolBar): # {{{
# }}} # }}}
class Action(QAction):
pass
class ShareConnMenu(QMenu): # {{{
connect_to_folder = pyqtSignal()
connect_to_itunes = pyqtSignal()
config_email = pyqtSignal()
toggle_server = pyqtSignal()
def __init__(self, parent=None):
QMenu.__init__(self, parent)
mitem = self.addAction(QIcon(I('devices/folder.svg')), _('Connect to folder'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
self.connect_to_folder_action = mitem
mitem = self.addAction(QIcon(I('devices/itunes.png')),
_('Connect to iTunes'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
self.connect_to_itunes_action = mitem
self.addSeparator()
self.toggle_server_action = \
self.addAction(QIcon(I('network-server.svg')),
_('Start Content Server'))
self.toggle_server_action.triggered.connect(lambda x:
self.toggle_server.emit())
self.addSeparator()
self.email_actions = []
def server_state_changed(self, running):
text = _('Start Content Server')
if running:
text = _('Stop Content Server')
self.toggle_server_action.setText(text)
def build_email_entries(self, sync_menu):
from calibre.gui2.device import DeviceAction
for ac in self.email_actions:
self.removeAction(ac)
self.email_actions = []
self.memory = []
opts = email_config().parse()
if opts.accounts:
self.email_to_menu = QMenu(_('Email to')+'...', self)
keys = sorted(opts.accounts.keys())
for account in keys:
formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats
action1 = DeviceAction(dest, False, False, I('mail.svg'),
_('Email to')+' '+account)
action2 = DeviceAction(dest, True, False, I('mail.svg'),
_('Email to')+' '+account+ _(' and delete from library'))
map(self.email_to_menu.addAction, (action1, action2))
map(self.memory.append, (action1, action2))
if default:
map(self.addAction, (action1, action2))
map(self.email_actions.append, (action1, action2))
self.email_to_menu.addSeparator()
action1.a_s.connect(sync_menu.action_triggered)
action2.a_s.connect(sync_menu.action_triggered)
ac = self.addMenu(self.email_to_menu)
self.email_actions.append(ac)
else:
ac = self.addAction(_('Setup email based sharing of books'))
self.email_actions.append(ac)
ac.triggered.connect(self.setup_email)
def setup_email(self, *args):
self.config_email.emit()
# }}}
class MainWindowMixin(object): class MainWindowMixin(object):
def __init__(self, db): def __init__(self, db):
self.device_connected = None
self.setObjectName('MainWindow') self.setObjectName('MainWindow')
self.setWindowIcon(QIcon(I('library.png'))) self.setWindowIcon(QIcon(I('library.png')))
self.setWindowTitle(__appname__) self.setWindowTitle(__appname__)
@ -412,230 +306,18 @@ class MainWindowMixin(object):
self.donate_button = ThrobbingButton(self.centralwidget) self.donate_button = ThrobbingButton(self.centralwidget)
self.location_manager = LocationManager(self) self.location_manager = LocationManager(self)
self.init_scheduler(db) self.iactions['Fetch News'].init_scheduler(db)
all_actions = self.setup_actions()
self.search_bar = SearchBar(self) self.search_bar = SearchBar(self)
self.tool_bar = ToolBar(all_actions, self.donate_button, self.tool_bar = ToolBar(self.donate_button,
self.location_manager, self) self.location_manager, self)
self.addToolBar(Qt.TopToolBarArea, self.tool_bar) self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
self.tool_bar.choose_action.triggered.connect(self.choose_library)
l = self.centralwidget.layout() l = self.centralwidget.layout()
l.addWidget(self.search_bar) l.addWidget(self.search_bar)
def init_scheduler(self, db):
self.scheduler = Scheduler(self, db)
self.scheduler.start_recipe_fetch.connect(
self.download_scheduled_recipe, type=Qt.QueuedConnection)
def read_toolbar_settings(self):
pass
def choose_library(self, *args):
from calibre.gui2.dialogs.choose_library import ChooseLibrary
db = self.library_view.model().db
c = ChooseLibrary(db, self.library_moved, self)
c.exec_()
def setup_actions(self): # {{{
all_actions = []
def ac(normal_order, device_order, separator_before,
name, text, icon, shortcut=None, tooltip=None):
action = Action(QIcon(I(icon)), text, self)
action.normal_order = normal_order
action.device_order = device_order
action.separator_before = separator_before
action.action_name = name
text = tooltip if tooltip else text
action.setToolTip(text)
action.setStatusTip(text)
action.setWhatsThis(text)
action.setAutoRepeat(False)
action.setObjectName('action_'+name)
if shortcut:
action.setShortcut(shortcut)
setattr(self, 'action_'+name, action)
all_actions.append(action)
ac(0, 0, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
ac(1, 1, 0, 'edit', _('Edit metadata'), 'edit_input.svg', _('E'))
ac(2, 2, 3, 'convert', _('Convert books'), 'convert.svg', _('C'))
ac(3, 3, 0, 'view', _('View'), 'view.svg', _('V'))
ac(-1, 4, 0, 'sync', _('Send to device'), 'sync.svg')
ac(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png',
tooltip=_('Choose calibre library to work with'))
ac(6, 6, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
ac(7, 7, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
ac(8, 8, 0, 'conn_share', _('Connect/share'), 'connect_share.svg')
ac(9, 9, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
ac(10, 10, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
ac(11, 11, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
ac(-1, -1, 0, 'merge', _('Merge book records'), 'merge_books.svg', _('M'))
ac(-1, -1, 0, 'open_containing_folder', _('Open containing folder'),
'document_open.svg')
ac(-1, -1, 0, 'show_book_details', _('Show book details'),
'dialog_information.svg')
ac(-1, -1, 0, 'books_by_same_author', _('Books by same author'),
'user_profile.svg')
ac(-1, -1, 0, 'books_in_this_series', _('Books in this series'),
'books_in_series.svg')
ac(-1, -1, 0, 'books_by_this_publisher', _('Books by this publisher'),
'publisher.png')
ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
'tags.svg')
self.action_news.setMenu(self.scheduler.news_menu)
self.action_news.triggered.connect(
self.scheduler.show_dialog)
self.share_conn_menu = ShareConnMenu(self)
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
self.share_conn_menu.config_email.connect(partial(self.do_config,
initial_category='email'))
self.action_conn_share.setMenu(self.share_conn_menu)
self.action_help.triggered.connect(self.show_help)
md = QMenu()
md.addAction(_('Edit metadata individually'),
partial(self.edit_metadata, False, bulk=False))
md.addSeparator()
md.addAction(_('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True))
md.addSeparator()
md.addAction(_('Download metadata and covers'),
partial(self.download_metadata, False, covers=True),
Qt.ControlModifier+Qt.Key_D)
md.addAction(_('Download only metadata'),
partial(self.download_metadata, False, covers=False))
md.addAction(_('Download only covers'),
partial(self.download_metadata, False, covers=True,
set_metadata=False, set_social_metadata=False))
md.addAction(_('Download only social metadata'),
partial(self.download_metadata, False, covers=False,
set_metadata=False, set_social_metadata=True))
self.metadata_menu = md
mb = QMenu()
mb.addAction(_('Merge into first selected book - delete others'),
self.merge_books)
mb.addSeparator()
mb.addAction(_('Merge into first selected book - keep others'),
partial(self.merge_books, safe_merge=True))
self.merge_menu = mb
self.action_merge.setMenu(mb)
md.addSeparator()
md.addAction(self.action_merge)
self.add_menu = QMenu()
self.add_menu.addAction(_('Add books from a single directory'),
self.add_books)
self.add_menu.addAction(_('Add books from directories, including '
'sub-directories (One book per directory, assumes every ebook '
'file is the same book in a different format)'),
self.add_recursive_single)
self.add_menu.addAction(_('Add books from directories, including '
'sub directories (Multiple books per directory, assumes every '
'ebook file is a different book)'), self.add_recursive_multiple)
self.add_menu.addSeparator()
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
'formats)'), self.add_empty)
self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn)
self.action_add.setMenu(self.add_menu)
self.action_add.triggered.connect(self.add_books)
self.action_del.triggered.connect(self.delete_books)
self.action_edit.triggered.connect(self.edit_metadata)
self.action_merge.triggered.connect(self.merge_books)
self.action_save.triggered.connect(self.save_to_disk)
self.save_menu = QMenu()
self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk,
False))
self.save_menu.addAction(_('Save to disk in a single directory'),
partial(self.save_to_single_dir, False))
self.save_menu.addAction(_('Save only %s format to disk')%
prefs['output_format'].upper(),
partial(self.save_single_format_to_disk, False))
self.save_menu.addAction(
_('Save only %s format to disk in a single directory')%
prefs['output_format'].upper(),
partial(self.save_single_fmt_to_single_dir, False))
self.save_sub_menu = SaveMenu(self)
self.save_menu.addMenu(self.save_sub_menu)
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
self.action_view.triggered.connect(self.view_book)
self.view_menu = QMenu()
self.view_menu.addAction(_('View'), partial(self.view_book, False))
ac = self.view_menu.addAction(_('View specific format'))
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
self.action_view.setMenu(self.view_menu)
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
self.delete_menu = QMenu()
self.delete_menu.addAction(_('Remove selected books'), self.delete_books)
self.delete_menu.addAction(
_('Remove files of a specific format from selected books..'),
self.delete_selected_formats)
self.delete_menu.addAction(
_('Remove all formats from selected books, except...'),
self.delete_all_but_selected_formats)
self.delete_menu.addAction(
_('Remove covers from selected books'), self.delete_covers)
self.delete_menu.addSeparator()
self.delete_menu.addAction(
_('Remove matching books from device'),
self.remove_matching_books_from_device)
self.action_del.setMenu(self.delete_menu)
self.action_open_containing_folder.setShortcut(Qt.Key_O)
self.addAction(self.action_open_containing_folder)
self.action_open_containing_folder.triggered.connect(self.view_folder)
self.action_sync.setShortcut(Qt.Key_D)
self.action_sync.setEnabled(True)
self.create_device_menu()
self.action_sync.triggered.connect(
self._sync_action_triggered)
self.action_edit.setMenu(md)
self.action_save.setMenu(self.save_menu)
cm = QMenu()
cm.addAction(_('Convert individually'), partial(self.convert_ebook,
False, bulk=False))
cm.addAction(_('Bulk convert'),
partial(self.convert_ebook, False, bulk=True))
cm.addSeparator()
ac = cm.addAction(
_('Create catalog of books in your calibre library'))
ac.triggered.connect(self.generate_catalog)
self.action_convert.setMenu(cm)
self.action_convert.triggered.connect(self.convert_ebook)
self.convert_menu = cm
pm = QMenu()
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
self.run_wizard)
self.action_preferences.setMenu(pm)
self.preferences_menu = pm
for x in (self.preferences_action, self.action_preferences):
x.triggered.connect(self.do_config)
return all_actions
# }}}
def show_help(self, *args):
open_url(QUrl('http://calibre-ebook.com/user_manual'))
def content_server_state_changed(self, running):
self.share_conn_menu.server_state_changed(running)
def toggle_content_server(self):
if self.content_server is None:
self.start_content_server()
else:
self.content_server.exit()
self.content_server = None

View File

@ -389,37 +389,10 @@ class BooksView(QTableView): # {{{
#}}} #}}}
# Context Menu {{{ # Context Menu {{{
def set_context_menu(self, edit_metadata, send_to_device, convert, view, def set_context_menu(self, menu, edit_collections_action):
save, open_folder, book_details, delete, conn_share,
similar_menu=None, add_to_library=None,
edit_device_collections=None):
self.setContextMenuPolicy(Qt.DefaultContextMenu) self.setContextMenuPolicy(Qt.DefaultContextMenu)
self.context_menu = QMenu(self) self.context_menu = menu
if edit_metadata is not None: self.edit_collections_action = edit_collections_action
self.context_menu.addAction(edit_metadata)
if send_to_device is not None:
self.context_menu.addAction(send_to_device)
if convert is not None:
self.context_menu.addAction(convert)
if conn_share is not None:
self.context_menu.addAction(conn_share)
self.context_menu.addAction(view)
self.context_menu.addAction(save)
if open_folder is not None:
self.context_menu.addAction(open_folder)
if delete is not None:
self.context_menu.addAction(delete)
if book_details is not None:
self.context_menu.addAction(book_details)
if similar_menu is not None:
self.context_menu.addMenu(similar_menu)
if add_to_library is not None:
func = partial(add_to_library[1], view=self)
self.context_menu.addAction(add_to_library[0], func)
if edit_device_collections is not None:
func = partial(edit_device_collections[1], view=self)
self.edit_collections_menu = \
self.context_menu.addAction(edit_device_collections[0], func)
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
self.context_menu.popup(event.globalPos()) self.context_menu.popup(event.globalPos())
@ -528,10 +501,11 @@ class DeviceBooksView(BooksView): # {{{
self.setAcceptDrops(False) self.setAcceptDrops(False)
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
self.edit_collections_menu.setVisible( edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
callable(getattr(self._model.db, 'supports_collections', None)) and \
self._model.db.supports_collections() and \ self._model.db.supports_collections() and \
prefs['manage_device_metadata'] == 'manual') prefs['manage_device_metadata'] == 'manual'
self.edit_collections_action.setVisible(edit_collections)
self.context_menu.popup(event.globalPos()) self.context_menu.popup(event.globalPos())
event.accept() event.accept()

View File

@ -155,7 +155,7 @@ class GuiRunner(QObject):
main.initialize(self.library_path, self.db, self.listener, self.actions) main.initialize(self.library_path, self.db, self.listener, self.actions)
if DEBUG: if DEBUG:
prints('Started up in', time.time() - self.startup_time) prints('Started up in', time.time() - self.startup_time)
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False) add_filesystem_book = partial(main.iactions['Add Books'].add_filesystem_book, allow_device=False)
sys.excepthook = main.unhandled_exception sys.excepthook = main.unhandled_exception
if len(self.args) > 1: if len(self.args) > 1:
p = os.path.abspath(self.args[1]) p = os.path.abspath(self.args[1])

View File

@ -15,7 +15,7 @@ from threading import Thread
from PyQt4.Qt import Qt, SIGNAL, QTimer, \ from PyQt4.Qt import Qt, SIGNAL, QTimer, \
QPixmap, QMenu, QIcon, pyqtSignal, \ QPixmap, QMenu, QIcon, pyqtSignal, \
QDialog, \ QDialog, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \ QSystemTrayIcon, QApplication, QKeySequence, \
QMessageBox, QHelpEvent QMessageBox, QHelpEvent
from calibre import prints from calibre import prints
@ -23,6 +23,8 @@ from calibre.constants import __appname__, isosx
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
from calibre.utils.ipc.server import Server from calibre.utils.ipc.server import Server
from calibre.library.database2 import LibraryDatabase2
from calibre.customize.ui import interface_actions
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \ from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
gprefs, max_available_height, config, info_dialog, Dispatcher gprefs, max_available_height, config, info_dialog, Dispatcher
from calibre.gui2.cover_flow import CoverFlowMixin from calibre.gui2.cover_flow import CoverFlowMixin
@ -32,17 +34,10 @@ from calibre.gui2.main_window import MainWindow
from calibre.gui2.layout import MainWindowMixin from calibre.gui2.layout import MainWindowMixin
from calibre.gui2.device import DeviceMixin from calibre.gui2.device import DeviceMixin
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.book_info import BookInfo
from calibre.library.database2 import LibraryDatabase2
from calibre.gui2.init import LibraryViewMixin, LayoutMixin from calibre.gui2.init import LibraryViewMixin, LayoutMixin
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_view import TagBrowserMixin from calibre.gui2.tag_view import TagBrowserMixin
from calibre.gui2.actions import AnnotationsAction, AddAction, DeleteAction, \
EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, \
ConvertAction, ViewAction
class Listener(Thread): # {{{ class Listener(Thread): # {{{
@ -91,16 +86,27 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin
AnnotationsAction, AddAction, DeleteAction, ):
EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction,
ConvertAction, ViewAction):
'The main GUI' 'The main GUI'
def __init__(self, opts, parent=None): def __init__(self, opts, parent=None):
MainWindow.__init__(self, opts, parent) MainWindow.__init__(self, opts, parent)
self.opts = opts self.opts = opts
self.device_connected = None
acmap = {}
for action in interface_actions():
mod, cls = action.actual_plugin.split(':')
ac = getattr(__import__(mod, fromlist=['1'], level=0), cls)(self,
action.site_customization)
if ac.name in acmap:
if ac.priority >= acmap[ac.name].priority:
acmap[ac.name] = ac
else:
acmap[ac.name] = ac
self.iactions = acmap
def initialize(self, library_path, db, listener, actions): def initialize(self, library_path, db, listener, actions):
opts = self.opts opts = self.opts
@ -119,6 +125,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.another_instance_wants_to_talk) self.another_instance_wants_to_talk)
self.check_messages_timer.start(1000) self.check_messages_timer.start(1000)
for ac in self.iactions.values():
ac.do_genesis()
MainWindowMixin.__init__(self, db) MainWindowMixin.__init__(self, db)
# Jobs Button {{{ # Jobs Button {{{
@ -140,9 +148,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.verbose = opts.verbose self.verbose = opts.verbose
self.get_metadata = GetMetadata() self.get_metadata = GetMetadata()
self.upload_memory = {} self.upload_memory = {}
self.delete_memory = {}
self.conversion_jobs = {}
self.persistent_files = []
self.metadata_dialogs = [] self.metadata_dialogs = []
self.default_thumbnail = None self.default_thumbnail = None
self.tb_wrapper = textwrap.TextWrapper(width=40) self.tb_wrapper = textwrap.TextWrapper(width=40)
@ -166,22 +171,13 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
QIcon(I('eject.svg')), _('&Eject connected device')) QIcon(I('eject.svg')), _('&Eject connected device'))
self.eject_action.setEnabled(False) self.eject_action.setEnabled(False)
self.addAction(self.quit_action) self.addAction(self.quit_action)
self.action_restart = QAction(_('&Restart'), self)
self.addAction(self.action_restart)
self.system_tray_menu.addAction(self.quit_action) self.system_tray_menu.addAction(self.quit_action)
self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q)) self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q))
self.action_restart.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
self.action_show_book_details.setShortcut(QKeySequence(Qt.Key_I))
self.addAction(self.action_show_book_details)
self.system_tray_icon.setContextMenu(self.system_tray_menu) self.system_tray_icon.setContextMenu(self.system_tray_menu)
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit) self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate) self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
self.connect(self.restore_action, SIGNAL('triggered()'), self.connect(self.restore_action, SIGNAL('triggered()'),
self.show_windows) self.show_windows)
self.connect(self.action_show_book_details,
SIGNAL('triggered(bool)'), self.show_book_info)
self.connect(self.action_restart, SIGNAL('triggered()'),
self.restart)
self.connect(self.system_tray_icon, self.connect(self.system_tray_icon,
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
self.system_tray_icon_activated) self.system_tray_icon_activated)
@ -209,9 +205,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
if self.system_tray_icon.isVisible() and opts.start_in_tray: if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows() self.hide_windows()
for t in (self.tool_bar, ): self.library_view.model().count_changed_signal.connect(
self.library_view.model().count_changed_signal.connect \ self.iactions['Choose Library'].count_changed)
(t.count_changed)
if not gprefs.get('quick_start_guide_added', False): if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember']) mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -249,22 +244,21 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.start_content_server() self.start_content_server()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
AddAction.__init__(self)
self.read_settings() self.read_settings()
self.finalize_layout() self.finalize_layout()
self.donate_button.start_animation() self.donate_button.start_animation()
self.scheduler.delete_old_news.connect( self.iactions['Fetch News'].connect_scheduler()
self.library_view.model().delete_books_by_id,
type=Qt.QueuedConnection)
def start_content_server(self): def start_content_server(self):
from calibre.library.server.main import start_threaded_server from calibre.library.server.main import start_threaded_server
from calibre.library.server import server_config from calibre.library.server import server_config
self.content_server = start_threaded_server( self.content_server = start_threaded_server(
self.library_view.model().db, server_config().parse()) self.library_view.model().db, server_config().parse())
self.content_server.state_callback = Dispatcher(self.content_server_state_changed) self.content_server.state_callback = Dispatcher(
self.iactions['Connect Share'].content_server_state_changed)
self.content_server.state_callback(True) self.content_server.state_callback(True)
self.test_server_timer = QTimer.singleShot(10000, self.test_server) self.test_server_timer = QTimer.singleShot(10000, self.test_server)
@ -329,7 +323,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
if len(argv) > 1: if len(argv) > 1:
path = os.path.abspath(argv[1]) path = os.path.abspath(argv[1])
if os.access(path, os.R_OK): if os.access(path, os.R_OK):
self.add_filesystem_book(path) self.iactions['Add Books'].add_filesystem_book(path)
self.setWindowState(self.windowState() & \ self.setWindowState(self.windowState() & \
~Qt.WindowMinimized|Qt.WindowActive) ~Qt.WindowMinimized|Qt.WindowActive)
self.show_windows() self.show_windows()
@ -357,41 +351,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
def do_config(self, checked=False, initial_category='general'):
if self.job_manager.has_jobs():
d = error_dialog(self, _('Cannot configure'),
_('Cannot configure while there are running jobs.'))
d.exec_()
return
if self.must_restart_before_config:
d = error_dialog(self, _('Cannot configure'),
_('Cannot configure before calibre is restarted.'))
d.exec_()
return
d = ConfigDialog(self, self.library_view,
server=self.content_server, initial_category=initial_category)
d.exec_()
self.content_server = d.server
if self.content_server is not None:
self.content_server.state_callback = \
Dispatcher(self.content_server_state_changed)
self.content_server.state_callback(self.content_server.is_running)
if d.result() == d.Accepted:
self.read_toolbar_settings()
self.search.search_as_you_type(config['search_as_you_type'])
self.save_menu.actions()[2].setText(
_('Save only %s format to disk')%
prefs['output_format'].upper())
self.save_menu.actions()[3].setText(
_('Save only %s format to disk in a single directory')%
prefs['output_format'].upper())
self.tags_view.set_new_model() # in case columns changed
self.tags_view.recount()
self.create_device_menu()
self.set_device_menu_items_state(bool(self.device_connected))
self.tool_bar.apply_settings()
def library_moved(self, newloc): def library_moved(self, newloc):
if newloc is None: return if newloc is None: return
@ -407,18 +366,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.saved_search.clear_to_help() self.saved_search.clear_to_help()
self.book_details.reset_info() self.book_details.reset_info()
self.library_view.model().count_changed() self.library_view.model().count_changed()
self.scheduler.database_changed(db) self.iactions['Fetch News'].database_changed(db)
prefs['library_path'] = self.library_path prefs['library_path'] = self.library_path
def show_book_info(self, *args):
if self.current_view() is not self.library_view:
error_dialog(self, _('No detailed info available'),
_('No detailed information is available for books '
'on the device.')).exec_()
return
index = self.library_view.currentIndex()
if index.isValid():
BookInfo(self, self.library_view, index).show()
def location_selected(self, location): def location_selected(self, location):
''' '''
@ -430,26 +380,12 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
for x in ('tb', 'cb'): for x in ('tb', 'cb'):
splitter = getattr(self, x+'_splitter') splitter = getattr(self, x+'_splitter')
splitter.button.setEnabled(location == 'library') splitter.button.setEnabled(location == 'library')
for action in self.iactions.values():
action.location_selected(location)
if location == 'library': if location == 'library':
self.action_edit.setEnabled(True)
self.action_merge.setEnabled(True)
self.action_convert.setEnabled(True)
self.view_menu.actions()[1].setEnabled(True)
self.action_open_containing_folder.setEnabled(True)
self.action_sync.setEnabled(True)
self.search_restriction.setEnabled(True) self.search_restriction.setEnabled(True)
for action in list(self.delete_menu.actions())[1:]:
action.setEnabled(True)
else: else:
self.action_edit.setEnabled(False)
self.action_merge.setEnabled(False)
self.action_convert.setEnabled(False)
self.view_menu.actions()[1].setEnabled(False)
self.action_open_containing_folder.setEnabled(False)
self.action_sync.setEnabled(False)
self.search_restriction.setEnabled(False) self.search_restriction.setEnabled(False)
for action in list(self.delete_menu.actions())[1:]:
action.setEnabled(False)
# Reset the view in case something changed while it was invisible # Reset the view in case something changed while it was invisible
self.current_view().reset() self.current_view().reset()
self.set_number_of_books_shown() self.set_number_of_books_shown()
@ -504,7 +440,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
geometry = config['main_window_geometry'] geometry = config['main_window_geometry']
if geometry is not None: if geometry is not None:
self.restoreGeometry(geometry) self.restoreGeometry(geometry)
self.read_toolbar_settings()
self.read_layout_settings() self.read_layout_settings()
def write_settings(self): def write_settings(self):
@ -512,9 +447,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
dynamic.set('sort_history', self.library_view.model().sort_history) dynamic.set('sort_history', self.library_view.model().sort_history)
self.save_layout_state() self.save_layout_state()
def restart(self):
self.quit(restart=True)
def quit(self, checked=True, restart=False): def quit(self, checked=True, restart=False):
if not self.confirm_quit(): if not self.confirm_quit():
return return

View File

@ -157,4 +157,11 @@ The base class for such devices is :class:`calibre.devices.usbms.driver.USBMS`.
:members: :members:
:member-order: bysource :member-order: bysource
User Interface Actions
--------------------------
.. autoclass:: calibre.gui2.actions.InterfaceAction
:show-inheritance:
:members:
:member-order: bysource