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.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)

View File

@ -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):
if self.type == self.TAG:
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):
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)
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

View File

@ -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