Virtual Libraries

This commit is contained in:
Kovid Goyal 2013-04-13 09:26:48 +05:30
commit a8d9d760ff
17 changed files with 653 additions and 144 deletions

View File

@ -422,6 +422,8 @@ class DB(object):
('uuid', False), ('comments', True), ('id', False), ('pubdate', False),
('last_modified', False), ('size', False), ('languages', False),
]
defs['virtual_libraries'] = {}
defs['virtual_lib_on_startup'] = defs['cs_virtual_lib_on_startup'] = ''
# Migrate the bool tristate tweak
defs['bools_are_tristate'] = \
@ -470,6 +472,24 @@ class DB(object):
except:
pass
# migrate the gui_restriction preference to a virtual library
gr_pref = self.prefs.get('gui_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['gui_restriction'] = ''
self.prefs['virtual_lib_on_startup'] = gr_pref
# migrate the cs_restriction preference to a virtual library
gr_pref = self.prefs.get('cs_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['cs_restriction'] = ''
self.prefs['cs_virtual_lib_on_startup'] = gr_pref
# Rename any user categories with names that differ only in case
user_cats = self.prefs.get('user_categories', [])
catmap = {}

View File

@ -49,7 +49,8 @@ class View(object):
self.cache = cache
self.marked_ids = {}
self.search_restriction_book_count = 0
self.search_restriction = ''
self.search_restriction = self.base_restriction = ''
self.search_restriction_name = self.base_restriction_name = ''
self._field_getters = {}
for col, idx in cache.backend.FIELD_MAP.iteritems():
if isinstance(col, int):
@ -168,8 +169,19 @@ class View(object):
return ans
self._map_filtered = tuple(ans)
def _build_restriction_string(self, restriction):
if self.base_restriction:
if restriction:
return u'(%s) and (%s)' % (self.base_restriction, restriction)
else:
return self.base_restriction
else:
return restriction
def search_getting_ids(self, query, search_restriction,
set_restriction_count=False):
set_restriction_count=False, use_virtual_library=True):
if use_virtual_library:
search_restriction = self._build_restriction_string(search_restriction)
q = ''
if not query or not query.strip():
q = search_restriction
@ -188,11 +200,32 @@ class View(object):
self.search_restriction_book_count = len(rv)
return rv
def get_search_restriction(self):
return self.search_restriction
def set_search_restriction(self, s):
self.search_restriction = s
def get_base_restriction(self):
return self.base_restriction
def set_base_restriction(self, s):
self.base_restriction = s
def get_base_restriction_name(self):
return self.base_restriction_name
def set_base_restriction_name(self, s):
self.base_restriction_name = s
def get_search_restriction_name(self):
return self.search_restriction_name
def set_search_restriction_name(self, s):
self.search_restriction_name = s
def search_restriction_applied(self):
return bool(self.search_restriction)
return bool(self.search_restriction) or bool(self.base_restriction)
def get_search_restriction_book_count(self):
return self.search_restriction_book_count

View File

@ -16,7 +16,6 @@ from calibre.constants import __appname__
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2.bars import BarsManager
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre.utils.config_base import tweaks
from calibre import human_readable
@ -173,11 +172,13 @@ class SearchBar(QWidget): # {{{
self.setLayout(self._layout)
self._layout.setContentsMargins(0,5,0,0)
x = ComboBoxWithHelp(self)
x.setMaximumSize(QSize(150, 16777215))
x.setObjectName("search_restriction")
x = QToolButton(self)
x.setText(_('Virtual Library'))
x.setIcon(QIcon(I('lt.png')))
x.setObjectName("virtual_library")
x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
l.addWidget(x)
parent.search_restriction = x
parent.virtual_library = x
x = QLabel(self)
x.setObjectName("search_count")

View File

@ -14,7 +14,6 @@ from calibre.gui2.preferences.behavior_ui import Ui_Form
from calibre.gui2 import config, info_dialog, dynamic, gprefs
from calibre.utils.config import prefs
from calibre.customize.ui import available_output_formats, all_input_formats
from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported
from calibre.constants import iswindows
@ -48,9 +47,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
choices = [(x.upper(), x) for x in output_formats]
r('output_format', prefs, choices=choices, setting=OutputFormatSetting)
restrictions = sorted(saved_searches().names(), key=sort_key)
restrictions = sorted(db.prefs['virtual_libraries'].iterkeys(), key=sort_key)
choices = [('', '')] + [(x, x) for x in restrictions]
r('gui_restriction', db.prefs, choices=choices)
# check that the virtual library still exists
vls = db.prefs['virtual_lib_on_startup']
if vls and vls not in restrictions:
db.prefs['virtual_lib_on_startup'] = ''
r('virtual_lib_on_startup', db.prefs, choices=choices)
self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs)
self.input_up_button.clicked.connect(self.up_input)

