Add title and series sorting tweak. SONY Driver: Set timestamp correctly on windows. Fix #5744 (Tag browser re-opening closed tree after editing metadata). Always read metadata from files on devices, ignoring the preference to read metadata from filenames. Deleting books now refreshes ondevice and inlibrary columns

This commit is contained in:
Kovid Goyal 2010-06-10 22:04:31 -06:00
commit 6475db508b
15 changed files with 282 additions and 168 deletions

View File

@ -61,3 +61,13 @@ sort_columns_at_startup = None
# default if not set: MMM yyyy # default if not set: MMM yyyy
gui_pubdate_display_format = 'MMM yyyy' gui_pubdate_display_format = 'MMM yyyy'
# Control title and series sorting in the library view.
# If set to 'library_order', Leading articles such as The and A will be ignored.
# If set to 'strictly_alphabetic', the titles will be sorted without processing
# For example, with library_order, The Client will sort under 'C'. With
# strictly_alphabetic, the book will sort under 'T'.
# This flag affects Calibre's library display. It has no effect on devices. In
# addition, titles for books added before changing the flag will retain their
# order until the title is edited. Double-clicking on a title and hitting return
# without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order'

View File

@ -12,7 +12,7 @@ from uuid import uuid4
from lxml import etree from lxml import etree
from calibre import prints, guess_type from calibre import prints, guess_type, iswindows
from calibre.devices.errors import DeviceError from calibre.devices.errors import DeviceError
from calibre.devices.usbms.driver import debug_print from calibre.devices.usbms.driver import debug_print
from calibre.constants import DEBUG from calibre.constants import DEBUG
@ -423,7 +423,10 @@ class XMLCache(object):
return ans return ans
def update_text_record(self, record, book, path, bl_index): def update_text_record(self, record, book, path, bl_index):
timestamp = os.path.getctime(path) timestamp = os.path.getmtime(path)
# Correct for MS DST time 'adjustment'
if iswindows and time.daylight:
timestamp -= time.altzone - time.timezone
date = strftime(timestamp) date = strftime(timestamp)
if date != record.get('date', None): if date != record.get('date', None):
record.set('date', date) record.set('date', date)

View File

@ -294,6 +294,18 @@ class USBMS(CLI, Device):
self.report_progress(1.0, _('Sending metadata to device...')) self.report_progress(1.0, _('Sending metadata to device...'))
debug_print('USBMS: finished sync_booklists') debug_print('USBMS: finished sync_booklists')
@classmethod
def build_template_regexp(cls):
def replfunc(match):
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
return '(?P<' + match.group(1) + '>.+?)'
elif match.group(1) == 'authors':
return '(?P<author>.+?)'
else:
return '(.+?)'
template = cls.save_template().rpartition('/')[2]
return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)')
@classmethod @classmethod
def path_to_unicode(cls, path): def path_to_unicode(cls, path):
if isbytestring(path): if isbytestring(path):
@ -355,22 +367,22 @@ class USBMS(CLI, Device):
from calibre.ebooks.metadata.meta import metadata_from_formats from calibre.ebooks.metadata.meta import metadata_from_formats
from calibre.customize.ui import quick_metadata from calibre.customize.ui import quick_metadata
with quick_metadata: with quick_metadata:
return metadata_from_formats(fmts) return metadata_from_formats(fmts, force_read_metadata=True,
pattern=cls.build_template_regexp())
@classmethod @classmethod
def book_from_path(cls, prefix, path): def book_from_path(cls, prefix, lpath):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
if cls.settings().read_metadata or cls.MUST_READ_METADATA: if cls.settings().read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, path))) mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
else: else:
from calibre.ebooks.metadata.meta import metadata_from_filename from calibre.ebooks.metadata.meta import metadata_from_filename
mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)), mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
re.compile(r'^(?P<title>[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+')) cls.build_template_regexp())
if mi is None: if mi is None:
mi = MetaInformation(os.path.splitext(os.path.basename(path))[0], mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0],
[_('Unknown')]) [_('Unknown')])
size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
book = cls.book_class(prefix, path, other=mi, size=size) book = cls.book_class(prefix, lpath, other=mi, size=size)
return book return book

