From beb817b5cbd10ce4ed6b97d8c4af32b0b3aeea23 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 21 May 2010 19:19:51 +0100 Subject: [PATCH 1/5] Build enough of custom_column metadata support to implement reserved column names. Fix search to not go pink when the timer expires, and to not be always pink for devices. --- src/calibre/devices/metadata_serializer.py | 90 +++++++++++++++++++ src/calibre/devices/usbms/books.py | 25 +----- src/calibre/devices/usbms/driver.py | 7 +- .../dialogs/config/create_custom_column.py | 3 + src/calibre/gui2/library/models.py | 2 +- src/calibre/gui2/search_box.py | 5 +- 6 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 src/calibre/devices/metadata_serializer.py diff --git a/src/calibre/devices/metadata_serializer.py b/src/calibre/devices/metadata_serializer.py new file mode 100644 index 0000000000..651ba1d678 --- /dev/null +++ b/src/calibre/devices/metadata_serializer.py @@ -0,0 +1,90 @@ +''' +Created on 21 May 2010 + +@author: charles +''' + +from calibre.constants import filesystem_encoding, preferred_encoding +from calibre import isbytestring +import json + +class MetadataSerializer(object): + + SERIALIZED_ATTRS = [ + 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort', + 'title_sort', 'comments', 'category', 'publisher', 'series', + 'series_index', 'rating', 'isbn', 'language', 'application_id', + 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type', + 'uuid', + ] + + def to_json(self): + json = {} + for attr in self.SERIALIZED_ATTRS: + val = getattr(self, attr) + if isbytestring(val): + enc = filesystem_encoding if attr == 'lpath' else preferred_encoding + val = val.decode(enc, 'replace') + elif isinstance(val, (list, tuple)): + val = [x.decode(preferred_encoding, 'replace') if + isbytestring(x) else x for x in val] + json[attr] = val + return json + + def read_json(self, cache_file): + with open(cache_file, 'rb') as f: + js = json.load(f, encoding='utf-8') + return js + + def write_json(self, js, cache_file): + with open(cache_file, 'wb') as f: + json.dump(js, f, indent=2, encoding='utf-8') + + def string_to_value(self, string, col_metadata, column_label=None): + ''' + if column_label is none, col_metadata must be a dict containing custom + column metadata for one column. If column_label is not none, then + col_metadata must be a dict of custom column metadata, with column + labels as keys. Metadata for standard columns is always assumed to be in + the col_metadata dict. If column_label is not standard and is not in + col_metadata, check if it matches a custom column. If so, use that + column metadata. See get_column_metadata below. + ''' + pass + + def value_to_display(self, value, col_metadata, column_label=None): + pass + + def value_to_string (self, value, col_metadata, column_label=None): + pass + + def get_column_metadata(self, column_label = None, from_book=None): + ''' + if column_label is None, then from_book must not be None. Returns the + complete set of custom column metadata for that book. + + If column_label is not None, return the column metadata for the given + column. This works even if the label is for a built-in column. If + from_book is None, then column_label must be a current custom column + label or a standard label. If from_book is not None, then the column + metadata from that metadata set is returned if it exists, otherwise the + standard metadata for that column is returned. If neither is found, + return {} + ''' + pass + + def get_custom_column_labels(self, book): + ''' + returns a list of custom column attributes in the book metadata. + ''' + pass + + def get_standard_column_labels(self): + ''' + returns a list of standard attributes that should be in any book's + metadata + ''' + pass + +metadata_serializer = MetadataSerializer() + diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 6e8811432a..8d79981ad7 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -9,20 +9,14 @@ import os, re, time, sys from calibre.ebooks.metadata import MetaInformation from calibre.devices.mime import mime_type_ext from calibre.devices.interface import BookList as _BookList -from calibre.constants import filesystem_encoding, preferred_encoding +from calibre.devices.metadata_serializer import MetadataSerializer +from calibre.constants import preferred_encoding from calibre import isbytestring -class Book(MetaInformation): +class Book(MetaInformation, MetadataSerializer): BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections'] - JSON_ATTRS = [ - 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort', - 'title_sort', 'comments', 'category', 'publisher', 'series', - 'series_index', 'rating', 'isbn', 'language', 'application_id', - 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type', - 'uuid', - ] def __init__(self, prefix, lpath, size=None, other=None): from calibre.ebooks.metadata.meta import path_to_ext @@ -82,19 +76,6 @@ class Book(MetaInformation): val = getattr(other, attr, None) setattr(self, attr, val) - def to_json(self): - json = {} - for attr in self.JSON_ATTRS: - val = getattr(self, attr) - if isbytestring(val): - enc = filesystem_encoding if attr == 'lpath' else preferred_encoding - val = val.decode(enc, 'replace') - elif isinstance(val, (list, tuple)): - val = [x.decode(preferred_encoding, 'replace') if - isbytestring(x) else x for x in val] - json[attr] = val - return json - class BookList(_BookList): def supports_collections(self): diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 97c212775a..3c30827dbc 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -17,6 +17,7 @@ from itertools import cycle from calibre import prints, isbytestring from calibre.constants import filesystem_encoding +from calibre.devices.metadata_serializer import metadata_serializer as ms from calibre.devices.usbms.cli import CLI from calibre.devices.usbms.device import Device from calibre.devices.usbms.books import BookList, Book @@ -260,8 +261,7 @@ class USBMS(CLI, Device): os.makedirs(self.normalize_path(prefix)) js = [item.to_json() for item in booklists[listid] if hasattr(item, 'to_json')] - with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f: - json.dump(js, f, indent=2, encoding='utf-8') + ms.write_json(js, self.normalize_path(os.path.join(prefix, self.METADATA_CACHE))) write_prefix(self._main_prefix, 0) write_prefix(self._card_a_prefix, 1) write_prefix(self._card_b_prefix, 2) @@ -293,8 +293,7 @@ class USBMS(CLI, Device): cache_file = cls.normalize_path(os.path.join(prefix, name)) if os.access(cache_file, os.R_OK): try: - with open(cache_file, 'rb') as f: - js = json.load(f, encoding='utf-8') + js = ms.read_json(cache_file) for item in js: book = cls.book_class(prefix, item.get('lpath', None)) for key in item.keys(): diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.py b/src/calibre/gui2/dialogs/config/create_custom_column.py index 5b470123a4..296a868fbf 100644 --- a/src/calibre/gui2/dialogs/config/create_custom_column.py +++ b/src/calibre/gui2/dialogs/config/create_custom_column.py @@ -8,6 +8,7 @@ from functools import partial from PyQt4.QtCore import SIGNAL from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant +from calibre.devices.metadata_serializer import metadata_serializer from calibre.gui2.dialogs.config.create_custom_column_ui import Ui_QCreateCustomColumn from calibre.gui2 import error_dialog @@ -102,6 +103,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): return self.simple_error('', _('No lookup name was provided')) if not col_heading: return self.simple_error('', _('No column heading was provided')) + if col in metadata_serializer.SERIALIZED_ATTRS: + return self.simple_error('', _('The lookup name %s is reserved and cannot be used')%col) bad_col = False if col in self.parent.custcols: if not self.editing_col or self.parent.custcols[col]['num'] != self.orig_column_number: diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 0fc2c7f7ed..bc0367b766 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -883,7 +883,7 @@ class DeviceBooksModel(BooksModel): # {{{ self.reset() self.last_search = text if self.last_search: - self.searched.emit(False) + self.searched.emit(True) def sort(self, col, order, reset=True): diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 230debd598..575f5563d6 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -136,12 +136,12 @@ class SearchBox2(QComboBox): def text_edited_slot(self, text): if self.as_you_type: text = unicode(text) - self.prev_text = text self.timer = self.startTimer(self.__class__.INTERVAL) def timerEvent(self, event): self.killTimer(event.timerId()) if event.timerId() == self.timer: + self.timer = None self.do_search() @property @@ -190,6 +190,9 @@ class SearchBox2(QComboBox): def set_search_string(self, txt): self.normalize_state() self.setEditText(txt) + if self.timer is not None: # Turn off any timers that got started in setEditText + self.killTimer(self.timer) + self.timer = None self.search.emit(txt, False) self.line_edit.end(False) self.initial_state = False From 7969c9ead533418dfced6e439dfd4eed53fc6868 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 22 May 2010 08:50:22 +0100 Subject: [PATCH 2/5] Back out metadata/json changes I made --- src/calibre/devices/metadata_serializer.py | 90 ---------------------- src/calibre/devices/usbms/books.py | 25 +++++- src/calibre/devices/usbms/driver.py | 7 +- 3 files changed, 26 insertions(+), 96 deletions(-) delete mode 100644 src/calibre/devices/metadata_serializer.py diff --git a/src/calibre/devices/metadata_serializer.py b/src/calibre/devices/metadata_serializer.py deleted file mode 100644 index 651ba1d678..0000000000 --- a/src/calibre/devices/metadata_serializer.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -Created on 21 May 2010 - -@author: charles -''' - -from calibre.constants import filesystem_encoding, preferred_encoding -from calibre import isbytestring -import json - -class MetadataSerializer(object): - - SERIALIZED_ATTRS = [ - 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort', - 'title_sort', 'comments', 'category', 'publisher', 'series', - 'series_index', 'rating', 'isbn', 'language', 'application_id', - 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type', - 'uuid', - ] - - def to_json(self): - json = {} - for attr in self.SERIALIZED_ATTRS: - val = getattr(self, attr) - if isbytestring(val): - enc = filesystem_encoding if attr == 'lpath' else preferred_encoding - val = val.decode(enc, 'replace') - elif isinstance(val, (list, tuple)): - val = [x.decode(preferred_encoding, 'replace') if - isbytestring(x) else x for x in val] - json[attr] = val - return json - - def read_json(self, cache_file): - with open(cache_file, 'rb') as f: - js = json.load(f, encoding='utf-8') - return js - - def write_json(self, js, cache_file): - with open(cache_file, 'wb') as f: - json.dump(js, f, indent=2, encoding='utf-8') - - def string_to_value(self, string, col_metadata, column_label=None): - ''' - if column_label is none, col_metadata must be a dict containing custom - column metadata for one column. If column_label is not none, then - col_metadata must be a dict of custom column metadata, with column - labels as keys. Metadata for standard columns is always assumed to be in - the col_metadata dict. If column_label is not standard and is not in - col_metadata, check if it matches a custom column. If so, use that - column metadata. See get_column_metadata below. - ''' - pass - - def value_to_display(self, value, col_metadata, column_label=None): - pass - - def value_to_string (self, value, col_metadata, column_label=None): - pass - - def get_column_metadata(self, column_label = None, from_book=None): - ''' - if column_label is None, then from_book must not be None. Returns the - complete set of custom column metadata for that book. - - If column_label is not None, return the column metadata for the given - column. This works even if the label is for a built-in column. If - from_book is None, then column_label must be a current custom column - label or a standard label. If from_book is not None, then the column - metadata from that metadata set is returned if it exists, otherwise the - standard metadata for that column is returned. If neither is found, - return {} - ''' - pass - - def get_custom_column_labels(self, book): - ''' - returns a list of custom column attributes in the book metadata. - ''' - pass - - def get_standard_column_labels(self): - ''' - returns a list of standard attributes that should be in any book's - metadata - ''' - pass - -metadata_serializer = MetadataSerializer() - diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 8d79981ad7..6e8811432a 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -9,14 +9,20 @@ import os, re, time, sys from calibre.ebooks.metadata import MetaInformation from calibre.devices.mime import mime_type_ext from calibre.devices.interface import BookList as _BookList -from calibre.devices.metadata_serializer import MetadataSerializer -from calibre.constants import preferred_encoding +from calibre.constants import filesystem_encoding, preferred_encoding from calibre import isbytestring -class Book(MetaInformation, MetadataSerializer): +class Book(MetaInformation): BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections'] + JSON_ATTRS = [ + 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort', + 'title_sort', 'comments', 'category', 'publisher', 'series', + 'series_index', 'rating', 'isbn', 'language', 'application_id', + 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type', + 'uuid', + ] def __init__(self, prefix, lpath, size=None, other=None): from calibre.ebooks.metadata.meta import path_to_ext @@ -76,6 +82,19 @@ class Book(MetaInformation, MetadataSerializer): val = getattr(other, attr, None) setattr(self, attr, val) + def to_json(self): + json = {} + for attr in self.JSON_ATTRS: + val = getattr(self, attr) + if isbytestring(val): + enc = filesystem_encoding if attr == 'lpath' else preferred_encoding + val = val.decode(enc, 'replace') + elif isinstance(val, (list, tuple)): + val = [x.decode(preferred_encoding, 'replace') if + isbytestring(x) else x for x in val] + json[attr] = val + return json + class BookList(_BookList): def supports_collections(self): diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 3c30827dbc..97c212775a 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -17,7 +17,6 @@ from itertools import cycle from calibre import prints, isbytestring from calibre.constants import filesystem_encoding -from calibre.devices.metadata_serializer import metadata_serializer as ms from calibre.devices.usbms.cli import CLI from calibre.devices.usbms.device import Device from calibre.devices.usbms.books import BookList, Book @@ -261,7 +260,8 @@ class USBMS(CLI, Device): os.makedirs(self.normalize_path(prefix)) js = [item.to_json() for item in booklists[listid] if hasattr(item, 'to_json')] - ms.write_json(js, self.normalize_path(os.path.join(prefix, self.METADATA_CACHE))) + with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f: + json.dump(js, f, indent=2, encoding='utf-8') write_prefix(self._main_prefix, 0) write_prefix(self._card_a_prefix, 1) write_prefix(self._card_b_prefix, 2) @@ -293,7 +293,8 @@ class USBMS(CLI, Device): cache_file = cls.normalize_path(os.path.join(prefix, name)) if os.access(cache_file, os.R_OK): try: - js = ms.read_json(cache_file) + with open(cache_file, 'rb') as f: + js = json.load(f, encoding='utf-8') for item in js: book = cls.book_class(prefix, item.get('lpath', None)) for key in item.keys(): From 3b557de4c724384587f99ff2ad2f496e3db46011 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 23 May 2010 21:34:19 +0100 Subject: [PATCH 3/5] First pass at converting db2.get_categories to return a complete dict --- src/calibre/gui2/tag_view.py | 8 +-- src/calibre/library/custom_columns.py | 4 +- src/calibre/library/database2.py | 61 +++++++++------- src/calibre/utils/ordered_dict.py | 100 ++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 32 deletions(-) create mode 100644 src/calibre/utils/ordered_dict.py diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 8a01b6ad27..0fb72e071b 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -199,8 +199,8 @@ class TagsModel(QAbstractItemModel): # {{{ categories_orig = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('Ratings'), _('News'), _('Tags')] - row_map_orig = ['author', 'series', 'format', 'publisher', 'rating', - 'news', 'tag'] + row_map_orig = ['authors', 'series', 'formats', 'publishers', 'ratings', + 'news', 'tags'] tags_categories_start= 7 search_keys=['search', _('Searches')] @@ -264,8 +264,8 @@ class TagsModel(QAbstractItemModel): # {{{ self.cat_icon_map.append(self.cat_icon_map_orig[i]) # Clean up the author's tags, getting rid of the '|' characters - if data['author'] is not None: - for t in data['author']: + if data['authors'] is not None: + for t in data['authors']: t.name = t.name.replace('|', ',') # Now do the user-defined categories. There is a time/space tradeoff here. diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index a8375c6b5c..b6ada01b8c 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -144,8 +144,8 @@ class CustomColumns(object): for i, v in self.custom_column_num_map.items(): if v['normalized']: tn = 'custom_column_{0}'.format(i) - self.tag_browser_categories[tn] = [v['label'], 'value'] - self.tag_browser_datatype[v['label']] = v['datatype'] + self.tag_browser_categories[v['label']] = {'table':tn, 'column':'value', 'type':v['datatype'], 'name':v['name']} + #self.tag_browser_datatype[v['label']] = v['datatype'] def get_custom(self, idx, label=None, num=None, index_is_id=False): if label is not None: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index ed56d35bdc..12398de918 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -33,6 +33,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.ordered_dict import OrderedDict from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format if iswindows: @@ -123,22 +124,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if isinstance(self.dbpath, unicode): self.dbpath = self.dbpath.encode(filesystem_encoding) - self.tag_browser_categories = { - 'tags' : ['tag', 'name'], - 'series' : ['series', 'name'], - 'publishers': ['publisher', 'name'], - 'authors' : ['author', 'name'], - 'news' : ['news', 'name'], - 'ratings' : ['rating', 'rating'] - } - self.tag_browser_datatype = { - 'tag' : 'textmult', - 'series' : None, - 'publisher' : 'text', - 'author' : 'text', - 'news' : None, - 'rating' : 'rating', - } + # Order as has been customary in the tags pane. + self.tag_browser_categories = OrderedDict([ + ('authors', {'table':'authors', 'column':'name', 'type':'text', 'name':_('Authors')}), + ('series', {'table':'series', 'column':'name', 'type':None, 'name':_('Series')}), + ('formats', {'table':None, 'column':None, 'type':None, 'name':_('Formats')}), + ('publishers',{'table':'publishers', 'column':'name', 'type':'text', 'name':_('Publishers')}), + ('ratings', {'table':'ratings', 'column':'rating', 'type':'rating', 'name':_('Ratings')}), + ('news', {'table':'news', 'column':'name', 'type':None, 'name':_('News')}), + ('tags', {'table':'tags', 'column':'name', 'type':'textmult', 'name':_('Tags')}), + ]) + +# self.tag_browser_datatype = { +# 'tag' : 'textmult', +# 'series' : None, +# 'publisher' : 'text', +# 'author' : 'text', +# 'news' : None, +# 'rating' : 'rating', +# } self.tag_browser_formatters = {'rating': lambda x:u'\u2605'*int(round(x/2.))} @@ -653,17 +657,22 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.books_list_filter.change([] if not ids else ids) categories = {} - for tn, cn in self.tag_browser_categories.items(): + for category in self.tag_browser_categories.keys(): + tn = self.tag_browser_categories[category]['table'] + categories[category] = [] #reserve the position in the ordered list + if tn is None: + continue + cn = self.tag_browser_categories[category]['column'] if ids is None: - query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn[1], tn) + query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn) else: - query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn[1], tn) + query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn, tn) if sort_on_count: query += ' ORDER BY count DESC' else: - query += ' ORDER BY {0} ASC'.format(cn[1]) + query += ' ORDER BY {0} ASC'.format(cn) data = self.conn.get(query) - category = cn[0] + # category = cn[0] icon, tooltip = None, '' if icon_map: if category in icon_map: @@ -671,14 +680,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): else: icon = icon_map['*custom'] tooltip = self.custom_column_label_map[category]['name'] - datatype = self.tag_browser_datatype[category] + datatype = self.tag_browser_categories[category]['type'] formatter = self.tag_browser_formatters.get(datatype, lambda x: x) categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], icon=icon, tooltip = tooltip) for r in data if r[2] > 0 and (datatype != 'rating' or len(formatter(r[1])) > 0)] - categories['format'] = [] + categories['formats'] = [] for fmt in self.conn.get('SELECT DISTINCT format FROM data'): fmt = fmt[0] if ids is not None: @@ -693,13 +702,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): WHERE format="%s"'''%fmt, all=False) if count > 0: - categories['format'].append(Tag(fmt, count=count)) + categories['formats'].append(Tag(fmt, count=count)) if sort_on_count: - categories['format'].sort(cmp=lambda x,y:cmp(x.count, y.count), + categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count), reverse=True) else: - categories['format'].sort(cmp=lambda x,y:cmp(x.name, y.name)) + categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name)) return categories def tags_older_than(self, tag, delta): diff --git a/src/calibre/utils/ordered_dict.py b/src/calibre/utils/ordered_dict.py new file mode 100644 index 0000000000..95a0af9e76 --- /dev/null +++ b/src/calibre/utils/ordered_dict.py @@ -0,0 +1,100 @@ +from UserDict import DictMixin + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + return len(self)==len(other) and self.items() == other.items() + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other From 0a16be06e8d55f0e4d0ae3dce22946cc987d828f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 24 May 2010 15:00:47 +0100 Subject: [PATCH 4/5] 1) Move all tag category code to DB2. 2) Fix bug where opening preferences resets the folder device menus even when connected --- src/calibre/ebooks/metadata/book/__init__.py | 10 +- src/calibre/gui2/__init__.py | 2 - src/calibre/gui2/dialogs/tag_categories.py | 8 +- src/calibre/gui2/tag_view.py | 88 +++-------- src/calibre/gui2/ui.py | 15 +- src/calibre/library/custom_columns.py | 12 +- src/calibre/library/database2.py | 151 +++++++++++++++---- src/calibre/utils/config.py | 4 +- 8 files changed, 179 insertions(+), 111 deletions(-) diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py index 9a44a36489..2e47ee71e3 100644 --- a/src/calibre/ebooks/metadata/book/__init__.py +++ b/src/calibre/ebooks/metadata/book/__init__.py @@ -88,17 +88,25 @@ CALIBRE_METADATA_FIELDS = frozenset([ ] ) +CALIBRE_RESERVED_LABELS = frozenset([ + # reserved for saved searches + 'search', + ] +) + RESERVED_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union( PUBLICATION_METADATA_FIELDS).union( BOOK_STRUCTURE_FIELDS).union( USER_METADATA_FIELDS).union( DEVICE_METADATA_FIELDS).union( - CALIBRE_METADATA_FIELDS) + CALIBRE_METADATA_FIELDS).union( + CALIBRE_RESERVED_LABELS) assert len(RESERVED_METADATA_FIELDS) == sum(map(len, ( SOCIAL_METADATA_FIELDS, PUBLICATION_METADATA_FIELDS, BOOK_STRUCTURE_FIELDS, USER_METADATA_FIELDS, DEVICE_METADATA_FIELDS, CALIBRE_METADATA_FIELDS, + CALIBRE_RESERVED_LABELS ))) SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union( diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 0cf565c928..478273dd0e 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -97,8 +97,6 @@ def _config(): help=_('Overwrite author and title with new metadata')) c.add_opt('enforce_cpu_limit', default=True, help=_('Limit max simultaneous jobs to number of CPUs')) - c.add_opt('user_categories', default={}, - help=_('User-created tag browser categories')) return ConfigProxy(c) diff --git a/src/calibre/gui2/dialogs/tag_categories.py b/src/calibre/gui2/dialogs/tag_categories.py index 0e15c06828..f49ae4ce83 100644 --- a/src/calibre/gui2/dialogs/tag_categories.py +++ b/src/calibre/gui2/dialogs/tag_categories.py @@ -7,7 +7,7 @@ from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories -from calibre.gui2 import config +from calibre.utils.config import prefs from calibre.gui2.dialogs.confirm_delete import confirm from calibre.constants import islinux @@ -22,7 +22,7 @@ class Item: return 'name=%s, label=%s, index=%s, exists='%(self.name, self.label, self.index, self.exists) class TagCategories(QDialog, Ui_TagCategories): - category_labels_orig = ['', 'author', 'series', 'publisher', 'tag'] + category_labels_orig = ['', 'authors', 'series', 'publishers', 'tags'] def __init__(self, window, db, index=None): QDialog.__init__(self, window) @@ -64,7 +64,7 @@ class TagCategories(QDialog, Ui_TagCategories): self.all_items.append(t) self.all_items_dict[label+':'+n] = t - self.categories = dict.copy(config['user_categories']) + self.categories = dict.copy(prefs['user_categories']) if self.categories is None: self.categories = {} for cat in self.categories: @@ -181,7 +181,7 @@ class TagCategories(QDialog, Ui_TagCategories): def accept(self): self.save_category() - config['user_categories'] = self.categories + prefs['user_categories'] = self.categories QDialog.accept(self) def save_category(self): diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 0fb72e071b..ba93b818c2 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -201,29 +201,34 @@ class TagsModel(QAbstractItemModel): # {{{ _('Ratings'), _('News'), _('Tags')] row_map_orig = ['authors', 'series', 'formats', 'publishers', 'ratings', 'news', 'tags'] - tags_categories_start= 7 search_keys=['search', _('Searches')] + def __init__(self, db, parent=None): QAbstractItemModel.__init__(self, parent) - self.cat_icon_map_orig = list(map(QIcon, [I('user_profile.svg'), - I('series.svg'), I('book.svg'), I('publisher.png'), I('star.png'), - I('news.svg'), I('tags.svg')])) + + # must do this here because 'QPixmap: Must construct a QApplication + # before a QPaintDevice' + self.category_icon_map = {'authors': QIcon(I('user_profile.svg')), + 'series': QIcon(I('series.svg')), + 'formats':QIcon(I('book.svg')), + 'publishers': QIcon(I('publisher.png')), + 'ratings':QIcon(I('star.png')), + 'news':QIcon(I('news.svg')), + 'tags':QIcon(I('tags.svg')), + '*custom':QIcon(I('column.svg')), + '*user':QIcon(I('drawer.svg')), + 'search':QIcon(I('search.svg'))} self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))] - self.custcol_icon = QIcon(I('column.svg')) - self.search_icon = QIcon(I('search.svg')) - self.usercat_icon = QIcon(I('drawer.svg')) - self.label_to_icon_map = dict(map(None, self.row_map_orig, self.cat_icon_map_orig)) - self.label_to_icon_map['*custom'] = self.custcol_icon self.db = db self.search_restriction = '' - self.user_categories = {} self.ignore_next_search = 0 data = self.get_node_tree(config['sort_by_popularity']) self.root_item = TagTreeItem() for i, r in enumerate(self.row_map): c = TagTreeItem(parent=self.root_item, - data=self.categories[i], category_icon=self.cat_icon_map[i]) + data=self.categories[i], + category_icon=self.category_icon_map[r]) for tag in data[r]: TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) @@ -233,66 +238,19 @@ class TagsModel(QAbstractItemModel): # {{{ def get_node_tree(self, sort): self.row_map = [] self.categories = [] - # strip the icons after the 'standard' categories. We will put them back later - if self.tags_categories_start < len(self.row_map_orig): - self.cat_icon_map = self.cat_icon_map_orig[:self.tags_categories_start-len(self.row_map_orig)] - else: - self.cat_icon_map = self.cat_icon_map_orig[:] - self.user_categories = dict.copy(config['user_categories']) - column_map = config['column_map'] - - for i in range(0, self.tags_categories_start): # First the standard categories - self.row_map.append(self.row_map_orig[i]) - self.categories.append(self.categories_orig[i]) if len(self.search_restriction): - data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map, + data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map, ids=self.db.search(self.search_restriction, return_matches=True)) else: - data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map) + data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map) - for c in data: # now the custom columns - if c not in self.row_map_orig and c in column_map: - self.row_map.append(c) - self.categories.append(self.db.custom_column_label_map[c]['name']) - self.cat_icon_map.append(self.custcol_icon) + tb_categories = self.db.get_tag_browser_categories() + for category in tb_categories.iterkeys(): + if category in data: # They should always be there, but ... + self.row_map.append(category) + self.categories.append(tb_categories[category]['name']) - # Now the rest of the normal tag categories - for i in range(self.tags_categories_start, len(self.row_map_orig)): - self.row_map.append(self.row_map_orig[i]) - self.categories.append(self.categories_orig[i]) - self.cat_icon_map.append(self.cat_icon_map_orig[i]) - - # Clean up the author's tags, getting rid of the '|' characters - if data['authors'] is not None: - for t in data['authors']: - t.name = t.name.replace('|', ',') - - # Now do the user-defined categories. There is a time/space tradeoff here. - # By converting the tags into a map, we can do the verification in the category - # loop much faster, at the cost of duplicating the categories lists. - taglist = {} - for c in self.row_map: - taglist[c] = dict(map(lambda t:(t.name, t), data[c])) - - for c in self.user_categories: - l = [] - for (name,label,ign) in self.user_categories[c]: - if label in taglist and name in taglist[label]: # use same node as the complete category - l.append(taglist[label][name]) - # else: do nothing, to eliminate nodes that have zero counts - if config['sort_by_popularity']: - data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.count, y.count))) - else: - data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower()))) - self.row_map.append(c+'*') - self.categories.append(c) - self.cat_icon_map.append(self.usercat_icon) - - data['search'] = self.get_search_nodes(self.search_icon) # Add the search category - self.row_map.append(self.search_keys[0]) - self.categories.append(self.search_keys[1]) - self.cat_icon_map.append(self.search_icon) return data def get_search_nodes(self, icon): diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 36848e33cf..91b2353469 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -183,7 +183,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): _('Error communicating with device'), ' ') self.device_error_dialog.setModal(Qt.NonModal) self.tb_wrapper = textwrap.TextWrapper(width=40) - self.device_connected = False + self.device_connected = None self.viewers = collections.deque() self.content_server = None self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self) @@ -675,6 +675,15 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self._sync_menu.fetch_annotations.connect(self.fetch_annotations) self._sync_menu.connect_to_folder.connect(self.connect_to_folder) self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder) + if self.device_connected: + self._sync_menu.connect_to_folder_action.setEnabled(False) + if self.device_connected == 'folder': + self._sync_menu.disconnect_from_folder_action.setEnabled(True) + else: + self._sync_menu.disconnect_from_folder_action.setEnabled(False) + else: + self._sync_menu.connect_to_folder_action.setEnabled(True) + self._sync_menu.disconnect_from_folder_action.setEnabled(False) def add_spare_server(self, *args): self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0))) @@ -944,7 +953,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.status_bar.showMessage(_('Device: ')+\ self.device_manager.device.__class__.get_gui_name()+\ _(' detected.'), 3000) - self.device_connected = True + self.device_connected = 'device' if not is_folder_device else 'folder' self._sync_menu.enable_device_actions(True, self.device_manager.device.card_prefix(), self.device_manager.device) @@ -955,7 +964,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self._sync_menu.connect_to_folder_action.setEnabled(True) self._sync_menu.disconnect_from_folder_action.setEnabled(False) self.save_device_view_settings() - self.device_connected = False + self.device_connected = None self._sync_menu.enable_device_actions(False) self.location_view.model().update_devices() self.vanity.setText(self.vanity_template%\ diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index b6ada01b8c..36ea49763e 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -141,11 +141,15 @@ class CustomColumns(object): } # Create Tag Browser categories for custom columns - for i, v in self.custom_column_num_map.items(): + for k in sorted(self.custom_column_label_map.keys()): + v = self.custom_column_label_map[k] if v['normalized']: - tn = 'custom_column_{0}'.format(i) - self.tag_browser_categories[v['label']] = {'table':tn, 'column':'value', 'type':v['datatype'], 'name':v['name']} - #self.tag_browser_datatype[v['label']] = v['datatype'] + tn = 'custom_column_{0}'.format(v['num']) + self.tag_browser_categories[v['label']] = { + 'table':tn, 'column':'value', + 'type':v['datatype'], 'is_multiple':v['is_multiple'], + 'kind':'custom', 'name':v['name'] + } def get_custom(self, idx, label=None, num=None, index_is_id=False): if label is not None: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 12398de918..6ca73d9656 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -34,6 +34,8 @@ 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.ordered_dict import OrderedDict +from calibre.utils.config import prefs +from calibre.utils.search_query_parser import saved_searches from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format if iswindows: @@ -125,26 +127,32 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.dbpath = self.dbpath.encode(filesystem_encoding) # Order as has been customary in the tags pane. - self.tag_browser_categories = OrderedDict([ - ('authors', {'table':'authors', 'column':'name', 'type':'text', 'name':_('Authors')}), - ('series', {'table':'series', 'column':'name', 'type':None, 'name':_('Series')}), - ('formats', {'table':None, 'column':None, 'type':None, 'name':_('Formats')}), - ('publishers',{'table':'publishers', 'column':'name', 'type':'text', 'name':_('Publishers')}), - ('ratings', {'table':'ratings', 'column':'rating', 'type':'rating', 'name':_('Ratings')}), - ('news', {'table':'news', 'column':'name', 'type':None, 'name':_('News')}), - ('tags', {'table':'tags', 'column':'name', 'type':'textmult', 'name':_('Tags')}), - ]) - -# self.tag_browser_datatype = { -# 'tag' : 'textmult', -# 'series' : None, -# 'publisher' : 'text', -# 'author' : 'text', -# 'news' : None, -# 'rating' : 'rating', -# } - - self.tag_browser_formatters = {'rating': lambda x:u'\u2605'*int(round(x/2.))} + tag_browser_categories_items = [ + ('authors', {'table':'authors', 'column':'name', + 'type':'text', 'is_multiple':False, + 'kind':'standard', 'name':_('Authors')}), + ('series', {'table':'series', 'column':'name', + 'type':None, 'is_multiple':False, + 'kind':'standard', 'name':_('Series')}), + ('formats', {'table':None, 'column':None, + 'type':None, 'is_multiple':False, + 'kind':'standard', 'name':_('Formats')}), + ('publishers',{'table':'publishers', 'column':'name', + 'type':'text', 'is_multiple':False, + 'kind':'standard', 'name':_('Publishers')}), + ('ratings', {'table':'ratings', 'column':'rating', + 'type':'rating', 'is_multiple':False, + 'kind':'standard', 'name':_('Ratings')}), + ('news', {'table':'news', 'column':'name', + 'type':None, 'is_multiple':False, + 'kind':'standard', 'name':_('News')}), + ('tags', {'table':'tags', 'column':'name', + 'type':'text', 'is_multiple':True, + 'kind':'standard', 'name':_('Tags')}), + ] + self.tag_browser_categories = OrderedDict() + for k,v in tag_browser_categories_items: + self.tag_browser_categories[k] = v self.connect() self.is_case_sensitive = not iswindows and not isosx and \ @@ -653,14 +661,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def get_recipe(self, id): return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False) + def get_tag_browser_categories(self): + return self.tag_browser_categories + def get_categories(self, sort_on_count=False, ids=None, icon_map=None): self.books_list_filter.change([] if not ids else ids) categories = {} + + #### First, build the standard and custom-column categories #### for category in self.tag_browser_categories.keys(): tn = self.tag_browser_categories[category]['table'] - categories[category] = [] #reserve the position in the ordered list - if tn is None: + categories[category] = [] #reserve the position in the ordered list + if tn is None: # Nothing to do for the moment continue cn = self.tag_browser_categories[category]['column'] if ids is None: @@ -672,22 +685,41 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): else: query += ' ORDER BY {0} ASC'.format(cn) data = self.conn.get(query) - # category = cn[0] + + # icon_map is not None if get_categories is to store an icon and + # possibly a tooltip in the tag structure. icon, tooltip = None, '' if icon_map: - if category in icon_map: - icon = icon_map[category] - else: + if self.tag_browser_categories[category]['kind'] == 'standard': + if category in icon_map: + icon = icon_map[category] + elif self.tag_browser_categories[category]['kind'] == 'custom': icon = icon_map['*custom'] + icon_map[category] = icon_map['*custom'] tooltip = self.custom_column_label_map[category]['name'] + datatype = self.tag_browser_categories[category]['type'] - formatter = self.tag_browser_formatters.get(datatype, lambda x: x) + if datatype == 'rating': + item_zero_func = (lambda x: len(formatter(r[1])) > 0) + formatter = (lambda x:u'\u2605'*int(round(x/2.))) + elif category == 'authors': + item_zero_func = (lambda x: x[2] > 0) + # Clean up the authors strings to human-readable form + formatter = (lambda x: x.replace('|', ',')) + else: + item_zero_func = (lambda x: x[2] > 0) + formatter = (lambda x:x) + categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], icon=icon, tooltip = tooltip) - for r in data - if r[2] > 0 and - (datatype != 'rating' or len(formatter(r[1])) > 0)] + for r in data if item_zero_func(r)] + + # We delayed computing the standard formats category because it does not + # use a view, but is computed dynamically categories['formats'] = [] + icon = None + if icon_map and 'formats' in icon_map: + icon = icon_map['formats'] for fmt in self.conn.get('SELECT DISTINCT format FROM data'): fmt = fmt[0] if ids is not None: @@ -702,13 +734,70 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): WHERE format="%s"'''%fmt, all=False) if count > 0: - categories['formats'].append(Tag(fmt, count=count)) + categories['formats'].append(Tag(fmt, count=count, icon=icon)) if sort_on_count: categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count), reverse=True) else: categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name)) + + #### Now do the user-defined categories. #### + user_categories = dict.copy(prefs['user_categories']) + + # remove all user categories from tag_browser_categories. They can + # easily come and go. We will add all the existing ones in below. + for k in self.tag_browser_categories.keys(): + if self.tag_browser_categories[k]['kind'] in ['user', 'search']: + del self.tag_browser_categories[k] + + # We want to use same node in the user category as in the source + # category. To do that, we need to find the original Tag node. There is + # a time/space tradeoff here. By converting the tags into a map, we can + # do the verification in the category loop much faster, at the cost of + # temporarily duplicating the categories lists. + taglist = {} + for c in categories.keys(): + taglist[c] = dict(map(lambda t:(t.name, t), categories[c])) + + for user_cat in sorted(user_categories.keys()): + items = [] + for (name,label,ign) in user_categories[user_cat]: + if label in taglist and name in taglist[label]: + items.append(taglist[label][name]) + # else: do nothing, to not include nodes w zero counts + if len(items): + cat_name = user_cat+'*' # add the * to avoid name collision + self.tag_browser_categories[cat_name] = { + 'table':None, 'column':None, + 'type':None, 'is_multiple':False, + 'kind':'user', 'name':user_cat} + # Not a problem if we accumulate entries in the icon map + if icon_map is not None: + icon_map[cat_name] = icon_map['*user'] + if sort_on_count: + categories[cat_name] = \ + sorted(items, cmp=(lambda x, y: cmp(y.count, x.count))) + else: + categories[cat_name] = \ + sorted(items, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower()))) + + #### Finally, the saved searches category #### + items = [] + icon = None + if icon_map and 'search' in icon_map: + icon = icon_map['search'] + for srch in saved_searches.names(): + items.append(Tag(srch, tooltip=saved_searches.lookup(srch), icon=icon)) + if len(items): + self.tag_browser_categories['search'] = { + 'table':None, 'column':None, + 'type':None, 'is_multiple':False, + 'kind':'search', 'name':_('Searches')} + if icon_map is not None: + icon_map['search'] = icon_map['search'] + categories['search'] = items + return categories def tags_older_than(self, tag, delta): diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 559721c193..69eee4d1ed 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -694,8 +694,10 @@ def _prefs(): help=_('Add new formats to existing book records')) c.add_opt('installation_uuid', default=None, help='Installation UUID') - # this is here instead of the gui preferences because calibredb can execute searches + # these are here instead of the gui preferences because calibredb and + # calibre server can execute searches c.add_opt('saved_searches', default={}, help=_('List of named saved searches')) + c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') return c From 2f7408d5bcb04254435262fddfeb6d6dcbe9da1a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 24 May 2010 16:24:21 +0100 Subject: [PATCH 5/5] Add tooltips to top-level categories in tags_view --- src/calibre/gui2/tag_view.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index ba93b818c2..5ff4fc23ba 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -126,7 +126,7 @@ class TagTreeItem(object): # {{{ TAG = 1 ROOT = 2 - def __init__(self, data=None, category_icon=None, icon_map=None, parent=None): + def __init__(self, data=None, category_icon=None, icon_map=None, parent=None, tooltip=None): self.parent = parent self.children = [] if self.parent is not None: @@ -144,6 +144,7 @@ class TagTreeItem(object): # {{{ elif self.type == self.TAG: icon_map[0] = data.icon self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) + self.tooltip = tooltip def __str__(self): if self.type == self.ROOT: @@ -175,6 +176,8 @@ class TagTreeItem(object): # {{{ return self.icon if role == Qt.FontRole: return self.bold_font + if role == Qt.ToolTipRole and self.tooltip is not None: + return QVariant(self.tooltip) return NONE def tag_data(self, role): @@ -228,7 +231,8 @@ class TagsModel(QAbstractItemModel): # {{{ for i, r in enumerate(self.row_map): c = TagTreeItem(parent=self.root_item, data=self.categories[i], - category_icon=self.category_icon_map[r]) + category_icon=self.category_icon_map[r], + tooltip=_('The lookup/search name is "{0}"').format(r)) for tag in data[r]: TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)