View File

@ -147,15 +147,15 @@ If not checked, the values can be Yes or No.</string>
<item>
<widget class="QLabel" name="label_170">
<property name="text">
<string>Restriction to apply when the current library is opened:</string>
<string>Virtual library to apply when the current library is opened:</string>
</property>
<property name="buddy">
<cstring>opt_gui_restriction</cstring>
<cstring>opt_virtual_lib_on_startup</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_gui_restriction">
<widget class="QComboBox" name="opt_virtual_lib_on_startup">
<property name="maximumSize">
<size>
<width>250</width>
@ -163,7 +163,7 @@ If not checked, the values can be Yes or No.</string>
</size>
</property>
<property name="toolTip">
<string>Apply this restriction on calibre startup if the current library is being used. Also applied when switching to this library. Note that this setting is per library. </string>
<string>Use this virtual library on calibre startup if the current library is being used. Also applied when switching to this library. Note that this setting is per library. </string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>

View File

@ -12,7 +12,6 @@ from PyQt4.Qt import Qt, QUrl, QDialog, QSize, QVBoxLayout, QLabel, \
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.server_ui import Ui_Form
from calibre.utils.search_query_parser import saved_searches
from calibre.library.server import server_config
from calibre.utils.config import ConfigProxy
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
@ -44,13 +43,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
else self.opt_password.Password))
self.opt_password.setEchoMode(self.opt_password.Password)
restrictions = sorted(saved_searches().names(), key=sort_key)
# verify that the current restriction still exists. If not, clear it.
csr = db.prefs.get('cs_restriction', None)
if csr and csr not in restrictions:
db.prefs.set('cs_restriction', '')
restrictions = sorted(db.prefs['virtual_libraries'].iterkeys(), key=sort_key)
choices = [('', '')] + [(x, x) for x in restrictions]
r('cs_restriction', db.prefs, choices=choices)
# check that the virtual library still exists
vls = db.prefs['cs_virtual_lib_on_startup']
if vls and vls not in restrictions:
db.prefs['cs_virtual_lib_on_startup'] = ''
r('cs_virtual_lib_on_startup', db.prefs, choices=choices)
self.start_button.setEnabled(not getattr(self.server, 'is_running', False))
self.test_button.setEnabled(not self.start_button.isEnabled())

View File

@ -139,14 +139,14 @@
<item row="7" column="0">
<widget class="QLabel" name="label_164">
<property name="text">
<string>Restriction (saved search) to apply:</string>
<string>Virtual library to apply:</string>
</property>
</widget>
</item>
<item row="7" column="1" colspan="2">
<widget class="QComboBox" name="opt_cs_restriction">
<widget class="QComboBox" name="opt_cs_virtual_lib_on_startup">
<property name="toolTip">
<string>This restriction (based on a saved search) will restrict the books the content server makes available to those matching the search. This setting is per library (i.e. you can have a different restriction per library).</string>
<string>Setting a virtual library will restrict the books the content server makes available to those in the library. This setting is per library (i.e. you can have a different value per library).</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>

View File