View File

@ -27,16 +27,16 @@ for i, ext in enumerate(_METADATA_PRIORITIES):
def path_to_ext(path): def path_to_ext(path):
return os.path.splitext(path)[1][1:].lower() return os.path.splitext(path)[1][1:].lower()
def metadata_from_formats(formats): def metadata_from_formats(formats, force_read_metadata=False, pattern=None):
try: try:
return _metadata_from_formats(formats) return _metadata_from_formats(formats, force_read_metadata, pattern)
except: except:
mi = metadata_from_filename(list(iter(formats))[0]) mi = metadata_from_filename(list(iter(formats), pattern)[0])
if not mi.authors: if not mi.authors:
mi.authors = [_('Unknown')] mi.authors = [_('Unknown')]
return mi return mi
def _metadata_from_formats(formats): def _metadata_from_formats(formats, force_read_metadata=False, pattern=None):
mi = MetaInformation(None, None) mi = MetaInformation(None, None)
formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)], formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)],
METADATA_PRIORITIES[path_to_ext(y)])) METADATA_PRIORITIES[path_to_ext(y)]))
@ -51,7 +51,9 @@ def _metadata_from_formats(formats):
with open(path, 'rb') as stream: with open(path, 'rb') as stream:
try: try:
newmi = get_metadata(stream, stream_type=ext, newmi = get_metadata(stream, stream_type=ext,
use_libprs_metadata=True) use_libprs_metadata=True,
force_read_metadata=force_read_metadata,
pattern=pattern)
mi.smart_update(newmi) mi.smart_update(newmi)
except: except:
continue continue
@ -69,18 +71,21 @@ def is_recipe(filename):
return filename.startswith('calibre') and \ return filename.startswith('calibre') and \
filename.rpartition('.')[0].endswith('_recipe_out') filename.rpartition('.')[0].endswith('_recipe_out')
def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False): def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False,
force_read_metadata=False, pattern=None):
pos = 0 pos = 0
if hasattr(stream, 'tell'): if hasattr(stream, 'tell'):
pos = stream.tell() pos = stream.tell()
try: try:
return _get_metadata(stream, stream_type, use_libprs_metadata) return _get_metadata(stream, stream_type, use_libprs_metadata,
force_read_metadata, pattern)
finally: finally:
if hasattr(stream, 'seek'): if hasattr(stream, 'seek'):
stream.seek(pos) stream.seek(pos)
def _get_metadata(stream, stream_type, use_libprs_metadata): def _get_metadata(stream, stream_type, use_libprs_metadata,
force_read_metadata=False, pattern=None):
if stream_type: stream_type = stream_type.lower() if stream_type: stream_type = stream_type.lower()
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'): if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
stream_type = 'html' stream_type = 'html'
@ -100,8 +105,8 @@ def _get_metadata(stream, stream_type, use_libprs_metadata):
mi = MetaInformation(None, None) mi = MetaInformation(None, None)
name = os.path.basename(getattr(stream, 'name', '')) name = os.path.basename(getattr(stream, 'name', ''))
base = metadata_from_filename(name) base = metadata_from_filename(name, pat=pattern)
if is_recipe(name) or prefs['read_file_metadata']: if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']:
mi = get_file_type_metadata(stream, stream_type) mi = get_file_type_metadata(stream, stream_type)
if base.title == os.path.splitext(name)[0] and base.authors is None: if base.title == os.path.splitext(name)[0] and base.authors is None:
# Assume that there was no metadata in the file and the user set pattern # Assume that there was no metadata in the file and the user set pattern
@ -139,7 +144,7 @@ def metadata_from_filename(name, pat=None):
pat = re.compile(prefs.get('filename_pattern')) pat = re.compile(prefs.get('filename_pattern'))
name = name.replace('_', ' ') name = name.replace('_', ' ')
match = pat.search(name) match = pat.search(name)
if match: if match is not None:
try: try:
mi.title = match.group('title') mi.title = match.group('title')
except IndexError: except IndexError:

