mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Rewrite the Tag browser to make it more efficient and remove one source of random crashes
This commit is contained in:
parent
8ae1ab6d6c
commit
6a0cc12ef7
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user