From e243aeeafff8a3db15b1f53b5baf2d38b6c8a39c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 11 Aug 2010 21:36:26 -0600 Subject: [PATCH 01/26] Split gui actions into separate files --- src/calibre/gui2/actions/__init__.py | 1361 ------------------- src/calibre/gui2/actions/add.py | 241 ++++ src/calibre/gui2/actions/annotate.py | 235 ++++ src/calibre/gui2/actions/catalog.py | 69 + src/calibre/gui2/actions/convert.py | 148 ++ src/calibre/gui2/actions/delete.py | 165 +++ src/calibre/gui2/actions/edit_metadata.py | 311 +++++ src/calibre/gui2/actions/fetch_news.py | 39 + src/calibre/gui2/actions/save_to_disk.py | 95 ++ src/calibre/gui2/actions/view.py | 159 +++ src/calibre/gui2/add.py | 2 +- src/calibre/gui2/dialogs/metadata_single.py | 5 +- src/calibre/gui2/dialogs/progress.py | 8 +- 13 files changed, 1471 insertions(+), 1367 deletions(-) create mode 100644 src/calibre/gui2/actions/add.py create mode 100644 src/calibre/gui2/actions/annotate.py create mode 100644 src/calibre/gui2/actions/catalog.py create mode 100644 src/calibre/gui2/actions/convert.py create mode 100644 src/calibre/gui2/actions/delete.py create mode 100644 src/calibre/gui2/actions/edit_metadata.py create mode 100644 src/calibre/gui2/actions/fetch_news.py create mode 100644 src/calibre/gui2/actions/save_to_disk.py create mode 100644 src/calibre/gui2/actions/view.py diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index f70332d15a..96aaa843a0 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -5,1368 +5,7 @@ __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 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 -class AnnotationsAction(object): # {{{ - - 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 - - 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.device_manager.device - - 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 - - # 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) - - # Map ids to paths - path_map = generate_annotation_paths(ids, db, device) - - # 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 - - 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.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) - - 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.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) - 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) - - # }}} - -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): - ''' - Edit metadata of selected books in library. - ''' - rows = self.library_view.selectionModel().selectedRows() - previous = self.library_view.currentIndex() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), - _('No books selected')) - d.exec_() - return - - if bulk or (bulk is None and len(rows) > 1): - return self.edit_bulk_metadata(checked) - - def accepted(id): - self.library_view.model().refresh_ids([id]) - - for row in rows: - self._metadata_view_id = self.library_view.model().db.id(row.row()) - d = MetadataSingleDialog(self, row.row(), - self.library_view.model().db, - accepted_callback=accepted, - 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): - ''' - Edit metadata of selected books in library in bulk. - ''' - rows = [r.row() for r in \ - self.library_view.selectionModel().selectedRows()] - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), - _('No books selected')) - d.exec_() - return - if MetadataBulkDialog(self, rows, - self.library_view.model().db).changed: - self.library_view.model().resort(reset=False) - self.library_view.model().research() - 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) - - # }}} diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py new file mode 100644 index 0000000000..18a9a4224b --- /dev/null +++ b/src/calibre/gui2/actions/add.py @@ -0,0 +1,241 @@ +#!/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 + + +from calibre.gui2 import error_dialog, Dispatcher, 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 + + +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) + + diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py new file mode 100644 index 0000000000..22f42156dd --- /dev/null +++ b/src/calibre/gui2/actions/annotate.py @@ -0,0 +1,235 @@ +#!/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, Dispatcher, gprefs +from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString +from calibre import strftime + +class AnnotationsAction(object): + + 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 + + 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.device_manager.device + + 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 + + # 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) + + # Map ids to paths + path_map = generate_annotation_paths(ids, db, device) + + # 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 + + 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.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.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() + + diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py new file mode 100644 index 0000000000..f8c9b24b30 --- /dev/null +++ b/src/calibre/gui2/actions/catalog.py @@ -0,0 +1,69 @@ +#!/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, Dispatcher, choose_dir +from calibre.gui2.tools import generate_catalog +from calibre.utils.config import dynamic + +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) + + diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py new file mode 100644 index 0000000000..9d5f1da048 --- /dev/null +++ b/src/calibre/gui2/actions/convert.py @@ -0,0 +1,148 @@ +#!/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 PyQt4.Qt import QModelIndex + +from calibre.gui2 import error_dialog, Dispatcher +from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook +from calibre.utils.config import prefs + +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()) + diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py new file mode 100644 index 0000000000..e5c6000863 --- /dev/null +++ b/src/calibre/gui2/actions/delete.py @@ -0,0 +1,165 @@ +#!/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 import error_dialog +from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog +from calibre.gui2.dialogs.confirm_delete import confirm + +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) + 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) + diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py new file mode 100644 index 0000000000..b4f299f54c --- /dev/null +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -0,0 +1,311 @@ +#!/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 PyQt4.Qt import Qt, QTimer + +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 + +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._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.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): + ''' + Edit metadata of selected books in library. + ''' + rows = self.library_view.selectionModel().selectedRows() + previous = self.library_view.currentIndex() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot edit metadata'), + _('No books selected')) + d.exec_() + return + + if bulk or (bulk is None and len(rows) > 1): + return self.edit_bulk_metadata(checked) + + def accepted(id): + self.library_view.model().refresh_ids([id]) + + for row in rows: + self._metadata_view_id = self.library_view.model().db.id(row.row()) + d = MetadataSingleDialog(self, row.row(), + self.library_view.model().db, + accepted_callback=accepted, + cancel_all=rows.index(row) < len(rows)-1) + d.view_format.connect(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): + ''' + Edit metadata of selected books in library in bulk. + ''' + rows = [r.row() for r in \ + self.library_view.selectionModel().selectedRows()] + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot edit metadata'), + _('No books selected')) + d.exec_() + return + if MetadataBulkDialog(self, rows, + self.library_view.model().db).changed: + self.library_view.model().resort(reset=False) + self.library_view.model().research() + 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() + + diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py new file mode 100644 index 0000000000..c051f362f1 --- /dev/null +++ b/src/calibre/gui2/actions/fetch_news.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 import Dispatcher +from calibre.gui2.tools import fetch_scheduled_recipe +from calibre.utils.config import dynamic + +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() + + 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..fb1ca58a3c --- /dev/null +++ b/src/calibre/gui2/actions/save_to_disk.py @@ -0,0 +1,95 @@ +#!/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 calibre.utils.config import prefs +from calibre.gui2 import error_dialog, Dispatcher, \ + choose_dir, warning_dialog, open_local_file + + +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) + + diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py new file mode 100644 index 0000000000..3ca7a807d7 --- /dev/null +++ b/src/calibre/gui2/actions/view.py @@ -0,0 +1,159 @@ +#!/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 PyQt4.Qt import Qt + +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 + +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_() == 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, _('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) + + diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py index 74f5a2148e..b646d4ac79 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.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/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..2369575e90 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 = 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.emit() def keyPressEvent(self, ev): if ev.key() == Qt.Key_Escape: From 5d96982933205dff5859c8274b2a25dfe0d4c164 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 11 Aug 2010 22:38:32 -0600 Subject: [PATCH 02/26] Plumbing for InterfaceAction plugins --- src/calibre/customize/__init__.py | 3 +++ src/calibre/customize/ui.py | 15 +++++++++++++-- src/calibre/gui2/__init__.py | 16 +++++++++++----- src/calibre/gui2/actions/__init__.py | 17 +++++++++++++++++ src/calibre/gui2/actions/add.py | 8 +++++--- src/calibre/manual/plugins.rst | 7 +++++++ 6 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 1348da5e5a..ea11259da2 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -351,3 +351,6 @@ class CatalogPlugin(Plugin): # Default implementation does nothing raise NotImplementedError('CatalogPlugin.generate_catalog() default ' 'method, should be overridden in subclass') + +class InterfaceActionBase(Plugin): + pass 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 96aaa843a0..5c1f2d1535 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -6,6 +6,23 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +from calibre.customize import InterfaceActionBase +class InterfaceAction(InterfaceActionBase): + supported_platforms = ['windows', 'osx', 'linux'] + author = 'Kovid Goyal' + type = _('User Interface Action') + + positions = frozenset([]) + separators = frozenset([]) + + def do_genesis(self, gui): + self.gui = gui + self.genesis() + + # Subclassable methods {{{ + def genesis(self): + raise NotImplementedError() + # }}} diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 18a9a4224b..b7be89360f 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -17,12 +17,14 @@ 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): -class AddAction(object): + def genesis(self): + self._add_filesystem_book = Dispatcher(self.__add_filesystem_book, + parent=self.gui) - 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', 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 From 82dc900a11bc9c6de9c7966deb41964ac6a01971 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 11:09:47 -0600 Subject: [PATCH 03/26] Migrated Add Books action --- src/calibre/gui2/actions/__init__.py | 31 +++++++- src/calibre/gui2/actions/add.py | 108 ++++++++++++++++----------- src/calibre/gui2/init.py | 7 +- src/calibre/gui2/layout.py | 16 ---- src/calibre/gui2/main.py | 2 +- src/calibre/gui2/ui.py | 2 +- 6 files changed, 98 insertions(+), 68 deletions(-) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 5c1f2d1535..fc5f3bd9b8 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -5,8 +5,12 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +from functools import partial + +from PyQt4.Qt import QToolButton, QAction, QIcon from calibre.customize import InterfaceActionBase +from calibre.gui2 import Dispatcher class InterfaceAction(InterfaceActionBase): @@ -17,12 +21,31 @@ class InterfaceAction(InterfaceActionBase): positions = frozenset([]) separators = frozenset([]) + popup_type = QToolButton.MenuPopup + + #: Of the form: (text, icon_path, tooltip, keyboard shortcut) + #: tooltip and keybard shortcut can be None + #: shortcut must be a translated string if not None + action_spec = ('text', 'icon', None, None) + def do_genesis(self, gui): self.gui = gui + self.Dispatcher = partial(Dispatcher, parent=gui) + self.create_action() self.genesis() - # Subclassable methods {{{ - def genesis(self): - raise NotImplementedError() - # }}} + def create_action(self): + text, icon, tooltip, shortcut = self.action_spec + action = QAction(QIcon(I(icon)), text, self) + text = tooltip if tooltip else text + action.setToolTip(text) + action.setStatusTip(text) + action.setWhatsThis(text) + action.setAutoRepeat(False) + if shortcut: + action.setShortcut(shortcut) + self.qaction = action + + def genesis(self): + pass diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index b7be89360f..8c6b627d5c 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -8,10 +8,10 @@ __docformat__ = 'restructuredtext en' import os from functools import partial -from PyQt4.Qt import QInputDialog, QPixmap +from PyQt4.Qt import QInputDialog, QPixmap, QMenu -from calibre.gui2 import error_dialog, Dispatcher, choose_files, \ +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 @@ -21,20 +21,41 @@ from calibre.gui2.actions import InterfaceAction class AddAction(InterfaceAction): + name = 'Add Books' + action_spec = (_('Add books'), 'add_book.svg', None, _('A')) + positions = frozenset([ + ('toolbar', 'all', 0), + ]) + def genesis(self): - self._add_filesystem_book = Dispatcher(self.__add_filesystem_book, - parent=self.gui) + 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 add_recursive(self, single): - root = choose_dir(self, 'recursive book import root dir dialog', + 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, - self.library_view.model().db, - Dispatcher(self._files_added), spare_server=self.spare_server) + 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): @@ -56,13 +77,13 @@ class AddAction(InterfaceAction): 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?'), + 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.library_view.model().db.import_book(MetaInformation(None), []) - self.library_view.model().books_added(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 @@ -70,21 +91,21 @@ class AddAction(InterfaceAction): 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) + ids.add(self.gui.library_view.model().db.import_book(mi, [])) + self.gui.library_view.model().books_added(len(isbns)) + self.gui.do_download_metadata(ids) def files_dropped(self, paths): - to_device = self.stack.currentIndex() != 0 + 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.current_view() is not self.library_view: + if self.gui.current_view() is not self.gui.library_view: return - db = self.library_view.model().db - current_idx = self.library_view.currentIndex() + 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: @@ -102,7 +123,7 @@ class AddAction(InterfaceAction): accept = True if accept: event.accept() - self.library_view.model().current_changed(current_idx, current_idx) + self.gui.library_view.model().current_changed(current_idx, current_idx) def __add_filesystem_book(self, paths, allow_device=True): if isinstance(paths, basestring): @@ -111,10 +132,10 @@ class AddAction(InterfaceAction): os.R_OK)] if books: - to_device = allow_device and self.stack.currentIndex() != 0 + to_device = allow_device and self.gui.stack.currentIndex() != 0 self._add_books(books, to_device) if to_device: - self.status_bar.show_message(\ + self.gui.status_bar.show_message(\ _('Uploading books to device.'), 2000) @@ -123,7 +144,7 @@ class AddAction(InterfaceAction): def add_from_isbn(self, *args): from calibre.gui2.dialogs.add_from_isbn import AddFromISBN - d = AddFromISBN(self) + d = AddFromISBN(self.gui) if d.exec_() == d.Accepted: self.add_isbns(d.isbns) @@ -144,11 +165,11 @@ class AddAction(InterfaceAction): (_('Comics'), ['cbz', 'cbr', 'cbc']), (_('Archives'), ['zip', 'rar']), ] - to_device = self.stack.currentIndex() != 0 + to_device = self.gui.stack.currentIndex() != 0 if to_device: - filters = [(_('Supported books'), self.device_manager.device.FORMATS)] + filters = [(_('Supported books'), self.gui.device_manager.device.FORMATS)] - books = choose_files(self, 'add books dialog dir', 'Select books', + books = choose_files(self.gui, 'add books dialog dir', 'Select books', filters=filters) if not books: return @@ -156,32 +177,33 @@ class AddAction(InterfaceAction): 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 + 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, - None if to_device else self.library_view.model().db, - Dispatcher(self.__adder_func), spare_server=self.spare_server) + 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.upload_books(paths, + self.gui.upload_books(paths, list(map(ascii_filename, names)), infos, on_card=on_card) - self.status_bar.show_message( + self.gui.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() + 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, _('Merged some 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): @@ -191,7 +213,7 @@ class AddAction(InterfaceAction): name = name.decode(filesystem_encoding, 'replace') det_msg.append(name+'\n'+log) - warning_dialog(self, _('Failed to read metadata'), + warning_dialog(self.gui, _('Failed to read metadata'), _('Failed to read metadata from the following')+':', det_msg='\n\n'.join(det_msg), show=True) @@ -205,17 +227,17 @@ class AddAction(InterfaceAction): # 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) + 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, _('Add to library'), _('No book selected')) + 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.device_manager.device.VIRTUAL_BOOK_EXTENSIONS + 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 @@ -223,21 +245,21 @@ class AddAction(InterfaceAction): 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'), + 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, _('Add to library'), _('No book files found')) + 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, self.library_view.model().db, - Dispatcher(self.__adder_func), spare_server=self.spare_server) + 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/init.py b/src/calibre/gui2/init.py index a3ae5b77aa..7f8ad2d23c 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -63,7 +63,8 @@ class LibraryViewMixin(object): # {{{ add_to_library = None, edit_device_collections=None, similar_menu=similar_menu) - add_to_library = (_('Add books to library'), self.add_books_from_device) + add_to_library = (_('Add books to library'), + self.iactions['Add Books'].add_books_from_device) edit_device_collections = (_('Manage collections'), partial(self.edit_device_collections, oncard=None)) @@ -89,7 +90,7 @@ class LibraryViewMixin(object): # {{{ 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)), @@ -305,7 +306,7 @@ 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.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book) self.book_details.open_containing_folder.connect(self.view_folder_for_id) self.book_details.view_specific_format.connect(self.view_format_by_id) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 8a919c59f5..05763db658 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -528,22 +528,6 @@ class MainWindowMixin(object): 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) 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..cca6a378d1 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -329,7 +329,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() From 206547021d8f1bf69b67a78d1111023f2fdfb861 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 12:12:16 -0600 Subject: [PATCH 04/26] Migrate Fetch Annotations action and refactor plugins so as to delay load the gui components --- src/calibre/customize/__init__.py | 8 +++++- src/calibre/customize/builtins.py | 12 ++++++++ src/calibre/gui2/actions/__init__.py | 26 ++++++++++------- src/calibre/gui2/actions/annotate.py | 43 +++++++++++++++++----------- src/calibre/gui2/device.py | 3 +- src/calibre/gui2/ui.py | 28 +++++++++++------- 6 files changed, 80 insertions(+), 40 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index ea11259da2..88c9324239 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -353,4 +353,10 @@ class CatalogPlugin(Plugin): 'method, should be overridden in subclass') class InterfaceActionBase(Plugin): - pass + + 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..d71d934e2c 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -574,3 +574,15 @@ 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 = 'action_add' + actual_plugin = 'calibre.gui2.actions.add:AddAction' + +class ActionFetchAnnotations(InterfaceActionBase): + name = 'action_fetch_annotations' + actual_plugin = 'calibre.gui2.actions.annotate:FetchAnnotationsAction' + +plugins += [ActionAdd, ActionFetchAnnotations] diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index fc5f3bd9b8..74a8cbe5f5 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -7,17 +7,14 @@ __docformat__ = 'restructuredtext en' from functools import partial -from PyQt4.Qt import QToolButton, QAction, QIcon +from PyQt4.Qt import QToolButton, QAction, QIcon, QObject -from calibre.customize import InterfaceActionBase from calibre.gui2 import Dispatcher -class InterfaceAction(InterfaceActionBase): - - supported_platforms = ['windows', 'osx', 'linux'] - author = 'Kovid Goyal' - type = _('User Interface Action') +class InterfaceAction(QObject): + name = 'Implement me' + priority = 1 positions = frozenset([]) separators = frozenset([]) @@ -28,15 +25,22 @@ class InterfaceAction(InterfaceActionBase): #: shortcut must be a translated string if not None action_spec = ('text', 'icon', None, None) - def do_genesis(self, gui): - self.gui = gui - self.Dispatcher = partial(Dispatcher, parent=gui) + def __init__(self, parent, site_customization): + QObject.__init__(self, parent) + self.gui = parent + self.site_customization = site_customization + + def do_genesis(self): + self.Dispatcher = partial(Dispatcher, parent=self) self.create_action() self.genesis() def create_action(self): text, icon, tooltip, shortcut = self.action_spec - action = QAction(QIcon(I(icon)), text, self) + if icon is not None: + action = QAction(QIcon(I(icon)), text, self.gui) + else: + action = QAction(text, self.gui) text = tooltip if tooltip else text action.setToolTip(text) action.setStatusTip(text) diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py index 22f42156dd..e4c45742c2 100644 --- a/src/calibre/gui2/actions/annotate.py +++ b/src/calibre/gui2/actions/annotate.py @@ -9,19 +9,26 @@ import os, datetime from PyQt4.Qt import pyqtSignal, QModelIndex, QThread, Qt -from calibre.gui2 import error_dialog, Dispatcher, gprefs +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 AnnotationsAction(object): +class FetchAnnotationsAction(InterfaceAction): + + name = 'Fetch Annotations' + action_spec = (_('Fetch Annotations'), 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.library_view.selectionModel().selectedRows() + rows = self.gui.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) + rows = xrange(self.gui.library_view.model().rowCount(QModelIndex())) + ids = map(self.gui.library_view.model().id, rows) return ids def get_formats(id): @@ -42,18 +49,18 @@ class AnnotationsAction(object): path_map[id] = dict(path=a_path, fmts=get_formats(id)) return path_map - device = self.device_manager.device + device = self.gui.device_manager.device - if self.current_view() is not self.library_view: - return error_dialog(self, _('Use library only'), + 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.library_view.model().db + 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, _('No books selected'), + return error_dialog(self.gui, _('No books selected'), _('No books selected to fetch annotations from'), show=True) @@ -61,7 +68,7 @@ class AnnotationsAction(object): path_map = generate_annotation_paths(ids, db, device) # Dispatch to devices.kindle.driver.get_annotations() - self.device_manager.annotations(Dispatcher(self.annotations_fetched), + self.gui.device_manager.annotations(self.Dispatcher(self.annotations_fetched), path_map) def annotations_fetched(self, job): @@ -70,7 +77,7 @@ class AnnotationsAction(object): from calibre.gui2.dialogs.progress import ProgressDialog from calibre.library.cli import do_add_format - class Updater(QThread): + class Updater(QThread): # {{{ update_progress = pyqtSignal(int) update_done = pyqtSignal() @@ -220,16 +227,18 @@ class AnnotationsAction(object): 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'), + 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.library_view.model().db + db = self.gui.library_view.model().db - self.__annotation_updater = Updater(self, db, job.result, - Dispatcher(self.library_view.model().refresh_ids)) + 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/device.py b/src/calibre/gui2/device.py index 789ae68723..0f83afa6dd 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -652,7 +652,8 @@ class DeviceMixin(object): # {{{ 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) if self.device_connected: self.share_conn_menu.connect_to_folder_action.setEnabled(False) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index cca6a378d1..63b1e72de4 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -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 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 @@ -33,16 +35,11 @@ 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 +88,26 @@ 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 + 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 @@ -120,6 +127,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.check_messages_timer.start(1000) MainWindowMixin.__init__(self, db) + for ac in self.iactions.values(): + ac.do_genesis() # Jobs Button {{{ self.job_manager = JobManager() @@ -249,7 +258,6 @@ 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() From cb07d093e25fddef5f569577c920a1cd33788740 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 17:59:51 -0600 Subject: [PATCH 05/26] Migrate catalog and convert actions. And remove layout spec from plugins --- src/calibre/customize/builtins.py | 15 +++- src/calibre/gui2/actions/__init__.py | 5 +- src/calibre/gui2/actions/add.py | 3 - src/calibre/gui2/actions/annotate.py | 2 +- src/calibre/gui2/actions/catalog.py | 38 +++++----- src/calibre/gui2/actions/convert.py | 99 ++++++++++++++++---------- src/calibre/gui2/actions/fetch_news.py | 3 + src/calibre/gui2/device.py | 8 +-- src/calibre/gui2/layout.py | 12 ---- src/calibre/gui2/ui.py | 1 - 10 files changed, 103 insertions(+), 83 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d71d934e2c..8462ae5d38 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -578,11 +578,20 @@ plugins += input_profiles + output_profiles from calibre.customize import InterfaceActionBase class ActionAdd(InterfaceActionBase): - name = 'action_add' + name = 'Add Books' actual_plugin = 'calibre.gui2.actions.add:AddAction' class ActionFetchAnnotations(InterfaceActionBase): - name = 'action_fetch_annotations' + name = 'Fetch Annotations' actual_plugin = 'calibre.gui2.actions.annotate:FetchAnnotationsAction' -plugins += [ActionAdd, ActionFetchAnnotations] +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' + +plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, + ActionConvert] diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 74a8cbe5f5..65d0078c50 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -16,17 +16,16 @@ class InterfaceAction(QObject): name = 'Implement me' priority = 1 positions = frozenset([]) - separators = frozenset([]) - popup_type = QToolButton.MenuPopup #: Of the form: (text, icon_path, tooltip, keyboard shortcut) - #: tooltip and keybard shortcut can be None + #: icon, tooltip and keybard shortcut can be None #: shortcut must be a translated string if not None action_spec = ('text', 'icon', None, None) def __init__(self, parent, site_customization): QObject.__init__(self, parent) + self.setObjectName(self.name) self.gui = parent self.site_customization = site_customization diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 8c6b627d5c..668af0957e 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -23,9 +23,6 @@ class AddAction(InterfaceAction): name = 'Add Books' action_spec = (_('Add books'), 'add_book.svg', None, _('A')) - positions = frozenset([ - ('toolbar', 'all', 0), - ]) def genesis(self): self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book) diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py index e4c45742c2..d8b7b829b2 100644 --- a/src/calibre/gui2/actions/annotate.py +++ b/src/calibre/gui2/actions/annotate.py @@ -17,7 +17,7 @@ from calibre.gui2.actions import InterfaceAction class FetchAnnotationsAction(InterfaceAction): name = 'Fetch Annotations' - action_spec = (_('Fetch Annotations'), None, None, None) + action_spec = (_('Fetch annotations (experimental)'), None, None, None) def genesis(self): pass diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py index f8c9b24b30..6807d7717e 100644 --- a/src/calibre/gui2/actions/catalog.py +++ b/src/calibre/gui2/actions/catalog.py @@ -9,58 +9,62 @@ import os, shutil from PyQt4.Qt import QModelIndex -from calibre.gui2 import error_dialog, Dispatcher, choose_dir +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(object): +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.library_view.selectionModel().selectedRows() + rows = self.gui.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) + 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, _('No books selected'), + 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, dbspec, ids, self.device_manager.device) + 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.job_manager.run_job( - Dispatcher(self.catalog_generated), func, args=args, + 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.status_bar.show_message(_('Generating %s catalog...')%fmt) + 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, _('No books found'), + return error_dialog(self.gui, _('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() + 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.status_bar.show_message(_('Catalog generated.'), 3000) - self.sync_catalogs() + 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, _('Export Catalog Directory'), + 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())) diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py index 9d5f1da048..ace877b315 100644 --- a/src/calibre/gui2/actions/convert.py +++ b/src/calibre/gui2/actions/convert.py @@ -6,133 +6,154 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os +from functools import partial -from PyQt4.Qt import QModelIndex +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 = {} -class ConvertAction(object): def auto_convert(self, book_ids, on_card, format): - previous = self.library_view.currentIndex() + previous = self.gui.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) + 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.library_view.currentIndex() + previous = self.gui.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) + 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.library_view.currentIndex() + previous = self.gui.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) + 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.library_view.currentIndex() + previous = self.gui.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) + 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.library_view.selectionModel().selectedRows()] + self.gui.library_view.selectionModel().selectedRows()] if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot convert'), + d = error_dialog(self.gui, _('Cannot convert'), _('No books selected')) d.exec_() return None - return [self.library_view.model().db.id(r) for r in rows] + 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.library_view.currentIndex() + previous = self.gui.library_view.currentIndex() rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] + 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, self.queue_convert_jobs, - self.library_view.model().db, book_ids, + 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, - self.library_view.model().db, book_ids, out_format=prefs['output_format']) + 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.status_bar.show_message(_('Starting conversion of %d book(s)') % + 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.job_manager.run_job(Dispatcher(converted_func), + 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.library_view.model().refresh_rows(rows) - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, previous) + 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.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) + 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.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) + 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.sync_news(send_ids=[book_id], do_auto_convert=False) + 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.sync_catalogs(send_ids=[book_id], do_auto_convert=False) + 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.job_exception(job) + self.gui.job_exception(job) return data = open(temp_files[-1].name, 'rb') - self.library_view.model().db.add_format(book_id, \ + self.gui.library_view.model().db.add_format(book_id, \ fmt, data, index_is_id=True) data.close() - self.status_bar.show_message(job.description + \ + self.gui.status_bar.show_message(job.description + \ (' completed'), 2000) finally: for f in temp_files: @@ -141,8 +162,8 @@ class ConvertAction(object): os.remove(f.name) except: pass - self.tags_view.recount() - if self.current_view() is self.library_view: - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, QModelIndex()) + self.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/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index c051f362f1..72c93937dc 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -11,6 +11,9 @@ from calibre.utils.config import dynamic class FetchNewsAction(object): + def genesis(self): + self.conversion_jobs = {} + def download_scheduled_recipe(self, arg): func, args, desc, fmt, temp_files = \ fetch_scheduled_recipe(arg) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 0f83afa6dd..6cc8e47aa5 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -925,7 +925,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) @@ -1027,7 +1027,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([])) @@ -1089,7 +1089,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 @@ -1208,7 +1208,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/layout.py b/src/calibre/gui2/layout.py index 05763db658..b0851c0b25 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -585,18 +585,6 @@ class MainWindowMixin(object): 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) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 63b1e72de4..79d16eeb5a 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -150,7 +150,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.get_metadata = GetMetadata() self.upload_memory = {} self.delete_memory = {} - self.conversion_jobs = {} self.persistent_files = [] self.metadata_dialogs = [] self.default_thumbnail = None From e09ecee42108652b745326dac30ce07a1d33c579 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 18:07:45 -0600 Subject: [PATCH 06/26] Automatic fixes --- src/calibre/gui2/actions/delete.py | 34 ++++++++--------- src/calibre/gui2/actions/edit_metadata.py | 46 +++++++++++------------ src/calibre/gui2/actions/fetch_news.py | 4 +- src/calibre/gui2/actions/save_to_disk.py | 10 ++--- src/calibre/gui2/actions/view.py | 26 ++++++------- 5 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index e5c6000863..de3c4d8868 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -13,19 +13,19 @@ 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() + fmts = self.gui.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() + rows = self.gui.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)) + return set(map(self.gui.library_view.model().id, rows)) def delete_selected_formats(self, *args): ids = self._get_selected_ids() @@ -37,11 +37,11 @@ class DeleteAction(object): return for id in ids: for fmt in fmts: - self.library_view.model().db.remove_format(id, fmt, + self.gui.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()) + 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.tags_view.recount() @@ -54,17 +54,17 @@ class DeleteAction(object): if fmts is None: return for id in ids: - bfmts = self.library_view.model().db.formats(id, index_is_id=True) + 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.library_view.model().db.remove_format(id, fmt, + self.gui.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()) + 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.tags_view.recount() @@ -113,16 +113,16 @@ class DeleteAction(object): 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()) + 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.current_view() + view = self.gui.current_view() rows = view.selectionModel().selectedRows() if not rows or len(rows) == 0: return diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index b4f299f54c..ad77a4e197 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -19,13 +19,13 @@ class EditMetadataAction(object): def download_metadata(self, checked, covers=True, set_metadata=True, set_social_metadata=None): - rows = self.library_view.selectionModel().selectedRows() + rows = self.gui.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 + 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, @@ -33,7 +33,7 @@ class EditMetadataAction(object): def do_download_metadata(self, ids, covers=True, set_metadata=True, set_social_metadata=None): - db = self.library_view.model().db + db = self.gui.library_view.model().db if set_social_metadata is None: get_social_metadata = config['get_social_metadata'] else: @@ -59,11 +59,11 @@ class EditMetadataAction(object): return self._book_metadata_download_check.stop() self.progress_indicator.stop() - cr = self.library_view.currentIndex().row() + cr = self.gui.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( + self.gui.library_view.model().refresh_ids( x.updated, cr) if self.cover_flow: self.cover_flow.dataChanged() @@ -83,8 +83,8 @@ class EditMetadataAction(object): ''' Edit metadata of selected books in library. ''' - rows = self.library_view.selectionModel().selectedRows() - previous = self.library_view.currentIndex() + rows = self.gui.library_view.selectionModel().selectedRows() + previous = self.gui.library_view.currentIndex() if not rows or len(rows) == 0: d = error_dialog(self, _('Cannot edit metadata'), _('No books selected')) @@ -95,12 +95,12 @@ class EditMetadataAction(object): return self.edit_bulk_metadata(checked) def accepted(id): - self.library_view.model().refresh_ids([id]) + self.gui.library_view.model().refresh_ids([id]) for row in rows: - self._metadata_view_id = self.library_view.model().db.id(row.row()) + self._metadata_view_id = self.gui.library_view.model().db.id(row.row()) d = MetadataSingleDialog(self, row.row(), - self.library_view.model().db, + self.gui.library_view.model().db, accepted_callback=accepted, cancel_all=rows.index(row) < len(rows)-1) d.view_format.connect(self.metadata_view_format) @@ -108,8 +108,8 @@ class EditMetadataAction(object): if d.cancel_all: break if rows: - current = self.library_view.currentIndex() - m = self.library_view.model() + current = self.gui.library_view.currentIndex() + m = self.gui.library_view.model() if self.cover_flow: self.cover_flow.dataChanged() m.current_changed(current, previous) @@ -120,16 +120,16 @@ class EditMetadataAction(object): Edit metadata of selected books in library in bulk. ''' rows = [r.row() for r in \ - self.library_view.selectionModel().selectedRows()] + self.gui.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.gui.library_view.model().db).changed: + self.gui.library_view.model().resort(reset=False) + self.gui.library_view.model().research() self.tags_view.recount() if self.cover_flow: self.cover_flow.dataChanged() @@ -141,7 +141,7 @@ class EditMetadataAction(object): ''' if self.stack.currentIndex() != 0: return - rows = self.library_view.selectionModel().selectedRows() + rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: return error_dialog(self, _('Cannot merge books'), _('No books selected'), show=True) @@ -186,22 +186,22 @@ class EditMetadataAction(object): for row in rows: if row.row() < rows[0].row(): dest_row -= 1 - ci = self.library_view.model().index(dest_row, 0) + ci = self.gui.library_view.model().index(dest_row, 0) if ci.isValid(): - self.library_view.setCurrentIndex(ci) + 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.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True, + 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.library_view.model() + m = self.gui.library_view.model() for i, row in enumerate(rows): id_ = m.id(row) if i == 0: @@ -216,10 +216,10 @@ class EditMetadataAction(object): 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) + self.gui.library_view.model().delete_books_by_id(ids_to_delete) def merge_metadata(self, dest_id, src_ids): - db = self.library_view.model().db + 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: diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index 72c93937dc..7e318fc2f1 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -29,8 +29,8 @@ class FetchNewsAction(object): 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() + 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) diff --git a/src/calibre/gui2/actions/save_to_disk.py b/src/calibre/gui2/actions/save_to_disk.py index fb1ca58a3c..5ce6addb1c 100644 --- a/src/calibre/gui2/actions/save_to_disk.py +++ b/src/calibre/gui2/actions/save_to_disk.py @@ -28,7 +28,7 @@ class SaveToDiskAction(object): single_format=prefs['output_format']) def save_to_disk(self, checked, single_dir=False, single_format=None): - rows = self.current_view().selectionModel().selectedRows() + rows = self.gui.current_view().selectionModel().selectedRows() if not rows or len(rows) == 0: return error_dialog(self, _('Cannot save to disk'), _('No books selected'), show=True) @@ -37,7 +37,7 @@ class SaveToDiskAction(object): if not path: return dpath = os.path.abspath(path).replace('/', os.sep) - lpath = self.library_view.model().db.library_path.replace('/', os.sep) + lpath = self.gui.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 ' @@ -45,7 +45,7 @@ class SaveToDiskAction(object): 'library. Save to disk is meant to export ' 'files from your calibre library elsewhere.'), show=True) - if self.current_view() is self.library_view: + 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() @@ -61,12 +61,12 @@ class SaveToDiskAction(object): opts.template = opts.template.split('/')[-1].strip() if not opts.template: opts.template = '{title} - {authors}' - self._saver = Saver(self, self.library_view.model().db, + self._saver = Saver(self, self.gui.library_view.model().db, Dispatcher(self._books_saved), rows, path, opts, spare_server=self.spare_server) else: - paths = self.current_view().model().paths(rows) + paths = self.gui.current_view().model().paths(rows) self.device_manager.save_books( Dispatcher(self.books_saved), paths, path) diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 3ca7a807d7..56eccf64b7 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -19,18 +19,18 @@ from calibre.ptempfile import PersistentTemporaryFile class ViewAction(object): def view_format(self, row, format): - fmt_path = self.library_view.model().db.format_abspath(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.library_view.model().db.format_abspath(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.library_view.model().db.\ + fmt_path = self.gui.library_view.model().db.\ format_abspath(self._metadata_view_id, fmt, index_is_id=True) if fmt_path: @@ -67,14 +67,14 @@ class ViewAction(object): self._launch_viewer(name, viewer, internal) def view_specific_format(self, triggered): - rows = self.library_view.selectionModel().selectedRows() + rows = self.gui.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(',') + formats = self.gui.library_view.model().db.formats(row).upper().split(',') d = ChooseFormatDialog(self, _('Choose the format to view'), formats) if d.exec_() == d.Accepted: format = d.format() @@ -91,7 +91,7 @@ class ViewAction(object): ) % num) def view_folder(self, *args): - rows = self.current_view().selectionModel().selectedRows() + rows = self.gui.current_view().selectionModel().selectedRows() if not rows or len(rows) == 0: d = error_dialog(self, _('Cannot open folder'), _('No book selected')) @@ -100,15 +100,15 @@ class ViewAction(object): if not self._view_check(len(rows)): return for row in rows: - path = self.library_view.model().db.abspath(row.row()) + path = self.gui.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) + path = self.gui.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() + rows = self.gui.current_view().selectionModel().selectedRows() self._view_books(rows) def view_specific_book(self, index): @@ -122,13 +122,13 @@ class ViewAction(object): if not self._view_check(len(rows)): return - if self.current_view() is self.library_view: + if self.gui.current_view() is self.gui.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) + formats = self.gui.library_view.model().db.formats(row) + title = self.gui.library_view.model().db.title(row) if not formats: error_dialog(self, _('Cannot view'), _('%s has no available formats.')%(title,), show=True) @@ -146,7 +146,7 @@ class ViewAction(object): if not in_prefs: self.view_format(row, formats[0]) else: - paths = self.current_view().model().paths(rows) + paths = self.gui.current_view().model().paths(rows) for path in paths: pt = PersistentTemporaryFile('_viewer_'+\ os.path.splitext(path)[1]) From 673dee204d0690e4b992b270752d9760b4cb6cc8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 18:22:01 -0600 Subject: [PATCH 07/26] More automatic migration --- src/calibre/gui2/actions/edit_metadata.py | 14 +++++++------- src/calibre/gui2/actions/fetch_news.py | 4 ++-- src/calibre/gui2/actions/save_to_disk.py | 8 ++++---- src/calibre/gui2/actions/view.py | 8 ++++---- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index ad77a4e197..5c323e609d 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -21,7 +21,7 @@ class EditMetadataAction(object): set_social_metadata=None): rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot download metadata'), + d = error_dialog(self.gui, _('Cannot download metadata'), _('No books selected')) d.exec_() return @@ -71,12 +71,12 @@ class EditMetadataAction(object): 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'), + 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, _('Error'), err, det_msg=x.tb).exec_() + error_dialog(self.gui, _('Error'), err, det_msg=x.tb).exec_() def edit_metadata(self, checked, bulk=None): @@ -86,7 +86,7 @@ class EditMetadataAction(object): rows = self.gui.library_view.selectionModel().selectedRows() previous = self.gui.library_view.currentIndex() if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), + d = error_dialog(self.gui, _('Cannot edit metadata'), _('No books selected')) d.exec_() return @@ -122,7 +122,7 @@ class EditMetadataAction(object): rows = [r.row() for r in \ self.gui.library_view.selectionModel().selectedRows()] if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), + d = error_dialog(self.gui, _('Cannot edit metadata'), _('No books selected')) d.exec_() return @@ -143,10 +143,10 @@ class EditMetadataAction(object): return rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: - return error_dialog(self, _('Cannot merge books'), + return error_dialog(self.gui, _('Cannot merge books'), _('No books selected'), show=True) if len(rows) < 2: - return error_dialog(self, _('Cannot merge books'), + 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) diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index 7e318fc2f1..361e3d63bd 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -21,7 +21,7 @@ class FetchNewsAction(object): 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) + 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) @@ -35,7 +35,7 @@ class FetchNewsAction(object): 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.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) self.email_news(id) self.sync_news() diff --git a/src/calibre/gui2/actions/save_to_disk.py b/src/calibre/gui2/actions/save_to_disk.py index 5ce6addb1c..39df0948d2 100644 --- a/src/calibre/gui2/actions/save_to_disk.py +++ b/src/calibre/gui2/actions/save_to_disk.py @@ -30,7 +30,7 @@ class SaveToDiskAction(object): 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, _('Cannot save to disk'), + return error_dialog(self.gui, _('Cannot save to disk'), _('No books selected'), show=True) path = choose_dir(self, 'save to disk dialog', _('Choose destination directory')) @@ -39,7 +39,7 @@ class SaveToDiskAction(object): dpath = os.path.abspath(path).replace('/', os.sep) lpath = self.gui.library_view.model().db.library_path.replace('/', os.sep) if dpath.startswith(lpath): - return error_dialog(self, _('Not allowed'), + 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 ' @@ -74,7 +74,7 @@ class SaveToDiskAction(object): def _books_saved(self, path, failures, error): self._saver = None if error: - return error_dialog(self, _('Error while saving'), + return error_dialog(self.gui, _('Error while saving'), _('There was an error while saving.'), error, show=True) if failures: @@ -82,7 +82,7 @@ class SaveToDiskAction(object): (title, '\n\t'.join(err.splitlines())) for title, err in failures] - warning_dialog(self, _('Could not save some books'), + 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) diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 56eccf64b7..bf445b40b2 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -69,7 +69,7 @@ class ViewAction(object): def view_specific_format(self, triggered): rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot view'), _('No book selected')) + d = error_dialog(self.gui, _('Cannot view'), _('No book selected')) d.exec_() return @@ -83,7 +83,7 @@ class ViewAction(object): def _view_check(self, num, max_=3): if num <= max_: return True - return question_dialog(self, _('Multiple Books Selected'), + 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 ' @@ -93,7 +93,7 @@ class ViewAction(object): def view_folder(self, *args): rows = self.gui.current_view().selectionModel().selectedRows() if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot open folder'), + d = error_dialog(self.gui, _('Cannot open folder'), _('No book selected')) d.exec_() return @@ -130,7 +130,7 @@ class ViewAction(object): formats = self.gui.library_view.model().db.formats(row) title = self.gui.library_view.model().db.title(row) if not formats: - error_dialog(self, _('Cannot view'), + error_dialog(self.gui, _('Cannot view'), _('%s has no available formats.')%(title,), show=True) continue From 020286b746241b54a09f9078203b6bab2560d30e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 18:50:57 -0600 Subject: [PATCH 08/26] Migrate delete action and location_selected code --- src/calibre/customize/builtins.py | 6 +- src/calibre/gui2/actions/__init__.py | 3 + src/calibre/gui2/actions/convert.py | 3 + src/calibre/gui2/actions/delete.py | 80 ++++++++++++++++------- src/calibre/gui2/actions/edit_metadata.py | 4 ++ src/calibre/gui2/actions/fetch_news.py | 4 ++ src/calibre/gui2/actions/view.py | 5 ++ src/calibre/gui2/device.py | 5 +- src/calibre/gui2/layout.py | 16 ----- src/calibre/gui2/ui.py | 13 +--- 10 files changed, 84 insertions(+), 55 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 8462ae5d38..8232a163e7 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -593,5 +593,9 @@ 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' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, - ActionConvert] + ActionConvert, ActionDelete] diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 65d0078c50..b198a69214 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -52,3 +52,6 @@ class InterfaceAction(QObject): def genesis(self): pass + def location_selected(self, loc): + pass + diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py index ace877b315..0641cc6a97 100644 --- a/src/calibre/gui2/actions/convert.py +++ b/src/calibre/gui2/actions/convert.py @@ -35,6 +35,9 @@ class ConvertAction(InterfaceAction): 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() diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index de3c4d8868..e0f3ae4d65 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -5,16 +5,46 @@ __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(object): +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) + d = SelectFormats([x.lower() for x in fmts], msg, parent=self.gui) if d.exec_() != d.Accepted: return None return d.selected_formats @@ -22,7 +52,7 @@ class DeleteAction(object): 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, err_title, _('No book selected')) + d = error_dialog(self.gui, err_title, _('No book selected')) d.exec_() return set([]) return set(map(self.gui.library_view.model().id, rows)) @@ -43,7 +73,7 @@ class DeleteAction(object): self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) if ids: - self.tags_view.recount() + self.gui.tags_view.recount() def delete_all_but_selected_formats(self, *args): ids = self._get_selected_ids() @@ -66,11 +96,11 @@ class DeleteAction(object): self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) if ids: - self.tags_view.recount() + self.gui.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'), + if not self.gui.device_manager.is_device_connected: + d = error_dialog(self.gui, _('Cannot delete books'), _('No device is connected')) d.exec_() return @@ -81,18 +111,18 @@ class DeleteAction(object): 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'))): + 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, _('No books 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, to_delete) + d = DeleteMatchingFromDeviceDialog(self.gui, to_delete) if d.exec_(): paths = {} ids = {} @@ -103,10 +133,10 @@ class DeleteAction(object): paths[model].append(path) ids[model].append(id) for model in paths: - job = self.remove_paths(paths[model]) + 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.status_bar.show_message(_('Deleting books from device.'), 1000) + self.gui.status_bar.show_message(_('Deleting books from device.'), 1000) def delete_covers(self, *args): ids = self._get_selected_ids() @@ -126,18 +156,18 @@ class DeleteAction(object): rows = view.selectionModel().selectedRows() if not rows or len(rows) == 0: return - if self.stack.currentIndex() == 0: + 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): + +'