View File

@ -1140,6 +1140,13 @@ class DeviceMixin(object):
in cache['authors']: in cache['authors']:
loc[i] = True loc[i] = True
continue continue
# Also check author sort, because it can be used as author in
# some formats
if mi.author_sort and \
re.sub('(?u)\W|[_]', '', mi.author_sort.lower()) \
in cache['authors']:
loc[i] = True
continue
return loc return loc
def set_books_in_library(self, booklists, reset=False): def set_books_in_library(self, booklists, reset=False):
@ -1152,10 +1159,16 @@ class DeviceMixin(object):
mi = db.get_metadata(id, index_is_id=True) mi = db.get_metadata(id, index_is_id=True)
title = re.sub('(?u)\W|[_]', '', mi.title.lower()) title = re.sub('(?u)\W|[_]', '', mi.title.lower())
if title not in self.db_book_title_cache: if title not in self.db_book_title_cache:
self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}} self.db_book_title_cache[title] = \
authors = authors_to_string(mi.authors).lower() if mi.authors else '' {'authors':{}, 'author_sort':{}, 'db_ids':{}}
authors = re.sub('(?u)\W|[_]', '', authors) if mi.authors:
self.db_book_title_cache[title]['authors'][authors] = mi authors = authors_to_string(mi.authors).lower()
authors = re.sub('(?u)\W|[_]', '', authors)
self.db_book_title_cache[title]['authors'][authors] = mi
if mi.author_sort:
aus = mi.author_sort.lower()
aus = re.sub('(?u)\W|[_]', '', aus)
self.db_book_title_cache[title]['author_sort'][aus] = mi
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
self.db_book_uuid_cache.add(mi.uuid) self.db_book_uuid_cache.add(mi.uuid)
@ -1186,12 +1199,19 @@ class DeviceMixin(object):
book.smart_update(d['db_ids'][book.db_id]) book.smart_update(d['db_ids'][book.db_id])
resend_metadata = True resend_metadata = True
continue continue
book_authors = authors_to_string(book.authors).lower() if book.authors else '' if book.authors:
book_authors = re.sub('(?u)\W|[_]', '', book_authors) # Compare against both author and author sort, because
if book_authors in d['authors']: # either can appear as the author
book.in_library = True book_authors = authors_to_string(book.authors).lower()
book.smart_update(d['authors'][book_authors]) book_authors = re.sub('(?u)\W|[_]', '', book_authors)
resend_metadata = True if book_authors in d['authors']:
book.in_library = True
book.smart_update(d['authors'][book_authors])
resend_metadata = True
elif book_authors in d['author_sort']:
book.in_library = True
book.smart_update(d['author_sort'][book_authors])
resend_metadata = True
# Set author_sort if it isn't already # Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None) asort = getattr(book, 'author_sort', None)
if not asort and book.authors: if not asort and book.authors:

View File

@ -279,7 +279,10 @@ class LibraryViewMixin(object): # {{{
if search: if search:
self.search.set_search_string(join.join(search)) self.search.set_search_string(join.join(search))
def search_done(self, view, ok):
if view is self.current_view():
self.search.search_done(ok)
self.set_number_of_books_shown()
# }}} # }}}

View File

@ -200,7 +200,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.count_changed() self.count_changed()
self.clear_caches() self.clear_caches()
self.reset() self.reset()
return ids
def delete_books_by_id(self, ids): def delete_books_by_id(self, ids):
for id in ids: for id in ids:
@ -882,6 +882,15 @@ class DeviceBooksModel(BooksModel): # {{{
ans.extend(v) ans.extend(v)
return ans return ans
def clear_ondevice(self, db_ids):
for data in self.db:
if data is None:
continue
app_id = getattr(data, 'application_id', None)
if app_id is not None and app_id in db_ids:
data.in_library = False
self.reset()
def flags(self, index): def flags(self, index):
if self.map[index.row()] in self.indices_to_be_deleted(): if self.map[index.row()] in self.indices_to_be_deleted():
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python

