%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/ |
| Current File : //lib/calibre/calibre/gui2/search_restriction_mixin.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from functools import partial
from gettext import pgettext
from qt.core import (
Qt, QMenu, QIcon, QDialog, QGridLayout, QLabel, QLineEdit, QComboBox, QFrame,
QDialogButtonBox, QSize, QVBoxLayout, QListWidget, QRadioButton, QAction, QTextBrowser, QAbstractItemView)
from calibre.gui2 import error_dialog, question_dialog, gprefs
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import ParseException
from calibre.utils.localization import localize_user_manual_link
class SelectNames(QDialog): # {{{
def __init__(self, names, txt, parent=None):
QDialog.__init__(self, parent)
self.l = l = QVBoxLayout(self)
self.setLayout(l)
self.la = la = QLabel(_('Create a Virtual library based on %s') % txt)
l.addWidget(la)
self._names = QListWidget(self)
self._names.addItems(sorted(names, key=sort_key))
self._names.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
l.addWidget(self._names)
self._or = QRadioButton(_('Match any of the selected %s')%txt)
self._and = QRadioButton(_('Match all of the selected %s')%txt)
self._or.setChecked(True)
l.addWidget(self._or)
l.addWidget(self._and)
self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
l.addWidget(self.bb)
self.resize(self.sizeHint())
@property
def names(self):
for item in self._names.selectedItems():
yield str(item.data(Qt.ItemDataRole.DisplayRole) or '')
@property
def match_type(self):
return ' and ' if self._and.isChecked() else ' or '
# }}}
MAX_VIRTUAL_LIBRARY_NAME_LENGTH = 40
def _build_full_search_string(gui):
search_templates = (
'',
'{cl}',
'{cr}',
'(({cl}) and ({cr}))',
'{sb}',
'(({cl}) and ({sb}))',
'(({cr}) and ({sb}))',
'(({cl}) and ({cr}) and ({sb}))'
)
sb = gui.search.current_text
db = gui.current_db
cr = db.data.get_search_restriction()
cl = db.data.get_base_restriction()
dex = 0
if sb:
dex += 4
if cr:
dex += 2
if cl:
dex += 1
template = search_templates[dex]
return template.format(cl=cl, cr=cr, sb=sb).strip()
class CreateVirtualLibrary(QDialog): # {{{
def __init__(self, gui, existing_names, editing=None):
QDialog.__init__(self, gui)
self.gui = gui
self.existing_names = existing_names
if editing:
self.setWindowTitle(_('Edit Virtual library'))
else:
self.setWindowTitle(_('Create Virtual library'))
self.setWindowIcon(QIcon(I('lt.png')))
gl = QGridLayout()
self.setLayout(gl)
self.la1 = la1 = QLabel(_('Virtual library &name:'))
gl.addWidget(la1, 0, 0)
self.vl_name = QComboBox()
self.vl_name.setEditable(True)
self.vl_name.lineEdit().setMaxLength(MAX_VIRTUAL_LIBRARY_NAME_LENGTH)
self.vl_name.lineEdit().setClearButtonEnabled(True)
la1.setBuddy(self.vl_name)
gl.addWidget(self.vl_name, 0, 1)
self.editing = editing
self.saved_searches_label = sl = QTextBrowser(self)
sl.viewport().setAutoFillBackground(False)
gl.addWidget(sl, 2, 0, 1, 2)
self.la2 = la2 = QLabel(_('&Search expression:'))
gl.addWidget(la2, 1, 0)
self.vl_text = QLineEdit()
self.vl_text.setClearButtonEnabled(True)
self.vl_text.textChanged.connect(self.search_text_changed)
la2.setBuddy(self.vl_text)
gl.addWidget(self.vl_text, 1, 1)
# Trigger the textChanged signal to initialize the saved searches box
self.vl_text.setText(' ')
self.vl_text.setText(_build_full_search_string(self.gui))
self.sl = sl = QLabel('<p>'+_('Create a Virtual library based on: ')+
('<a href="author.{0}">{0}</a>, '
'<a href="tag.{1}">{1}</a>, '
'<a href="publisher.{2}">{2}</a>, '
'<a href="series.{3}">{3}</a>, '
'<a href="search.{4}">{4}</a>.').format(_('Authors'), _('Tags'),
_('Publishers'), ngettext('Series', 'Series', 2), _('Saved searches')))
sl.setWordWrap(True)
sl.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse)
sl.linkActivated.connect(self.link_activated)
gl.addWidget(sl, 3, 0, 1, 2)
gl.setRowStretch(3,10)
self.hl = hl = QLabel(_('''
<h2>Virtual libraries</h2>
<p>With <i>Virtual libraries</i>, you can restrict calibre to only show
you books that match a search. When a Virtual library is in effect, calibre
behaves as though the library contains only the matched books. The Tag browser
display only the tags/authors/series/etc. that belong to the matched books and any searches
you do will only search within the books in the Virtual library. This
is a good way to partition your large library into smaller and easier to work with subsets.</p>
<p>For example you can use a Virtual library to only show you books with the tag <i>Unread</i>
or only books by <i>My favorite author</i> or only books in a particular series.</p>
<p>More information and examples are available in the
<a href="%s">User Manual</a>.</p>
''') % localize_user_manual_link('https://manual.calibre-ebook.com/virtual_libraries.html'))
hl.setWordWrap(True)
hl.setOpenExternalLinks(True)
hl.setFrameStyle(QFrame.Shape.StyledPanel)
gl.addWidget(hl, 0, 3, 4, 1)
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
gl.addWidget(bb, 4, 0, 1, 0)
if editing:
db = self.gui.current_db
virt_libs = db.new_api.pref('virtual_libraries', {})
for dex,vl in enumerate(sorted(virt_libs.keys(), key=sort_key)):
self.vl_name.addItem(vl, virt_libs.get(vl, ''))
if vl == editing:
self.vl_name.setCurrentIndex(dex)
self.original_index = dex
self.original_search = virt_libs.get(editing, '')
self.vl_text.setText(self.original_search)
self.new_name = editing
self.vl_name.currentIndexChanged[int].connect(self.name_index_changed)
self.vl_name.lineEdit().textEdited.connect(self.name_text_edited)
self.resize(self.sizeHint()+QSize(150, 25))
def search_text_changed(self, txt):
db = self.gui.current_db
searches = [_('Saved searches recognized in the expression:')]
txt = str(txt)
while txt:
p = txt.partition('search:')
if p[1]: # found 'search:'
possible_search = p[2]
if possible_search: # something follows the 'search:'
if possible_search[0] == '"': # strip any quotes
possible_search = possible_search[1:].partition('"')
else: # find end of the search name. Is EOL, space, rparen
sp = possible_search.find(' ')
pp = possible_search.find(')')
if pp < 0 or (sp > 0 and sp <= pp):
# space in string before rparen, or neither found
possible_search = possible_search.partition(' ')
else:
# rparen in string before space
possible_search = possible_search.partition(')')
txt = possible_search[2] # grab remainder of the string
search_name = possible_search[0]
if search_name.startswith('='):
search_name = search_name[1:]
if search_name in db.saved_search_names():
searches.append(search_name + '=' +
db.saved_search_lookup(search_name))
else:
txt = ''
else:
txt = ''
self.saved_searches_label.setPlainText('\n'.join(searches))
def name_text_edited(self, new_name):
self.new_name = str(new_name)
def name_index_changed(self, dex):
if self.editing and (self.vl_text.text() != self.original_search or
self.new_name != self.editing):
if not question_dialog(self.gui, _('Search text changed'),
_('The Virtual library name or the search text has changed. '
'Do you want to discard these changes?'),
default_yes=False):
self.vl_name.blockSignals(True)
self.vl_name.setCurrentIndex(self.original_index)
self.vl_name.lineEdit().setText(self.new_name)
self.vl_name.blockSignals(False)
return
self.new_name = self.editing = self.vl_name.currentText()
self.original_index = dex
self.original_search = str(self.vl_name.itemData(dex) or '')
self.vl_text.setText(self.original_search)
def link_activated(self, url):
db = self.gui.current_db
f, txt = str(url).partition('.')[0::2]
if f == 'search':
names = db.saved_search_names()
else:
names = getattr(db, 'all_%s_names'%f)()
d = SelectNames(names, txt, parent=self)
if d.exec() == QDialog.DialogCode.Accepted:
prefix = f+'s' if f in {'tag', 'author'} else f
if f == 'search':
search = ['(%s)'%(db.saved_search_lookup(x)) for x in d.names]
else:
search = ['%s:"=%s"'%(prefix, x.replace('"', '\\"')) for x in d.names]
if search:
if not self.editing:
self.vl_name.lineEdit().setText(next(d.names))
self.vl_name.lineEdit().setCursorPosition(0)
self.vl_text.setText(d.match_type.join(search))
self.vl_text.setCursorPosition(0)
def accept(self):
n = str(self.vl_name.currentText()).strip()
if not n:
error_dialog(self.gui, _('No name'),
_('You must provide a name for the new Virtual library'),
show=True)
return
if n.startswith('*'):
error_dialog(self.gui, _('Invalid name'),
_('A Virtual library name cannot begin with "*"'),
show=True)
return
if n in self.existing_names and n != self.editing:
if not question_dialog(self.gui, _('Name already in use'),
_('That name is already in use. Do you want to replace it '
'with the new search?'),
default_yes=False):
return
v = str(self.vl_text.text()).strip()
if not v:
error_dialog(self.gui, _('No search string'),
_('You must provide a search to define the new Virtual library'),
show=True)
return
try:
db = self.gui.library_view.model().db
recs = db.data.search_getting_ids('', v, use_virtual_library=False, sort_results=False)
except ParseException as e:
error_dialog(self.gui, _('Invalid search'),
_('The search in the search box is not valid'),
det_msg=e.msg, show=True)
return
if not recs and not question_dialog(
self.gui, _('Search found no books'),
_('The search found no books, so the Virtual library '
'will be empty. Do you really want to use that search?'),
default_yes=False):
return
self.library_name = n
self.library_search = v
QDialog.accept(self)
# }}}
class SearchRestrictionMixin:
no_restriction = '<' + _('None') + '>'
def __init__(self, *args, **kwargs):
pass
def init_search_restriction_mixin(self):
self.checked = QIcon(I('ok.png'))
self.empty = QIcon(I('blank.png'))
self.current_search_action = QAction(self.empty, _('*current search'), self)
self.current_search_action.triggered.connect(partial(self.apply_virtual_library, library='*'))
self.addAction(self.current_search_action)
self.keyboard.register_shortcut(
'vl-from-current-search', _('Virtual library from current search'), description=_(
'Create a temporary Virtual library from the current search'), group=pgettext('search restriction group name', 'Miscellaneous'),
default_keys=('Ctrl+*',), action=self.current_search_action)
self.search_based_vl_name = None
self.search_based_vl = None
self.virtual_library_menu = QMenu(self.virtual_library)
self.virtual_library.setMenu(self.virtual_library_menu)
self.virtual_library_menu.aboutToShow.connect(self.virtual_library_menu_about_to_show)
self.clear_vl.clicked.connect(lambda x: (self.apply_virtual_library(), self.clear_additional_restriction()))
self.virtual_library_tooltip = \
_('Use a "Virtual library" to show only a subset of the books present in this library')
self.virtual_library.setToolTip(self.virtual_library_tooltip)
self.search_restriction = ComboBoxWithHelp(self)
self.search_restriction.setVisible(False)
self.clear_vl.setText(_("(all books)"))
self.ar_menu = QMenu(_('Additional restriction'), self.virtual_library_menu)
self.edit_menu = QMenu(_('Edit Virtual library'), self.virtual_library_menu)
self.rm_menu = QMenu(_('Remove Virtual library'), self.virtual_library_menu)
self.search_restriction_list_built = False
def add_virtual_library(self, db, name, search):
virt_libs = db.new_api.pref('virtual_libraries', {})
virt_libs[name] = search
db.new_api.set_pref('virtual_libraries', virt_libs)
db.new_api.clear_search_caches()
self.library_view.model().db.refresh()
def do_create_edit(self, name=None):
db = self.library_view.model().db
virt_libs = db.new_api.pref('virtual_libraries', {})
cd = CreateVirtualLibrary(self, virt_libs.keys(), editing=name)
if cd.exec() == QDialog.DialogCode.Accepted:
if name:
self._remove_vl(name, reapply=False)
self.add_virtual_library(db, cd.library_name, cd.library_search)
if not name or name == db.data.get_base_restriction_name():
self.apply_virtual_library(cd.library_name)
self.rebuild_vl_tabs()
def build_virtual_library_menu(self, m, add_tabs_action=True):
m.clear()
a = m.addAction(QIcon.ic('plus.png'), _('Create Virtual library'))
a.triggered.connect(partial(self.do_create_edit, name=None))
db = self.current_db
virt_libs = db.new_api.pref('virtual_libraries', {})
a = self.edit_menu
self.build_virtual_library_list(a, self.do_create_edit)
if virt_libs:
m.addMenu(a).setIcon(QIcon.ic('edit_input.png'))
a = self.rm_menu
self.build_virtual_library_list(a, self.remove_vl_triggered)
if virt_libs:
m.addMenu(a).setIcon(QIcon.ic('minus.png'))
if virt_libs:
m.addAction(QIcon.ic('toc.png'), _('Quick select Virtual library'), self.choose_vl_triggerred)
if add_tabs_action:
if gprefs['show_vl_tabs']:
m.addAction(_('Hide Virtual library tabs'), self.vl_tabs.disable_bar)
else:
m.addAction(_('Show Virtual libraries as tabs'), self.vl_tabs.enable_bar)
m.addSeparator()
a = self.ar_menu
a.clear()
a.setIcon(self.checked if db.data.get_search_restriction_name() else self.empty)
self.build_search_restriction_list()
m.addMenu(a)
m.addSeparator()
current_lib = db.data.get_base_restriction_name()
if not current_lib:
a = m.addAction(self.checked, self.no_restriction)
else:
a = m.addAction(self.empty, self.no_restriction)
a.triggered.connect(partial(self.apply_virtual_library, library=''))
a = m.addAction(self.current_search_action)
if self.search_based_vl_name:
a = m.addAction(
self.checked if db.data.get_base_restriction_name().startswith('*') else self.empty,
self.search_based_vl_name)
a.triggered.connect(partial(self.apply_virtual_library,
library=self.search_based_vl_name))
m.addSeparator()
for vl in sorted(virt_libs.keys(), key=sort_key):
is_current = vl == current_lib
a = m.addAction(self.checked if is_current else self.empty, vl.replace('&', '&&'))
if is_current:
a.triggered.connect(self.apply_virtual_library)
else:
a.triggered.connect(partial(self.apply_virtual_library, library=vl))
def virtual_library_menu_about_to_show(self):
self.build_virtual_library_menu(self.virtual_library_menu)
def rebuild_vl_tabs(self):
self.vl_tabs.rebuild()
def apply_virtual_library(self, library=None, update_tabs=True):
db = self.library_view.model().db
virt_libs = db.new_api.pref('virtual_libraries', {})
if not library:
db.data.set_base_restriction('')
db.data.set_base_restriction_name('')
elif library == '*':
if not self.search.current_text:
error_dialog(self, _('No search'),
_('There is no current search to use'), show=True)
return
txt = _build_full_search_string(self)
try:
db.data.search_getting_ids('', txt, use_virtual_library=False)
except ParseException as e:
error_dialog(self, _('Invalid search'),
_('The search in the search box is not valid'),
det_msg=e.msg, show=True)
return
self.search_based_vl = txt
db.data.set_base_restriction(txt)
self.search_based_vl_name = self._trim_restriction_name('*' + txt)
db.data.set_base_restriction_name(self.search_based_vl_name)
elif library == self.search_based_vl_name:
db.data.set_base_restriction(self.search_based_vl)
db.data.set_base_restriction_name(self.search_based_vl_name)
elif library in virt_libs:
db.data.set_base_restriction(virt_libs[library])
db.data.set_base_restriction_name(library)
self.virtual_library.setToolTip(self.virtual_library_tooltip + '\n' +
db.data.get_base_restriction())
self._apply_search_restriction(db.data.get_search_restriction(),
db.data.get_search_restriction_name())
if update_tabs:
self.vl_tabs.update_current()
def build_virtual_library_list(self, menu, handler):
db = self.library_view.model().db
virt_libs = db.new_api.pref('virtual_libraries', {})
menu.clear()
menu.setIcon(self.empty)
def add_action(name, search):
a = menu.addAction(name.replace('&', '&&'))
a.triggered.connect(partial(handler, name=name))
a.setIcon(self.empty)
libs = sorted(virt_libs.keys(), key=sort_key)
if libs:
menu.setEnabled(True)
for n in libs:
add_action(n, virt_libs[n])
else:
menu.setEnabled(False)
def remove_vl_triggered(self, name=None):
if not confirm(
_('Are you sure you want to remove the Virtual library <b>{0}</b>?').format(name),
'confirm_vl_removal', parent=self):
return
self._remove_vl(name, reapply=True)
self.library_view.model().db.refresh()
def choose_vl_triggerred(self):
from calibre.gui2.tweak_book.widgets import QuickOpen, emphasis_style
db = self.library_view.model().db
virt_libs = db.new_api.pref('virtual_libraries', {})
if not virt_libs:
return error_dialog(self, _('No Virtual libraries'), _(
'No Virtual libraries present, create some first'), show=True)
example = '<pre>{0}S{1}ome {0}B{1}ook {0}C{1}ollection</pre>'.format(
'<span style="%s">' % emphasis_style(), '</span>')
chars = '<pre style="%s">sbc</pre>' % emphasis_style()
help_text = _('''<p>Quickly choose a Virtual library by typing in just a few characters from the library name into the field above.
For example, if want to choose the VL:
{example}
Simply type in the characters:
{chars}
and press Enter.''').format(example=example, chars=chars)
d = QuickOpen(
sorted(virt_libs.keys(), key=sort_key), parent=self, title=_('Choose Virtual library'),
name='vl-open', level1=' ', help_text=help_text)
if d.exec() == QDialog.DialogCode.Accepted and d.selected_result:
self.apply_virtual_library(library=d.selected_result)
def _remove_vl(self, name, reapply=True):
db = self.library_view.model().db
virt_libs = db.new_api.pref('virtual_libraries', {})
virt_libs.pop(name, None)
db.new_api.set_pref('virtual_libraries', virt_libs)
if reapply and db.data.get_base_restriction_name() == name:
self.apply_virtual_library('')
self.rebuild_vl_tabs()
def _trim_restriction_name(self, name):
return name[0:MAX_VIRTUAL_LIBRARY_NAME_LENGTH].strip()
def build_search_restriction_list(self):
self.search_restriction_list_built = True
from calibre.gui2.ui import get_gui
m = self.ar_menu
m.clear()
current_restriction_text = None
if self.search_restriction.count() > 1:
txt = str(self.search_restriction.itemText(2))
if txt.startswith('*'):
current_restriction_text = txt
self.search_restriction.clear()
current_restriction = self.library_view.model().db.data.get_search_restriction_name()
m.setIcon(self.checked if current_restriction else self.empty)
def add_action(txt, index):
self.search_restriction.addItem(txt)
txt = self._trim_restriction_name(txt)
if txt == current_restriction:
a = m.addAction(self.checked, txt if txt else self.no_restriction)
else:
a = m.addAction(self.empty, txt if txt else self.no_restriction)
a.triggered.connect(partial(self.search_restriction_triggered,
action=a, index=index))
add_action('', 0)
add_action(_('*current search'), 1)
dex = 2
if current_restriction_text:
add_action(current_restriction_text, 2)
dex += 1
for n in sorted(get_gui().current_db.saved_search_names(), key=sort_key):
add_action(n, dex)
dex += 1
def search_restriction_triggered(self, action=None, index=None):
self.search_restriction.setCurrentIndex(index)
self.apply_search_restriction(index)
def apply_named_search_restriction(self, name=None):
if not self.search_restriction_list_built:
self.build_search_restriction_list()
if not name:
r = 0
else:
r = self.search_restriction.findText(name)
if r < 0:
r = 0
self.search_restriction.setCurrentIndex(r)
self.apply_search_restriction(r)
def apply_text_search_restriction(self, search):
if not self.search_restriction_list_built:
self.build_search_restriction_list()
search = str(search)
if not search:
self.search_restriction.setCurrentIndex(0)
self._apply_search_restriction('', '')
else:
s = '*' + search
if self.search_restriction.count() > 1:
txt = str(self.search_restriction.itemText(2))
if txt.startswith('*'):
self.search_restriction.setItemText(2, s)
else:
self.search_restriction.insertItem(2, s)
else:
self.search_restriction.insertItem(2, s)
self.search_restriction.setCurrentIndex(2)
self._apply_search_restriction(search, self._trim_restriction_name(s))
def apply_search_restriction(self, i):
if not self.search_restriction_list_built:
self.build_search_restriction_list()
if i == 1:
self.apply_text_search_restriction(str(self.search.currentText()))
elif i == 2 and str(self.search_restriction.currentText()).startswith('*'):
self.apply_text_search_restriction(
str(self.search_restriction.currentText())[1:])
else:
r = str(self.search_restriction.currentText())
if r is not None and r != '':
restriction = 'search:"%s"'%(r)
else:
restriction = ''
self._apply_search_restriction(restriction, r)
def clear_additional_restriction(self):
self.search_restriction.setCurrentIndex(0)
self._apply_search_restriction('', '')
def _apply_search_restriction(self, restriction, name):
self.saved_search.clear()
# The order below is important. Set the restriction, force a '' search
# to apply it, reset the tag browser to take it into account, then set
# the book count.
self.library_view.model().db.data.set_search_restriction(restriction)
self.library_view.model().db.data.set_search_restriction_name(name)
self.search.clear(emit_search=True)
self.tags_view.recount()
self.set_number_of_books_shown()
self.current_view().setFocus(Qt.FocusReason.OtherFocusReason)
self.set_window_title()
v = self.current_view()
if not v.currentIndex().isValid():
v.set_current_row()
if not v.refresh_book_details():
self.book_details.reset_info()
def set_number_of_books_shown(self):
db = self.library_view.model().db
if self.current_view() == self.library_view and db is not None and \
db.data.search_restriction_applied():
restrictions = [x for x in (db.data.get_base_restriction_name(),
db.data.get_search_restriction_name()) if x]
t = ' :: '.join(restrictions)
if len(t) > 20:
t = t[:19] + '…'
self.clear_vl.setVisible(True)
self.clear_vl.setVisible(not gprefs['show_vl_tabs'])
else: # No restriction or not library view
t = ''
self.clear_vl.setVisible(False)
self.clear_vl.setText(t.replace('&', '&&'))
if __name__ == '__main__':
from calibre.gui2 import Application
from calibre.gui2.preferences import init_gui
app = Application([])
app
gui = init_gui()
d = CreateVirtualLibrary(gui, [])
d.exec()