Rewrite the Tag browser to make it more efficient and remove one source of random crashes

This commit is contained in:
Kovid Goyal 2009-09-27 19:25:24 -06:00
parent 8ae1ab6d6c
commit 6a0cc12ef7
3 changed files with 171 additions and 106 deletions

View File

@ -111,7 +111,7 @@ class Command(object):
self.b = os.path.basename self.b = os.path.basename
self.s = os.path.splitext self.s = os.path.splitext
self.e = os.path.exists 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_uid = os.environ.get('SUDO_UID', None)
self.real_gid = os.environ.get('SUDO_GID', None) self.real_gid = os.environ.get('SUDO_GID', None)
self.real_user = os.environ.get('SUDO_USER', None) self.real_user = os.environ.get('SUDO_USER', None)

View File

@ -6,9 +6,13 @@ __docformat__ = 'restructuredtext en'
''' '''
Browsing book collection by tags. Browsing book collection by tags.
''' '''
from PyQt4.Qt import QStandardItemModel, Qt, QTreeView, QStandardItem, \
QFont, SIGNAL, QSize, QIcon, QPoint, QPixmap from itertools import izip
from calibre.gui2 import config
from PyQt4.Qt import Qt, QTreeView, \
QFont, SIGNAL, QSize, QIcon, QPoint, \
QAbstractItemModel, QVariant, QModelIndex
from calibre.gui2 import config, NONE
class TagsView(QTreeView): class TagsView(QTreeView):
@ -19,7 +23,7 @@ class TagsView(QTreeView):
self.setIconSize(QSize(30, 30)) self.setIconSize(QSize(30, 30))
def set_database(self, db, match_all, popularity): def set_database(self, db, match_all, popularity):
self._model = TagsModel(db) self._model = TagsModel(db, parent=self)
self.popularity = popularity self.popularity = popularity
self.match_all = match_all self.match_all = match_all
self.setModel(self._model) self.setModel(self._model)
@ -47,56 +51,87 @@ class TagsView(QTreeView):
if ci.isValid(): if ci.isValid():
self.scrollTo(ci, QTreeView.PositionAtTop) self.scrollTo(ci, QTreeView.PositionAtTop)
class CategoryItem(QStandardItem): class TagTreeItem(object):
def __init__(self, category, display_text, tags, icon, font, icon_map): CATEGORY = 0
self.category = category TAG = 1
self.tags = tags ROOT = 2
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))
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): def row(self):
self.icon_map = icon_map if self.parent is not None:
self.tag = tag return self.parent.children.index(self)
QStandardItem.__init__(self, tag.as_string()) return 0
self.set_icon()
self.setEditable(False) def append(self, child):
self.setSelectable(False) 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): def toggle(self):
self.tag.state = (self.tag.state + 1)%3 if self.type == self.TAG:
self.set_icon() self.tag.state = (self.tag.state + 1)%3
def set_icon(self):
self.setIcon(self.icon_map[self.tag.state])
class TagsModel(QStandardItemModel):
class TagsModel(QAbstractItemModel):
categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags')] categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags')]
row_map = ['author', 'series', 'format', 'publisher', 'news', 'tag'] 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'), self.cmap = tuple(map(QIcon, [I('user_profile.svg'),
I('series.svg'), I('book.svg'), I('publisher.png'), I('series.svg'), I('book.svg'), I('publisher.png'),
I('news.svg'), I('tags.svg')])) I('news.svg'), I('tags.svg')]))
p = QPixmap(30, 30) self.icon_map = [QIcon(), QIcon(I('plus.svg')),
p.fill(Qt.transparent)
self.icon_map = [QIcon(p), QIcon(I('plus.svg')),
QIcon(I('minus.svg'))] QIcon(I('minus.svg'))]
QStandardItemModel.__init__(self)
self.db = db self.db = db
self.ignore_next_search = 0 self.ignore_next_search = 0
self._data = {} self.root_item = TagTreeItem()
self.bold_font = QFont() data = self.db.get_categories(config['sort_by_popularity'])
self.bold_font.setBold(True) 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.refresh()
self.db.add_listener(self.database_changed) self.db.add_listener(self.database_changed)
@ -104,72 +139,98 @@ class TagsModel(QStandardItemModel):
self.refresh() self.refresh()
def refresh(self): def refresh(self):
old_data = self._data data = self.db.get_categories(config['sort_by_popularity'])
self._data = self.db.get_categories(config['sort_by_popularity']) for i, r in enumerate(self.row_map):
for key in old_data.keys(): category = self.root_item.children[i]
for tag in old_data[key]: names = [t.tag.name for t in category.children]
try: states = [t.tag.state for t in category.children]
index = self._data[key].index(tag) state_map = dict(izip(names, states))
if index > -1: category_index = self.index(i, 0, QModelIndex())
self._data[key][index].state = tag.state if len(category.children) > 0:
except: self.beginRemoveRows(category_index, 0,
continue len(category.children)-1)
self.clear() category.children = []
root = self.invisibleRootItem() self.endRemoveRows()
for r, category in enumerate(self.row_map): if len(data[r]) > 0:
tags = self._data.get(category, []) self.beginInsertRows(category_index, 0, len(data[r])-1)
root.appendRow(CategoryItem(category, self.categories[r], for tag in data[r]:
self._data[category], self.cmap[r], self.bold_font, self.icon_map)) tag.state = state_map.get(tag.name, 0)
#self.reset() 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): def reset_all_states(self):
changed_indices = [] for i in xrange(self.rowCount(QModelIndex())):
for category in self._data.values(): category_index = self.index(i, 0, QModelIndex())
Category = self.find_category(category) category_item = category_index.internalPointer()
for tag in category: 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: if tag.state != 0:
tag.state = 0 tag.state = 0
if Category is not None: self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
Tag = self.find_tag(tag, Category) tag_index, tag_index)
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)
def clear_state(self): def clear_state(self):
for category in self._data.values():
for tag in category:
tag.state = 0
self.reset_all_states() 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): def reinit(self, *args, **kwargs):
if self.ignore_next_search == 0: if self.ignore_next_search == 0:
self.reset_all_states() self.reset_all_states()
else: else:
self.ignore_next_search -= 1 self.ignore_next_search -= 1
def toggle(self, index): def toggle(self, index):
if index.parent().isValid(): if not index.isValid(): return False
category = self.row_map[index.parent().row()] item = index.internalPointer()
tag = self._data[category][index.row()] if item.type == TagTreeItem.TAG:
self.invisibleRootItem().child(index.parent().row()).child(index.row()).toggle() item.toggle()
self.ignore_next_search = 2 self.ignore_next_search = 2
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index) self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index)
return True return True
@ -177,12 +238,14 @@ class TagsModel(QStandardItemModel):
def tokens(self): def tokens(self):
ans = [] ans = []
for key in self.row_map: for i, key in enumerate(self.row_map):
for tag in self._data[key]: 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' category = key if key != 'news' else 'tag'
if tag.state > 0: if tag.state > 0:
prefix = ' not ' if tag.state == 2 else '' 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 return ans