View File

@ -169,6 +169,12 @@
</item> </item>
<item> <item>
<widget class="QComboBox" name="search_restriction"> <widget class="QComboBox" name="search_restriction">
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Books display will be restricted to those matching the selected saved search</string> <string>Books display will be restricted to those matching the selected saved search</string>
</property> </property>

View File

@ -7,11 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \ from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
pyqtSignal, SIGNAL pyqtSignal, SIGNAL, QObject, QDialog
from PyQt4.QtGui import QCompleter from PyQt4.QtGui import QCompleter
from calibre.gui2 import config from calibre.gui2 import config
from calibre.gui2.dialogs.confirm_delete import confirm 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.config import prefs
from calibre.utils.search_query_parser import saved_searches
class SearchLineEdit(QLineEdit): class SearchLineEdit(QLineEdit):
@ -79,8 +83,7 @@ class SearchBox2(QComboBox):
self.setMinimumContentsLength(25) self.setMinimumContentsLength(25)
self._in_a_search = False self._in_a_search = False
def initialize(self, opt_name, colorize=False, def initialize(self, opt_name, colorize=False, help_text=_('Search')):
help_text=_('Search')):
self.as_you_type = config['search_as_you_type'] self.as_you_type = config['search_as_you_type']
self.opt_name = opt_name self.opt_name = opt_name
self.addItems(QStringList(list(set(config[opt_name])))) self.addItems(QStringList(list(set(config[opt_name]))))
@ -239,9 +242,9 @@ class SavedSearchBox(QComboBox):
self.setInsertPolicy(self.NoInsert) self.setInsertPolicy(self.NoInsert)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.setMinimumContentsLength(10) self.setMinimumContentsLength(10)
self.tool_tip_text = self.toolTip()
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')): def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
self.tool_tip_text = self.toolTip()
self.saved_searches = _saved_searches self.saved_searches = _saved_searches
self.search_box = _search_box self.search_box = _search_box
self.help_text = help_text self.help_text = help_text
@ -331,3 +334,69 @@ class SavedSearchBox(QComboBox):
if idx < 0: if idx < 0:
return return
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText()))) self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
class SearchBoxMixin(object):
def __init__(self):
self.search.initialize('main_search_history', colorize=True,
help_text=_('Search (For Advanced Search click the button to the left)'))
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
self.do_advanced_search)
self.search.clear()
self.search.setFocus(Qt.OtherFocusReason)
self.search.setMaximumWidth(self.width()-150)
def search_box_cleared(self):
self.tags_view.clear()
self.saved_search.clear_to_help()
self.set_number_of_books_shown()
def do_advanced_search(self, *args):
d = SearchDialog(self)
if d.exec_() == QDialog.Accepted:
self.search.set_search_string(d.search_string())
class SavedSearchBoxMixin(object):
def __init__(self):
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
self.saved_searches_changed()
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
self.saved_search.initialize(saved_searches, self.search, colorize=True,
help_text=_('Saved Searches'))
self.connect(self.save_search_button, SIGNAL('clicked()'),
self.saved_search.save_search_button_clicked)
self.connect(self.delete_search_button, SIGNAL('clicked()'),
self.saved_search.delete_search_button_clicked)
self.connect(self.copy_search_button, SIGNAL('clicked()'),
self.saved_search.copy_search_button_clicked)
def saved_searches_changed(self):
p = prefs['saved_searches'].keys()
p.sort()
t = unicode(self.search_restriction.currentText())
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
self.search_restriction.addItem('')
self.tags_view.recount()
for s in p:
self.search_restriction.addItem(s)
if t:
if t in p: # redo the current restriction, if there was one
self.search_restriction.setCurrentIndex(self.search_restriction.findText(t))
# self.tags_view.set_search_restriction(t)
else:
self.search_restriction.setCurrentIndex(0)
self.apply_search_restriction('')
def do_saved_search_edit(self, search):
d = SavedSearchEditor(self, search)
d.exec_()
if d.result() == d.Accepted:
self.saved_searches_changed()
self.saved_search.clear_to_help()