@ -19,7 +19,6 @@ from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.dialogs.search import SearchDialog
from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key
class SearchLineEdit(QLineEdit): # {{{
key_pressed = pyqtSignal(object)
@ -332,6 +331,10 @@ class SavedSearchBox(QComboBox): # {{{
name = unicode(self.currentText())
if not name.strip():
name = unicode(self.search_box.text()).replace('"', '')
if not (name and self.search_box.text()):
error_dialog(self, _('Create saved search'),
_('There is no search to save'), show=True)
return
saved_searches().delete(name)
saved_searches().add(name, unicode(self.search_box.text()))
# now go through an initialization cycle to ensure that the combobox has
@ -362,7 +365,7 @@ class SavedSearchBox(QComboBox): # {{{
# SIGNALed from the main UI
def copy_search_button_clicked(self):
idx = self.currentIndex();
idx = self.currentIndex()
if idx < 0:
return
self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText())))
@ -452,7 +455,7 @@ class SavedSearchBoxMixin(object): # {{{
self.saved_search.save_search_button_clicked)
self.copy_search_button.clicked.connect(
self.saved_search.copy_search_button_clicked)
self.saved_searches_changed()
# self.saved_searches_changed()
self.saved_search.initialize(self.search, colorize=True,
help_text=_('Saved Searches'))
self.saved_search.setToolTip(
@ -479,17 +482,9 @@ class SavedSearchBoxMixin(object): # {{{
partial(self.do_saved_search_edit, None))
def saved_searches_changed(self, set_restriction=None, recount=True):
p = sorted(saved_searches().names(), key=sort_key)
if set_restriction is None:
set_restriction = unicode(self.search_restriction.currentText())
# rebuild the restrictions combobox using current saved searches
self.search_restriction.clear()
self.search_restriction.addItem('')
self.search_restriction.addItem(_('*Current search'))
self.build_search_restriction_list()
if recount:
self.tags_view.recount()
for s in p:
self.search_restriction.addItem(s)
if set_restriction: # redo the search restriction if there was one
self.apply_named_search_restriction(set_restriction)

View File

