Example #1
0
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
Example #2
0
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)