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
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 calibre import prints, guess_type
from calibre import prints, guess_type, iswindows
from calibre.devices.errors import DeviceError
from calibre.devices.usbms.driver import debug_print
from calibre.constants import DEBUG
@ -423,7 +423,10 @@ class XMLCache(object):
return ans
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)
if date != record.get('date', None):
record.set('date', date)

View File

@ -294,6 +294,18 @@ class USBMS(CLI, Device):
self.report_progress(1.0, _('Sending metadata to device...'))
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
def path_to_unicode(cls, path):
if isbytestring(path):
@ -355,22 +367,22 @@ class USBMS(CLI, Device):
from calibre.ebooks.metadata.meta import metadata_from_formats
from calibre.customize.ui import 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
def book_from_path(cls, prefix, path):
def book_from_path(cls, prefix, lpath):
from calibre.ebooks.metadata import MetaInformation
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:
from calibre.ebooks.metadata.meta import metadata_from_filename
mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)),
re.compile(r'^(?P<title>[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+'))
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
cls.build_template_regexp())
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')])
size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size
book = cls.book_class(prefix, path, other=mi, size=size)
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
book = cls.book_class(prefix, lpath, other=mi, size=size)
return book

View File

@ -27,16 +27,16 @@ for i, ext in enumerate(_METADATA_PRIORITIES):
def path_to_ext(path):
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:
return _metadata_from_formats(formats)
return _metadata_from_formats(formats, force_read_metadata, pattern)
except:
mi = metadata_from_filename(list(iter(formats))[0])
mi = metadata_from_filename(list(iter(formats), pattern)[0])
if not mi.authors:
mi.authors = [_('Unknown')]
return mi
def _metadata_from_formats(formats):
def _metadata_from_formats(formats, force_read_metadata=False, pattern=None):
mi = MetaInformation(None, None)
formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)],
METADATA_PRIORITIES[path_to_ext(y)]))
@ -51,7 +51,9 @@ def _metadata_from_formats(formats):
with open(path, 'rb') as stream:
try:
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)
except:
continue
@ -69,18 +71,21 @@ def is_recipe(filename):
return filename.startswith('calibre') and \
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
if hasattr(stream, 'tell'):
pos = stream.tell()
try:
return _get_metadata(stream, stream_type, use_libprs_metadata)
return _get_metadata(stream, stream_type, use_libprs_metadata,
force_read_metadata, pattern)
finally:
if hasattr(stream, 'seek'):
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 in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
stream_type = 'html'
@ -100,8 +105,8 @@ def _get_metadata(stream, stream_type, use_libprs_metadata):
mi = MetaInformation(None, None)
name = os.path.basename(getattr(stream, 'name', ''))
base = metadata_from_filename(name)
if is_recipe(name) or prefs['read_file_metadata']:
base = metadata_from_filename(name, pat=pattern)
if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']:
mi = get_file_type_metadata(stream, stream_type)
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
@ -139,7 +144,7 @@ def metadata_from_filename(name, pat=None):
pat = re.compile(prefs.get('filename_pattern'))
name = name.replace('_', ' ')
match = pat.search(name)
if match:
if match is not None:
try:
mi.title = match.group('title')
except IndexError:

View File

@ -1140,6 +1140,13 @@ class DeviceMixin(object):
in cache['authors']:
loc[i] = True
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
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)
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
if title not in self.db_book_title_cache:
self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}}
authors = authors_to_string(mi.authors).lower() if mi.authors else ''
authors = re.sub('(?u)\W|[_]', '', authors)
self.db_book_title_cache[title]['authors'][authors] = mi
self.db_book_title_cache[title] = \
{'authors':{}, 'author_sort':{}, 'db_ids':{}}
if mi.authors:
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_uuid_cache.add(mi.uuid)
@ -1186,12 +1199,19 @@ class DeviceMixin(object):
book.smart_update(d['db_ids'][book.db_id])
resend_metadata = True
continue
book_authors = authors_to_string(book.authors).lower() if book.authors else ''
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
if book_authors in d['authors']:
book.in_library = True
book.smart_update(d['authors'][book_authors])
resend_metadata = True
if book.authors:
# Compare against both author and author sort, because
# either can appear as the author
book_authors = authors_to_string(book.authors).lower()
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
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
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:

View File

@ -279,7 +279,10 @@ class LibraryViewMixin(object): # {{{
if 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.clear_caches()
self.reset()
return ids
def delete_books_by_id(self, ids):
for id in ids:
@ -882,6 +882,15 @@ class DeviceBooksModel(BooksModel): # {{{
ans.extend(v)
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):
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

View File

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

View File