@ -4,23 +4,422 @@ Created on 10 Jun 2010
@author: charles
'''
from PyQt4.Qt import Qt
from functools import partial
from PyQt4.Qt import (
Qt, QMenu, QPoint, QIcon, QDialog, QGridLayout, QLabel, QLineEdit,
QDialogButtonBox, QSize, QVBoxLayout, QListWidget, QStringList, QCheckBox)
from calibre.gui2 import error_dialog, question_dialog
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre.utils.icu import sort_key
from calibre.utils.pyparsing import ParseException
from calibre.utils.search_query_parser import saved_searches
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(QStringList(sorted(names, key=sort_key)))
self._names.setSelectionMode(self._names.ExtendedSelection)
l.addWidget(self._names)
self._and = QCheckBox(_('Match all selected %s names')%txt)
l.addWidget(self._and)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.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 unicode(item.data(Qt.DisplayRole).toString())
@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 = QLineEdit()
self.vl_name.setMaxLength(MAX_VIRTUAL_LIBRARY_NAME_LENGTH)
la1.setBuddy(self.vl_name)
gl.addWidget(self.vl_name, 0, 1)
self.editing = editing
if editing:
self.vl_name.setText(editing)
self.la2 = la2 = QLabel(_('&Search expression:'))
gl.addWidget(la2, 1, 0)
self.vl_text = QLineEdit()
la2.setBuddy(self.vl_text)
gl.addWidget(self.vl_text, 1, 1)
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>.').format(_('Authors'), _('Tags'), _('Publishers'), _('Series')))
sl.setWordWrap(True)
sl.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
sl.linkActivated.connect(self.link_activated)
gl.addWidget(sl, 2, 0, 1, 2)
self.hl = hl = QLabel(_('''
<h2>Virtual Libraries</h2>
<p>Using <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>
'''))
hl.setWordWrap(True)
hl.setFrameStyle(hl.StyledPanel)
gl.addWidget(hl, 0, 3, 4, 1)
bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.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.prefs.get('virtual_libraries', {})
self.vl_text.setText(virt_libs.get(editing, ''))
self.resize(self.sizeHint()+QSize(150, 25))
def link_activated(self, url):
db = self.gui.current_db
f, txt = unicode(url).partition('.')[0::2]
names = getattr(db, 'all_%s_names'%f)()
d = SelectNames(names, txt, parent=self)
if d.exec_() == d.Accepted:
prefix = f+'s' if f in {'tag', 'author'} else f
search = ['%s:"=%s"'%(prefix, x.replace('"', '\\"')) for x in d.names]
if search:
self.vl_name.setText(d.names.next())
self.vl_text.setText(d.match_type.join(search))
self.vl_text.setCursorPosition(0)
self.vl_name.setCursorPosition(0)
def accept(self):
n = unicode(self.vl_name.text()).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 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) == self.Rejected:
return
v = unicode(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)
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(object):
no_restriction = _('<None>')
def __init__(self):
self.search_restriction.initialize(help_text=_('Restrict to'))
self.search_restriction.activated[int].connect(self.apply_search_restriction)
self.library_view.model().count_changed_signal.connect(self.set_number_of_books_shown)
self.search_restriction.setSizeAdjustPolicy(
self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
self.search_restriction.setStatusTip(self.search_restriction.toolTip())
self.checked = QIcon(I('ok.png'))
self.empty = QIcon()
self.search_based_vl_name = None
self.search_based_vl = None
self.virtual_library_menu = QMenu()
self.virtual_library.clicked.connect(self.virtual_library_clicked)
self.virtual_library_tooltip = \
_('Books display will show only those books matching the search')
self.virtual_library.setToolTip(self.virtual_library_tooltip)
self.search_restriction = ComboBoxWithHelp(self)
self.search_restriction.setVisible(False)
self.search_count.setText(_("(all books)"))
self.search_restriction_tooltip = \
_('Books display will be restricted to those matching a '
'selected saved search')
self.search_restriction.setToolTip(self.search_restriction_tooltip)
self.ar_menu = QMenu(_('Additional restriction'))
def add_virtual_library(self, db, name, search):
virt_libs = db.prefs.get('virtual_libraries', {})
virt_libs[name] = search
db.prefs.set('virtual_libraries', virt_libs)
def do_create_edit(self, editing=None):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
cd = CreateVirtualLibrary(self, virt_libs.keys(), editing=editing)
if cd.exec_() == cd.Accepted:
if editing:
self._remove_vl(editing, reapply=False)
self.add_virtual_library(db, cd.library_name, cd.library_search)
self.apply_virtual_library(cd.library_name)
def virtual_library_clicked(self):
m = self.virtual_library_menu
m.clear()
a = m.addAction(_('Create Virtual Library'))
a.triggered.connect(partial(self.do_create_edit, editing=None))
self.edit_menu = a = QMenu()
a.setTitle(_('Edit Virtual Library'))
a.aboutToShow.connect(partial(self.build_virtual_library_list, remove=False))
m.addMenu(a)
self.rm_menu = a = QMenu()
a.setTitle(_('Remove Virtual Library'))
a.aboutToShow.connect(partial(self.build_virtual_library_list, remove=True))
m.addMenu(a)
m.addSeparator()
db = self.library_view.model().db
a = self.ar_menu
a.clear()
a.setIcon(self.checked if db.data.get_search_restriction_name() else self.empty)
a.aboutToShow.connect(self.build_search_restriction_list)
m.addMenu(a)
m.addSeparator()
current_lib = db.data.get_base_restriction_name()
if 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.empty, _('*current search'))
a.triggered.connect(partial(self.apply_virtual_library, library='*'))
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))
virt_libs = db.prefs.get('virtual_libraries', {})
for vl in sorted(virt_libs.keys(), key=sort_key):
a = m.addAction(self.checked if vl == current_lib else self.empty, vl)
a.triggered.connect(partial(self.apply_virtual_library, library=vl))
p = QPoint(0, self.virtual_library.height())
self.virtual_library_menu.popup(self.virtual_library.mapToGlobal(p))
def apply_virtual_library(self, library=None):
db = self.library_view.model().db
virt_libs = db.prefs.get('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._apply_search_restriction(db.data.get_search_restriction(),
db.data.get_search_restriction_name())
def build_virtual_library_list(self, remove=False):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
if remove:
m = self.rm_menu
else:
m = self.edit_menu
m.clear()
def add_action(name, search):
a = m.addAction(name)
if remove:
a.triggered.connect(partial(self.remove_vl_triggered, name=name))
else:
a.triggered.connect(partial(self.do_create_edit, editing=name))
for n in sorted(virt_libs.keys(), key=sort_key):
add_action(n, virt_libs[n])
def remove_vl_triggered(self, name=None):
if not question_dialog(self, _('Are you sure?'),
_('Are you sure you want to remove '
'the virtual library {0}').format(name),
default_yes=False):
return
self._remove_vl(name, reapply=True)
def _remove_vl(self, name, reapply=True):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
virt_libs.pop(name, None)
db.prefs.set('virtual_libraries', virt_libs)
if reapply and db.data.get_base_restriction_name() == name:
self.apply_virtual_library('')
def _trim_restriction_name(self, name):
return name[0:MAX_VIRTUAL_LIBRARY_NAME_LENGTH].strip()
def build_search_restriction_list(self):
m = self.ar_menu
m.clear()
current_restriction_text = None
if self.search_restriction.count() > 1:
txt = unicode(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(saved_searches().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):
if not name:
@ -29,7 +428,6 @@ class SearchRestrictionMixin(object):
r = self.search_restriction.findText(name)
if r < 0:
r = 0
if r != self.search_restriction.currentIndex():
self.search_restriction.setCurrentIndex(r)
self.apply_search_restriction(r)
@ -37,7 +435,7 @@ class SearchRestrictionMixin(object):
search = unicode(search)
if not search:
self.search_restriction.setCurrentIndex(0)
self._apply_search_restriction('')
self._apply_search_restriction('', '')
else:
s = '*' + search
if self.search_restriction.count() > 1:
@ -49,10 +447,7 @@ class SearchRestrictionMixin(object):
else:
self.search_restriction.insertItem(2, s)
self.search_restriction.setCurrentIndex(2)
self.search_restriction.setToolTip('<p>' +
self.search_restriction_tooltip +
_(' or the search ') + "'" + search + "'</p>")
self._apply_search_restriction(search)
self._apply_search_restriction(search, self._trim_restriction_name(s))
def apply_search_restriction(self, i):
if i == 1:
@ -66,18 +461,20 @@ class SearchRestrictionMixin(object):
restriction = 'search:"%s"'%(r)
else:
restriction = ''
self._apply_search_restriction(restriction)
self._apply_search_restriction(restriction, r)
def _apply_search_restriction(self, 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.set_search_restriction(restriction)
self.tags_view.recount()
self.set_number_of_books_shown()
self.current_view().setFocus(Qt.OtherFocusReason)
self.set_window_title()
def set_number_of_books_shown(self):
db = self.library_view.model().db
@ -86,8 +483,8 @@ class SearchRestrictionMixin(object):
rows = self.current_view().row_count()
rbc = max(rows, db.data.get_search_restriction_book_count())
t = _("({0} of {1})").format(rows, rbc)
self.search_count.setStyleSheet \
('QLabel { border-radius: 8px; background-color: yellow; }')
self.search_count.setStyleSheet(
'QLabel { border-radius: 8px; background-color: yellow; }')
else: # No restriction or not library view
if not self.search.in_a_search():
t = _("(all books)")
@ -96,3 +493,14 @@ class SearchRestrictionMixin(object):
self.search_count.setStyleSheet(
'QLabel { background-color: transparent; }')
self.search_count.setText(t)
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_()

View File

@ -264,13 +264,8 @@ class TagsModel(QAbstractItemModel): # {{{
if rebuild:
self.rebuild_node_tree(state_map)
def set_search_restriction(self, s):
self.search_restriction = s
self.rebuild_node_tree()
def set_database(self, db):
self.beginResetModel()
self.search_restriction = None
hidden_cats = db.prefs.get('tag_browser_hidden_categories', None)
# migrate from config to db prefs
if hidden_cats is None:
@ -848,7 +843,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.categories = {}
# Get the categories
if self.search_restriction:
if self.db.data.get_base_restriction() or self.db.data.get_search_restriction():
try:
data = self.db.get_categories(sort=sort,
icon_map=self.category_icon_map,

View File

@ -232,10 +232,6 @@ class TagsView(QTreeView): # {{{
except:
pass
def set_search_restriction(self, s):
s = s if s else None
self._model.set_search_restriction(s)
def mouseMoveEvent(self, event):
dex = self.indexAt(event.pos())
if self.in_drag_drop or not dex.isValid():

View File

@ -15,7 +15,7 @@ from threading import Thread
from collections import OrderedDict
from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction,
QMenu, QIcon, pyqtSignal, QUrl,
QMenu, QIcon, pyqtSignal, QUrl, QFont,
QDialog, QSystemTrayIcon, QApplication)
from calibre import prints, force_unicode
@ -187,7 +187,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
else:
stmap[st.name] = st
def initialize(self, library_path, db, listener, actions, show_gui=True):
opts = self.opts
self.preferences_action, self.quit_action = actions
@ -279,6 +278,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
UpdateMixin.__init__(self, opts)
####################### Search boxes ########################
SearchRestrictionMixin.__init__(self)
SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__init__(self)
@ -313,9 +313,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
TagBrowserMixin.__init__(self, db)
######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self)
if db.prefs['gui_restriction']:
self.apply_named_search_restriction(db.prefs['gui_restriction'])
if db.prefs['virtual_lib_on_startup']:
self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
########################### Cover Flow ################################
@ -339,7 +338,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if config['autolaunch_server']:
self.start_content_server()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
self.read_settings()
@ -494,7 +492,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
path = os.path.abspath(argv[1])
if os.access(path, os.R_OK):
self.iactions['Add Books'].add_filesystem_book(path)
self.setWindowState(self.windowState() & \
self.setWindowState(self.windowState() &
~Qt.WindowMinimized|Qt.WindowActive)
self.show_windows()
self.raise_()
@ -526,7 +524,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def library_moved(self, newloc, copy_structure=False, call_close=True,
allow_rebuild=False):
if newloc is None: return
if newloc is None:
return
default_prefs = None
try:
olddb = self.library_view.model().db
@ -537,7 +536,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
try:
db = LibraryDatabase2(newloc, default_prefs=default_prefs)
except (DatabaseException, sqlite.Error):
if not allow_rebuild: raise
if not allow_rebuild:
raise
import traceback
repair = question_dialog(self, _('Corrupted database'),
_('The library database at %s appears to be corrupted. Do '
@ -596,9 +596,19 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
# interface later
gc.collect()
def set_window_title(self):
self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name())
db = self.current_db
restrictions = [x for x in (db.data.get_base_restriction_name(),
db.data.get_search_restriction_name()) if x]
restrictions = ' :: '.join(restrictions)
font = QFont()
if restrictions:
restrictions = ' :: ' + restrictions
font.setBold(True)
self.virtual_library.setFont(font)
title = u'{0} - || {1}{2} ||'.format(
__appname__, self.iactions['Choose Library'].library_name(), restrictions)
self.setWindowTitle(title)
def location_selected(self, location):
'''
@ -613,17 +623,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
for action in self.iactions.values():
action.location_selected(location)
if location == 'library':
self.search_restriction.setEnabled(True)
self.virtual_library_menu.setEnabled(True)
self.highlight_only_button.setEnabled(True)
else:
self.search_restriction.setEnabled(False)
self.virtual_library_menu.setEnabled(False)
self.highlight_only_button.setEnabled(False)
# Reset the view in case something changed while it was invisible
self.current_view().reset()
self.set_number_of_books_shown()
def job_exception(self, job, dialog_title=_('Conversion Error')):
if not hasattr(self, '_modeless_dialogs'):
self._modeless_dialogs = []
@ -748,7 +756,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
return False
return True
def shutdown(self, write_settings=True):
try:
db = self.library_view.model().db
@ -808,13 +815,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
pass
QApplication.instance().quit()
def closeEvent(self, e):
self.write_settings()
if self.system_tray_icon.isVisible():
if not dynamic['systray_msg'] and not isosx:
info_dialog(self, 'calibre', 'calibre '+ \
info_dialog(self, 'calibre', 'calibre '+
_('will keep running in the system tray. To close it, '
'choose <b>Quit</b> in the context menu of the '
'system tray.'), show_copy_button=False).exec_()

View File

@ -209,7 +209,8 @@ class ResultCache(SearchQueryParser): # {{{
self._data = []
self._map = self._map_filtered = []
self.first_sort = True
self.search_restriction = ''
self.search_restriction = self.base_restriction = ''
self.base_restriction_name = self.search_restriction_name = ''
self.search_restriction_book_count = 0
self.marked_ids_dict = {}
self.field_metadata = field_metadata
@ -825,8 +826,19 @@ class ResultCache(SearchQueryParser): # {{{
return ans
self._map_filtered = ans
def _build_restriction_string(self, restriction):
if self.base_restriction:
if restriction:
return u'(%s) and (%s)' % (self.base_restriction, restriction)
else:
return self.base_restriction
else:
return restriction
def search_getting_ids(self, query, search_restriction,
set_restriction_count=False):
set_restriction_count=False, use_virtual_library=True):
if use_virtual_library:
search_restriction = self._build_restriction_string(search_restriction)
q = ''
if not query or not query.strip():
q = search_restriction
@ -847,11 +859,32 @@ class ResultCache(SearchQueryParser): # {{{
self.search_restriction_book_count = len(rv)
return rv
def get_search_restriction(self):
return self.search_restriction
def set_search_restriction(self, s):
self.search_restriction = s
def get_base_restriction(self):
return self.base_restriction
def set_base_restriction(self, s):
self.base_restriction = s
def get_base_restriction_name(self):
return self.base_restriction_name
def set_base_restriction_name(self, s):
self.base_restriction_name = s
def get_search_restriction_name(self):
return self.search_restriction_name
def set_search_restriction_name(self, s):
self.search_restriction_name = s
def search_restriction_applied(self):
return bool(self.search_restriction)
return bool(self.search_restriction) or bool((self.base_restriction))
def get_search_restriction_book_count(self):
return self.search_restriction_book_count
@ -1002,7 +1035,7 @@ class ResultCache(SearchQueryParser): # {{{
if field is not None:
self.sort(field, ascending)
self._map_filtered = list(self._map)
if self.search_restriction:
if self.search_restriction or self.base_restriction:
self.search('', return_matches=False)
# Sorting functions {{{