View File

@ -0,0 +1,57 @@
'''
Created on 10 Jun 2010
@author: charles
'''
class SearchRestrictionMixin(object):
def __init__(self):
self.search_restriction.activated[str].connect(self.apply_search_restriction)
self.library_view.model().count_changed_signal.connect(self.restriction_count_changed)
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
'''
Adding and deleting books while restricted creates a complexity. When added,
they are displayed regardless of whether they match a search restriction.
However, if they do not, they are removed at the next search. The counts
must take this behavior into effect.
'''
def restriction_count_changed(self, c):
self.restriction_count_of_books_in_view += \
c - self.restriction_count_of_books_in_library
self.restriction_count_of_books_in_library = c
if self.restriction_in_effect:
self.set_number_of_books_shown()
def apply_search_restriction(self, r):
r = unicode(r)
if r is not None and r != '':
self.restriction_in_effect = True
restriction = 'search:"%s"'%(r)
else:
self.restriction_in_effect = False
restriction = ''
self.restriction_count_of_books_in_view = \
self.library_view.model().set_search_restriction(restriction)
self.search.clear_to_help()
self.saved_search.clear_to_help()
self.tags_view.set_search_restriction(restriction)
self.set_number_of_books_shown()
def set_number_of_books_shown(self):
if self.current_view() == self.library_view and self.restriction_in_effect:
t = _("({0} of {1})").format(self.current_view().row_count(),
self.restriction_count_of_books_in_view)
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)")
else:
t = _("({0} of all)").format(self.current_view().row_count())
self.search_count.setStyleSheet(
'QLabel { background-color: transparent; }')
self.search_count.setText(t)

View File

@ -599,6 +599,7 @@ class TagsModel(QAbstractItemModel): # {{{
class TagBrowserMixin(object): # {{{ class TagBrowserMixin(object): # {{{
def __init__(self, db): def __init__(self, db):
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
self.tags_view.set_database(self.library_view.model().db, self.tags_view.set_database(self.library_view.model().db,
self.tag_match, self.popularity) self.tag_match, self.popularity)
self.tags_view.tags_marked.connect(self.search.search_from_tags) self.tags_view.tags_marked.connect(self.search.search_from_tags)
@ -608,6 +609,7 @@ class TagBrowserMixin(object): # {{{
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help) self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
self.edit_categories.clicked.connect(self.do_user_categories_edit)
def do_user_categories_edit(self, on_category=None): def do_user_categories_edit(self, on_category=None):
d = TagCategories(self, self.library_view.model().db, on_category) d = TagCategories(self, self.library_view.model().db, on_category)
@ -658,5 +660,6 @@ class TagBrowserWidget(QWidget): # {{{
parent.edit_categories = QPushButton(_('Manage &user categories'), parent) parent.edit_categories = QPushButton(_('Manage &user categories'), parent)
self._layout.addWidget(parent.edit_categories) self._layout.addWidget(parent.edit_categories)
# }}} # }}}

View File