@ -7,11 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
pyqtSignal, SIGNAL
pyqtSignal, SIGNAL, QObject, QDialog
from PyQt4.QtGui import QCompleter
from calibre.gui2 import config
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):
@ -79,8 +83,7 @@ class SearchBox2(QComboBox):
self.setMinimumContentsLength(25)
self._in_a_search = False
def initialize(self, opt_name, colorize=False,
help_text=_('Search')):
def initialize(self, opt_name, colorize=False, help_text=_('Search')):
self.as_you_type = config['search_as_you_type']
self.opt_name = opt_name
self.addItems(QStringList(list(set(config[opt_name]))))
@ -239,9 +242,9 @@ class SavedSearchBox(QComboBox):
self.setInsertPolicy(self.NoInsert)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.setMinimumContentsLength(10)
self.tool_tip_text = self.toolTip()
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
self.tool_tip_text = self.toolTip()
self.saved_searches = _saved_searches
self.search_box = _search_box
self.help_text = help_text
@ -331,3 +334,69 @@ class SavedSearchBox(QComboBox):
if idx < 0:
return
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): # {{{
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.tag_match, self.popularity)
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.tag_item_renamed.connect(self.do_tag_item_renamed)
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):
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)
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.utils.config import prefs, dynamic
from calibre.utils.ipc.server import Server
from calibre.utils.search_query_parser import saved_searches
from calibre.devices.errors import UserFeedback
from calibre.gui2 import warning_dialog, choose_files, error_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, \
fetch_scheduled_recipe, generate_catalog
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.book_info import BookInfo
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.caches import CoverCache
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.search_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_view import TagBrowserMixin
class Listener(Thread): # {{{
@ -106,7 +105,8 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
# }}}
class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, LayoutMixin):
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin):
'The main GUI'
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_library = 0
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.verbose = opts.verbose
@ -229,8 +215,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.connect(self.system_tray_icon,
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
self.system_tray_icon_activated)
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
self.do_advanced_search)
DeviceMixin.__init__(self)
@ -269,6 +253,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.update_checker.update_found.connect(self.update_found,
type=Qt.QueuedConnection)
self.update_checker.start()
####################### Status Bar #####################
self.status_bar.initialize(self.system_tray_icon)
self.status_bar.show_book_info.connect(self.show_book_info)
@ -277,6 +262,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
####################### Setup Toolbar #####################
ToolbarMixin.__init__(self)
####################### Search boxes ########################
SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__init__(self)
####################### Library view ########################
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:
self.hide_windows()
self.stack.setCurrentIndex(0)
self.search.setFocus(Qt.OtherFocusReason)
self.cover_cache = CoverCache(self.library_path)
self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
self.search_restriction.activated[str].connect(self.apply_search_restriction)
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()
self.library_view.model().count_changed_signal.connect \
(self.location_view.count_changed)
if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -318,8 +298,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
########################### Tags Browser ##############################
TagBrowserMixin.__init__(self, db)
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self)
########################### Cover Flow ################################
@ -328,7 +309,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self._calculated_available_height = min(max_available_height()-15,
self.height())
self.resize(self.width(), self._calculated_available_height)
self.search.setMaximumWidth(self.width()-150)
if config['autolaunch_server']:
@ -358,13 +338,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.read_settings()
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):
MainWindow.resizeEvent(self, ev)
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'),
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):
try:
@ -554,8 +457,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
def booklists(self):
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
########################## Connect to device ##############################
def save_device_view_settings(self):
@ -1130,7 +1031,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
row = None
if ci.isValid():
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:
ci = view.model().index(row, 0)
if ci.isValid():
@ -1175,6 +1080,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
self.booklists())
model.paths_deleted(paths)
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 ##################################

View File

@ -619,9 +619,12 @@ class ResultCache(SearchQueryParser):
if self.first_sort:
subsort = True
self.first_sort = False
fcmp = self.seriescmp if field == 'series' else \
functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort,
asstr=as_string)
fcmp = self.seriescmp \
if field == 'series' and \
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_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.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.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick_draw import save_cover_data_to
@ -736,8 +736,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
icon=icon, tooltip = tooltip)
for r in data if item_not_zero_func(r)]
if category == 'series' and not sort_on_count:
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(),
title_sort(y.name).lower()))
if tweaks['title_series_sorting'] == 'library_order':
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
# use a view, but is computed dynamically
@ -950,7 +954,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
title = title.decode(preferred_encoding, 'replace')
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['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.conn.commit()
if notify:

View File

@ -15,6 +15,7 @@ from threading import RLock
from datetime import datetime
from calibre.ebooks.metadata import title_sort
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat
global_lock = RLock()
@ -115,7 +116,10 @@ class DBThread(Thread):
self.conn.create_aggregate('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
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()))
# Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1)