From 6a0cc12ef7cb0ea794a93dc5cdb2440f3d6ce843 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Sep 2009 19:25:24 -0600 Subject: [PATCH] Rewrite the Tag browser to make it more efficient and remove one source of random crashes --- setup/__init__.py | 2 +- src/calibre/gui2/tag_view.py | 249 +++++++++++++++++++------------ src/calibre/library/database2.py | 26 ++-- 3 files changed, 171 insertions(+), 106 deletions(-) diff --git a/setup/__init__.py b/setup/__init__.py index d947042fc4..d7b9d2321d 100644 --- a/setup/__init__.py +++ b/setup/__init__.py @@ -111,7 +111,7 @@ class Command(object): self.b = os.path.basename self.s = os.path.splitext self.e = os.path.exists - self.orig_euid = os.geteuid() + self.orig_euid = os.geteuid() if hasattr(os, 'geteuid') else None self.real_uid = os.environ.get('SUDO_UID', None) self.real_gid = os.environ.get('SUDO_GID', None) self.real_user = os.environ.get('SUDO_USER', None) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index e76259cdef..6a759f1bbb 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -6,9 +6,13 @@ __docformat__ = 'restructuredtext en' ''' Browsing book collection by tags. ''' -from PyQt4.Qt import QStandardItemModel, Qt, QTreeView, QStandardItem, \ - QFont, SIGNAL, QSize, QIcon, QPoint, QPixmap -from calibre.gui2 import config + +from itertools import izip + +from PyQt4.Qt import Qt, QTreeView, \ + QFont, SIGNAL, QSize, QIcon, QPoint, \ + QAbstractItemModel, QVariant, QModelIndex +from calibre.gui2 import config, NONE class TagsView(QTreeView): @@ -19,7 +23,7 @@ class TagsView(QTreeView): self.setIconSize(QSize(30, 30)) def set_database(self, db, match_all, popularity): - self._model = TagsModel(db) + self._model = TagsModel(db, parent=self) self.popularity = popularity self.match_all = match_all self.setModel(self._model) @@ -47,56 +51,87 @@ class TagsView(QTreeView): if ci.isValid(): self.scrollTo(ci, QTreeView.PositionAtTop) -class CategoryItem(QStandardItem): +class TagTreeItem(object): - def __init__(self, category, display_text, tags, icon, font, icon_map): - self.category = category - self.tags = tags - QStandardItem.__init__(self, icon, display_text) - self.setFont(font) - self.setSelectable(False) - self.setSizeHint(QSize(100, 40)) - self.setEditable(False) - for tag in tags: - self.appendRow(TagItem(tag, icon_map)) + CATEGORY = 0 + TAG = 1 + ROOT = 2 -class TagItem(QStandardItem): + def __init__(self, data=None, tag=None, category_icon=None, icon_map=None, parent=None): + self.parent = parent + self.children = [] + if self.parent is not None: + self.parent.append(self) + if data is None: + self.type = self.ROOT + else: + self.type = self.TAG if category_icon is None else self.CATEGORY + if self.type == self.CATEGORY: + self.name, self.icon = map(QVariant, (data, category_icon)) + self.py_name = data + self.bold_font = QFont() + self.bold_font.setBold(True) + self.bold_font = QVariant(self.bold_font) + elif self.type == self.TAG: + self.tag, self.icon_map = data, list(map(QVariant, icon_map)) - def __init__(self, tag, icon_map): - self.icon_map = icon_map - self.tag = tag - QStandardItem.__init__(self, tag.as_string()) - self.set_icon() - self.setEditable(False) - self.setSelectable(False) + def row(self): + if self.parent is not None: + return self.parent.children.index(self) + return 0 + + def append(self, child): + child.parent = self + self.children.append(child) + + def data(self, role): + if self.type == self.TAG: + return self.tag_data(role) + if self.type == self.CATEGORY: + return self.category_data(role) + return NONE + + def category_data(self, role): + if role == Qt.DisplayRole: + return self.name + if role == Qt.DecorationRole: + return self.icon + if role == Qt.FontRole: + return self.bold_font + return NONE + + def tag_data(self, role): + if role == Qt.DisplayRole: + return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) + if role == Qt.DecorationRole: + return self.icon_map[self.tag.state] + return NONE def toggle(self): - self.tag.state = (self.tag.state + 1)%3 - self.set_icon() - - def set_icon(self): - self.setIcon(self.icon_map[self.tag.state]) - - -class TagsModel(QStandardItemModel): + if self.type == self.TAG: + self.tag.state = (self.tag.state + 1)%3 +class TagsModel(QAbstractItemModel): categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags')] row_map = ['author', 'series', 'format', 'publisher', 'news', 'tag'] - def __init__(self, db): + def __init__(self, db, parent=None): + QAbstractItemModel.__init__(self, parent) self.cmap = tuple(map(QIcon, [I('user_profile.svg'), I('series.svg'), I('book.svg'), I('publisher.png'), I('news.svg'), I('tags.svg')])) - p = QPixmap(30, 30) - p.fill(Qt.transparent) - self.icon_map = [QIcon(p), QIcon(I('plus.svg')), + self.icon_map = [QIcon(), QIcon(I('plus.svg')), QIcon(I('minus.svg'))] - QStandardItemModel.__init__(self) self.db = db self.ignore_next_search = 0 - self._data = {} - self.bold_font = QFont() - self.bold_font.setBold(True) + self.root_item = TagTreeItem() + data = self.db.get_categories(config['sort_by_popularity']) + for i, r in enumerate(self.row_map): + c = TagTreeItem(parent=self.root_item, + data=self.categories[i], category_icon=self.cmap[i]) + for tag in data[r]: + t = TagTreeItem(parent=c, data=tag, icon_map=self.icon_map) + self.refresh() self.db.add_listener(self.database_changed) @@ -104,72 +139,98 @@ class TagsModel(QStandardItemModel): self.refresh() def refresh(self): - old_data = self._data - self._data = self.db.get_categories(config['sort_by_popularity']) - for key in old_data.keys(): - for tag in old_data[key]: - try: - index = self._data[key].index(tag) - if index > -1: - self._data[key][index].state = tag.state - except: - continue - self.clear() - root = self.invisibleRootItem() - for r, category in enumerate(self.row_map): - tags = self._data.get(category, []) - root.appendRow(CategoryItem(category, self.categories[r], - self._data[category], self.cmap[r], self.bold_font, self.icon_map)) - #self.reset() + data = self.db.get_categories(config['sort_by_popularity']) + for i, r in enumerate(self.row_map): + category = self.root_item.children[i] + names = [t.tag.name for t in category.children] + states = [t.tag.state for t in category.children] + state_map = dict(izip(names, states)) + category_index = self.index(i, 0, QModelIndex()) + if len(category.children) > 0: + self.beginRemoveRows(category_index, 0, + len(category.children)-1) + category.children = [] + self.endRemoveRows() + if len(data[r]) > 0: + self.beginInsertRows(category_index, 0, len(data[r])-1) + for tag in data[r]: + tag.state = state_map.get(tag.name, 0) + t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_map) + self.endInsertRows() + + def columnCount(self, parent): + return 1 + + def data(self, index, role): + if not index.isValid(): + return NONE + item = index.internalPointer() + return item.data(role) + + def index(self, row, column, parent): + if not self.hasIndex(row, column, parent): + return QModelIndex() + + if not parent.isValid(): + parent_item = self.root_item + else: + parent_item = parent.internalPointer() + + child_item = parent_item.children[row] + ans = self.createIndex(row, column, child_item) + return ans + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + + child_item = index.internalPointer() + parent_item = child_item.parent + + if parent_item is self.root_item or parent_item is None: + return QModelIndex() + + ans = self.createIndex(parent_item.row(), 0, parent_item) + return ans + + def rowCount(self, parent): + if parent.column() > 0: + return 0 + + if not parent.isValid(): + parent_item = self.root_item + else: + parent_item = parent.internalPointer() + + return len(parent_item.children) def reset_all_states(self): - changed_indices = [] - for category in self._data.values(): - Category = self.find_category(category) - for tag in category: + for i in xrange(self.rowCount(QModelIndex())): + category_index = self.index(i, 0, QModelIndex()) + category_item = category_index.internalPointer() + for j in xrange(self.rowCount(category_index)): + tag_index = self.index(j, 0, category_index) + tag_item = tag_index.internalPointer() + tag = tag_item.tag if tag.state != 0: tag.state = 0 - if Category is not None: - Tag = self.find_tag(tag, Category) - if Tag is not None: - changed_indices.append(Tag.index()) - for idx in changed_indices: - if idx.isValid(): - self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), - idx, idx) + self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), + tag_index, tag_index) def clear_state(self): - for category in self._data.values(): - for tag in category: - tag.state = 0 self.reset_all_states() - def find_category(self, name): - root = self.invisibleRootItem() - for i in range(root.rowCount()): - child = root.child(i) - if getattr(child, 'category', None) == name: - return child - - def find_tag(self, tag, category): - for i in range(category.rowCount()): - child = category.child(i) - if getattr(child, 'tag', None) == tag: - return child - - def reinit(self, *args, **kwargs): if self.ignore_next_search == 0: self.reset_all_states() else: self.ignore_next_search -= 1 - def toggle(self, index): - if index.parent().isValid(): - category = self.row_map[index.parent().row()] - tag = self._data[category][index.row()] - self.invisibleRootItem().child(index.parent().row()).child(index.row()).toggle() + if not index.isValid(): return False + item = index.internalPointer() + if item.type == TagTreeItem.TAG: + item.toggle() self.ignore_next_search = 2 self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index) return True @@ -177,12 +238,14 @@ class TagsModel(QStandardItemModel): def tokens(self): ans = [] - for key in self.row_map: - for tag in self._data[key]: + for i, key in enumerate(self.row_map): + category_item = self.root_item.children[i] + for tag_item in category_item.children: + tag = tag_item.tag category = key if key != 'news' else 'tag' if tag.state > 0: prefix = ' not ' if tag.state == 2 else '' - ans.append('%s%s:"%s"'%(prefix, category, tag)) + ans.append('%s%s:"%s"'%(prefix, category, tag.name)) return ans diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index d7adcd7fee..ef5583fee9 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -365,16 +365,14 @@ class ResultCache(SearchQueryParser): self._map_filtered = [id for id in self._map if id in matches] -class Tag(unicode): +class Tag(object): - def __new__(cls, *args): - obj = super(Tag, cls).__new__(cls, *args) - obj.count = 0 - obj.state = 0 - return obj + def __init__(self, name, id=None, count=0, state=0): + self.name = name + self.id = id + self.count = count + self.state = state - def as_string(self): - return u'[%d] %s'%(self.count, self) class LibraryDatabase2(LibraryDatabase): ''' @@ -987,15 +985,19 @@ class LibraryDatabase2(LibraryDatabase): tags = categories[category] if name != 'data': for tag in tags: - id = self.conn.get('SELECT id FROM %s WHERE %s=?'%(name, field), (tag,), all=False) + id = self.conn.get('SELECT id FROM %s WHERE %s=?'%(name, + field), (tag.name,), all=False) tag.id = id for tag in tags: if tag.id is not None: tag.count = self.conn.get('SELECT COUNT(id) FROM books_%s_link WHERE %s=?'%(name, category), (tag.id,), all=False) else: for tag in tags: - tag.count = self.conn.get('SELECT COUNT(format) FROM data WHERE format=?', (tag,), all=False) - tags.sort(reverse=sort_on_count, cmp=(lambda x,y:cmp(x.count,y.count)) if sort_on_count else cmp) + tag.count = self.conn.get('SELECT COUNT(format) FROM data WHERE format=?', + (tag.name,), all=False) + tags.sort(reverse=sort_on_count, cmp=(lambda + x,y:cmp(x.count,y.count)) if sort_on_count else (lambda + x,y:cmp(x.name, y.name))) for x in (('authors', 'author'), ('tags', 'tag'), ('publishers', 'publisher'), ('series', 'series')): get(*x) @@ -1011,7 +1013,7 @@ class LibraryDatabase2(LibraryDatabase): pass categories['news'] = list(map(Tag, newspapers)) for tag in categories['news']: - tag.count = self.conn.get('SELECT COUNT(id) FROM books_tags_link WHERE tag IN (SELECT DISTINCT id FROM tags WHERE name=?)', (tag,), all=False) + tag.count = self.conn.get('SELECT COUNT(id) FROM books_tags_link WHERE tag IN (SELECT DISTINCT id FROM tags WHERE name=?)', (tag.name,), all=False) return categories