@ -29,7 +29,6 @@ from calibre.utils.filenames import ascii_filename
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
from calibre.utils.ipc.server import Server from calibre.utils.ipc.server import Server
from calibre.utils.search_query_parser import saved_searches
from calibre.devices.errors import UserFeedback from calibre.devices.errors import UserFeedback
from calibre.gui2 import warning_dialog, choose_files, error_dialog, \ from calibre.gui2 import warning_dialog, choose_files, error_dialog, \
question_dialog,\ question_dialog,\
@ -51,7 +50,6 @@ from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
fetch_scheduled_recipe, generate_catalog fetch_scheduled_recipe, generate_catalog
from calibre.gui2.dialogs.config import ConfigDialog from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.search import SearchDialog
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2.dialogs.book_info import BookInfo from calibre.gui2.dialogs.book_info import BookInfo
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
@ -59,9 +57,10 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
from calibre.library.caches import CoverCache from calibre.library.caches import CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.tag_view import TagBrowserMixin
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_view import TagBrowserMixin
class Listener(Thread): # {{{ class Listener(Thread): # {{{
@ -106,7 +105,8 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
# }}} # }}}
class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, LayoutMixin): TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin):
'The main GUI' 'The main GUI'
def set_default_thumbnail(self, height): def set_default_thumbnail(self, height):
@ -156,20 +156,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.restriction_count_of_books_in_view = 0 self.restriction_count_of_books_in_view = 0
self.restriction_count_of_books_in_library = 0 self.restriction_count_of_books_in_library = 0
self.restriction_in_effect = False self.restriction_in_effect = False
self.search.initialize('main_search_history', colorize=True,
help_text=_('Search (For Advanced Search click the button to the left)'))
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
self.search.clear()
self.saved_search.initialize(saved_searches, self.search, colorize=True,
help_text=_('Saved Searches'))
self.connect(self.save_search_button, SIGNAL('clicked()'),
self.saved_search.save_search_button_clicked)
self.connect(self.delete_search_button, SIGNAL('clicked()'),
self.saved_search.delete_search_button_clicked)
self.connect(self.copy_search_button, SIGNAL('clicked()'),
self.saved_search.copy_search_button_clicked)
self.progress_indicator = ProgressIndicator(self) self.progress_indicator = ProgressIndicator(self)
self.verbose = opts.verbose self.verbose = opts.verbose
@ -229,8 +215,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.connect(self.system_tray_icon, self.connect(self.system_tray_icon,
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
self.system_tray_icon_activated) self.system_tray_icon_activated)
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
self.do_advanced_search)
DeviceMixin.__init__(self) DeviceMixin.__init__(self)
@ -269,6 +253,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.update_checker.update_found.connect(self.update_found, self.update_checker.update_found.connect(self.update_found,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.update_checker.start() self.update_checker.start()
####################### Status Bar ##################### ####################### Status Bar #####################
self.status_bar.initialize(self.system_tray_icon) self.status_bar.initialize(self.system_tray_icon)
self.status_bar.show_book_info.connect(self.show_book_info) self.status_bar.show_book_info.connect(self.show_book_info)
@ -277,6 +262,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
####################### Setup Toolbar ##################### ####################### Setup Toolbar #####################
ToolbarMixin.__init__(self) ToolbarMixin.__init__(self)
####################### Search boxes ########################
SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__init__(self)
####################### Library view ######################## ####################### Library view ########################
LibraryViewMixin.__init__(self, db) LibraryViewMixin.__init__(self, db)
@ -284,20 +273,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
if self.system_tray_icon.isVisible() and opts.start_in_tray: if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows() self.hide_windows()
self.stack.setCurrentIndex(0)
self.search.setFocus(Qt.OtherFocusReason)
self.cover_cache = CoverCache(self.library_path) self.cover_cache = CoverCache(self.library_path)
self.cover_cache.start() self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache self.library_view.model().cover_cache = self.cover_cache
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit) self.library_view.model().count_changed_signal.connect \
self.search_restriction.activated[str].connect(self.apply_search_restriction) (self.location_view.count_changed)
for x in (self.location_view.count_changed, self.tags_view.recount,
self.restriction_count_changed):
self.library_view.model().count_changed_signal.connect(x)
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
self.saved_searches_changed()
if not gprefs.get('quick_start_guide_added', False): if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember']) mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -318,8 +298,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
########################### Tags Browser ############################## ########################### Tags Browser ##############################
TagBrowserMixin.__init__(self, db) TagBrowserMixin.__init__(self, db)
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10) ######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self)
########################### Cover Flow ################################ ########################### Cover Flow ################################
@ -328,7 +309,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self._calculated_available_height = min(max_available_height()-15, self._calculated_available_height = min(max_available_height()-15,
self.height()) self.height())
self.resize(self.width(), self._calculated_available_height) self.resize(self.width(), self._calculated_available_height)
self.search.setMaximumWidth(self.width()-150)
if config['autolaunch_server']: if config['autolaunch_server']:
@ -358,13 +338,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.read_settings() self.read_settings()
self.finalize_layout() self.finalize_layout()
def do_saved_search_edit(self, search):
d = SavedSearchEditor(self, search)
d.exec_()
if d.result() == d.Accepted:
self.saved_searches_changed()
self.saved_search.clear_to_help()
def resizeEvent(self, ev): def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev) MainWindow.resizeEvent(self, ev)
self.search.setMaximumWidth(self.width()-150) self.search.setMaximumWidth(self.width()-150)
@ -446,76 +419,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
error_dialog(self, _('Failed to start content server'), error_dialog(self, _('Failed to start content server'),
unicode(self.content_server.exception)).exec_() unicode(self.content_server.exception)).exec_()
'''
Restrictions.
Adding and deleting books creates a complexity. When added, they are
displayed regardless of whether they match a search restriction. However, if
they do not, they are removed at the next search. The counts must take this
behavior into effect.
'''
def restriction_count_changed(self, c):
self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library
self.restriction_count_of_books_in_library = c
if self.restriction_in_effect:
self.set_number_of_books_shown()
def apply_search_restriction(self, r):
r = unicode(r)
if r is not None and r != '':
self.restriction_in_effect = True
restriction = 'search:"%s"'%(r)
else:
self.restriction_in_effect = False
restriction = ''
self.restriction_count_of_books_in_view = \
self.library_view.model().set_search_restriction(restriction)
self.search.clear_to_help()
self.saved_search.clear_to_help()
self.tags_view.set_search_restriction(restriction)
self.set_number_of_books_shown()
def set_number_of_books_shown(self):
if self.current_view() == self.library_view and self.restriction_in_effect:
t = _("({0} of {1})").format(self.current_view().row_count(),
self.restriction_count_of_books_in_view)
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)")
else:
t = _("({0} of all)").format(self.current_view().row_count())
self.search_count.setStyleSheet(
'QLabel { background-color: transparent; }')
self.search_count.setText(t)
def search_box_cleared(self):
self.tags_view.clear()
self.saved_search.clear_to_help()
self.set_number_of_books_shown()
def search_done(self, view, ok):
if view is self.current_view():
self.search.search_done(ok)
self.set_number_of_books_shown()
def saved_searches_changed(self):
p = prefs['saved_searches'].keys()
p.sort()
t = unicode(self.search_restriction.currentText())
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
self.search_restriction.addItem('')
self.tags_view.recount()
for s in p:
self.search_restriction.addItem(s)
if t:
if t in p: # redo the current restriction, if there was one
self.search_restriction.setCurrentIndex(self.search_restriction.findText(t))
# self.tags_view.set_search_restriction(t)
else:
self.search_restriction.setCurrentIndex(0)
self.apply_search_restriction('')
def another_instance_wants_to_talk(self): def another_instance_wants_to_talk(self):
try: try:
@ -554,8 +457,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
def booklists(self): def booklists(self):
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
########################## Connect to device ############################## ########################## Connect to device ##############################
def save_device_view_settings(self): def save_device_view_settings(self):
@ -1130,7 +1031,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
row = None row = None
if ci.isValid(): if ci.isValid():
row = ci.row() row = ci.row()
view.model().delete_books(rows) ids_deleted = view.model().delete_books(rows)
for v in (self.memory_view, self.card_a_view, self.card_b_view):
if v is None:
continue
v.model().clear_ondevice(ids_deleted)
if row is not None: if row is not None:
ci = view.model().index(row, 0) ci = view.model().index(row, 0)
if ci.isValid(): if ci.isValid():
@ -1175,6 +1080,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.booklists()) self.booklists())
model.paths_deleted(paths) model.paths_deleted(paths)
self.upload_booklists() self.upload_booklists()
# Clear the ondevice info so it will be recomputed
self.book_on_device(None, None, reset=True)
# We want to reset all the ondevice flags in the library. Use a big
# hammer, so we don't need to worry about whether some succeeded or not
self.library_view.model().refresh()
############################################################################ ############################################################################
@ -1845,13 +1755,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
############################################################################ ############################################################################
########################### Do advanced search #############################
def do_advanced_search(self, *args):
d = SearchDialog(self)
if d.exec_() == QDialog.Accepted:
self.search.set_search_string(d.search_string())
############################################################################ ############################################################################
############################### Do config ################################## ############################### Do config ##################################

