def show_context_menu(self, point): idx = self.table.indexAt(point) if idx.column() != 0: return m = self.au_context_menu = QMenu(self) item = self.table.itemAt(point) disable_copy_paste_search = len( self.table.selectedItems()) != 1 or item.is_deleted ca = m.addAction(_('Copy')) ca.triggered.connect(partial(self.copy_to_clipboard, item)) ca.setIcon(QIcon(I('edit-copy.png'))) if disable_copy_paste_search: ca.setEnabled(False) ca = m.addAction(_('Paste')) ca.setIcon(QIcon(I('edit-paste.png'))) ca.triggered.connect(partial(self.paste_from_clipboard, item)) if disable_copy_paste_search: ca.setEnabled(False) ca = m.addAction(_('Undo')) ca.setIcon(QIcon(I('edit-undo.png'))) ca.triggered.connect(self.undo_edit) ca.setEnabled(False) for item in self.table.selectedItems(): if (item.text() != self.original_names[int( item.data(Qt.ItemDataRole.UserRole))] or item.is_deleted): ca.setEnabled(True) break ca = m.addAction(_('Edit')) ca.setIcon(QIcon(I('edit_input.png'))) ca.triggered.connect(self.rename_tag) ca = m.addAction(_('Delete')) ca.setIcon(QIcon(I('trash.png'))) ca.triggered.connect(self.delete_tags) item_name = str(item.text()) ca = m.addAction(_('Search for {}').format(item_name)) ca.setIcon(QIcon(I('search.png'))) ca.triggered.connect(partial(self.set_search_text, item_name)) item_name = str(item.text()) ca = m.addAction(_('Filter by {}').format(item_name)) ca.setIcon(QIcon(I('filter.png'))) ca.triggered.connect(partial(self.set_filter_text, item_name)) if self.category is not None: ca = m.addAction(_("Search the library for {0}").format(item_name)) ca.setIcon(QIcon(I('lt.png'))) ca.triggered.connect(partial(self.search_for_books, item)) if disable_copy_paste_search: ca.setEnabled(False) if self.table.state() == QAbstractItemView.State.EditingState: m.addSeparator() case_menu = QMenu(_('Change case')) case_menu.setIcon(QIcon(I('font_size_larger.png'))) action_upper_case = case_menu.addAction(_('Upper case')) action_lower_case = case_menu.addAction(_('Lower case')) action_swap_case = case_menu.addAction(_('Swap case')) action_title_case = case_menu.addAction(_('Title case')) action_capitalize = case_menu.addAction(_('Capitalize')) action_upper_case.triggered.connect( partial(self.do_case, icu_upper)) action_lower_case.triggered.connect( partial(self.do_case, icu_lower)) action_swap_case.triggered.connect( partial(self.do_case, self.swap_case)) action_title_case.triggered.connect( partial(self.do_case, titlecase)) action_capitalize.triggered.connect( partial(self.do_case, capitalize)) m.addMenu(case_menu) m.exec(self.table.mapToGlobal(point))
def details_context_menu_event(view, ev, book_info, add_popup_action=False, edit_metadata=None): url = view.anchorAt(ev.pos()) menu = QMenu(view) copy_menu = menu.addMenu(QIcon(I('edit-copy.png')), _('Copy')) copy_menu.addAction(QIcon(I('edit-copy.png')), _('All book details'), partial(copy_all, view)) if view.textCursor().hasSelection(): copy_menu.addAction(QIcon(I('edit-copy.png')), _('Selected text'), view.copy) copy_menu.addSeparator() copy_links_added = False search_internet_added = False search_menu = QMenu(_('Search'), menu) search_menu.setIcon(QIcon(I('search.png'))) if url and url.startswith('action:'): data = json_loads(from_hex_bytes(url.split(':', 1)[1])) search_internet_added = add_item_specific_entries( menu, data, book_info, copy_menu, search_menu) create_copy_links(copy_menu, data) copy_links_added = True elif url and not url.startswith('#'): ac = book_info.copy_link_action ac.current_url = url ac.setText(_('Copy link location')) menu.addAction(ac) if not copy_links_added: create_copy_links(copy_menu) if not search_internet_added and hasattr(book_info, 'search_internet'): sim = create_search_internet_menu(book_info.search_internet) if search_menu.isEmpty(): search_menu = sim else: search_menu.addSeparator() for ac in sim.actions(): search_menu.addAction(ac) ac.setText(_('Search {0} for this book').format(ac.text())) if not search_menu.isEmpty(): menu.addMenu(search_menu) for ac in tuple(menu.actions()): if not ac.isEnabled(): menu.removeAction(ac) menu.addSeparator() from calibre.gui2.ui import get_gui if add_popup_action: ema = get_gui().iactions['Show Book Details'].menuless_qaction menu.addAction( _('Open the Book details window') + '\t' + ema.shortcut().toString(QKeySequence.SequenceFormat.NativeText), book_info.show_book_info) else: ema = get_gui().iactions['Edit Metadata'].menuless_qaction menu.addAction( _('Open the Edit metadata window') + '\t' + ema.shortcut().toString(QKeySequence.SequenceFormat.NativeText), edit_metadata) if len(menu.actions()) > 0: menu.exec(ev.globalPos())
class ChooseLibraryAction(InterfaceAction): name = 'Choose Library' action_spec = (_('Choose library'), 'lt.png', _('Choose calibre library to work with'), None) dont_add_to = frozenset(('context-menu-device', )) action_add_menu = True action_menu_clone_qaction = _('Switch/create library') restore_view_state = pyqtSignal(object) rebuild_change_library_menus = pyqtSignal() def genesis(self): self.prev_lname = self.last_lname = '' self.count_changed(0) self.action_choose = self.menuless_qaction self.action_exim = ac = QAction(_('Export/import all calibre data'), self.gui) ac.triggered.connect(self.exim_data) self.stats = LibraryUsageStats() self.popup_type = (QToolButton.ToolButtonPopupMode.InstantPopup if len(self.stats.stats) > 1 else QToolButton.ToolButtonPopupMode.MenuButtonPopup) if len(self.stats.stats) > 1: self.action_choose.triggered.connect(self.choose_library) else: self.qaction.triggered.connect(self.choose_library) self.choose_menu = self.qaction.menu() ac = self.create_action(spec=(_('Pick a random book'), 'random.png', None, None), attr='action_pick_random') ac.triggered.connect(self.pick_random) self.choose_library_icon_menu = QMenu( _('Change the icon for this library')) self.choose_library_icon_menu.setIcon(QIcon(I('icon_choose.png'))) self.choose_library_icon_action = self.create_action( spec=(_('Choose an icon'), 'icon_choose.png', None, None), attr='action_choose_library_icon') self.remove_library_icon_action = self.create_action( spec=(_('Remove current icon'), 'trash.png', None, None), attr='action_remove_library_icon') self.choose_library_icon_action.triggered.connect( self.get_library_icon) self.remove_library_icon_action.triggered.connect( partial(self.remove_library_icon, '')) self.choose_library_icon_menu.addAction( self.choose_library_icon_action) self.choose_library_icon_menu.addAction( self.remove_library_icon_action) self.original_library_icon = library_qicon.default_icon = self.qaction.icon( ) if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): self.choose_menu.addAction(self.action_choose) self.quick_menu = QMenu(_('Quick switch')) self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu) self.choose_menu.addMenu(self.choose_library_icon_menu) self.rename_menu = QMenu(_('Rename library')) self.rename_menu_action = self.choose_menu.addMenu( self.rename_menu) self.choose_menu.addAction(ac) self.delete_menu = QMenu(_('Remove library')) self.delete_menu_action = self.choose_menu.addMenu( self.delete_menu) self.vl_to_apply_menu = QMenu('waiting ...') self.vl_to_apply_action = self.choose_menu.addMenu( self.vl_to_apply_menu) self.rebuild_change_library_menus.connect( self.build_menus, type=Qt.ConnectionType.QueuedConnection) self.choose_menu.addAction(self.action_exim) else: self.choose_menu.addMenu(self.choose_library_icon_menu) self.choose_menu.addAction(ac) self.rename_separator = self.choose_menu.addSeparator() self.switch_actions = [] for i in range(5): ac = self.create_action(spec=('', None, None, None), attr='switch_action%d' % i) ac.setObjectName(str(i)) self.switch_actions.append(ac) ac.setVisible(False) connect_lambda( ac.triggered, self, lambda self: self.switch_requested(self.qs_locations[int( self.gui.sender().objectName())]), type=Qt.ConnectionType.QueuedConnection) self.choose_menu.addAction(ac) self.rename_separator = self.choose_menu.addSeparator() self.maintenance_menu = QMenu(_('Library maintenance')) ac = self.create_action(spec=(_('Library metadata backup status'), 'lt.png', None, None), attr='action_backup_status') ac.triggered.connect(self.backup_status, type=Qt.ConnectionType.QueuedConnection) self.maintenance_menu.addAction(ac) ac = self.create_action(spec=(_('Check library'), 'lt.png', None, None), attr='action_check_library') ac.triggered.connect(self.check_library, type=Qt.ConnectionType.QueuedConnection) self.maintenance_menu.addAction(ac) ac = self.create_action(spec=(_('Restore database'), 'lt.png', None, None), attr='action_restore_database') ac.triggered.connect(self.restore_database, type=Qt.ConnectionType.QueuedConnection) self.maintenance_menu.addAction(ac) self.choose_menu.addMenu(self.maintenance_menu) self.view_state_map = {} self.restore_view_state.connect( self._restore_view_state, type=Qt.ConnectionType.QueuedConnection) ac = self.create_action(spec=(_('Switch to previous library'), 'lt.png', None, None), attr='action_previous_library') ac.triggered.connect(self.switch_to_previous_library, type=Qt.ConnectionType.QueuedConnection) self.gui.keyboard.register_shortcut(self.unique_name + '-' + 'action_previous_library', ac.text(), action=ac, group=self.action_spec[0], default_keys=('Ctrl+Alt+p', )) self.gui.addAction(ac) @property def preserve_state_on_switch(self): ans = getattr(self, '_preserve_state_on_switch', None) if ans is None: self._preserve_state_on_switch = ans = \ self.gui.library_view.preserve_state(require_selected_ids=False) return ans def pick_random(self, *args): self.gui.iactions['Pick Random Book'].pick_random() def get_library_icon(self): try: paths = choose_images( self.gui, 'choose_library_icon', _('Select icon for library "%s"') % current_library_name()) if paths: path = paths[0] p = QIcon(path).pixmap(QSize(256, 256)) icp = library_icon_path() os.makedirs(os.path.dirname(icp), exist_ok=True) with open(icp, 'wb') as f: f.write(pixmap_to_data(p, format='PNG')) self.set_library_icon() library_qicon.cache_clear() except Exception: import traceback traceback.print_exc() def rename_library_icon(self, old_name, new_name): old_path = library_icon_path(old_name) new_path = library_icon_path(new_name) try: if os.path.exists(old_path): os.replace(old_path, new_path) library_qicon.cache_clear() except Exception: import traceback traceback.print_exc() def remove_library_icon(self, name=''): try: with suppress(FileNotFoundError): os.remove(library_icon_path(name or current_library_name())) self.set_library_icon() library_qicon.cache_clear() except Exception: import traceback traceback.print_exc() def set_library_icon(self): icon = QIcon(library_icon_path()) has_icon = not icon.isNull() and len(icon.availableSizes()) > 0 if not has_icon: icon = self.original_library_icon self.qaction.setIcon(icon) self.gui.setWindowIcon(icon) self.remove_library_icon_action.setEnabled(has_icon) def exim_data(self): if isportable: return error_dialog( self.gui, _('Cannot export/import'), _('You are running calibre portable, all calibre data is already in the' ' calibre portable folder. Export/import is unavailable.'), show=True) if self.gui.job_manager.has_jobs(): return error_dialog( self.gui, _('Cannot export/import'), _('Cannot export/import data while there are running jobs.'), show=True) from calibre.gui2.dialogs.exim import EximDialog d = EximDialog(parent=self.gui) if d.exec() == QDialog.DialogCode.Accepted: if d.restart_needed: self.gui.iactions['Restart'].restart() def library_name(self): db = self.gui.library_view.model().db path = db.library_path if isbytestring(path): path = path.decode(filesystem_encoding) path = path.replace(os.sep, '/') return self.stats.pretty(path) def update_tooltip(self, count): tooltip = self.action_spec[2] + '\n\n' + ngettext( '{0} [{1} book]', '{0} [{1} books]', count).format( getattr(self, 'last_lname', ''), count) a = self.qaction a.setToolTip(tooltip) a.setStatusTip(tooltip) a.setWhatsThis(tooltip) def library_changed(self, db): lname = self.stats.library_used(db) if lname != self.last_lname: self.prev_lname = self.last_lname self.last_lname = lname if len(lname) > 16: lname = lname[:16] + '…' a = self.qaction a.setText(lname.replace( '&', '&&&')) # I have no idea why this requires a triple ampersand self.update_tooltip(db.count()) self.build_menus() self.set_library_icon() state = self.view_state_map.get( self.stats.canonicalize_path(db.library_path), None) if state is not None: self.restore_view_state.emit(state) def _restore_view_state(self, state): self.preserve_state_on_switch.state = state def initialization_complete(self): self.library_changed(self.gui.library_view.model().db) set_change_library_action_plugin(self) def switch_to_previous_library(self): db = self.gui.library_view.model().db locations = list(self.stats.locations(db)) for name, loc in locations: is_prev_lib = name == self.prev_lname if is_prev_lib: self.switch_requested(loc) break def build_menus(self): if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): return db = self.gui.library_view.model().db lname = self.stats.library_used(db) self.vl_to_apply_action.setText( _('Apply Virtual library when %s is opened') % lname) locations = list(self.stats.locations(db)) for ac in self.switch_actions: ac.setVisible(False) self.quick_menu.clear() self.rename_menu.clear() self.delete_menu.clear() quick_actions, rename_actions, delete_actions = [], [], [] for name, loc in locations: is_prev_lib = name == self.prev_lname ic = library_qicon(name) name = name.replace('&', '&&') ac = self.quick_menu.addAction( ic, name, Dispatcher(partial(self.switch_requested, loc))) ac.setStatusTip(_('Switch to: %s') % loc) if is_prev_lib: f = ac.font() f.setBold(True) ac.setFont(f) quick_actions.append(ac) ac = self.rename_menu.addAction( name, Dispatcher(partial(self.rename_requested, name, loc))) rename_actions.append(ac) ac.setStatusTip(_('Rename: %s') % loc) ac = self.delete_menu.addAction( name, Dispatcher(partial(self.delete_requested, name, loc))) delete_actions.append(ac) ac.setStatusTip(_('Remove: %s') % loc) if is_prev_lib: ac.setFont(f) qs_actions = [] locations_by_frequency = locations if len(locations) >= tweaks['many_libraries']: locations_by_frequency = list( self.stats.locations(db, limit=sys.maxsize)) for i, x in enumerate( locations_by_frequency[:len(self.switch_actions)]): name, loc = x ic = library_qicon(name) name = name.replace('&', '&&') ac = self.switch_actions[i] ac.setText(name) ac.setIcon(ic) ac.setStatusTip(_('Switch to: %s') % loc) ac.setVisible(True) qs_actions.append(ac) self.qs_locations = [i[1] for i in locations_by_frequency] self.quick_menu_action.setVisible(bool(locations)) self.rename_menu_action.setVisible(bool(locations)) self.delete_menu_action.setVisible(bool(locations)) self.gui.location_manager.set_switch_actions(quick_actions, rename_actions, delete_actions, qs_actions, self.action_choose) # VL at startup self.vl_to_apply_menu.clear() restrictions = sorted(db.prefs['virtual_libraries'], key=sort_key) # check that the virtual library choice still exists vl_at_startup = db.prefs['virtual_lib_on_startup'] if vl_at_startup and vl_at_startup not in restrictions: vl_at_startup = db.prefs['virtual_lib_on_startup'] = '' restrictions.insert(0, '') for vl in restrictions: if vl == vl_at_startup: self.vl_to_apply_menu.addAction( QIcon(I('ok.png')), vl if vl else _('No Virtual library'), Dispatcher(partial(self.change_vl_at_startup_requested, vl))) else: self.vl_to_apply_menu.addAction( vl if vl else _('No Virtual library'), Dispatcher(partial(self.change_vl_at_startup_requested, vl))) # Allow the cloned actions in the OS X global menubar to update for a in (self.qaction, self.menuless_qaction): a.changed.emit() def change_vl_at_startup_requested(self, vl): self.gui.library_view.model().db.prefs['virtual_lib_on_startup'] = vl self.build_menus() def location_selected(self, loc): enabled = loc == 'library' self.qaction.setEnabled(enabled) self.menuless_qaction.setEnabled(enabled) def rename_requested(self, name, location): LibraryDatabase = db_class() loc = location.replace('/', os.sep) base = os.path.dirname(loc) old_name = name.replace('&&', '&') newname, ok = QInputDialog.getText( self.gui, _('Rename') + ' ' + old_name, '<p>' + _('Choose a new name for the library <b>%s</b>. ') % name + '<p>' + _('Note that the actual library folder will be renamed.'), text=old_name) newname = sanitize_file_name(str(newname)) if not ok or not newname or newname == old_name: return newloc = os.path.join(base, newname) if os.path.exists(newloc): return error_dialog( self.gui, _('Already exists'), _('The folder %s already exists. Delete it first.') % newloc, show=True) if (iswindows and len(newloc) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT): return error_dialog( self.gui, _('Too long'), _('Path to library too long. It must be less than' ' %d characters.') % LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT, show=True) if not os.path.exists(loc): error_dialog( self.gui, _('Not found'), _('Cannot rename as no library was found at %s. ' 'Try switching to this library first, then switch back ' 'and retry the renaming.') % loc, show=True) return self.gui.library_broker.remove_library(loc) try: os.rename(loc, newloc) except: import traceback det_msg = 'Location: %r New Location: %r\n%s' % ( loc, newloc, traceback.format_exc()) error_dialog( self.gui, _('Rename failed'), _('Failed to rename the library at %s. ' 'The most common cause for this is if one of the files' ' in the library is open in another program.') % loc, det_msg=det_msg, show=True) return self.stats.rename(location, newloc) self.rename_library_icon(old_name, newname) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() def delete_requested(self, name, location): loc = location.replace('/', os.sep) if not question_dialog( self.gui, _('Library removed'), _('The library %s has been removed from calibre. ' 'The files remain on your computer, if you want ' 'to delete them, you will have to do so manually.') % ('<code>%s</code>' % loc), override_icon='dialog_information.png', yes_text=_('&OK'), no_text=_('&Undo'), yes_icon='ok.png', no_icon='edit-undo.png'): return self.remove_library_icon(name) self.stats.remove(location) self.gui.library_broker.remove_library(location) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() if os.path.exists(loc): open_local_file(loc) def backup_status(self, location): self.__backup_status_dialog = d = BackupStatus(self.gui) d.show() def mark_dirty(self): db = self.gui.library_view.model().db db.dirtied(list(db.data.iterallids())) info_dialog( self.gui, _('Backup metadata'), _('Metadata will be backed up while calibre is running, at the ' 'rate of approximately 1 book every three seconds.'), show=True) def restore_database(self): LibraryDatabase = db_class() m = self.gui.library_view.model() db = m.db if (iswindows and len(db.library_path) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT): return error_dialog( self.gui, _('Too long'), _('Path to library too long. It must be less than' ' %d characters. Move your library to a location with' ' a shorter path using Windows Explorer, then point' ' calibre to the new location and try again.') % LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT, show=True) from calibre.gui2.dialogs.restore_library import restore_database m = self.gui.library_view.model() m.stop_metadata_backup() db = m.db db.prefs.disable_setting = True if restore_database(db, self.gui): self.gui.library_moved(db.library_path) def check_library(self): from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck self.gui.library_view.save_state() m = self.gui.library_view.model() m.stop_metadata_backup() db = m.db db.prefs.disable_setting = True library_path = db.library_path d = DBCheck(self.gui, db) d.start() try: m.close() except: pass d.break_cycles() self.gui.library_moved(library_path) if d.rejected: return if d.error is None: if not question_dialog( self.gui, _('Success'), _('Found no errors in your calibre library database.' ' Do you want calibre to check if the files in your' ' library match the information in the database?')): return else: return error_dialog( self.gui, _('Failed'), _('Database integrity check failed, click "Show details"' ' for details.'), show=True, det_msg=d.error[1]) self.gui.status_bar.show_message( _('Starting library scan, this may take a while')) try: QCoreApplication.processEvents() d = CheckLibraryDialog(self.gui, m.db) if not d.do_exec(): info_dialog( self.gui, _('No problems found'), _('The files in your library match the information ' 'in the database.'), show=True) finally: self.gui.status_bar.clear_message() def look_for_portable_lib(self, db, location): base = get_portable_base() if base is None: return False, None loc = location.replace('/', os.sep) candidate = os.path.join(base, os.path.basename(loc)) if db.exists_at(candidate): newloc = candidate.replace(os.sep, '/') self.stats.rename(location, newloc) return True, newloc return False, None def switch_requested(self, location): if not self.change_library_allowed(): return db = self.gui.library_view.model().db current_lib = self.stats.canonicalize_path(db.library_path) self.view_state_map[current_lib] = self.preserve_state_on_switch.state loc = location.replace('/', os.sep) exists = db.exists_at(loc) if not exists: exists, new_location = self.look_for_portable_lib(db, location) if exists: location = new_location loc = location.replace('/', os.sep) if not exists: d = MovedDialog(self.stats, location, self.gui) ret = d.exec() self.build_menus() self.gui.iactions['Copy To Library'].build_menus() if ret == QDialog.DialogCode.Accepted: loc = d.newloc.replace('/', os.sep) else: return # from calibre.utils.mem import memory # import weakref # from qt.core import QTimer # self.dbref = weakref.ref(self.gui.library_view.model().db) # self.before_mem = memory() self.gui.library_moved(loc, allow_rebuild=True) # QTimer.singleShot(5000, self.debug_leak) def debug_leak(self): import gc from calibre.utils.mem import memory ref = self.dbref for i in range(3): gc.collect() if ref() is not None: print('DB object alive:', ref()) for r in gc.get_referrers(ref())[:10]: print(r) print() print('before:', self.before_mem) print('after:', memory()) print() self.dbref = self.before_mem = None def count_changed(self, new_count): self.update_tooltip(new_count) def choose_library(self, *args): if not self.change_library_allowed(): return from calibre.gui2.dialogs.choose_library import ChooseLibrary self.gui.library_view.save_state() db = self.gui.library_view.model().db location = self.stats.canonicalize_path(db.library_path) self.pre_choose_dialog_location = location c = ChooseLibrary(db, self.choose_library_callback, self.gui) c.exec() def choose_library_callback(self, newloc, copy_structure=False, library_renamed=False): self.gui.library_moved(newloc, copy_structure=copy_structure, allow_rebuild=True) if library_renamed: self.stats.rename(self.pre_choose_dialog_location, prefs['library_path']) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() def change_library_allowed(self): if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): warning_dialog( self.gui, _('Not allowed'), _('You cannot change libraries while using the environment' ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'), show=True) return False if self.gui.job_manager.has_jobs(): warning_dialog(self.gui, _('Not allowed'), _('You cannot change libraries while jobs' ' are running.'), show=True) return False if self.gui.proceed_question.questions: warning_dialog(self.gui, _('Not allowed'), _('You cannot change libraries until all' ' updates are accepted or rejected.'), show=True) return False return True
class MarkBooksAction(InterfaceAction): name = 'Mark Books' action_spec = (_('Mark books'), 'marked.png', _('Temporarily mark books for easy access'), 'Ctrl+M') action_type = 'current' action_add_menu = True dont_add_to = frozenset([ 'context-menu-device', 'menubar-device', 'context-menu-cover-browser' ]) action_menu_clone_qaction = _('Toggle mark for selected books') accepts_drops = True def accept_enter_event(self, event, mime_data): if mime_data.hasFormat("application/calibre+from_library"): return True return False def accept_drag_move_event(self, event, mime_data): if mime_data.hasFormat("application/calibre+from_library"): return True return False def drop_event(self, event, mime_data): mime = 'application/calibre+from_library' if mime_data.hasFormat(mime): self.dropped_ids = tuple( map(int, mime_data.data(mime).data().split())) QTimer.singleShot(1, self.do_drop) return True return False def do_drop(self): book_ids = self.dropped_ids del self.dropped_ids if book_ids: self.toggle_ids(book_ids) def genesis(self): self.search_icon = QIcon.ic('search.png') self.qaction.triggered.connect(self.toggle_selected) self.menu = m = self.qaction.menu() m.aboutToShow.connect(self.about_to_show_menu) ma = partial(self.create_menu_action, m) self.show_marked_action = a = ma('mark_with_text', _('Mark books with text label'), icon='marked.png') a.triggered.connect(partial(self.mark_with_text, book_ids=None)) global mark_books_with_text mark_books_with_text = self.mark_with_text self.show_marked_action = a = ma('show-marked', _('Show marked books'), icon='search.png', shortcut='Shift+Ctrl+M') a.triggered.connect(self.show_marked) self.show_marked_with_text = QMenu( _('Show marked books with text label')) self.show_marked_with_text.setIcon(self.search_icon) m.addMenu(self.show_marked_with_text) self.clear_selected_marked_action = a = ma( 'clear-marks-on-selected', _('Clear marks for selected books'), icon='clear_left.png') a.triggered.connect(self.clear_marks_on_selected_books) self.clear_marked_action = a = ma('clear-all-marked', _('Clear all marked books'), icon='clear_left.png') a.triggered.connect(self.clear_all_marked) m.addSeparator() self.mark_author_action = a = ma( 'mark-author', _('Mark all books by selected author(s)'), icon='plus.png') connect_lambda(a.triggered, self, lambda self: self.mark_field('authors', True)) self.mark_series_action = a = ma( 'mark-series', _('Mark all books in the selected series'), icon='plus.png') connect_lambda(a.triggered, self, lambda self: self.mark_field('series', True)) m.addSeparator() self.unmark_author_action = a = ma( 'unmark-author', _('Clear all books by selected author(s)'), icon='minus.png') connect_lambda(a.triggered, self, lambda self: self.mark_field('authors', False)) self.unmark_series_action = a = ma( 'unmark-series', _('Clear all books in the selected series'), icon='minus.png') connect_lambda(a.triggered, self, lambda self: self.mark_field('series', False)) def gui_layout_complete(self): for x in self.gui.bars_manager.main_bars + self.gui.bars_manager.child_bars: try: w = x.widgetForAction(self.qaction) w.installEventFilter(self) except: continue def eventFilter(self, obj, ev): if ev.type() == QEvent.Type.MouseButtonPress and ev.button( ) == Qt.MouseButton.LeftButton: mods = QApplication.keyboardModifiers() if mods & Qt.KeyboardModifier.ControlModifier or mods & Qt.KeyboardModifier.ShiftModifier: self.show_marked() return True return False def about_to_show_menu(self): db = self.gui.current_db marked_ids = db.data.marked_ids num = len( frozenset(marked_ids).intersection(db.new_api.all_book_ids())) text = _('Show marked book') if num == 1 else (_('Show marked books') + (' (%d)' % num)) self.show_marked_action.setText(text) counts = dict() for v in marked_ids.values(): counts[v] = counts.get(v, 0) + 1 labels = sorted(counts.keys(), key=sort_key) self.show_marked_with_text.clear() if len(labels): labs = labels[0:40] self.show_marked_with_text.setEnabled(True) for t in labs: ac = self.show_marked_with_text.addAction( self.search_icon, f'{t} ({counts[t]})') ac.triggered.connect(partial(self.show_marked_text, txt=t)) if len(labs) < len(labels): self.show_marked_with_text.addAction( _('{0} labels not shown').format(len(labels) - len(labs))) else: self.show_marked_with_text.setEnabled(False) def location_selected(self, loc): enabled = loc == 'library' self.qaction.setEnabled(enabled) self.menuless_qaction.setEnabled(enabled) for action in self.menu.actions(): action.setEnabled(enabled) def toggle_selected(self): book_ids = self._get_selected_ids() if book_ids: self.toggle_ids(book_ids) def _get_selected_ids(self): rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: d = error_dialog(self.gui, _('Cannot mark'), _('No books selected')) d.exec() return set() return set(map(self.gui.library_view.model().id, rows)) def toggle_ids(self, book_ids): self.gui.current_db.data.toggle_marked_ids(book_ids) def show_marked(self): self.gui.search.set_search_string('marked:true') def show_marked_text(self, txt=None): self.gui.search.set_search_string(f'marked:"={txt}"') def clear_all_marked(self): self.gui.current_db.data.set_marked_ids(()) if str(self.gui.search.text()).startswith('marked:'): self.gui.search.set_search_string('') def mark_field(self, field, add): book_ids = self._get_selected_ids() if not book_ids: return db = self.gui.current_db items = set() for book_id in book_ids: items |= set(db.new_api.field_ids_for(field, book_id)) book_ids = set() for item_id in items: book_ids |= db.new_api.books_for_field(field, item_id) mids = db.data.marked_ids.copy() for book_id in book_ids: if add: mids[book_id] = True else: mids.pop(book_id, None) db.data.set_marked_ids(mids) def mark_with_text(self, book_ids=None): if book_ids is None: book_ids = self._get_selected_ids() if not book_ids: return dialog = MarkWithTextDialog(self.gui) if dialog.exec_() != QDialog.DialogCode.Accepted: return txt = dialog.text() txt = txt if txt else 'true' db = self.gui.current_db mids = db.data.marked_ids.copy() for book_id in book_ids: mids[book_id] = txt db.data.set_marked_ids(mids) def clear_marks_on_selected_books(self): book_ids = self._get_selected_ids() if not book_ids: return db = self.gui.current_db items = db.data.marked_ids.copy() for book_id in book_ids: items.pop(book_id, None) self.gui.current_db.data.set_marked_ids(items)