diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 1348da5e5a..88c9324239 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -351,3 +351,12 @@ class CatalogPlugin(Plugin): # Default implementation does nothing raise NotImplementedError('CatalogPlugin.generate_catalog() default ' 'method, should be overridden in subclass') + +class InterfaceActionBase(Plugin): + + supported_platforms = ['windows', 'osx', 'linux'] + author = 'Kovid Goyal' + type = _('User Interface Action') + can_be_disabled = False + + actual_plugin = None diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 998bfa7b1e..cd2896f232 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -574,3 +574,92 @@ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ x.__name__.endswith('MetadataWriter')] plugins += input_profiles + output_profiles + +from calibre.customize import InterfaceActionBase + +class ActionAdd(InterfaceActionBase): + name = 'Add Books' + actual_plugin = 'calibre.gui2.actions.add:AddAction' + +class ActionFetchAnnotations(InterfaceActionBase): + name = 'Fetch Annotations' + actual_plugin = 'calibre.gui2.actions.annotate:FetchAnnotationsAction' + +class ActionGenerateCatalog(InterfaceActionBase): + name = 'Generate Catalog' + actual_plugin = 'calibre.gui2.actions.catalog:GenerateCatalogAction' + +class ActionConvert(InterfaceActionBase): + name = 'Convert Books' + actual_plugin = 'calibre.gui2.actions.convert:ConvertAction' + +class ActionDelete(InterfaceActionBase): + name = 'Remove Books' + actual_plugin = 'calibre.gui2.actions.delete:DeleteAction' + +class ActionEditMetadata(InterfaceActionBase): + name = 'Edit Metadata' + actual_plugin = 'calibre.gui2.actions.edit_metadata:EditMetadataAction' + +class ActionView(InterfaceActionBase): + name = 'View' + actual_plugin = 'calibre.gui2.actions.view:ViewAction' + +class ActionFetchNews(InterfaceActionBase): + name = 'Fetch News' + actual_plugin = 'calibre.gui2.actions.fetch_news:FetchNewsAction' + +class ActionSaveToDisk(InterfaceActionBase): + name = 'Save To Disk' + actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction' + +class ActionShowBookDetails(InterfaceActionBase): + name = 'Show Book Details' + actual_plugin = 'calibre.gui2.actions.show_book_details:ShowBookDetailsAction' + +class ActionRestart(InterfaceActionBase): + name = 'Restart' + actual_plugin = 'calibre.gui2.actions.restart:RestartAction' + +class ActionOpenFolder(InterfaceActionBase): + name = 'Open Folder' + actual_plugin = 'calibre.gui2.actions.open:OpenFolderAction' + +class ActionSendToDevice(InterfaceActionBase): + name = 'Send To Device' + actual_plugin = 'calibre.gui2.actions.device:SendToDeviceAction' + +class ActionConnectShare(InterfaceActionBase): + name = 'Connect Share' + actual_plugin = 'calibre.gui2.actions.device:ConnectShareAction' + +class ActionHelp(InterfaceActionBase): + name = 'Help' + actual_plugin = 'calibre.gui2.actions.help:HelpAction' + +class ActionPreferences(InterfaceActionBase): + name = 'Preferences' + actual_plugin = 'calibre.gui2.actions.preferences:PreferencesAction' + +class ActionSimilarBooks(InterfaceActionBase): + name = 'Similar Books' + actual_plugin = 'calibre.gui2.actions.similar_books:SimilarBooksAction' + +class ActionChooseLibrary(InterfaceActionBase): + name = 'Choose Library' + actual_plugin = 'calibre.gui2.actions.choose_library:ChooseLibraryAction' + +class ActionAddToLibrary(InterfaceActionBase): + name = 'Add To Library' + actual_plugin = 'calibre.gui2.actions.add_to_library:AddToLibraryAction' + +class ActionEditCollections(InterfaceActionBase): + name = 'Edit Collections' + actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction' + +plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, + ActionConvert, ActionDelete, ActionEditMetadata, ActionView, + ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, + ActionRestart, ActionOpenFolder, ActionConnectShare, + ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, + ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary] diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 7b70bfbb4b..b720964c92 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -6,7 +6,8 @@ import os, shutil, traceback, functools, sys, re from contextlib import closing from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \ - MetadataReaderPlugin, MetadataWriterPlugin + MetadataReaderPlugin, MetadataWriterPlugin, \ + InterfaceActionBase as InterfaceAction from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin from calibre.customize.profiles import InputProfile, OutputProfile from calibre.customize.builtins import plugins as builtin_plugins @@ -19,7 +20,6 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \ plugin_dir, OptionParser, prefs from calibre.ebooks.epub.fix import ePubFixer - platform = 'linux' if iswindows: platform = 'windows' @@ -246,6 +246,17 @@ def cover_sources(): # }}} +# Interface Actions # {{{ + +def interface_actions(): + customization = config['plugin_customization'] + for plugin in _initialized_plugins: + if isinstance(plugin, InterfaceAction): + if not is_disabled(plugin): + plugin.site_customization = customization.get(plugin.name, '') + yield plugin +# }}} + # Metadata read/write {{{ _metadata_readers = {} _metadata_writers = {} diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 686d705abb..062e4eeab9 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -244,14 +244,20 @@ def info_dialog(parent, title, msg, det_msg='', show=False): class Dispatcher(QObject): - '''Convenience class to ensure that a function call always happens in the - thread the receiver was created in.''' + ''' + Convenience class to use Qt signals with arbitrary python callables. + By default, ensures that a function call always happens in the + thread this Dispatcher was created in. + ''' dispatch_signal = pyqtSignal(object, object) - def __init__(self, func): - QObject.__init__(self) + def __init__(self, func, queued=True, parent=None): + QObject.__init__(self, parent) self.func = func - self.dispatch_signal.connect(self.dispatch, type=Qt.QueuedConnection) + typ = Qt.QueuedConnection + if not queued: + typ = Qt.AutoConnection if queued is None else Qt.DirectConnection + self.dispatch_signal.connect(self.dispatch, type=typ) def __call__(self, *args, **kwargs): self.dispatch_signal.emit(args, kwargs) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index f70332d15a..c4d755aaea 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -5,1368 +5,106 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import shutil, os, datetime, time from functools import partial -from PyQt4.Qt import QInputDialog, pyqtSignal, QModelIndex, QThread, Qt, \ - SIGNAL, QPixmap, QTimer, QDialog +from PyQt4.Qt import QToolButton, QAction, QIcon, QObject -from calibre import strftime -from calibre.ptempfile import PersistentTemporaryFile -from calibre.utils.config import prefs, dynamic -from calibre.gui2 import error_dialog, Dispatcher, gprefs, choose_files, \ - choose_dir, warning_dialog, info_dialog, question_dialog, config, \ - open_local_file -from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString -from calibre.utils.filenames import ascii_filename -from calibre.gui2.widgets import IMAGE_EXTENSIONS -from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog -from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog -from calibre.gui2.dialogs.tag_list_editor import TagListEditor -from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ - fetch_scheduled_recipe, generate_catalog -from calibre.constants import preferred_encoding, filesystem_encoding, \ - isosx -from calibre.gui2.dialogs.choose_format import ChooseFormatDialog -from calibre.ebooks import BOOK_EXTENSIONS -from calibre.gui2.dialogs.confirm_delete import confirm -from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog +from calibre.gui2 import Dispatcher -class AnnotationsAction(object): # {{{ +class InterfaceAction(QObject): - def fetch_annotations(self, *args): - # Generate a path_map from selected ids - def get_ids_from_selected_rows(): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) < 2: - rows = xrange(self.library_view.model().rowCount(QModelIndex())) - ids = map(self.library_view.model().id, rows) - return ids + ''' + A plugin representing an "action" that can be taken in the graphical user + interface. All the items in the toolbar and context menus are implemented + by these plugins. - 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 + Note that this class is the base class for these plugins, however, to + integrate the plugin with calibre's plugin system, you have to make a + wrapper class that references the actual plugin. See the + :mod:`calibre.customize.builtins` module for examples. - 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('/'), mi, 'x.bookmark', create_dirs=False) - path_map[id] = dict(path=a_path, fmts=get_formats(id)) - return path_map + If two :class:`InterfaceAction` objects have the same name, the one with higher + priority takes precedence. - device = self.device_manager.device + Sub-classes should implement the :meth:`genesis` and + :meth:`location_selected` methods. - if self.current_view() is not self.library_view: - return error_dialog(self, _('Use library only'), - _('User annotations generated from main library only'), - show=True) - db = self.library_view.model().db + Once initialized, this plugin has access to the main calibre GUI via the + :attr:`gui` member. You can access other plugins by name, for example:: - # Get the list of ids - ids = get_ids_from_selected_rows() - if not ids: - return error_dialog(self, _('No books selected'), - _('No books selected to fetch annotations from'), - show=True) + self.gui.iactions['Save To Disk'] - # Map ids to paths - path_map = generate_annotation_paths(ids, db, device) + The QAction specified by :attr:`action_spec` is automatically create and + made available as ``self.qaction``. - # Dispatch to devices.kindle.driver.get_annotations() - self.device_manager.annotations(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 + #: The plugin name. If two plugins with the same name are present, the one + #: with higher priority takes precedence. + name = 'Implement me' - class Updater(QThread): + #: The plugin priority. If two plugins with the same name are present, the one + #: with higher priority takes precedence. + priority = 1 - update_progress = pyqtSignal(int) - update_done = pyqtSignal() - FINISHED_READING_PCT_THRESHOLD = 96 + #: The menu popup type for when this plugin is added to a toolbar + popup_type = QToolButton.MenuButtonPopup - 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) + #: Whether this action should be auto repeated when its shortcut + #: key is held down. + auto_repeat = False - self.am = annotation_map - self.done_callback = done_callback - self.connect(self.pd, SIGNAL('canceled()'), 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) + #: Of the form: (text, icon_path, tooltip, keyboard shortcut) + #: icon, tooltip and keyboard shortcut can be None + #: shortcut must be a translated string if not None + action_spec = ('text', 'icon', None, None) - def generate_annotation_html(self, bookmark): - # Returns
...
- last_read_location = bookmark.last_read_location - timestamp = datetime.datetime.utcfromtimestamp(bookmark.timestamp) - percent_read = bookmark.percent_read + def __init__(self, parent, site_customization): + QObject.__init__(self, parent) + self.setObjectName(self.name) + self.gui = parent + self.site_customization = site_customization - ka_soup = BeautifulSoup() - dtc = 0 - divTag = Tag(ka_soup,'div') - divTag['class'] = 'user_annotations' + def do_genesis(self): + self.Dispatcher = partial(Dispatcher, parent=self) + self.create_action() + self.gui.addAction(self.qaction) + self.genesis() - # 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
Last Page Read: %d (%d%%)") % \ - (strftime(u'%x', timestamp.timetuple()), - last_read_location, - percent_read))) - else: - spanTag.insert(0,NavigableString( - _("%s
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( - _('Location %d • %s
%s
') % \ - (user_notes[location]['displayed_location'], - user_notes[location]['type'], - user_notes[location]['text'] if \ - user_notes[location]['type'] == 'Note' else \ - '%s' % user_notes[location]['text'])) - else: - if bookmark.book_format == 'pdf': - annotations.append( - _('Page %d • %s
') % \ - (user_notes[location]['displayed_location'], - user_notes[location]['type'])) - else: - annotations.append( - _('Location %d • %s
') % \ - (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('
') - ad_offset = mi.comments.find('
') - - 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.current_view() is not self.library_view: - return error_dialog(self, _('Use library only'), - _('User annotations generated from main library only'), - show=True) - db = self.library_view.model().db - - self.__annotation_updater = Updater(self, db, job.result, - Dispatcher(self.library_view.model().refresh_ids)) - self.__annotation_updater.start() - - # }}} - -class AddAction(object): # {{{ - - def __init__(self): - self._add_filesystem_book = Dispatcher(self.__add_filesystem_book) - - def add_recursive(self, single): - root = choose_dir(self, 'recursive book import root dir dialog', - 'Select root folder') - if not root: - return - from calibre.gui2.add import Adder - self._adder = Adder(self, - self.library_view.model().db, - Dispatcher(self._files_added), spare_server=self.spare_server) - self._adder.add_recursive(root, single) - - def add_recursive_single(self, *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, _('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.library_view.model().db.import_book(MetaInformation(None), []) - self.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.library_view.model().db.import_book(mi, [])) - self.library_view.model().books_added(len(isbns)) - self.do_download_metadata(ids) - - - def files_dropped(self, paths): - to_device = self.stack.currentIndex() != 0 - self._add_books(paths, to_device) - - def files_dropped_on_book(self, event, paths): - accept = False - if self.current_view() is not self.library_view: - return - db = self.library_view.model().db - current_idx = self.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.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.stack.currentIndex() != 0 - self._add_books(books, to_device) - if to_device: - self.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) - 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.stack.currentIndex() != 0 - if to_device: - filters = [(_('Supported books'), self.device_manager.device.FORMATS)] - - books = choose_files(self, '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.stack.currentIndex() == 2 else 'cardb' if self.stack.currentIndex() == 3 else None - if not paths: - return - from calibre.gui2.add import Adder - self.__adder_func = partial(self._files_added, on_card=on_card) - self._adder = Adder(self, - None if to_device else self.library_view.model().db, - Dispatcher(self.__adder_func), spare_server=self.spare_server) - self._adder.add(paths) - - def _files_added(self, paths=[], names=[], infos=[], on_card=None): - if paths: - self.upload_books(paths, - list(map(ascii_filename, names)), - infos, on_card=on_card) - self.status_bar.show_message( - _('Uploading books to device.'), 2000) - if getattr(self._adder, 'number_of_books_added', 0) > 0: - self.library_view.model().books_added(self._adder.number_of_books_added) - if hasattr(self, 'db_images'): - self.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, _('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, _('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.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, _('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.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, _('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, _('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, self.library_view.model().db, - Dispatcher(self.__adder_func), spare_server=self.spare_server) - self._adder.add(paths) - - # }}} - -class DeleteAction(object): # {{{ - - def _get_selected_formats(self, msg): - from calibre.gui2.dialogs.select_formats import SelectFormats - fmts = self.library_view.model().db.all_formats() - d = SelectFormats([x.lower() for x in fmts], msg, parent=self) - if d.exec_() != d.Accepted: - return None - return d.selected_formats - - def _get_selected_ids(self, err_title=_('Cannot delete')): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, err_title, _('No book selected')) - d.exec_() - return set([]) - return set(map(self.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.library_view.model().db.remove_format(id, fmt, - index_is_id=True, notify=False) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - if ids: - self.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( - '

'+_('Choose formats not to be deleted')) - if fmts is None: - return - for id in ids: - bfmts = self.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.library_view.model().db.remove_format(id, fmt, - index_is_id=True, notify=False) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - if ids: - self.tags_view.recount() - - def remove_matching_books_from_device(self, *args): - if not self.device_manager.is_device_connected: - d = error_dialog(self, _('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.memory_view.model(), _('Main memory')), - (self.card_a_view.model(), _('Storage Card A')), - (self.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, _('No books to delete'), - _('None of the selected books are on the device')) - d.exec_() - return - d = DeleteMatchingFromDeviceDialog(self, 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.remove_paths(paths[model]) - self.delete_memory[job] = (paths[model], model) - model.mark_for_deletion(job, ids[model], rows_are_ids=True) - self.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.library_view.model().db.remove_cover(id) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - - def delete_books(self, *args): - ''' - Delete selected books from device or library. - ''' - view = self.current_view() - rows = view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - return - if self.stack.currentIndex() == 0: - if not confirm('

'+_('The selected books will be ' - 'permanently deleted and the files ' - 'removed from your computer. Are you sure?') - +'

', 'library_delete_books', self): - return - ci = view.currentIndex() - row = None - if ci.isValid(): - row = ci.row() - ids_deleted = view.model().delete_books(rows) - for v in (self.memory_view, self.card_a_view, self.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) + def create_action(self, spec=None, attr='qaction'): + if spec is None: + spec = self.action_spec + text, icon, tooltip, shortcut = spec + if icon is not None: + action = QAction(QIcon(I(icon)), text, self.gui) else: - if not confirm('

'+_('The selected books will be ' - 'permanently deleted ' - 'from your device. Are you sure?') - +'

', 'device_delete_books', self): - return - if self.stack.currentIndex() == 1: - view = self.memory_view - elif self.stack.currentIndex() == 2: - view = self.card_a_view - else: - view = self.card_b_view - paths = view.model().paths(rows) - job = self.remove_paths(paths) - self.delete_memory[job] = (paths, view.model()) - view.model().mark_for_deletion(job, rows) - self.status_bar.show_message(_('Deleting books from device.'), 1000) + action = QAction(text, self.gui) + action.setAutoRepeat(self.auto_repeat) + text = tooltip if tooltip else text + action.setToolTip(text) + action.setStatusTip(text) + action.setWhatsThis(text) + action.setAutoRepeat(False) + if shortcut: + action.setShortcut(shortcut) + setattr(self, attr, action) + return action - # }}} - -class EditMetadataAction(object): # {{{ - - def download_metadata(self, checked, covers=True, set_metadata=True, - set_social_metadata=None): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot download metadata'), - _('No books selected')) - d.exec_() - return - db = self.library_view.model().db - ids = [db.id(row.row()) for row in rows] - 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.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.progress_indicator.start( - _('Downloading %s for %d book(s)')%(x, len(ids))) - self._book_metadata_download_check = QTimer(self) - self.connect(self._book_metadata_download_check, - SIGNAL('timeout()'), self.book_metadata_download_check, - 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.progress_indicator.stop() - cr = self.library_view.currentIndex().row() - x = self._download_book_metadata - self._download_book_metadata = None - if x.exception is None: - self.library_view.model().refresh_ids( - x.updated, cr) - if self.cover_flow: - self.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, _('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, _('Error'), err, det_msg=x.tb).exec_() - - - def edit_metadata(self, checked, bulk=None): + def genesis(self): ''' - Edit metadata of selected books in library. + Setup this plugin. Only called once during initialization. self.gui is + available. The action secified by :attr:`action_spec` is available as + ``self.qaction``. ''' - rows = self.library_view.selectionModel().selectedRows() - previous = self.library_view.currentIndex() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), - _('No books selected')) - d.exec_() - return + pass - if bulk or (bulk is None and len(rows) > 1): - return self.edit_bulk_metadata(checked) - - def accepted(id): - self.library_view.model().refresh_ids([id]) - - for row in rows: - self._metadata_view_id = self.library_view.model().db.id(row.row()) - d = MetadataSingleDialog(self, row.row(), - self.library_view.model().db, - accepted_callback=accepted, - cancel_all=rows.index(row) < len(rows)-1) - self.connect(d, SIGNAL('view_format(PyQt_PyObject)'), - self.metadata_view_format) - d.exec_() - if d.cancel_all: - break - if rows: - current = self.library_view.currentIndex() - m = self.library_view.model() - if self.cover_flow: - self.cover_flow.dataChanged() - m.current_changed(current, previous) - self.tags_view.recount() - - def edit_bulk_metadata(self, checked): + def location_selected(self, loc): ''' - Edit metadata of selected books in library in bulk. + Called whenever the book list being displayed in calibre changes. + Currently values for loc are: ``library, main, card and cardb``. + + This method should enable/disable this action and its sub actions as + appropriate for the location. ''' - rows = [r.row() for r in \ - self.library_view.selectionModel().selectedRows()] - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), - _('No books selected')) - d.exec_() - return - if MetadataBulkDialog(self, rows, - self.library_view.model().db).changed: - self.library_view.model().resort(reset=False) - self.library_view.model().research() - self.tags_view.recount() - if self.cover_flow: - self.cover_flow.dataChanged() - - # Merge books {{{ - def merge_books(self, safe_merge=False): - ''' - Merge selected books in library. - ''' - if self.stack.currentIndex() != 0: - return - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - return error_dialog(self, _('Cannot merge books'), - _('No books selected'), show=True) - if len(rows) < 2: - return error_dialog(self, _('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('

'+_( - 'All book formats and metadata from the selected books ' - 'will be added to the first selected book.

' - 'The second and subsequently selected books will not ' - 'be deleted or changed.

' - 'Please confirm you want to proceed.') - +'

', 'merge_books_safe', self): - return - self.add_formats(dest_id, src_books) - self.merge_metadata(dest_id, src_ids) - else: - if not confirm('

'+_( - 'All book formats and metadata from the selected books will be merged ' - 'into the first selected book.

' - 'After merger the second and ' - 'subsequently selected books will be deleted.

' - '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 deleted from your computer.

' - 'Are you sure you want to proceed?') - +'

', 'merge_books', self): - return - if len(rows)>5: - if not confirm('

'+_('You are about to merge more than 5 books. ' - 'Are you sure you want to proceed?') - +'

', 'merge_too_many_books', self): - 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.library_view.model().index(dest_row, 0) - if ci.isValid(): - self.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.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.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.library_view.model().delete_books_by_id(ids_to_delete) - - def merge_metadata(self, dest_id, src_ids): - db = self.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, 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.upload_collections(model.db, view=view, oncard=oncard) - view.reset() - - # }}} - -class SaveToDiskAction(object): # {{{ - - 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.current_view().selectionModel().selectedRows() - if not rows or len(rows) == 0: - return error_dialog(self, _('Cannot save to disk'), - _('No books selected'), show=True) - path = choose_dir(self, 'save to disk dialog', - _('Choose destination directory')) - if not path: - return - dpath = os.path.abspath(path).replace('/', os.sep) - lpath = self.library_view.model().db.library_path.replace('/', os.sep) - if dpath.startswith(lpath): - return error_dialog(self, _('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.current_view() is self.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, self.library_view.model().db, - Dispatcher(self._books_saved), rows, path, opts, - spare_server=self.spare_server) - - else: - paths = self.current_view().model().paths(rows) - self.device_manager.save_books( - Dispatcher(self.books_saved), paths, path) - - - def _books_saved(self, path, failures, error): - self._saver = None - if error: - return error_dialog(self, _('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, _('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.device_job_exception(job) - - # }}} - -class GenerateCatalogAction(object): # {{{ - - def generate_catalog(self): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) < 2: - rows = xrange(self.library_view.model().rowCount(QModelIndex())) - ids = map(self.library_view.model().id, rows) - - dbspec = None - if not ids: - return error_dialog(self, _('No books selected'), - _('No books selected to generate catalog for'), - show=True) - - # Calling gui2.tools:generate_catalog() - ret = generate_catalog(self, dbspec, ids, self.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.job_manager.run_job( - 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.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, _('No books found'), - _("No books to catalog\nCheck exclude tags"), - show=True) - if job.failed: - return self.job_exception(job) - id = self.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title) - self.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.status_bar.show_message(_('Catalog generated.'), 3000) - self.sync_catalogs() - if job.fmt not in ['EPUB','MOBI']: - export_dir = choose_dir(self, _('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) - - # }}} - -class FetchNewsAction(object): # {{{ - - def download_scheduled_recipe(self, arg): - func, args, desc, fmt, temp_files = \ - fetch_scheduled_recipe(arg) - job = self.job_manager.run_job( - Dispatcher(self.scheduled_recipe_fetched), func, args=args, - description=desc) - self.conversion_jobs[job] = (temp_files, fmt, arg) - self.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.job_exception(job) - id = self.library_view.model().add_news(pt.name, arg) - self.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.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) - self.email_news(id) - self.sync_news() - - # }}} - -class ConvertAction(object): # {{{ - - def auto_convert(self, book_ids, on_card, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - 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.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - 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.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted_news) - - def auto_convert_catalogs(self, book_ids, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - 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.library_view.selectionModel().selectedRows()] - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot convert'), - _('No books selected')) - d.exec_() - return None - return [self.library_view.model().db.id(r) for r in rows] - - def convert_ebook(self, checked, bulk=None): - book_ids = self.get_books_for_conversion() - if book_ids is None: return - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - num = 0 - if bulk or (bulk is None and len(book_ids) > 1): - self.__bulk_queue = convert_bulk_ebook(self, self.queue_convert_jobs, - self.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, - self.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.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.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.library_view.model().refresh_rows(rows) - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, previous) - - def book_auto_converted(self, job): - temp_files, fmt, book_id, on_card = self.conversion_jobs[job] - self.book_converted(job) - self.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) - - def book_auto_converted_mail(self, job): - temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs[job] - self.book_converted(job) - self.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) - - def book_auto_converted_news(self, job): - temp_files, fmt, book_id = self.conversion_jobs[job] - self.book_converted(job) - self.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.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.job_exception(job) - return - data = open(temp_files[-1].name, 'rb') - self.library_view.model().db.add_format(book_id, \ - fmt, data, index_is_id=True) - data.close() - self.status_bar.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.tags_view.recount() - if self.current_view() is self.library_view: - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, QModelIndex()) - - # }}} - -class ViewAction(object): # {{{ - - def view_format(self, row, format): - fmt_path = self.library_view.model().db.format_abspath(row, format) - if fmt_path: - self._view_file(fmt_path) - - def view_format_by_id(self, id_, format): - fmt_path = self.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.library_view.model().db.\ - format_abspath(self._metadata_view_id, - fmt, index_is_id=True) - if fmt_path: - self._view_file(fmt_path) - - - def book_downloaded_for_viewing(self, job): - if job.failed: - self.device_job_exception(job) - return - self._view_file(job.result) - - def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True): - self.setCursor(Qt.BusyCursor) - try: - if internal: - args = [viewer] - if isosx and 'ebook' in viewer: - args.append('--raise-window') - if name is not None: - args.append(name) - self.job_manager.launch_gui_app(viewer, - kwargs=dict(args=args)) - else: - open_local_file(name) - time.sleep(2) # User feedback - finally: - self.unsetCursor() - - def _view_file(self, name): - ext = os.path.splitext(name)[1].upper().replace('.', '') - viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer' - internal = ext in config['internally_viewed_formats'] - self._launch_viewer(name, viewer, internal) - - def view_specific_format(self, triggered): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot view'), _('No book selected')) - d.exec_() - return - - row = rows[0].row() - formats = self.library_view.model().db.formats(row).upper().split(',') - d = ChooseFormatDialog(self, _('Choose the format to view'), formats) - if d.exec_() == QDialog.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, _('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.current_view().selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot open folder'), - _('No book selected')) - d.exec_() - return - if not self._view_check(len(rows)): - return - for row in rows: - path = self.library_view.model().db.abspath(row.row()) - open_local_file(path) - - def view_folder_for_id(self, id_): - path = self.library_view.model().db.abspath(id_, index_is_id=True) - open_local_file(path) - - def view_book(self, triggered): - rows = self.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.current_view() is self.library_view: - for row in rows: - if hasattr(row, 'row'): - row = row.row() - - formats = self.library_view.model().db.formats(row) - title = self.library_view.model().db.title(row) - if not formats: - error_dialog(self, _('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.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.device_manager.view_book(\ - Dispatcher(self.book_downloaded_for_viewing), - path, pt.name) - - # }}} + pass diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py new file mode 100644 index 0000000000..deb03021c5 --- /dev/null +++ b/src/calibre/gui2/actions/add.py @@ -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 ' +__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) + + diff --git a/src/calibre/gui2/actions/add_to_library.py b/src/calibre/gui2/actions/add_to_library.py new file mode 100644 index 0000000000..b2d1091dbb --- /dev/null +++ b/src/calibre/gui2/actions/add_to_library.py @@ -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 ' +__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()) diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py new file mode 100644 index 0000000000..5356d63e98 --- /dev/null +++ b/src/calibre/gui2/actions/annotate.py @@ -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 ' +__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('/'), 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
...
+ 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
Last Page Read: %d (%d%%)") % \ + (strftime(u'%x', timestamp.timetuple()), + last_read_location, + percent_read))) + else: + spanTag.insert(0,NavigableString( + _("%s
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( + _('Location %d • %s
%s
') % \ + (user_notes[location]['displayed_location'], + user_notes[location]['type'], + user_notes[location]['text'] if \ + user_notes[location]['type'] == 'Note' else \ + '%s' % user_notes[location]['text'])) + else: + if bookmark.book_format == 'pdf': + annotations.append( + _('Page %d • %s
') % \ + (user_notes[location]['displayed_location'], + user_notes[location]['type'])) + else: + annotations.append( + _('Location %d • %s
') % \ + (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('
') + ad_offset = mi.comments.find('
') + + 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() + + diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py new file mode 100644 index 0000000000..6807d7717e --- /dev/null +++ b/src/calibre/gui2/actions/catalog.py @@ -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 ' +__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) + + diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py new file mode 100644 index 0000000000..30e2f79424 --- /dev/null +++ b/src/calibre/gui2/actions/choose_library.py @@ -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 ' +__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_() + + diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py new file mode 100644 index 0000000000..0641cc6a97 --- /dev/null +++ b/src/calibre/gui2/actions/convert.py @@ -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 ' +__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()) + diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py new file mode 100644 index 0000000000..e0f3ae4d65 --- /dev/null +++ b/src/calibre/gui2/actions/delete.py @@ -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 ' +__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( + '

'+_('Choose formats not 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('

'+_('The selected books will be ' + 'permanently deleted and the files ' + 'removed from your computer. Are you sure?') + +'

', '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('

'+_('The selected books will be ' + 'permanently deleted ' + 'from your device. Are you sure?') + +'

', '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) + diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py new file mode 100644 index 0000000000..f8b2e751f1 --- /dev/null +++ b/src/calibre/gui2/actions/device.py @@ -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 ' +__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 diff --git a/src/calibre/gui2/actions/edit_collections.py b/src/calibre/gui2/actions/edit_collections.py new file mode 100644 index 0000000000..8f13b012e2 --- /dev/null +++ b/src/calibre/gui2/actions/edit_collections.py @@ -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 ' +__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) + diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py new file mode 100644 index 0000000000..05b4bdf7fc --- /dev/null +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -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 ' +__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('

'+_( + 'All book formats and metadata from the selected books ' + 'will be added to the first selected book.

' + 'The second and subsequently selected books will not ' + 'be deleted or changed.

' + 'Please confirm you want to proceed.') + +'

', 'merge_books_safe', self.gui): + return + self.add_formats(dest_id, src_books) + self.merge_metadata(dest_id, src_ids) + else: + if not confirm('

'+_( + 'All book formats and metadata from the selected books will be merged ' + 'into the first selected book.

' + 'After merger the second and ' + 'subsequently selected books will be deleted.

' + '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 deleted from your computer.

' + 'Are you sure you want to proceed?') + +'

', 'merge_books', self.gui): + return + if len(rows)>5: + if not confirm('

'+_('You are about to merge more than 5 books. ' + 'Are you sure you want to proceed?') + +'

', '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() + + diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py new file mode 100644 index 0000000000..bf44f5ab0e --- /dev/null +++ b/src/calibre/gui2/actions/fetch_news.py @@ -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 ' +__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() + + diff --git a/src/calibre/gui2/actions/help.py b/src/calibre/gui2/actions/help.py new file mode 100644 index 0000000000..0c6b257b80 --- /dev/null +++ b/src/calibre/gui2/actions/help.py @@ -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 ' +__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')) + + + diff --git a/src/calibre/gui2/actions/open.py b/src/calibre/gui2/actions/open.py new file mode 100644 index 0000000000..40fc6372b7 --- /dev/null +++ b/src/calibre/gui2/actions/open.py @@ -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 ' +__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) + + diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py new file mode 100644 index 0000000000..d047e51dac --- /dev/null +++ b/src/calibre/gui2/actions/preferences.py @@ -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 ' +__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() + + + diff --git a/src/calibre/gui2/actions/restart.py b/src/calibre/gui2/actions/restart.py new file mode 100644 index 0000000000..be940fa32e --- /dev/null +++ b/src/calibre/gui2/actions/restart.py @@ -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 ' +__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) + + diff --git a/src/calibre/gui2/actions/save_to_disk.py b/src/calibre/gui2/actions/save_to_disk.py new file mode 100644 index 0000000000..fcd7c4e332 --- /dev/null +++ b/src/calibre/gui2/actions/save_to_disk.py @@ -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 ' +__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) + + diff --git a/src/calibre/gui2/actions/show_book_details.py b/src/calibre/gui2/actions/show_book_details.py new file mode 100644 index 0000000000..06c63714a7 --- /dev/null +++ b/src/calibre/gui2/actions/show_book_details.py @@ -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 ' +__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() + diff --git a/src/calibre/gui2/actions/similar_books.py b/src/calibre/gui2/actions/similar_books.py new file mode 100644 index 0000000000..5ce74fd2dc --- /dev/null +++ b/src/calibre/gui2/actions/similar_books.py @@ -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 ' +__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)) + + diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py new file mode 100644 index 0000000000..22592be2fe --- /dev/null +++ b/src/calibre/gui2/actions/view.py @@ -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 ' +__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) + + diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py index 74f5a2148e..04d60525f1 100644 --- a/src/calibre/gui2/add.py +++ b/src/calibre/gui2/add.py @@ -443,7 +443,7 @@ class Saver(QObject): from calibre.ebooks.metadata.worker import SaveWorker self.worker = SaveWorker(self.rq, db, self.ids, path, self.opts, spare_server=self.spare_server) - self.connect(self.pd, SIGNAL('canceled()'), self.canceled) + self.pd.canceled_signal.connect(self.canceled) self.timer = QTimer(self) self.connect(self.timer, SIGNAL('timeout()'), self.update) self.timer.start(200) diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index 925aa15adf..b0252210e3 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -144,7 +144,7 @@ class CoverFlowMixin(object): self.sync_cf_to_listview) self.db_images = DatabaseImages(self.library_view.model()) self.cover_flow.setImages(self.db_images) - self.cover_flow.itemActivated.connect(self.view_specific_book) + self.cover_flow.itemActivated.connect(self.iactions['View'].view_specific_book) else: self.cover_flow = QLabel('

'+_('Cover browser could not be loaded') +'
'+pictureflowerror) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 789ae68723..a9beb317a2 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -608,8 +608,6 @@ class DeviceMixin(object): # {{{ self.device_error_dialog = error_dialog(self, _('Error'), _('Error communicating with device'), ' ') self.device_error_dialog.setModal(Qt.NonModal) - self.share_conn_menu.connect_to_folder.connect(self.connect_to_folder) - self.share_conn_menu.connect_to_itunes.connect(self.connect_to_itunes) self.emailer = Emailer() self.emailer.start() self.device_manager = DeviceManager(Dispatcher(self.device_detected), @@ -647,20 +645,18 @@ class DeviceMixin(object): # {{{ def create_device_menu(self): self._sync_menu = DeviceMenu(self) - self.share_conn_menu.build_email_entries(self._sync_menu) - self.action_sync.setMenu(self._sync_menu) + self.iactions['Send To Device'].qaction.setMenu(self._sync_menu) + self.iactions['Connect Share'].build_email_entries() self.connect(self._sync_menu, SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), self.dispatch_sync_event) - self._sync_menu.fetch_annotations.connect(self.fetch_annotations) + self._sync_menu.fetch_annotations.connect( + self.iactions['Fetch Annotations'].fetch_annotations) self._sync_menu.disconnect_mounted_device.connect(self.disconnect_mounted_device) + self.iactions['Connect Share'].set_state(self.device_connected) if self.device_connected: - self.share_conn_menu.connect_to_folder_action.setEnabled(False) - self.share_conn_menu.connect_to_itunes_action.setEnabled(False) self._sync_menu.disconnect_mounted_device_action.setEnabled(True) else: - self.share_conn_menu.connect_to_folder_action.setEnabled(True) - self.share_conn_menu.connect_to_itunes_action.setEnabled(True) self._sync_menu.disconnect_mounted_device_action.setEnabled(False) def device_job_exception(self, job): @@ -696,17 +692,14 @@ class DeviceMixin(object): # {{{ # Device connected {{{ def set_device_menu_items_state(self, connected): + self.iactions['Connect Share'].set_state(connected) if connected: - self.share_conn_menu.connect_to_folder_action.setEnabled(False) - self.share_conn_menu.connect_to_itunes_action.setEnabled(False) self._sync_menu.disconnect_mounted_device_action.setEnabled(True) self._sync_menu.enable_device_actions(True, self.device_manager.device.card_prefix(), self.device_manager.device) self.eject_action.setEnabled(True) else: - self.share_conn_menu.connect_to_folder_action.setEnabled(True) - self.share_conn_menu.connect_to_itunes_action.setEnabled(True) self._sync_menu.disconnect_mounted_device_action.setEnabled(False) self._sync_menu.enable_device_actions(False) self.eject_action.setEnabled(False) @@ -791,8 +784,9 @@ class DeviceMixin(object): # {{{ self.device_job_exception(job) return - if self.delete_memory.has_key(job): - paths, model = self.delete_memory.pop(job) + dm = self.iactions['Remove Books'].delete_memory + if dm.has_key(job): + paths, model = dm.pop(job) self.device_manager.remove_books_from_metadata(paths, self.booklists()) model.paths_deleted(paths) @@ -924,7 +918,7 @@ class DeviceMixin(object): # {{{ _('Auto convert the following books before sending via ' 'email?'), det_msg=autos, buttons=QMessageBox.Yes|QMessageBox.Cancel): - self.auto_convert_mail(to, fmts, delete_from_library, auto, format) + self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format) if bad: bad = '\n'.join('%s'%(i,) for i in bad) @@ -1026,7 +1020,7 @@ class DeviceMixin(object): # {{{ _('Auto convert the following books before uploading to ' 'the device?'), det_msg=autos, buttons=QMessageBox.Yes|QMessageBox.Cancel): - self.auto_convert_catalogs(auto, format) + self.iactions['Convert Books'].auto_convert_catalogs(auto, format) files = [f for f in files if f is not None] if not files: dynamic.set('catalogs_to_be_synced', set([])) @@ -1088,7 +1082,7 @@ class DeviceMixin(object): # {{{ _('Auto convert the following books before uploading to ' 'the device?'), det_msg=autos, buttons=QMessageBox.Yes|QMessageBox.Cancel): - self.auto_convert_news(auto, format) + self.iactions['Convert Books'].auto_convert_news(auto, format) files = [f for f in files if f is not None] for f in files: f.deleted_after_upload = del_on_upload @@ -1207,7 +1201,7 @@ class DeviceMixin(object): # {{{ _('Auto convert the following books before uploading to ' 'the device?'), det_msg=autos, buttons=QMessageBox.Yes|QMessageBox.Cancel): - self.auto_convert(auto, on_card, format) + self.iactions['Convert Books'].auto_convert(auto, on_card, format) if bad: bad = '\n'.join('%s'%(i,) for i in bad) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 6cb351898b..50223923c1 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -12,7 +12,7 @@ import time import traceback from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \ - QPixmap, QListWidgetItem, QDialog + QPixmap, QListWidgetItem, QDialog, pyqtSignal from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \ choose_files, choose_images, ResizableDialog, \ @@ -99,6 +99,7 @@ class Format(QListWidgetItem): class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): COVER_FETCH_TIMEOUT = 240 # seconds + view_format = pyqtSignal(object) def do_reset_cover(self, *args): pix = QPixmap(I('default_cover.svg')) @@ -474,7 +475,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): def show_format(self, item, *args): fmt = item.ext - self.emit(SIGNAL('view_format(PyQt_PyObject)'), fmt) + self.view_format.emit(fmt) def deduce_author_sort(self): au = unicode(self.authors.text()) diff --git a/src/calibre/gui2/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py index 91f6edd252..40404050ec 100644 --- a/src/calibre/gui2/dialogs/progress.py +++ b/src/calibre/gui2/dialogs/progress.py @@ -5,12 +5,14 @@ __docformat__ = 'restructuredtext en' '''''' -from PyQt4.Qt import QDialog, SIGNAL, Qt +from PyQt4.Qt import QDialog, pyqtSignal, Qt from calibre.gui2.dialogs.progress_ui import Ui_Dialog class ProgressDialog(QDialog, Ui_Dialog): + canceled_signal = pyqtSignal() + def __init__(self, title, msg='', min=0, max=99, parent=None): QDialog.__init__(self, parent) self.setupUi(self) @@ -23,7 +25,7 @@ class ProgressDialog(QDialog, Ui_Dialog): self.bar.setValue(min) self.canceled = False - self.connect(self.button_box, SIGNAL('rejected()'), self._canceled) + self.button_box.rejected.connect(self._canceled) def set_msg(self, msg=''): self.message.setText(msg) @@ -50,7 +52,7 @@ class ProgressDialog(QDialog, Ui_Dialog): self.canceled = True self.button_box.setDisabled(True) self.title.setText(_('Aborting...')) - self.emit(SIGNAL('canceled()')) + self.canceled_signal.emit() def keyPressEvent(self, ev): if ev.key() == Qt.Key_Escape: diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 47f118c0c2..73de77b13b 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' import functools, sys, os -from PyQt4.Qt import QMenu, Qt, QStackedWidget, \ +from PyQt4.Qt import Qt, QStackedWidget, QMenu, \ QSize, QSizePolicy, QStatusBar, QLabel, QFont from calibre.utils.config import prefs @@ -27,69 +27,35 @@ def partial(*args, **kwargs): _keep_refs.append(ans) return ans +LIBRARY_CONTEXT_MENU = ( + 'Edit Metadata', 'Send To Device', 'Save To Disk', 'Connect Share', None, + 'Convert Books', 'View', 'Open Folder', 'Show Book Details', None, + 'Remove Books', + ) + +DEVICE_CONTEXT_MENU = ('View', 'Save To Disk', None, 'Remove Books', None, + 'Add To Library', 'Edit Collections', + ) + class LibraryViewMixin(object): # {{{ def __init__(self, db): - similar_menu = QMenu(_('Similar books...')) - similar_menu.addAction(self.action_books_by_same_author) - similar_menu.addAction(self.action_books_in_this_series) - similar_menu.addAction(self.action_books_with_the_same_tags) - similar_menu.addAction(self.action_books_by_this_publisher) - self.action_books_by_same_author.setShortcut(Qt.ALT + Qt.Key_A) - self.action_books_in_this_series.setShortcut(Qt.ALT + Qt.Key_S) - self.action_books_by_this_publisher.setShortcut(Qt.ALT + Qt.Key_P) - self.action_books_with_the_same_tags.setShortcut(Qt.ALT+Qt.Key_T) - self.addAction(self.action_books_by_same_author) - self.addAction(self.action_books_by_this_publisher) - self.addAction(self.action_books_in_this_series) - self.addAction(self.action_books_with_the_same_tags) - self.similar_menu = similar_menu - self.action_books_by_same_author.triggered.connect( - partial(self.show_similar_books, 'authors')) - self.action_books_in_this_series.triggered.connect( - partial(self.show_similar_books, 'series')) - self.action_books_with_the_same_tags.triggered.connect( - partial(self.show_similar_books, 'tag')) - self.action_books_by_this_publisher.triggered.connect( - partial(self.show_similar_books, 'publisher')) + lm = QMenu(self) + def populate_menu(m, items): + for what in items: + if what is None: + m.addSeparator() + elif what in self.iactions: + m.addAction(self.iactions[what].qaction) + populate_menu(lm, LIBRARY_CONTEXT_MENU) + dm = QMenu(self) + populate_menu(dm, DEVICE_CONTEXT_MENU) + ec = self.iactions['Edit Collections'].qaction + self.library_view.set_context_menu(lm, ec) + for v in (self.memory_view, self.card_a_view, self.card_b_view): + v.set_context_menu(dm, ec) - self.library_view.set_context_menu(self.action_edit, self.action_sync, - self.action_convert, self.action_view, - self.action_save, - self.action_open_containing_folder, - self.action_show_book_details, - self.action_del, - self.action_conn_share, - add_to_library = None, - edit_device_collections=None, - similar_menu=similar_menu) - add_to_library = (_('Add books to library'), self.add_books_from_device) - - edit_device_collections = (_('Manage collections'), - partial(self.edit_device_collections, oncard=None)) - self.memory_view.set_context_menu(None, None, None, - self.action_view, self.action_save, None, None, - self.action_del, None, - add_to_library=add_to_library, - edit_device_collections=edit_device_collections) - - edit_device_collections = (_('Manage collections'), - partial(self.edit_device_collections, oncard='carda')) - self.card_a_view.set_context_menu(None, None, None, - self.action_view, self.action_save, None, None, - self.action_del, None, - add_to_library=add_to_library, - edit_device_collections=edit_device_collections) - - edit_device_collections = (_('Manage collections'), - partial(self.edit_device_collections, oncard='cardb')) - self.card_b_view.set_context_menu(None, None, None, - self.action_view, self.action_save, None, None, - self.action_del, None, - add_to_library=add_to_library, - edit_device_collections=edit_device_collections) - - self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection) + self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection) for func, args in [ ('connect_to_search_box', (self.search, self.search_done)), @@ -116,37 +82,10 @@ class LibraryViewMixin(object): # {{{ for view in ('library', 'memory', 'card_a', 'card_b'): view = getattr(self, view+'_view') - view.verticalHeader().sectionDoubleClicked.connect(self.view_specific_book) + view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book) - def show_similar_books(self, type, *args): - search, join = [], ' ' - idx = self.library_view.currentIndex() - if not idx.isValid(): - return - row = idx.row() - if type == 'series': - series = idx.model().db.series(row) - if series: - search = ['series:"'+series+'"'] - elif type == 'publisher': - publisher = idx.model().db.publisher(row) - if publisher: - search = ['publisher:"'+publisher+'"'] - elif type == 'tag': - tags = idx.model().db.tags(row) - if tags: - search = ['tag:"='+t+'"' for t in tags.split(',')] - elif type in ('author', 'authors'): - authors = idx.model().db.authors(row) - if authors: - search = ['author:"='+a.strip().replace('|', ',')+'"' \ - for a in authors.split(',')] - join = ' or ' - if search: - self.search.set_search_string(join.join(search)) - def search_done(self, view, ok): if view is self.current_view(): self.search.search_done(ok) @@ -308,10 +247,10 @@ class LayoutMixin(object): # {{{ def finalize_layout(self): self.status_bar.initialize(self.system_tray_icon) - self.book_details.show_book_info.connect(self.show_book_info) - self.book_details.files_dropped.connect(self.files_dropped_on_book) - self.book_details.open_containing_folder.connect(self.view_folder_for_id) - self.book_details.view_specific_format.connect(self.view_format_by_id) + self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info) + self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book) + self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id) + self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id) m = self.library_view.model() if m.rowCount(None) > 0: diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 8a919c59f5..6c87fe9da3 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -5,42 +5,32 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from operator import attrgetter from functools import partial -from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \ +from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \ pyqtSignal, QToolButton, \ QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \ - QMenu, QUrl + QMenu -from calibre.constants import __appname__, isosx +from calibre.constants import __appname__ from calibre.gui2.search_box import SearchBox2, SavedSearchBox from calibre.gui2.throbber import ThrobbingButton -from calibre.gui2 import config, open_url, gprefs +from calibre.gui2 import config, gprefs from calibre.gui2.widgets import ComboBoxWithHelp from calibre import human_readable -from calibre.utils.config import prefs -from calibre.ebooks import BOOK_EXTENSIONS -from calibre.gui2.dialogs.scheduler import Scheduler -from calibre.utils.smtp import config as email_config +TOOLBAR_NO_DEVICE = ( + 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None, + 'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk', + 'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences', + ) -class SaveMenu(QMenu): # {{{ - - save_fmt = pyqtSignal(object) - - def __init__(self, parent): - QMenu.__init__(self, _('Save single format to disk...'), parent) - for ext in sorted(BOOK_EXTENSIONS): - action = self.addAction(ext.upper()) - setattr(self, 'do_'+ext, partial(self.do, ext)) - action.triggered.connect( - getattr(self, 'do_'+ext)) - - def do(self, ext, *args): - self.save_fmt.emit(ext) - -# }}} +TOOLBAR_DEVICE = ( + 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', + 'Send To Device', None, None, 'Location Manager', None, None, + 'Fetch News', 'Save To Disk', 'Connect Share', None, + 'Remove Books', None, 'Help', 'Preferences', + ) class LocationManager(QObject): # {{{ @@ -221,8 +211,9 @@ class SearchBar(QWidget): # {{{ class ToolBar(QToolBar): # {{{ - def __init__(self, actions, donate, location_manager, parent=None): + def __init__(self, donate, location_manager, parent): QToolBar.__init__(self, parent) + self.gui = parent self.setContextMenuPolicy(Qt.PreventContextMenu) self.setMovable(False) self.setFloatable(False) @@ -232,7 +223,6 @@ class ToolBar(QToolBar): # {{{ self.donate = donate self.apply_settings() - self.all_actions = actions self.location_manager = location_manager self.location_manager.locations_changed.connect(self.build_bar) self.d_widget = QWidget() @@ -258,51 +248,30 @@ class ToolBar(QToolBar): # {{{ def build_bar(self): showing_device = self.location_manager.has_device - order_field = 'device' if showing_device else 'normal' - o = attrgetter(order_field+'_order') - sepvals = [2] if showing_device else [1] - sepvals += [3] - actions = [x for x in self.all_actions if o(x) > -1] - actions.sort(cmp=lambda x,y : cmp(o(x), o(y))) + actions = TOOLBAR_DEVICE if showing_device else TOOLBAR_NO_DEVICE + self.clear() + for what in actions: + if what is None: + self.addSeparator() + elif what == 'Location Manager': + for ac in self.location_manager.available_actions: + self.addAction(ac) + self.setup_tool_button(ac, QToolButton.MenuButtonPopup) + elif what == 'Donate' and config['show_donate_button']: + self.addWidget(self.d_widget) + elif what in self.gui.iactions: + action = self.gui.iactions[what] + self.addAction(action.qaction) + self.setup_tool_button(action.qaction, action.popup_type) - def setup_tool_button(ac): - ch = self.widgetForAction(ac) - ch.setCursor(Qt.PointingHandCursor) - ch.setAutoRaise(True) - if ac.menu() is not None: - name = getattr(ac, 'action_name', None) - ch.setPopupMode(ch.InstantPopup if name == 'conn_share' - else ch.MenuButtonPopup) - - for x in actions: - self.addAction(x) - setup_tool_button(x) - - if x.action_name == 'choose_library': - self.choose_action = x - if showing_device: - self.addSeparator() - for ac in self.location_manager.available_actions: - self.addAction(ac) - setup_tool_button(ac) - self.addSeparator() - self.location_manager.location_library.trigger() - elif config['show_donate_button']: - self.addWidget(self.d_widget) - - for x in actions: - if x.separator_before in sepvals: - self.insertSeparator(x) - - self.choose_action.setVisible(not showing_device) - - def count_changed(self, new_count): - text = _('%d books')%new_count - a = self.choose_action - a.setText(text) - a.setToolTip(_('Choose calibre library to work with') + '\n\n' + text) + def setup_tool_button(self, ac, menu_mode=None): + ch = self.widgetForAction(ac) + ch.setCursor(Qt.PointingHandCursor) + ch.setAutoRaise(True) + if ac.menu() is not None and menu_mode is not None: + ch.setPopupMode(menu_mode) def resizeEvent(self, ev): QToolBar.resizeEvent(self, ev) @@ -321,84 +290,9 @@ class ToolBar(QToolBar): # {{{ # }}} -class Action(QAction): - pass - -class ShareConnMenu(QMenu): # {{{ - - connect_to_folder = pyqtSignal() - connect_to_itunes = pyqtSignal() - config_email = pyqtSignal() - toggle_server = pyqtSignal() - - def __init__(self, parent=None): - QMenu.__init__(self, parent) - mitem = self.addAction(QIcon(I('devices/folder.svg')), _('Connect to folder')) - mitem.setEnabled(True) - mitem.triggered.connect(lambda x : self.connect_to_folder.emit()) - self.connect_to_folder_action = mitem - mitem = self.addAction(QIcon(I('devices/itunes.png')), - _('Connect to iTunes')) - mitem.setEnabled(True) - mitem.triggered.connect(lambda x : self.connect_to_itunes.emit()) - self.connect_to_itunes_action = mitem - self.addSeparator() - self.toggle_server_action = \ - self.addAction(QIcon(I('network-server.svg')), - _('Start Content Server')) - self.toggle_server_action.triggered.connect(lambda x: - self.toggle_server.emit()) - self.addSeparator() - - self.email_actions = [] - - def server_state_changed(self, running): - text = _('Start Content Server') - if running: - text = _('Stop Content Server') - self.toggle_server_action.setText(text) - - def build_email_entries(self, sync_menu): - from calibre.gui2.device import DeviceAction - for ac in self.email_actions: - self.removeAction(ac) - self.email_actions = [] - self.memory = [] - opts = email_config().parse() - if opts.accounts: - self.email_to_menu = QMenu(_('Email to')+'...', self) - keys = sorted(opts.accounts.keys()) - for account in keys: - formats, auto, default = opts.accounts[account] - dest = 'mail:'+account+';'+formats - action1 = DeviceAction(dest, False, False, I('mail.svg'), - _('Email to')+' '+account) - action2 = DeviceAction(dest, True, False, I('mail.svg'), - _('Email to')+' '+account+ _(' and delete from library')) - map(self.email_to_menu.addAction, (action1, action2)) - map(self.memory.append, (action1, action2)) - if default: - map(self.addAction, (action1, action2)) - map(self.email_actions.append, (action1, action2)) - self.email_to_menu.addSeparator() - action1.a_s.connect(sync_menu.action_triggered) - action2.a_s.connect(sync_menu.action_triggered) - ac = self.addMenu(self.email_to_menu) - self.email_actions.append(ac) - else: - ac = self.addAction(_('Setup email based sharing of books')) - self.email_actions.append(ac) - ac.triggered.connect(self.setup_email) - - def setup_email(self, *args): - self.config_email.emit() - -# }}} - class MainWindowMixin(object): def __init__(self, db): - self.device_connected = None self.setObjectName('MainWindow') self.setWindowIcon(QIcon(I('library.png'))) self.setWindowTitle(__appname__) @@ -412,230 +306,18 @@ class MainWindowMixin(object): self.donate_button = ThrobbingButton(self.centralwidget) self.location_manager = LocationManager(self) - self.init_scheduler(db) - all_actions = self.setup_actions() + self.iactions['Fetch News'].init_scheduler(db) self.search_bar = SearchBar(self) - self.tool_bar = ToolBar(all_actions, self.donate_button, + self.tool_bar = ToolBar(self.donate_button, self.location_manager, self) self.addToolBar(Qt.TopToolBarArea, self.tool_bar) - self.tool_bar.choose_action.triggered.connect(self.choose_library) l = self.centralwidget.layout() l.addWidget(self.search_bar) - def init_scheduler(self, db): - self.scheduler = Scheduler(self, db) - self.scheduler.start_recipe_fetch.connect( - self.download_scheduled_recipe, type=Qt.QueuedConnection) - - def read_toolbar_settings(self): - pass - - def choose_library(self, *args): - from calibre.gui2.dialogs.choose_library import ChooseLibrary - db = self.library_view.model().db - c = ChooseLibrary(db, self.library_moved, self) - c.exec_() - - def setup_actions(self): # {{{ - all_actions = [] - - def ac(normal_order, device_order, separator_before, - name, text, icon, shortcut=None, tooltip=None): - action = Action(QIcon(I(icon)), text, self) - action.normal_order = normal_order - action.device_order = device_order - action.separator_before = separator_before - action.action_name = name - text = tooltip if tooltip else text - action.setToolTip(text) - action.setStatusTip(text) - action.setWhatsThis(text) - action.setAutoRepeat(False) - action.setObjectName('action_'+name) - if shortcut: - action.setShortcut(shortcut) - setattr(self, 'action_'+name, action) - all_actions.append(action) - - ac(0, 0, 0, 'add', _('Add books'), 'add_book.svg', _('A')) - ac(1, 1, 0, 'edit', _('Edit metadata'), 'edit_input.svg', _('E')) - ac(2, 2, 3, 'convert', _('Convert books'), 'convert.svg', _('C')) - ac(3, 3, 0, 'view', _('View'), 'view.svg', _('V')) - ac(-1, 4, 0, 'sync', _('Send to device'), 'sync.svg') - ac(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png', - tooltip=_('Choose calibre library to work with')) - ac(6, 6, 3, 'news', _('Fetch news'), 'news.svg', _('F')) - ac(7, 7, 0, 'save', _('Save to disk'), 'save.svg', _('S')) - ac(8, 8, 0, 'conn_share', _('Connect/share'), 'connect_share.svg') - ac(9, 9, 3, 'del', _('Remove books'), 'trash.svg', _('Del')) - ac(10, 10, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual")) - ac(11, 11, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P')) - - ac(-1, -1, 0, 'merge', _('Merge book records'), 'merge_books.svg', _('M')) - ac(-1, -1, 0, 'open_containing_folder', _('Open containing folder'), - 'document_open.svg') - ac(-1, -1, 0, 'show_book_details', _('Show book details'), - 'dialog_information.svg') - ac(-1, -1, 0, 'books_by_same_author', _('Books by same author'), - 'user_profile.svg') - ac(-1, -1, 0, 'books_in_this_series', _('Books in this series'), - 'books_in_series.svg') - ac(-1, -1, 0, 'books_by_this_publisher', _('Books by this publisher'), - 'publisher.png') - ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'), - 'tags.svg') - - self.action_news.setMenu(self.scheduler.news_menu) - self.action_news.triggered.connect( - self.scheduler.show_dialog) - self.share_conn_menu = ShareConnMenu(self) - self.share_conn_menu.toggle_server.connect(self.toggle_content_server) - self.share_conn_menu.config_email.connect(partial(self.do_config, - initial_category='email')) - self.action_conn_share.setMenu(self.share_conn_menu) - - self.action_help.triggered.connect(self.show_help) - md = QMenu() - md.addAction(_('Edit metadata individually'), - partial(self.edit_metadata, False, bulk=False)) - md.addSeparator() - md.addAction(_('Edit metadata in bulk'), - partial(self.edit_metadata, False, bulk=True)) - md.addSeparator() - md.addAction(_('Download metadata and covers'), - partial(self.download_metadata, False, covers=True), - Qt.ControlModifier+Qt.Key_D) - md.addAction(_('Download only metadata'), - partial(self.download_metadata, False, covers=False)) - md.addAction(_('Download only covers'), - partial(self.download_metadata, False, covers=True, - set_metadata=False, set_social_metadata=False)) - md.addAction(_('Download only social metadata'), - partial(self.download_metadata, False, covers=False, - set_metadata=False, set_social_metadata=True)) - self.metadata_menu = md - - mb = QMenu() - mb.addAction(_('Merge into first selected book - delete others'), - self.merge_books) - mb.addSeparator() - mb.addAction(_('Merge into first selected book - keep others'), - partial(self.merge_books, safe_merge=True)) - self.merge_menu = mb - self.action_merge.setMenu(mb) - md.addSeparator() - md.addAction(self.action_merge) - - self.add_menu = QMenu() - self.add_menu.addAction(_('Add books from a single directory'), - self.add_books) - self.add_menu.addAction(_('Add books from directories, including ' - 'sub-directories (One book per directory, assumes every ebook ' - 'file is the same book in a different format)'), - self.add_recursive_single) - self.add_menu.addAction(_('Add books from directories, including ' - 'sub directories (Multiple books per directory, assumes every ' - 'ebook file is a different book)'), self.add_recursive_multiple) - self.add_menu.addSeparator() - self.add_menu.addAction(_('Add Empty book. (Book entry with no ' - 'formats)'), self.add_empty) - self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn) - self.action_add.setMenu(self.add_menu) - self.action_add.triggered.connect(self.add_books) - self.action_del.triggered.connect(self.delete_books) - self.action_edit.triggered.connect(self.edit_metadata) - self.action_merge.triggered.connect(self.merge_books) - - self.action_save.triggered.connect(self.save_to_disk) - self.save_menu = QMenu() - self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk, - False)) - self.save_menu.addAction(_('Save to disk in a single directory'), - partial(self.save_to_single_dir, False)) - self.save_menu.addAction(_('Save only %s format to disk')% - prefs['output_format'].upper(), - partial(self.save_single_format_to_disk, False)) - self.save_menu.addAction( - _('Save only %s format to disk in a single directory')% - prefs['output_format'].upper(), - partial(self.save_single_fmt_to_single_dir, False)) - self.save_sub_menu = SaveMenu(self) - self.save_menu.addMenu(self.save_sub_menu) - self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk) - - self.action_view.triggered.connect(self.view_book) - self.view_menu = QMenu() - self.view_menu.addAction(_('View'), partial(self.view_book, False)) - ac = self.view_menu.addAction(_('View specific format')) - ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V) - self.action_view.setMenu(self.view_menu) - ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection) - - self.delete_menu = QMenu() - self.delete_menu.addAction(_('Remove selected books'), self.delete_books) - self.delete_menu.addAction( - _('Remove files of a specific format from selected books..'), - self.delete_selected_formats) - self.delete_menu.addAction( - _('Remove all formats from selected books, except...'), - self.delete_all_but_selected_formats) - self.delete_menu.addAction( - _('Remove covers from selected books'), self.delete_covers) - self.delete_menu.addSeparator() - self.delete_menu.addAction( - _('Remove matching books from device'), - self.remove_matching_books_from_device) - self.action_del.setMenu(self.delete_menu) - - self.action_open_containing_folder.setShortcut(Qt.Key_O) - self.addAction(self.action_open_containing_folder) - self.action_open_containing_folder.triggered.connect(self.view_folder) - self.action_sync.setShortcut(Qt.Key_D) - self.action_sync.setEnabled(True) - self.create_device_menu() - self.action_sync.triggered.connect( - self._sync_action_triggered) - - self.action_edit.setMenu(md) - self.action_save.setMenu(self.save_menu) - - cm = QMenu() - cm.addAction(_('Convert individually'), partial(self.convert_ebook, - False, bulk=False)) - cm.addAction(_('Bulk convert'), - partial(self.convert_ebook, False, bulk=True)) - cm.addSeparator() - ac = cm.addAction( - _('Create catalog of books in your calibre library')) - ac.triggered.connect(self.generate_catalog) - self.action_convert.setMenu(cm) - self.action_convert.triggered.connect(self.convert_ebook) - self.convert_menu = cm - - pm = QMenu() - pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config) - pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'), - self.run_wizard) - self.action_preferences.setMenu(pm) - self.preferences_menu = pm - for x in (self.preferences_action, self.action_preferences): - x.triggered.connect(self.do_config) - return all_actions - # }}} - def show_help(self, *args): - open_url(QUrl('http://calibre-ebook.com/user_manual')) - def content_server_state_changed(self, running): - self.share_conn_menu.server_state_changed(running) - def toggle_content_server(self): - if self.content_server is None: - self.start_content_server() - else: - self.content_server.exit() - self.content_server = None diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 7ccbc027f6..389208fdcd 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -389,37 +389,10 @@ class BooksView(QTableView): # {{{ #}}} # Context Menu {{{ - def set_context_menu(self, edit_metadata, send_to_device, convert, view, - save, open_folder, book_details, delete, conn_share, - similar_menu=None, add_to_library=None, - edit_device_collections=None): + def set_context_menu(self, menu, edit_collections_action): self.setContextMenuPolicy(Qt.DefaultContextMenu) - self.context_menu = QMenu(self) - if edit_metadata is not None: - self.context_menu.addAction(edit_metadata) - if send_to_device is not None: - self.context_menu.addAction(send_to_device) - if convert is not None: - self.context_menu.addAction(convert) - if conn_share is not None: - self.context_menu.addAction(conn_share) - self.context_menu.addAction(view) - self.context_menu.addAction(save) - if open_folder is not None: - self.context_menu.addAction(open_folder) - if delete is not None: - self.context_menu.addAction(delete) - if book_details is not None: - self.context_menu.addAction(book_details) - if similar_menu is not None: - self.context_menu.addMenu(similar_menu) - if add_to_library is not None: - func = partial(add_to_library[1], view=self) - self.context_menu.addAction(add_to_library[0], func) - if edit_device_collections is not None: - func = partial(edit_device_collections[1], view=self) - self.edit_collections_menu = \ - self.context_menu.addAction(edit_device_collections[0], func) + self.context_menu = menu + self.edit_collections_action = edit_collections_action def contextMenuEvent(self, event): self.context_menu.popup(event.globalPos()) @@ -528,10 +501,11 @@ class DeviceBooksView(BooksView): # {{{ self.setAcceptDrops(False) def contextMenuEvent(self, event): - self.edit_collections_menu.setVisible( - callable(getattr(self._model.db, 'supports_collections', None)) and \ + edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \ self._model.db.supports_collections() and \ - prefs['manage_device_metadata'] == 'manual') + prefs['manage_device_metadata'] == 'manual' + + self.edit_collections_action.setVisible(edit_collections) self.context_menu.popup(event.globalPos()) event.accept() diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index ca896fc014..f9d7d80b24 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -155,7 +155,7 @@ class GuiRunner(QObject): main.initialize(self.library_path, self.db, self.listener, self.actions) if DEBUG: prints('Started up in', time.time() - self.startup_time) - add_filesystem_book = partial(main.add_filesystem_book, allow_device=False) + add_filesystem_book = partial(main.iactions['Add Books'].add_filesystem_book, allow_device=False) sys.excepthook = main.unhandled_exception if len(self.args) > 1: p = os.path.abspath(self.args[1]) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 0b1091f848..9e4b4f3865 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -15,7 +15,7 @@ from threading import Thread from PyQt4.Qt import Qt, SIGNAL, QTimer, \ QPixmap, QMenu, QIcon, pyqtSignal, \ QDialog, \ - QSystemTrayIcon, QApplication, QKeySequence, QAction, \ + QSystemTrayIcon, QApplication, QKeySequence, \ QMessageBox, QHelpEvent from calibre import prints @@ -23,6 +23,8 @@ from calibre.constants import __appname__, isosx from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import prefs, dynamic from calibre.utils.ipc.server import Server +from calibre.library.database2 import LibraryDatabase2 +from calibre.customize.ui import interface_actions from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \ gprefs, max_available_height, config, info_dialog, Dispatcher from calibre.gui2.cover_flow import CoverFlowMixin @@ -32,17 +34,10 @@ from calibre.gui2.main_window import MainWindow from calibre.gui2.layout import MainWindowMixin from calibre.gui2.device import DeviceMixin from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton -from calibre.gui2.dialogs.config import ConfigDialog - -from calibre.gui2.dialogs.book_info import BookInfo -from calibre.library.database2 import LibraryDatabase2 from calibre.gui2.init import LibraryViewMixin, LayoutMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.tag_view import TagBrowserMixin -from calibre.gui2.actions import AnnotationsAction, AddAction, DeleteAction, \ - EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, \ - ConvertAction, ViewAction class Listener(Thread): # {{{ @@ -91,16 +86,27 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, - SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, - AnnotationsAction, AddAction, DeleteAction, - EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, - ConvertAction, ViewAction): + SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin + ): 'The main GUI' def __init__(self, opts, parent=None): MainWindow.__init__(self, opts, parent) self.opts = opts + self.device_connected = None + acmap = {} + for action in interface_actions(): + mod, cls = action.actual_plugin.split(':') + ac = getattr(__import__(mod, fromlist=['1'], level=0), cls)(self, + action.site_customization) + if ac.name in acmap: + if ac.priority >= acmap[ac.name].priority: + acmap[ac.name] = ac + else: + acmap[ac.name] = ac + + self.iactions = acmap def initialize(self, library_path, db, listener, actions): opts = self.opts @@ -119,6 +125,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.another_instance_wants_to_talk) self.check_messages_timer.start(1000) + for ac in self.iactions.values(): + ac.do_genesis() MainWindowMixin.__init__(self, db) # Jobs Button {{{ @@ -140,9 +148,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.verbose = opts.verbose self.get_metadata = GetMetadata() self.upload_memory = {} - self.delete_memory = {} - self.conversion_jobs = {} - self.persistent_files = [] self.metadata_dialogs = [] self.default_thumbnail = None self.tb_wrapper = textwrap.TextWrapper(width=40) @@ -166,22 +171,13 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ QIcon(I('eject.svg')), _('&Eject connected device')) self.eject_action.setEnabled(False) self.addAction(self.quit_action) - self.action_restart = QAction(_('&Restart'), self) - self.addAction(self.action_restart) self.system_tray_menu.addAction(self.quit_action) self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q)) - self.action_restart.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R)) - self.action_show_book_details.setShortcut(QKeySequence(Qt.Key_I)) - self.addAction(self.action_show_book_details) self.system_tray_icon.setContextMenu(self.system_tray_menu) self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit) self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate) self.connect(self.restore_action, SIGNAL('triggered()'), self.show_windows) - self.connect(self.action_show_book_details, - SIGNAL('triggered(bool)'), self.show_book_info) - self.connect(self.action_restart, SIGNAL('triggered()'), - self.restart) self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), self.system_tray_icon_activated) @@ -209,9 +205,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ if self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows() - for t in (self.tool_bar, ): - self.library_view.model().count_changed_signal.connect \ - (t.count_changed) + self.library_view.model().count_changed_signal.connect( + self.iactions['Choose Library'].count_changed) if not gprefs.get('quick_start_guide_added', False): from calibre.ebooks.metadata import MetaInformation mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember']) @@ -249,22 +244,21 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.start_content_server() self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) - AddAction.__init__(self) + self.read_settings() self.finalize_layout() self.donate_button.start_animation() - self.scheduler.delete_old_news.connect( - self.library_view.model().delete_books_by_id, - type=Qt.QueuedConnection) + self.iactions['Fetch News'].connect_scheduler() def start_content_server(self): from calibre.library.server.main import start_threaded_server from calibre.library.server import server_config self.content_server = start_threaded_server( self.library_view.model().db, server_config().parse()) - self.content_server.state_callback = Dispatcher(self.content_server_state_changed) + self.content_server.state_callback = Dispatcher( + self.iactions['Connect Share'].content_server_state_changed) self.content_server.state_callback(True) self.test_server_timer = QTimer.singleShot(10000, self.test_server) @@ -329,7 +323,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ if len(argv) > 1: path = os.path.abspath(argv[1]) if os.access(path, os.R_OK): - self.add_filesystem_book(path) + self.iactions['Add Books'].add_filesystem_book(path) self.setWindowState(self.windowState() & \ ~Qt.WindowMinimized|Qt.WindowActive) self.show_windows() @@ -357,41 +351,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db - def do_config(self, checked=False, initial_category='general'): - if self.job_manager.has_jobs(): - d = error_dialog(self, _('Cannot configure'), - _('Cannot configure while there are running jobs.')) - d.exec_() - return - if self.must_restart_before_config: - d = error_dialog(self, _('Cannot configure'), - _('Cannot configure before calibre is restarted.')) - d.exec_() - return - d = ConfigDialog(self, self.library_view, - server=self.content_server, initial_category=initial_category) - - d.exec_() - self.content_server = d.server - if self.content_server is not None: - self.content_server.state_callback = \ - Dispatcher(self.content_server_state_changed) - self.content_server.state_callback(self.content_server.is_running) - - if d.result() == d.Accepted: - self.read_toolbar_settings() - self.search.search_as_you_type(config['search_as_you_type']) - self.save_menu.actions()[2].setText( - _('Save only %s format to disk')% - prefs['output_format'].upper()) - self.save_menu.actions()[3].setText( - _('Save only %s format to disk in a single directory')% - prefs['output_format'].upper()) - self.tags_view.set_new_model() # in case columns changed - self.tags_view.recount() - self.create_device_menu() - self.set_device_menu_items_state(bool(self.device_connected)) - self.tool_bar.apply_settings() def library_moved(self, newloc): if newloc is None: return @@ -407,18 +366,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.saved_search.clear_to_help() self.book_details.reset_info() self.library_view.model().count_changed() - self.scheduler.database_changed(db) + self.iactions['Fetch News'].database_changed(db) prefs['library_path'] = self.library_path - def show_book_info(self, *args): - if self.current_view() is not self.library_view: - error_dialog(self, _('No detailed info available'), - _('No detailed information is available for books ' - 'on the device.')).exec_() - return - index = self.library_view.currentIndex() - if index.isValid(): - BookInfo(self, self.library_view, index).show() def location_selected(self, location): ''' @@ -430,26 +380,12 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ for x in ('tb', 'cb'): splitter = getattr(self, x+'_splitter') splitter.button.setEnabled(location == 'library') + for action in self.iactions.values(): + action.location_selected(location) if location == 'library': - self.action_edit.setEnabled(True) - self.action_merge.setEnabled(True) - self.action_convert.setEnabled(True) - self.view_menu.actions()[1].setEnabled(True) - self.action_open_containing_folder.setEnabled(True) - self.action_sync.setEnabled(True) self.search_restriction.setEnabled(True) - for action in list(self.delete_menu.actions())[1:]: - action.setEnabled(True) else: - self.action_edit.setEnabled(False) - self.action_merge.setEnabled(False) - self.action_convert.setEnabled(False) - self.view_menu.actions()[1].setEnabled(False) - self.action_open_containing_folder.setEnabled(False) - self.action_sync.setEnabled(False) self.search_restriction.setEnabled(False) - for action in list(self.delete_menu.actions())[1:]: - action.setEnabled(False) # Reset the view in case something changed while it was invisible self.current_view().reset() self.set_number_of_books_shown() @@ -504,7 +440,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ geometry = config['main_window_geometry'] if geometry is not None: self.restoreGeometry(geometry) - self.read_toolbar_settings() self.read_layout_settings() def write_settings(self): @@ -512,9 +447,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ dynamic.set('sort_history', self.library_view.model().sort_history) self.save_layout_state() - def restart(self): - self.quit(restart=True) - def quit(self, checked=True, restart=False): if not self.confirm_quit(): return diff --git a/src/calibre/manual/plugins.rst b/src/calibre/manual/plugins.rst index 26e544d766..8b6919db90 100644 --- a/src/calibre/manual/plugins.rst +++ b/src/calibre/manual/plugins.rst @@ -157,4 +157,11 @@ The base class for such devices is :class:`calibre.devices.usbms.driver.USBMS`. :members: :member-order: bysource +User Interface Actions +-------------------------- + +.. autoclass:: calibre.gui2.actions.InterfaceAction + :show-inheritance: + :members: + :member-order: bysource