View File

@ -619,9 +619,12 @@ class ResultCache(SearchQueryParser):
if self.first_sort: if self.first_sort:
subsort = True subsort = True
self.first_sort = False self.first_sort = False
fcmp = self.seriescmp if field == 'series' else \ fcmp = self.seriescmp \
functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort, if field == 'series' and \
asstr=as_string) tweaks['title_series_sorting'] == 'library_order' \
else \
functools.partial(self.cmp, self.FIELD_MAP[field],
subsort=subsort, asstr=as_string)
self._map.sort(cmp=fcmp, reverse=not ascending) self._map.sort(cmp=fcmp, reverse=not ascending)
self._map_filtered = [id for id in self._map if id in self._map_filtered] self._map_filtered = [id for id in self._map if id in self._map_filtered]

View File

@ -28,7 +28,7 @@ from calibre.customize.ui import run_plugins_on_import
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.config import prefs from calibre.utils.config import prefs, tweaks
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick_draw import save_cover_data_to from calibre.utils.magick_draw import save_cover_data_to
@ -736,8 +736,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
icon=icon, tooltip = tooltip) icon=icon, tooltip = tooltip)
for r in data if item_not_zero_func(r)] for r in data if item_not_zero_func(r)]
if category == 'series' and not sort_on_count: if category == 'series' and not sort_on_count:
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(), if tweaks['title_series_sorting'] == 'library_order':
title_sort(y.name).lower())) ts = lambda x: title_sort(x)
else:
ts = lambda x:x
categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(),
ts(y.name).lower()))
# We delayed computing the standard formats category because it does not # We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically # use a view, but is computed dynamically
@ -950,7 +954,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
title = title.decode(preferred_encoding, 'replace') title = title.decode(preferred_encoding, 'replace')
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id)) self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True) self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True)
self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) if tweaks['title_series_sorting'] == 'library_order':
self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True)
else:
self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True)
self.set_path(id, True) self.set_path(id, True)
self.conn.commit() self.conn.commit()
if notify: if notify:

View File

@ -15,6 +15,7 @@ from threading import RLock
from datetime import datetime from datetime import datetime
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat from calibre.utils.date import parse_date, isoformat
global_lock = RLock() global_lock = RLock()
@ -115,7 +116,10 @@ class DBThread(Thread):
self.conn.create_aggregate('concat', 1, Concatenate) self.conn.create_aggregate('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
self.conn.create_function('title_sort', 1, title_sort) if tweaks['title_series_sorting'] == 'library_order':
self.conn.create_function('title_sort', 1, title_sort)
else:
self.conn.create_function('title_sort', 1, lambda x:x)
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4())) self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
# Dummy functions for dynamically created filters # Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1) self.conn.create_function('books_list_filter', 1, lambda x: 1)