', '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.memory_view, self.card_a_view, self.card_b_view): + 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) @@ -149,17 +179,17 @@ class DeleteAction(object): if not confirm('

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

', 'device_delete_books', self): + +'

', 'device_delete_books', self.gui): return - if self.stack.currentIndex() == 1: - view = self.memory_view - elif self.stack.currentIndex() == 2: - view = self.card_a_view + 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.card_b_view + view = self.gui.card_b_view paths = view.model().paths(rows) - job = self.remove_paths(paths) + job = self.gui.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) + self.gui.status_bar.show_message(_('Deleting books from device.'), 1000) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 5c323e609d..e4b2145da0 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -17,6 +17,10 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor class EditMetadataAction(object): + def location_selected(self, loc): + enabled = loc == 'library' + self.qaction.setEnabled(enabled) + def download_metadata(self, checked, covers=True, set_metadata=True, set_social_metadata=None): rows = self.gui.library_view.selectionModel().selectedRows() diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index 361e3d63bd..d161877cea 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -11,6 +11,10 @@ from calibre.utils.config import dynamic class FetchNewsAction(object): + def location_selected(self, loc): + enabled = loc == 'library' + self.qaction.setEnabled(enabled) + def genesis(self): self.conversion_jobs = {} diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index bf445b40b2..bdad55d142 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -18,6 +18,11 @@ from calibre.ptempfile import PersistentTemporaryFile class ViewAction(object): + 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: diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 6cc8e47aa5..1e716a85fe 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -792,8 +792,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) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index b0851c0b25..74da5f53d3 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -528,7 +528,6 @@ class MainWindowMixin(object): md.addSeparator() md.addAction(self.action_merge) - self.action_del.triggered.connect(self.delete_books) self.action_edit.triggered.connect(self.edit_metadata) self.action_merge.triggered.connect(self.merge_books) @@ -557,21 +556,6 @@ class MainWindowMixin(object): 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) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 79d16eeb5a..df0c9091b5 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -149,7 +149,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.verbose = opts.verbose self.get_metadata = GetMetadata() self.upload_memory = {} - self.delete_memory = {} self.persistent_files = [] self.metadata_dialogs = [] self.default_thumbnail = None @@ -437,26 +436,18 @@ 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() From e4b2729e78112945d4ebcdc08ad8080d86035a54 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 19:39:49 -0600 Subject: [PATCH 09/26] Edit metadata migrated --- src/calibre/customize/builtins.py | 6 +- src/calibre/gui2/actions/__init__.py | 8 +- src/calibre/gui2/actions/add.py | 2 +- src/calibre/gui2/actions/edit_metadata.py | 90 +++++++++++++++++------ src/calibre/gui2/actions/view.py | 7 +- src/calibre/gui2/init.py | 7 +- src/calibre/gui2/layout.py | 39 ---------- src/calibre/gui2/ui.py | 2 - 8 files changed, 88 insertions(+), 73 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 8232a163e7..bdb76a6066 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -597,5 +597,9 @@ class ActionDelete(InterfaceActionBase): name = 'Remove Books' actual_plugin = 'calibre.gui2.actions.delete:DeleteAction' +class ActionEditMetadata(InterfaceActionBase): + name = 'Edit Metadata' + actual_plugin = 'calibre.gui2.actions.delete:EditMetadataAction' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, - ActionConvert, ActionDelete] + ActionConvert, ActionDelete, ActionEditMetadata] diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index b198a69214..4798828074 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -34,8 +34,10 @@ class InterfaceAction(QObject): self.create_action() self.genesis() - def create_action(self): - text, icon, tooltip, shortcut = self.action_spec + 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: @@ -47,7 +49,7 @@ class InterfaceAction(QObject): action.setAutoRepeat(False) if shortcut: action.setShortcut(shortcut) - self.qaction = action + setattr(self, attr, action) def genesis(self): pass diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 668af0957e..9aa78298dc 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -90,7 +90,7 @@ class AddAction(InterfaceAction): 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.do_download_metadata(ids) + self.gui.iactions['Edit Metadata'].do_download_metadata(ids) def files_dropped(self, paths): diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index e4b2145da0..05b4bdf7fc 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -6,20 +6,64 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os +from functools import partial -from PyQt4.Qt import Qt, QTimer +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(object): +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): @@ -51,9 +95,9 @@ class EditMetadataAction(object): x = _('social metadata') else: x = _('covers') if covers and not set_metadata else _('metadata') - self.progress_indicator.start( + self.gui.progress_indicator.start( _('Downloading %s for %d book(s)')%(x, len(ids))) - self._book_metadata_download_check = QTimer(self) + 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) @@ -62,15 +106,15 @@ class EditMetadataAction(object): if self._download_book_metadata.is_alive(): return self._book_metadata_download_check.stop() - self.progress_indicator.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.cover_flow: - self.cover_flow.dataChanged() + 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()] @@ -102,22 +146,22 @@ class EditMetadataAction(object): self.gui.library_view.model().refresh_ids([id]) for row in rows: - self._metadata_view_id = self.gui.library_view.model().db.id(row.row()) - d = MetadataSingleDialog(self, row.row(), + 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.metadata_view_format) + 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.cover_flow: - self.cover_flow.dataChanged() + if self.gui.cover_flow: + self.gui.cover_flow.dataChanged() m.current_changed(current, previous) - self.tags_view.recount() + self.gui.tags_view.recount() def edit_bulk_metadata(self, checked): ''' @@ -130,20 +174,20 @@ class EditMetadataAction(object): _('No books selected')) d.exec_() return - if MetadataBulkDialog(self, rows, + 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.tags_view.recount() - if self.cover_flow: - self.cover_flow.dataChanged() + 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.stack.currentIndex() != 0: + if self.gui.stack.currentIndex() != 0: return rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: @@ -161,7 +205,7 @@ class EditMetadataAction(object): 'The second and subsequently selected books will not ' 'be deleted or changed.

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

', 'merge_books_safe', self): + +'

', 'merge_books_safe', self.gui): return self.add_formats(dest_id, src_books) self.merge_metadata(dest_id, src_ids) @@ -175,12 +219,12 @@ class EditMetadataAction(object): '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): + +'