View File

@ -365,16 +365,14 @@ class ResultCache(SearchQueryParser):
self._map_filtered = [id for id in self._map if id in matches] self._map_filtered = [id for id in self._map if id in matches]
class Tag(unicode): class Tag(object):
def __new__(cls, *args): def __init__(self, name, id=None, count=0, state=0):
obj = super(Tag, cls).__new__(cls, *args) self.name = name
obj.count = 0 self.id = id
obj.state = 0 self.count = count
return obj self.state = state
def as_string(self):
return u'[%d] %s'%(self.count, self)
class LibraryDatabase2(LibraryDatabase): class LibraryDatabase2(LibraryDatabase):
''' '''
@ -987,15 +985,19 @@ class LibraryDatabase2(LibraryDatabase):
tags = categories[category] tags = categories[category]
if name != 'data': if name != 'data':
for tag in tags: 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 tag.id = id
for tag in tags: for tag in tags:
if tag.id is not None: 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) tag.count = self.conn.get('SELECT COUNT(id) FROM books_%s_link WHERE %s=?'%(name, category), (tag.id,), all=False)
else: else:
for tag in tags: for tag in tags:
tag.count = self.conn.get('SELECT COUNT(format) FROM data WHERE format=?', (tag,), all=False) tag.count = self.conn.get('SELECT COUNT(format) FROM data WHERE format=?',
tags.sort(reverse=sort_on_count, cmp=(lambda x,y:cmp(x.count,y.count)) if sort_on_count else cmp) (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'), for x in (('authors', 'author'), ('tags', 'tag'), ('publishers', 'publisher'),
('series', 'series')): ('series', 'series')):
get(*x) get(*x)
@ -1011,7 +1013,7 @@ class LibraryDatabase2(LibraryDatabase):
pass pass
categories['news'] = list(map(Tag, newspapers)) categories['news'] = list(map(Tag, newspapers))
for tag in categories['news']: 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 return categories