View File

@ -229,6 +229,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
('uuid', False), ('comments', True), ('id', False), ('pubdate', False),
('last_modified', False), ('size', False), ('languages', False),
]
defs['virtual_libraries'] = {}
defs['virtual_lib_on_startup'] = defs['cs_virtual_lib_on_startup'] = ''
# Migrate the bool tristate tweak
defs['bools_are_tristate'] = \
@ -279,6 +281,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
except:
pass
# migrate the gui_restriction preference to a virtual library
gr_pref = self.prefs.get('gui_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['gui_restriction'] = ''
self.prefs['virtual_lib_on_startup'] = gr_pref
# migrate the cs_restriction preference to a virtual library
gr_pref = self.prefs.get('cs_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['cs_restriction'] = ''
self.prefs['cs_virtual_lib_on_startup'] = gr_pref
# Rename any user categories with names that differ only in case
user_cats = self.prefs.get('user_categories', [])
catmap = {}

View File

@ -205,26 +205,32 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
def set_database(self, db):
self.db = db
virt_libs = db.prefs.get('virtual_libraries', {})
sr = getattr(self.opts, 'restriction', None)
sr = db.prefs.get('cs_restriction', '') if sr is None else sr
self.set_search_restriction(sr)
if sr:
if sr in virt_libs:
sr = virt_libs[sr]
elif sr not in saved_searches().names():
prints('WARNING: Content server: search restriction ',
sr, ' does not exist')
sr = ''
else:
sr = 'search:"%s"'%sr
else:
sr = db.prefs.get('cs_virtual_lib_on_startup', '')
if sr:
if sr not in virt_libs:
prints('WARNING: Content server: virtual library ',
sr, ' does not exist')
sr = ''
else:
sr = virt_libs[sr]
self.search_restriction = sr
self.reset_caches()
def graceful(self):
cherrypy.engine.graceful()
def set_search_restriction(self, restriction):
self.search_restriction_name = restriction
if restriction:
if restriction not in saved_searches().names():
prints('WARNING: Content server: search restriction ',
restriction, ' does not exist')
self.search_restriction = ''
else:
self.search_restriction = 'search:"%s"'%restriction
else:
self.search_restriction = ''
self.reset_caches()
def setup_loggers(self):
access_file = log_access_file
error_file = log_error_file

View File

@ -145,10 +145,7 @@ def render_rating(rating, url_prefix, container='span', prefix=None): # {{{
# }}}
def get_category_items(category, items, restriction, datatype, prefix): # {{{
if category == 'search':
items = [x for x in items if x.name != restriction]
def get_category_items(category, items, datatype, prefix): # {{{
def item(i):
templ = (u'<div title="{4}" class="category-item">'
@ -489,8 +486,7 @@ class BrowseServer(object):
if not cats and len(items) == 1:
# Only one item in category, go directly to book list
html = get_category_items(category, items,
self.search_restriction_name, datatype,
self.opts.url_prefix)
datatype, self.opts.url_prefix)
href = re.search(r'<a href="([^"]+)"', html)
if href is not None:
raise cherrypy.HTTPRedirect(href.group(1))
@ -498,8 +494,7 @@ class BrowseServer(object):
if len(items) <= self.opts.max_opds_ungrouped_items:
script = 'false'
items = get_category_items(category, items,
self.search_restriction_name, datatype,
self.opts.url_prefix)
datatype, self.opts.url_prefix)
else:
getter = lambda x: unicode(getattr(x, 'sort', x.name))
starts = set([])
@ -588,8 +583,7 @@ class BrowseServer(object):
sort = self.browse_sort_categories(entries, sort)
entries = get_category_items(category, entries,
self.search_restriction_name, datatype,
self.opts.url_prefix)
datatype, self.opts.url_prefix)
return json.dumps(entries, ensure_ascii=True)

View File

@ -55,10 +55,11 @@ The OPDS interface is advertised via BonJour automatically.
help=_('Write process PID to the specified file'))
parser.add_option('--daemonize', default=False, action='store_true',
help='Run process in background as a daemon. No effect on windows.')
parser.add_option('--restriction', default=None,
help=_('Specifies a restriction to be used for this invocation. '
parser.add_option('--restriction', '--virtual-library', default=None,
help=_('Specifies a virtual library to be used for this invocation. '
'This option overrides any per-library settings specified'
' in the GUI'))
' in the GUI. For compatibility, if the value is not a '
'virtual library but is a saved search, that saved search is used.'))
parser.add_option('--auto-reload', default=False, action='store_true',
help=_('Auto reload server when source code changes. May not'
' work in all environments.'))