', '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): + +'

', 'merge_too_many_books', self.gui): return self.add_formats(dest_id, src_books) self.merge_metadata(dest_id, src_ids) @@ -299,7 +343,7 @@ class EditMetadataAction(object): 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 = 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 @@ -309,7 +353,7 @@ class EditMetadataAction(object): 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) + self.gui.upload_collections(model.db, view=view, oncard=oncard) view.reset() diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index bdad55d142..4a6e545da6 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -18,6 +18,11 @@ from calibre.ptempfile import PersistentTemporaryFile class ViewAction(object): + name = 'View' + + def genesis(self): + self.metadata_view_id = None + def location_selected(self, loc): enabled = loc == 'library' for action in list(self.view_menu.actions())[1:]: @@ -36,7 +41,7 @@ class ViewAction(object): def metadata_view_format(self, fmt): fmt_path = self.gui.library_view.model().db.\ - format_abspath(self._metadata_view_id, + format_abspath(self.metadata_view_id, fmt, index_is_id=True) if fmt_path: self._view_file(fmt_path) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 7f8ad2d23c..359ea7465c 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -66,8 +66,9 @@ class LibraryViewMixin(object): # {{{ add_to_library = (_('Add books to library'), self.iactions['Add Books'].add_books_from_device) + edc = self.iactions['Edit Metadata'].edit_device_collections edit_device_collections = (_('Manage collections'), - partial(self.edit_device_collections, oncard=None)) + partial(edc, oncard=None)) self.memory_view.set_context_menu(None, None, None, self.action_view, self.action_save, None, None, self.action_del, None, @@ -75,7 +76,7 @@ class LibraryViewMixin(object): # {{{ edit_device_collections=edit_device_collections) edit_device_collections = (_('Manage collections'), - partial(self.edit_device_collections, oncard='carda')) + partial(edc, oncard='carda')) self.card_a_view.set_context_menu(None, None, None, self.action_view, self.action_save, None, None, self.action_del, None, @@ -83,7 +84,7 @@ class LibraryViewMixin(object): # {{{ edit_device_collections=edit_device_collections) edit_device_collections = (_('Manage collections'), - partial(self.edit_device_collections, oncard='cardb')) + partial(edc, oncard='cardb')) self.card_b_view.set_context_menu(None, None, None, self.action_view, self.action_save, None, None, self.action_del, None, diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 74da5f53d3..582818a593 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -459,10 +459,6 @@ class MainWindowMixin(object): 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')) @@ -473,7 +469,6 @@ class MainWindowMixin(object): 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'), @@ -497,39 +492,6 @@ class MainWindowMixin(object): 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.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() @@ -566,7 +528,6 @@ class MainWindowMixin(object): self.action_sync.triggered.connect( self._sync_action_triggered) - self.action_edit.setMenu(md) self.action_save.setMenu(self.save_menu) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index df0c9091b5..80165ec515 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -439,12 +439,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ for action in self.iactions.values(): action.location_selected(location) if location == 'library': - self.action_merge.setEnabled(True) self.action_open_containing_folder.setEnabled(True) self.action_sync.setEnabled(True) self.search_restriction.setEnabled(True) else: - self.action_merge.setEnabled(False) self.action_open_containing_folder.setEnabled(False) self.action_sync.setEnabled(False) self.search_restriction.setEnabled(False) From d9ac3a0e0ab574f8c73620b9c67f6cc349a686b4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 19:56:40 -0600 Subject: [PATCH 10/26] Migrate view action --- src/calibre/customize/builtins.py | 8 ++++++-- src/calibre/gui2/actions/view.py | 28 ++++++++++++++++++++-------- src/calibre/gui2/cover_flow.py | 2 +- src/calibre/gui2/init.py | 6 +++--- src/calibre/gui2/layout.py | 12 ++---------- src/calibre/gui2/ui.py | 1 - 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index bdb76a6066..215a78e862 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -599,7 +599,11 @@ class ActionDelete(InterfaceActionBase): class ActionEditMetadata(InterfaceActionBase): name = 'Edit Metadata' - actual_plugin = 'calibre.gui2.actions.delete:EditMetadataAction' + actual_plugin = 'calibre.gui2.actions.edit_metadata:EditMetadataAction' + +class ActionView(InterfaceActionBase): + name = 'View' + actual_plugin = 'calibre.gui2.actions.view:ViewAction' plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, - ActionConvert, ActionDelete, ActionEditMetadata] + ActionConvert, ActionDelete, ActionEditMetadata, ActionView] diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 4a6e545da6..22592be2fe 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -6,8 +6,9 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os, time +from functools import partial -from PyQt4.Qt import Qt +from PyQt4.Qt import Qt, QMenu from calibre.constants import isosx from calibre.gui2 import error_dialog, Dispatcher, question_dialog, config, \ @@ -15,13 +16,24 @@ from calibre.gui2 import error_dialog, Dispatcher, question_dialog, config, \ 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(object): +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' @@ -49,12 +61,12 @@ class ViewAction(object): def book_downloaded_for_viewing(self, job): if job.failed: - self.device_job_exception(job) + self.gui.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) + self.gui.setCursor(Qt.BusyCursor) try: if internal: args = [viewer] @@ -62,13 +74,13 @@ class ViewAction(object): args.append('--raise-window') if name is not None: args.append(name) - self.job_manager.launch_gui_app(viewer, + self.gui.job_manager.launch_gui_app(viewer, kwargs=dict(args=args)) else: open_local_file(name) time.sleep(2) # User feedback finally: - self.unsetCursor() + self.gui.unsetCursor() def _view_file(self, name): ext = os.path.splitext(name)[1].upper().replace('.', '') @@ -85,7 +97,7 @@ class ViewAction(object): row = rows[0].row() formats = self.gui.library_view.model().db.formats(row).upper().split(',') - d = ChooseFormatDialog(self, _('Choose the format to view'), formats) + d = ChooseFormatDialog(self.gui, _('Choose the format to view'), formats) if d.exec_() == d.Accepted: format = d.format() self.view_format(row, format) @@ -162,7 +174,7 @@ class ViewAction(object): os.path.splitext(path)[1]) self.persistent_files.append(pt) pt.close() - self.device_manager.view_book(\ + self.gui.device_manager.view_book(\ Dispatcher(self.book_downloaded_for_viewing), path, pt.name) diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index c72f53201f..d3ca6bcd81 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -122,7 +122,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/init.py b/src/calibre/gui2/init.py index 359ea7465c..839bfd536c 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -118,7 +118,7 @@ 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) @@ -308,8 +308,8 @@ class LayoutMixin(object): # {{{ 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.iactions['Add Books'].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.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 582818a593..f456201e39 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -13,7 +13,7 @@ from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \ QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \ QMenu, QUrl -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 @@ -510,18 +510,10 @@ class MainWindowMixin(object): 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.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_open_containing_folder.triggered.connect(self.iactions['View'].view_folder) self.action_sync.setShortcut(Qt.Key_D) self.action_sync.setEnabled(True) self.create_device_menu() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 80165ec515..f3fa6f743e 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -149,7 +149,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.verbose = opts.verbose self.get_metadata = GetMetadata() self.upload_memory = {} - self.persistent_files = [] self.metadata_dialogs = [] self.default_thumbnail = None self.tb_wrapper = textwrap.TextWrapper(width=40) From 5c9ec13a295cbc76618cebbca332c5f84c40c168 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 20:04:27 -0600 Subject: [PATCH 11/26] Migrated Fetch News --- src/calibre/customize/builtins.py | 7 ++++++- src/calibre/gui2/actions/fetch_news.py | 23 ++++++++++++++++------- src/calibre/gui2/layout.py | 7 ++----- src/calibre/gui2/ui.py | 1 + 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 215a78e862..d64f36adc9 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -605,5 +605,10 @@ 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' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, - ActionConvert, ActionDelete, ActionEditMetadata, ActionView] + ActionConvert, ActionDelete, ActionEditMetadata, ActionView, + ActionFetchNews] diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index d161877cea..a63ac6b4a3 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -8,8 +8,12 @@ __docformat__ = 'restructuredtext en' 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(object): +class FetchNewsAction(InterfaceAction): + + name = 'Fetch News' + action_spec = (_('Fetch news'), 'news.svg', None, _('F')) def location_selected(self, loc): enabled = loc == 'library' @@ -18,10 +22,15 @@ class FetchNewsAction(object): def genesis(self): self.conversion_jobs = {} + def connect_scheduler(self, scheduler): + self.qaction.setMenu(scheduler.news_menu) + self.qaction.triggered.connect( + scheduler.show_dialog) + def download_scheduled_recipe(self, arg): func, args, desc, fmt, temp_files = \ fetch_scheduled_recipe(arg) - job = self.job_manager.run_job( + 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) @@ -31,16 +40,16 @@ class FetchNewsAction(object): 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) + self.gui.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.scheduler.recipe_downloaded(arg) self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) - self.email_news(id) - self.sync_news() + self.gui.email_news(id) + self.gui.sync_news() diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index f456201e39..c5ed84ff00 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -427,7 +427,8 @@ class MainWindowMixin(object): def init_scheduler(self, db): self.scheduler = Scheduler(self, db) self.scheduler.start_recipe_fetch.connect( - self.download_scheduled_recipe, type=Qt.QueuedConnection) + self.iactions['Fetch News'].download_scheduled_recipe, type=Qt.QueuedConnection) + self.iactions['Fetch News'].connect_scheduler(self.scheduler) def read_toolbar_settings(self): pass @@ -462,7 +463,6 @@ class MainWindowMixin(object): 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')) @@ -482,9 +482,6 @@ class MainWindowMixin(object): 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, diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index f3fa6f743e..2ba2a4ec52 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -256,6 +256,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) + self.read_settings() self.finalize_layout() self.donate_button.start_animation() From 886db1b6a8c285470acaa28ae75941a027b5cd56 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 20:22:15 -0600 Subject: [PATCH 12/26] Migrate save to disk --- src/calibre/customize/builtins.py | 6 ++- src/calibre/gui2/actions/save_to_disk.py | 64 +++++++++++++++++++++--- src/calibre/gui2/layout.py | 38 -------------- src/calibre/gui2/ui.py | 7 +-- 4 files changed, 64 insertions(+), 51 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d64f36adc9..22730ba53f 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -609,6 +609,10 @@ 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' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, - ActionFetchNews] + ActionFetchNews, ActionSaveToDisk] diff --git a/src/calibre/gui2/actions/save_to_disk.py b/src/calibre/gui2/actions/save_to_disk.py index 39df0948d2..4cfcc4d692 100644 --- a/src/calibre/gui2/actions/save_to_disk.py +++ b/src/calibre/gui2/actions/save_to_disk.py @@ -6,13 +6,65 @@ __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(object): +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 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']) @@ -32,7 +84,7 @@ class SaveToDiskAction(object): 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, 'save to disk dialog', + path = choose_dir(self.gui, 'save to disk dialog', _('Choose destination directory')) if not path: return @@ -61,13 +113,13 @@ class SaveToDiskAction(object): opts.template = opts.template.split('/')[-1].strip() if not opts.template: opts.template = '{title} - {authors}' - self._saver = Saver(self, self.gui.library_view.model().db, + self._saver = Saver(self.gui, self.gui.library_view.model().db, Dispatcher(self._books_saved), rows, path, opts, - spare_server=self.spare_server) + spare_server=self.gui.spare_server) else: paths = self.gui.current_view().model().paths(rows) - self.device_manager.save_books( + self.gui.device_manager.save_books( Dispatcher(self.books_saved), paths, path) @@ -90,6 +142,6 @@ class SaveToDiskAction(object): def books_saved(self, job): if job.failed: - return self.device_job_exception(job) + return self.gui.device_job_exception(job) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index c5ed84ff00..fcd3be398b 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -19,28 +19,10 @@ from calibre.gui2.throbber import ThrobbingButton from calibre.gui2 import config, open_url, 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 -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 LocationManager(QObject): # {{{ @@ -463,9 +445,7 @@ class MainWindowMixin(object): 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(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')) @@ -490,23 +470,6 @@ class MainWindowMixin(object): self.action_help.triggered.connect(self.show_help) - 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_open_containing_folder.setShortcut(Qt.Key_O) self.addAction(self.action_open_containing_folder) @@ -517,7 +480,6 @@ class MainWindowMixin(object): self.action_sync.triggered.connect( self._sync_action_triggered) - self.action_save.setMenu(self.save_menu) pm = QMenu() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 2ba2a4ec52..581d02f9ab 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -387,13 +387,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ 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.iactions['Save To Disk'].reread_prefs() self.tags_view.recount() self.create_device_menu() self.set_device_menu_items_state(bool(self.device_connected)) From 72c2d636b0ffe2008082afd4cd18400c8e72ae40 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 22:34:40 -0600 Subject: [PATCH 13/26] Migrate device actions and other minor actions --- src/calibre/customize/builtins.py | 25 ++- src/calibre/gui2/actions/__init__.py | 1 + src/calibre/gui2/actions/device.py | 143 ++++++++++++++++++ src/calibre/gui2/actions/open.py | 24 +++ src/calibre/gui2/actions/restart.py | 22 +++ src/calibre/gui2/actions/show_book_details.py | 31 ++++ src/calibre/gui2/device.py | 16 +- src/calibre/gui2/init.py | 2 +- src/calibre/gui2/layout.py | 101 +------------ src/calibre/gui2/ui.py | 33 +--- 10 files changed, 255 insertions(+), 143 deletions(-) create mode 100644 src/calibre/gui2/actions/device.py create mode 100644 src/calibre/gui2/actions/open.py create mode 100644 src/calibre/gui2/actions/restart.py create mode 100644 src/calibre/gui2/actions/show_book_details.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 22730ba53f..aa0948e3f8 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -613,6 +613,29 @@ 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 = 'OpenFolder' + 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' + + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, - ActionFetchNews, ActionSaveToDisk] + ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, + ActionRestart, ActionOpenFolder, ActionConnectShare, + ActionSendToDevice, ] diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 4798828074..d5c6a0cf7e 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -32,6 +32,7 @@ class InterfaceAction(QObject): def do_genesis(self): self.Dispatcher = partial(Dispatcher, parent=self) self.create_action() + self.gui.addAction(self.qaction) self.genesis() def create_action(self, spec=None, attr='qaction'): diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py new file mode 100644 index 0000000000..f1cfd84235 --- /dev/null +++ b/src/calibre/gui2/actions/device.py @@ -0,0 +1,143 @@ +#!/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 + +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() + + 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/open.py b/src/calibre/gui2/actions/open.py new file mode 100644 index 0000000000..569f037327 --- /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.action_open_containing_folder.triggered.connect(self.iactions['View'].view_folder) + + def location_selected(self, loc): + enabled = loc == 'library' + self.qaction.setEnabled(enabled) + + 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/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/device.py b/src/calibre/gui2/device.py index 1e716a85fe..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,21 +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.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): @@ -697,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) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 839bfd536c..2b89057fc4 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -306,7 +306,7 @@ 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.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) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index fcd3be398b..72d548f287 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -20,7 +20,6 @@ from calibre.gui2 import config, open_url, gprefs from calibre.gui2.widgets import ComboBoxWithHelp from calibre import human_readable from calibre.gui2.dialogs.scheduler import Scheduler -from calibre.utils.smtp import config as email_config @@ -306,77 +305,6 @@ 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): @@ -442,17 +370,11 @@ class MainWindowMixin(object): setattr(self, 'action_'+name, action) all_actions.append(action) - 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(8, 8, 0, 'conn_share', _('Connect/share'), 'connect_share.svg') 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, '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'), @@ -462,24 +384,10 @@ class MainWindowMixin(object): ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'), 'tags.svg') - 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) - self.action_open_containing_folder.setShortcut(Qt.Key_O) - self.addAction(self.action_open_containing_folder) - self.action_open_containing_folder.triggered.connect(self.iactions['View'].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) - pm = QMenu() @@ -498,12 +406,5 @@ class MainWindowMixin(object): 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/ui.py b/src/calibre/gui2/ui.py index 581d02f9ab..c62a2d3fc8 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 @@ -35,7 +35,6 @@ 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.gui2.init import LibraryViewMixin, LayoutMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin @@ -172,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) @@ -270,7 +260,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ 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) @@ -381,7 +372,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.content_server = d.server if self.content_server is not None: self.content_server.state_callback = \ - Dispatcher(self.content_server_state_changed) + Dispatcher(self.iactions['Connect Share'].content_server_state_changed) self.content_server.state_callback(self.content_server.is_running) if d.result() == d.Accepted: @@ -411,15 +402,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.scheduler.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): ''' @@ -434,12 +416,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ for action in self.iactions.values(): action.location_selected(location) if location == 'library': - self.action_open_containing_folder.setEnabled(True) - self.action_sync.setEnabled(True) self.search_restriction.setEnabled(True) else: - self.action_open_containing_folder.setEnabled(False) - self.action_sync.setEnabled(False) self.search_restriction.setEnabled(False) # Reset the view in case something changed while it was invisible self.current_view().reset() @@ -503,9 +481,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 From 0ffaa50f6ddce98096dc548428eb2e0994a6fd11 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 22:50:53 -0600 Subject: [PATCH 14/26] ... --- src/calibre/gui2/actions/open.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/actions/open.py b/src/calibre/gui2/actions/open.py index 569f037327..40fc6372b7 100644 --- a/src/calibre/gui2/actions/open.py +++ b/src/calibre/gui2/actions/open.py @@ -15,7 +15,7 @@ class OpenFolderAction(InterfaceAction): _('O')) def genesis(self): - self.action_open_containing_folder.triggered.connect(self.iactions['View'].view_folder) + self.qaction.triggered.connect(self.gui.iactions['View'].view_folder) def location_selected(self, loc): enabled = loc == 'library' From bfb8cad41af7e867c876ce489513ff463254bad3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 23:18:31 -0600 Subject: [PATCH 15/26] Migrate help and prefs actions --- src/calibre/customize/builtins.py | 10 +++- src/calibre/gui2/actions/help.py | 25 ++++++++++ src/calibre/gui2/actions/preferences.py | 62 +++++++++++++++++++++++++ src/calibre/gui2/layout.py | 23 +-------- src/calibre/gui2/ui.py | 31 ------------- 5 files changed, 98 insertions(+), 53 deletions(-) create mode 100644 src/calibre/gui2/actions/help.py create mode 100644 src/calibre/gui2/actions/preferences.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index aa0948e3f8..83413b3d3d 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -633,9 +633,17 @@ 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' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionRestart, ActionOpenFolder, ActionConnectShare, - ActionSendToDevice, ] + ActionSendToDevice, ActionHelp, ActionPreferences] 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/preferences.py b/src/calibre/gui2/actions/preferences.py new file mode 100644 index 0000000000..b33c165316 --- /dev/null +++ b/src/calibre/gui2/actions/preferences.py @@ -0,0 +1,62 @@ +#!/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.read_toolbar_settings() + 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/layout.py b/src/calibre/gui2/layout.py index 72d548f287..8143c7355b 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -11,12 +11,12 @@ from functools import partial from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \ pyqtSignal, QToolButton, \ QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \ - QMenu, QUrl + QMenu 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.gui2.dialogs.scheduler import Scheduler @@ -372,8 +372,6 @@ class MainWindowMixin(object): ac(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png', tooltip=_('Choose calibre library to work with')) - 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, 'books_by_same_author', _('Books by same author'), 'user_profile.svg') @@ -385,26 +383,9 @@ class MainWindowMixin(object): 'tags.svg') - self.action_help.triggered.connect(self.show_help) - - - - - 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')) - diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index c62a2d3fc8..0fb52ad7b9 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -34,7 +34,6 @@ 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.init import LibraryViewMixin, LayoutMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin @@ -354,36 +353,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.iactions['Connect Share'].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.tags_view.set_new_model() # in case columns changed - self.iactions['Save To Disk'].reread_prefs() - 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 From 4635ae37d7143f43031c6caf8ab376543fc38d92 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 23:33:21 -0600 Subject: [PATCH 16/26] Migrate show similar books action --- src/calibre/customize/builtins.py | 6 ++- src/calibre/gui2/actions/__init__.py | 1 + src/calibre/gui2/actions/similar_books.py | 60 +++++++++++++++++++++++ src/calibre/gui2/init.py | 51 +------------------ src/calibre/gui2/layout.py | 9 ---- 5 files changed, 67 insertions(+), 60 deletions(-) create mode 100644 src/calibre/gui2/actions/similar_books.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 83413b3d3d..1e84570e54 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -641,9 +641,13 @@ 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' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionRestart, ActionOpenFolder, ActionConnectShare, - ActionSendToDevice, ActionHelp, ActionPreferences] + ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks] diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index d5c6a0cf7e..f2905885bf 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -51,6 +51,7 @@ class InterfaceAction(QObject): if shortcut: action.setShortcut(shortcut) setattr(self, attr, action) + return action def genesis(self): pass diff --git a/src/calibre/gui2/actions/similar_books.py b/src/calibre/gui2/actions/similar_books.py new file mode 100644 index 0000000000..9c3a52e694 --- /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/init.py b/src/calibre/gui2/init.py index 2b89057fc4..86452fecee 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, \ QSize, QSizePolicy, QStatusBar, QLabel, QFont from calibre.utils.config import prefs @@ -30,28 +30,6 @@ def partial(*args, **kwargs): 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')) self.library_view.set_context_menu(self.action_edit, self.action_sync, self.action_convert, self.action_view, @@ -122,33 +100,6 @@ class LibraryViewMixin(object): # {{{ - 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) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 8143c7355b..aaf94f7585 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -373,15 +373,6 @@ class MainWindowMixin(object): ac(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png', tooltip=_('Choose calibre library to work with')) - 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') - return all_actions From c02272593eae36f69fed1ae9531b67b600fe81ac Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 23:53:06 -0600 Subject: [PATCH 17/26] Migrate choose library and cleanup fetch news connections --- src/calibre/customize/builtins.py | 4 ++ src/calibre/gui2/actions/choose_library.py | 39 +++++++++++++++++ src/calibre/gui2/actions/fetch_news.py | 17 ++++++-- src/calibre/gui2/layout.py | 49 +--------------------- src/calibre/gui2/ui.py | 11 ++--- 5 files changed, 62 insertions(+), 58 deletions(-) create mode 100644 src/calibre/gui2/actions/choose_library.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 1e84570e54..b71e06ed38 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -645,6 +645,10 @@ 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' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py new file mode 100644 index 0000000000..3e5ddd163f --- /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')%0, 'lt.png', + _('Choose calibre library to work with'), None) + + def genesis(self): + pass + + def location_selected(self, loc): + enabled = loc == 'library' + self.qaction.setEnabled(enabled) + self.qaction.triggered.connect(self.choose_library) + + 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/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index a63ac6b4a3..fbdbb3fd85 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -5,6 +5,8 @@ __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 @@ -22,10 +24,19 @@ class FetchNewsAction(InterfaceAction): def genesis(self): self.conversion_jobs = {} - def connect_scheduler(self, scheduler): - self.qaction.setMenu(scheduler.news_menu) + 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( - scheduler.show_dialog) + 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 = \ diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index aaf94f7585..4b99f2fbaa 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -19,7 +19,6 @@ from calibre.gui2.throbber import ThrobbingButton from calibre.gui2 import config, gprefs from calibre.gui2.widgets import ComboBoxWithHelp from calibre import human_readable -from calibre.gui2.dialogs.scheduler import Scheduler @@ -279,11 +278,6 @@ class ToolBar(QToolBar): # {{{ self.choose_action.setVisible(not showing_device) - def count_changed(self, new_count): - text = _('%d books')%new_count - a = self.choose_action - a.setText(text) - a.setToolTip(_('Choose calibre library to work with') + '\n\n' + text) def resizeEvent(self, ev): QToolBar.resizeEvent(self, ev) @@ -322,61 +316,20 @@ 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.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.iactions['Fetch News'].download_scheduled_recipe, type=Qt.QueuedConnection) - self.iactions['Fetch News'].connect_scheduler(self.scheduler) 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(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png', - tooltip=_('Choose calibre library to work with')) - - - - return all_actions - # }}} diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 0fb52ad7b9..266023bfaa 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -204,9 +204,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']) @@ -250,9 +249,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ 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 @@ -368,7 +365,7 @@ 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 From d605d6b340732fdd902a9b68e00efc96653ac546 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 23:54:15 -0600 Subject: [PATCH 18/26] ... --- src/calibre/gui2/actions/preferences.py | 1 - src/calibre/gui2/ui.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py index b33c165316..d047e51dac 100644 --- a/src/calibre/gui2/actions/preferences.py +++ b/src/calibre/gui2/actions/preferences.py @@ -49,7 +49,6 @@ class PreferencesAction(InterfaceAction): self.gui.content_server.state_callback(self.gui.content_server.is_running) if d.result() == d.Accepted: - self.gui.read_toolbar_settings() 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() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 266023bfaa..3b8040db94 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -439,7 +439,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): From e121e77b63d91317265d90501114f6ce0336c75e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Aug 2010 23:54:34 -0600 Subject: [PATCH 19/26] ... --- src/calibre/gui2/layout.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 4b99f2fbaa..c47e50faab 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -327,8 +327,6 @@ class MainWindowMixin(object): l.addWidget(self.search_bar) - def read_toolbar_settings(self): - pass From 9e10b0b71c074595cccae905cd08cc186c043cd9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Aug 2010 00:03:29 -0600 Subject: [PATCH 20/26] ... --- src/calibre/gui2/layout.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index c47e50faab..ffd8bfe4ea 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -8,7 +8,7 @@ __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 @@ -201,7 +201,7 @@ 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.setContextMenuPolicy(Qt.PreventContextMenu) self.setMovable(False) @@ -212,7 +212,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() @@ -296,9 +295,6 @@ class ToolBar(QToolBar): # {{{ # }}} -class Action(QAction): - pass - class MainWindowMixin(object): def __init__(self, db): @@ -319,7 +315,7 @@ class MainWindowMixin(object): 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) From d81e4c655b28f6dd1975e23969bc5792c613c214 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Aug 2010 19:14:06 -0600 Subject: [PATCH 21/26] Migrate toolbar and context menus to new plugin system --- src/calibre/customize/builtins.py | 12 +++- src/calibre/gui2/actions/add_to_library.py | 22 ++++++ src/calibre/gui2/actions/edit_collections.py | 29 ++++++++ src/calibre/gui2/init.py | 63 +++++++---------- src/calibre/gui2/layout.py | 71 +++++++++----------- src/calibre/gui2/library/views.py | 32 +-------- 6 files changed, 120 insertions(+), 109 deletions(-) create mode 100644 src/calibre/gui2/actions/add_to_library.py create mode 100644 src/calibre/gui2/actions/edit_collections.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index b71e06ed38..b99bbdc977 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -622,7 +622,7 @@ class ActionRestart(InterfaceActionBase): actual_plugin = 'calibre.gui2.actions.restart:RestartAction' class ActionOpenFolder(InterfaceActionBase): - name = 'OpenFolder' + name = 'Open Folder' actual_plugin = 'calibre.gui2.actions.open:OpenFolderAction' class ActionSendToDevice(InterfaceActionBase): @@ -649,9 +649,17 @@ 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] + ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, + ActionAddToLibrary, ActionEditCollections] 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/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/init.py b/src/calibre/gui2/init.py index b1dbc3edc5..458d3e4482 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 Qt, QStackedWidget, \ +from PyQt4.Qt import Qt, QStackedWidget, QMenu, \ QSize, QSizePolicy, QStatusBar, QLabel, QFont from calibre.utils.config import prefs @@ -27,47 +27,32 @@ 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): - - 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.iactions['Add Books'].add_books_from_device) - - edc = self.iactions['Edit Metadata'].edit_device_collections - edit_device_collections = (_('Manage collections'), - partial(edc, 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(edc, 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(edc, 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) + lm = QMenu(self) + def populate_menu(m, items): + for what in items: + if what is None: + lm.addSeparator() + elif what in self.iactions: + lm.addAction(self.iactions[what].qaction) + populate_menu(lm, LIBRARY_CONTEXT_MENU) + dm = QMenu(self) + populate_menu(dm, DEVICE_CONTEXT_MENU) + self.library_view.set_context_menu(lm) + for v in (self.memory_view, self.card_a_view, self.card_b_view): + v.set_context_menu(dm) self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection) for func, args in [ diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index ffd8bfe4ea..bf166e5fcf 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -5,7 +5,6 @@ __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, QToolBar, QSize, \ @@ -20,7 +19,18 @@ from calibre.gui2 import config, gprefs from calibre.gui2.widgets import ComboBoxWithHelp from calibre import human_readable +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', + ) +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): # {{{ @@ -203,6 +213,7 @@ class ToolBar(QToolBar): # {{{ def __init__(self, donate, location_manager, parent): QToolBar.__init__(self, parent) + self.gui = parent self.setContextMenuPolicy(Qt.PreventContextMenu) self.setMovable(False) self.setFloatable(False) @@ -237,46 +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.MenuPopup) + 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 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) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 7ccbc027f6..027e0e4706 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -389,37 +389,9 @@ 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): 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 def contextMenuEvent(self, event): self.context_menu.popup(event.globalPos()) From 4b8ba2c740be61caa5db1bc5935989fb1aef5621 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Aug 2010 19:58:27 -0600 Subject: [PATCH 22/26] Migration progressed to a point where calibre at least starts --- src/calibre/customize/builtins.py | 2 +- src/calibre/gui2/actions/__init__.py | 2 +- src/calibre/gui2/actions/choose_library.py | 4 ++-- src/calibre/gui2/actions/device.py | 3 +++ src/calibre/gui2/actions/similar_books.py | 4 ++-- src/calibre/gui2/init.py | 4 ++-- src/calibre/gui2/layout.py | 3 +-- src/calibre/gui2/ui.py | 5 +++-- 8 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index b99bbdc977..cd2896f232 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -662,4 +662,4 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionRestart, ActionOpenFolder, ActionConnectShare, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, - ActionAddToLibrary, ActionEditCollections] + ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary] diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index f2905885bf..7dfdd31f37 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -16,7 +16,7 @@ class InterfaceAction(QObject): name = 'Implement me' priority = 1 positions = frozenset([]) - popup_type = QToolButton.MenuPopup + popup_type = QToolButton.MenuButtonPopup #: Of the form: (text, icon_path, tooltip, keyboard shortcut) #: icon, tooltip and keybard shortcut can be None diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 3e5ddd163f..0688080c5d 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -10,11 +10,11 @@ from calibre.gui2.actions import InterfaceAction class ChooseLibraryAction(InterfaceAction): name = 'Choose Library' - action_spec = (_('%d books')%0, 'lt.png', + action_spec = (_('%d books'), 'lt.png', _('Choose calibre library to work with'), None) def genesis(self): - pass + self.count_changed(0) def location_selected(self, loc): enabled = loc == 'library' diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index f1cfd84235..f8b2e751f1 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -11,6 +11,7 @@ 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): # {{{ @@ -30,6 +31,8 @@ class ShareConnMenu(QMenu): # {{{ 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')), diff --git a/src/calibre/gui2/actions/similar_books.py b/src/calibre/gui2/actions/similar_books.py index 9c3a52e694..5ce74fd2dc 100644 --- a/src/calibre/gui2/actions/similar_books.py +++ b/src/calibre/gui2/actions/similar_books.py @@ -21,8 +21,8 @@ class SimilarBooksAction(InterfaceAction): 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'))]: + (_('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) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 458d3e4482..1af8f28063 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -44,9 +44,9 @@ class LibraryViewMixin(object): # {{{ def populate_menu(m, items): for what in items: if what is None: - lm.addSeparator() + m.addSeparator() elif what in self.iactions: - lm.addAction(self.iactions[what].qaction) + m.addAction(self.iactions[what].qaction) populate_menu(lm, LIBRARY_CONTEXT_MENU) dm = QMenu(self) populate_menu(dm, DEVICE_CONTEXT_MENU) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index bf166e5fcf..6c87fe9da3 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -258,7 +258,7 @@ class ToolBar(QToolBar): # {{{ elif what == 'Location Manager': for ac in self.location_manager.available_actions: self.addAction(ac) - self.setup_tool_button(ac, QToolButton.MenuPopup) + 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: @@ -293,7 +293,6 @@ class ToolBar(QToolBar): # {{{ class MainWindowMixin(object): def __init__(self, db): - self.device_connected = None self.setObjectName('MainWindow') self.setWindowIcon(QIcon(I('library.png'))) self.setWindowTitle(__appname__) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 3b8040db94..9e4b4f3865 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -24,7 +24,7 @@ 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 import interface_actions +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 @@ -94,6 +94,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ 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(':') @@ -124,9 +125,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.another_instance_wants_to_talk) self.check_messages_timer.start(1000) - MainWindowMixin.__init__(self, db) for ac in self.iactions.values(): ac.do_genesis() + MainWindowMixin.__init__(self, db) # Jobs Button {{{ self.job_manager = JobManager() From f5ead896082b9653444c9f916d78d804f44f33af Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Aug 2010 20:29:07 -0600 Subject: [PATCH 23/26] ... --- src/calibre/gui2/actions/choose_library.py | 2 +- src/calibre/gui2/actions/fetch_news.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 0688080c5d..30e2f79424 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -15,11 +15,11 @@ class ChooseLibraryAction(InterfaceAction): 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) - self.qaction.triggered.connect(self.choose_library) def count_changed(self, new_count): text = self.action_spec[0]%new_count diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index fbdbb3fd85..bf44f5ab0e 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -51,14 +51,14 @@ class FetchNewsAction(InterfaceAction): temp_files, fmt, arg = self.conversion_jobs.pop(job) pt = temp_files[0] if job.failed: - self.gui.scheduler.recipe_download_failed(arg) + 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.gui.scheduler.recipe_downloaded(arg) + self.scheduler.recipe_downloaded(arg) self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) self.gui.email_news(id) self.gui.sync_news() From 0214d576d41094ca88f51e68ccf5c31d52e6aea7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Aug 2010 21:03:16 -0600 Subject: [PATCH 24/26] Various minor fixes --- src/calibre/gui2/actions/add.py | 4 ++++ src/calibre/gui2/actions/annotate.py | 2 +- src/calibre/gui2/actions/save_to_disk.py | 10 ++++++++-- src/calibre/gui2/add.py | 2 +- src/calibre/gui2/dialogs/progress.py | 4 ++-- src/calibre/gui2/init.py | 5 +++-- src/calibre/gui2/library/views.py | 10 ++++++---- 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 9aa78298dc..deb03021c5 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -43,6 +43,10 @@ class AddAction(InterfaceAction): 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', diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py index d8b7b829b2..5356d63e98 100644 --- a/src/calibre/gui2/actions/annotate.py +++ b/src/calibre/gui2/actions/annotate.py @@ -91,7 +91,7 @@ class FetchAnnotationsAction(InterfaceAction): self.am = annotation_map self.done_callback = done_callback - self.pd.canceled.connect(self.canceled) + self.pd.canceled_signal.connect(self.canceled) self.pd.setModal(True) self.pd.show() self.update_progress.connect(self.pd.set_value, diff --git a/src/calibre/gui2/actions/save_to_disk.py b/src/calibre/gui2/actions/save_to_disk.py index 4cfcc4d692..fcd7c4e332 100644 --- a/src/calibre/gui2/actions/save_to_disk.py +++ b/src/calibre/gui2/actions/save_to_disk.py @@ -58,6 +58,11 @@ class SaveToDiskAction(InterfaceAction): 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')% @@ -88,8 +93,9 @@ class SaveToDiskAction(InterfaceAction): _('Choose destination directory')) if not path: return - dpath = os.path.abspath(path).replace('/', os.sep) - lpath = self.gui.library_view.model().db.library_path.replace('/', os.sep) + 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 ' diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py index b646d4ac79..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.pd.canceled.connect(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/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py index 2369575e90..40404050ec 100644 --- a/src/calibre/gui2/dialogs/progress.py +++ b/src/calibre/gui2/dialogs/progress.py @@ -11,7 +11,7 @@ from calibre.gui2.dialogs.progress_ui import Ui_Dialog class ProgressDialog(QDialog, Ui_Dialog): - canceled = pyqtSignal() + canceled_signal = pyqtSignal() def __init__(self, title, msg='', min=0, max=99, parent=None): QDialog.__init__(self, parent) @@ -52,7 +52,7 @@ class ProgressDialog(QDialog, Ui_Dialog): self.canceled = True self.button_box.setDisabled(True) self.title.setText(_('Aborting...')) - self.canceled.emit() + 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 1af8f28063..73de77b13b 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -50,9 +50,10 @@ class LibraryViewMixin(object): # {{{ populate_menu(lm, LIBRARY_CONTEXT_MENU) dm = QMenu(self) populate_menu(dm, DEVICE_CONTEXT_MENU) - self.library_view.set_context_menu(lm) + 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) + v.set_context_menu(dm, ec) self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection) for func, args in [ diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 027e0e4706..389208fdcd 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -389,9 +389,10 @@ class BooksView(QTableView): # {{{ #}}} # Context Menu {{{ - def set_context_menu(self, menu): + def set_context_menu(self, menu, edit_collections_action): self.setContextMenuPolicy(Qt.DefaultContextMenu) self.context_menu = menu + self.edit_collections_action = edit_collections_action def contextMenuEvent(self, event): self.context_menu.popup(event.globalPos()) @@ -500,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() From 7eac6b269721c889bb4d178269c340ff26ab2955 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Aug 2010 21:25:49 -0600 Subject: [PATCH 25/26] Disable auto repeat on all interface actions be default --- src/calibre/gui2/actions/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 7dfdd31f37..236bb32233 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -17,6 +17,7 @@ class InterfaceAction(QObject): priority = 1 positions = frozenset([]) popup_type = QToolButton.MenuButtonPopup + auto_repeat = False #: Of the form: (text, icon_path, tooltip, keyboard shortcut) #: icon, tooltip and keybard shortcut can be None @@ -43,6 +44,7 @@ class InterfaceAction(QObject): action = QAction(QIcon(I(icon)), text, self.gui) else: action = QAction(text, self.gui) + action.setAutoRepeat(self.auto_repeat) text = tooltip if tooltip else text action.setToolTip(text) action.setStatusTip(text) From 359c89417c9e01789bf3487e69a671671348fc03 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Aug 2010 21:39:46 -0600 Subject: [PATCH 26/26] InterfaceAction documentation --- src/calibre/gui2/actions/__init__.py | 51 ++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 236bb32233..c4d755aaea 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -13,14 +13,49 @@ from calibre.gui2 import Dispatcher class InterfaceAction(QObject): + ''' + 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. + + 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. + + If two :class:`InterfaceAction` objects have the same name, the one with higher + priority takes precedence. + + Sub-classes should implement the :meth:`genesis` and + :meth:`location_selected` methods. + + 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:: + + self.gui.iactions['Save To Disk'] + + The QAction specified by :attr:`action_spec` is automatically create and + made available as ``self.qaction``. + + ''' + + #: The plugin name. If two plugins with the same name are present, the one + #: with higher priority takes precedence. name = 'Implement me' + + #: The plugin priority. If two plugins with the same name are present, the one + #: with higher priority takes precedence. priority = 1 - positions = frozenset([]) + + #: The menu popup type for when this plugin is added to a toolbar popup_type = QToolButton.MenuButtonPopup + + #: Whether this action should be auto repeated when its shortcut + #: key is held down. auto_repeat = False #: Of the form: (text, icon_path, tooltip, keyboard shortcut) - #: icon, tooltip and keybard shortcut can be None + #: icon, tooltip and keyboard shortcut can be None #: shortcut must be a translated string if not None action_spec = ('text', 'icon', None, None) @@ -56,8 +91,20 @@ class InterfaceAction(QObject): return action def genesis(self): + ''' + Setup this plugin. Only called once during initialization. self.gui is + available. The action secified by :attr:`action_spec` is available as + ``self.qaction``. + ''' pass def location_selected(self, loc): + ''' + 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. + ''' pass