~ubuntu-branches/ubuntu/oneiric/calibre/oneiric

« back to all changes in this revision

Viewing changes to src/calibre/gui2/tag_view.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2010-06-21 10:18:08 UTC
  • mfrom: (1.3.12 upstream)
  • Revision ID: james.westby@ubuntu.com-20100621101808-aue828f532tmo4zt
Tags: 0.7.2+dfsg-1
* New major upstream version. See http://calibre-ebook.com/new-in/seven for
  details.
* Refresh patches to apply cleanly.
* debian/control: Bump python-cssutils to >= 0.9.7~ to ensure the existence
  of the CSSRuleList.rulesOfType attribute. This makes epub conversion work
  again. (Closes: #584756)
* Add debian/local/calibre-mount-helper: Simple and safe replacement for
  upstream's calibre-mount-helper, using udisks --mount and eject.
  (Closes: #584915, LP: #561958)

Show diffs side-by-side

added added

removed removed

Lines of Context:
8
8
'''
9
9
 
10
10
from itertools import izip
11
 
 
12
 
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
13
 
                     QFont, SIGNAL, QSize, QIcon, QPoint, \
14
 
                     QAbstractItemModel, QVariant, QModelIndex
 
11
from functools import partial
 
12
 
 
13
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
 
14
                     QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
 
15
                     QAbstractItemModel, QVariant, QModelIndex, QMenu, \
 
16
                     QPushButton, QWidget
 
17
 
15
18
from calibre.gui2 import config, NONE
 
19
from calibre.utils.config import prefs
 
20
from calibre.library.field_metadata import TagsIcons
16
21
from calibre.utils.search_query_parser import saved_searches
17
 
from calibre.library.database2 import Tag
18
 
 
19
 
class TagsView(QTreeView):
20
 
 
21
 
    need_refresh = pyqtSignal()
22
 
 
23
 
    def __init__(self, *args):
24
 
        QTreeView.__init__(self, *args)
 
22
from calibre.gui2 import error_dialog
 
23
from calibre.gui2.dialogs.tag_categories import TagCategories
 
24
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
 
25
 
 
26
class TagsView(QTreeView): # {{{
 
27
 
 
28
    refresh_required    = pyqtSignal()
 
29
    tags_marked         = pyqtSignal(object, object)
 
30
    user_category_edit  = pyqtSignal(object)
 
31
    tag_list_edit       = pyqtSignal(object, object)
 
32
    saved_search_edit   = pyqtSignal(object)
 
33
    tag_item_renamed    = pyqtSignal()
 
34
    search_item_renamed = pyqtSignal()
 
35
 
 
36
    def __init__(self, parent=None):
 
37
        QTreeView.__init__(self, parent=None)
 
38
        self.tag_match = None
25
39
        self.setUniformRowHeights(True)
26
40
        self.setCursor(Qt.PointingHandCursor)
27
41
        self.setIconSize(QSize(30, 30))
28
 
        self.tag_match = None
 
42
        self.setTabKeyNavigation(True)
 
43
        self.setAlternatingRowColors(True)
 
44
        self.setAnimated(True)
 
45
        self.setHeaderHidden(True)
29
46
 
30
47
    def set_database(self, db, tag_match, popularity):
31
 
        self._model = TagsModel(db, parent=self)
 
48
        self.hidden_categories = config['tag_browser_hidden_categories']
 
49
        self._model = TagsModel(db, parent=self,
 
50
                                hidden_categories=self.hidden_categories,
 
51
                                search_restriction=None)
32
52
        self.popularity = popularity
33
53
        self.tag_match = tag_match
 
54
        self.db = db
 
55
        self.search_restriction = None
34
56
        self.setModel(self._model)
35
 
        self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle)
 
57
        self.setContextMenuPolicy(Qt.CustomContextMenu)
 
58
        self.clicked.connect(self.toggle)
 
59
        self.customContextMenuRequested.connect(self.show_context_menu)
36
60
        self.popularity.setChecked(config['sort_by_popularity'])
37
 
        self.connect(self.popularity, SIGNAL('stateChanged(int)'), self.sort_changed)
38
 
        self.need_refresh.connect(self.recount, type=Qt.QueuedConnection)
 
61
        self.popularity.stateChanged.connect(self.sort_changed)
 
62
        self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
39
63
        db.add_listener(self.database_changed)
40
64
 
41
65
    def database_changed(self, event, ids):
42
 
        self.need_refresh.emit()
 
66
        self.refresh_required.emit()
43
67
 
44
68
    @property
45
69
    def match_all(self):
47
71
 
48
72
    def sort_changed(self, state):
49
73
        config.set('sort_by_popularity', state == Qt.Checked)
50
 
        self.model().refresh()
 
74
        self.recount()
 
75
 
 
76
    def set_search_restriction(self, s):
 
77
        if s:
 
78
            self.search_restriction = s
 
79
        else:
 
80
            self.search_restriction = None
 
81
        self.set_new_model()
 
82
 
 
83
    def mouseReleaseEvent(self, event):
 
84
        # Swallow everything except leftButton so context menus work correctly
 
85
        if event.button() == Qt.LeftButton:
 
86
            QTreeView.mouseReleaseEvent(self, event)
 
87
 
 
88
    def mouseDoubleClickEvent(self, event):
 
89
        # swallow these to avoid toggling and editing at the same time
 
90
        pass
51
91
 
52
92
    def toggle(self, index):
53
93
        modifiers = int(QApplication.keyboardModifiers())
54
94
        exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
55
95
        if self._model.toggle(index, exclusive):
56
 
            self.emit(SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
57
 
                      self._model.tokens(), self.match_all)
 
96
            self.tags_marked.emit(self._model.tokens(), self.match_all)
 
97
 
 
98
    def context_menu_handler(self, action=None, category=None,
 
99
                             key=None, index=None):
 
100
        if not action:
 
101
            return
 
102
        try:
 
103
            if action == 'edit_item':
 
104
                self.edit(index)
 
105
                return
 
106
            if action == 'open_editor':
 
107
                self.tag_list_edit.emit(category, key)
 
108
                return
 
109
            if action == 'manage_categories':
 
110
                self.user_category_edit.emit(category)
 
111
                return
 
112
            if action == 'manage_searches':
 
113
                self.saved_search_edit.emit(category)
 
114
                return
 
115
            if action == 'hide':
 
116
                self.hidden_categories.add(category)
 
117
            elif action == 'show':
 
118
                self.hidden_categories.discard(category)
 
119
            elif action == 'defaults':
 
120
                self.hidden_categories.clear()
 
121
            config.set('tag_browser_hidden_categories', self.hidden_categories)
 
122
            self.set_new_model()
 
123
        except:
 
124
            return
 
125
 
 
126
    def show_context_menu(self, point):
 
127
        index = self.indexAt(point)
 
128
        if not index.isValid():
 
129
            return False
 
130
        item = index.internalPointer()
 
131
        tag_name = ''
 
132
        if item.type == TagTreeItem.TAG:
 
133
            tag_item = item
 
134
            tag_name = item.tag.name
 
135
            item = item.parent
 
136
        if item.type == TagTreeItem.CATEGORY:
 
137
            category = unicode(item.name.toString())
 
138
            key = item.category_key
 
139
            # Verify that we are working with a field that we know something about
 
140
            if key not in self.db.field_metadata:
 
141
                return True
 
142
 
 
143
            self.context_menu = QMenu(self)
 
144
            # If the user right-clicked on an editable item, then offer
 
145
            # the possibility of renaming that item
 
146
            if tag_name and \
 
147
                    (key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
 
148
                     self.db.field_metadata[key]['is_custom'] and \
 
149
                     self.db.field_metadata[key]['datatype'] != 'rating'):
 
150
                self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
 
151
                        partial(self.context_menu_handler, action='edit_item',
 
152
                                category=tag_item, index=index))
 
153
                self.context_menu.addSeparator()
 
154
            # Hide/Show/Restore categories
 
155
            self.context_menu.addAction(_('Hide category %s') % category,
 
156
                partial(self.context_menu_handler, action='hide', category=category))
 
157
            if self.hidden_categories:
 
158
                m = self.context_menu.addMenu(_('Show category'))
 
159
                for col in sorted(self.hidden_categories, cmp=lambda x,y: cmp(x.lower(), y.lower())):
 
160
                    m.addAction(col,
 
161
                        partial(self.context_menu_handler, action='show', category=col))
 
162
                self.context_menu.addAction(_('Show all categories'),
 
163
                            partial(self.context_menu_handler, action='defaults'))
 
164
 
 
165
            # Offer specific editors for tags/series/publishers/saved searches
 
166
            self.context_menu.addSeparator()
 
167
            if key in ['tags', 'publisher', 'series'] or \
 
168
                        self.db.field_metadata[key]['is_custom']:
 
169
                self.context_menu.addAction(_('Manage ') + category,
 
170
                        partial(self.context_menu_handler, action='open_editor',
 
171
                                category=tag_name, key=key))
 
172
            elif key == 'search':
 
173
                self.context_menu.addAction(_('Manage Saved Searches'),
 
174
                    partial(self.context_menu_handler, action='manage_searches',
 
175
                            category=tag_name))
 
176
 
 
177
            # Always show the user categories editor
 
178
            self.context_menu.addSeparator()
 
179
            if category in prefs['user_categories'].keys():
 
180
                self.context_menu.addAction(_('Manage User Categories'),
 
181
                        partial(self.context_menu_handler, action='manage_categories',
 
182
                                category=category))
 
183
            else:
 
184
                self.context_menu.addAction(_('Manage User Categories'),
 
185
                        partial(self.context_menu_handler, action='manage_categories',
 
186
                                category=None))
 
187
 
 
188
            self.context_menu.popup(self.mapToGlobal(point))
 
189
        return True
58
190
 
59
191
    def clear(self):
60
 
        self.model().clear_state()
 
192
        if self.model():
 
193
            self.model().clear_state()
 
194
 
 
195
    def is_visible(self, idx):
 
196
        item = idx.internalPointer()
 
197
        if getattr(item, 'type', None) == TagTreeItem.TAG:
 
198
            idx = idx.parent()
 
199
        return self.isExpanded(idx)
61
200
 
62
201
    def recount(self, *args):
63
202
        ci = self.currentIndex()
64
203
        if not ci.isValid():
65
204
            ci = self.indexAt(QPoint(10, 10))
66
 
        path = self.model().path_for_index(ci)
 
205
        path = self.model().path_for_index(ci) if self.is_visible(ci) else None
67
206
        try:
68
 
            self.model().refresh()
 
207
            if not self.model().refresh(): # categories changed!
 
208
                self.set_new_model()
 
209
                path = None
69
210
        except: #Database connection could be closed if an integrity check is happening
70
211
            pass
71
212
        if path:
74
215
                self.setCurrentIndex(idx)
75
216
                self.scrollTo(idx, QTreeView.PositionAtCenter)
76
217
 
77
 
class TagTreeItem(object):
 
218
    # If the number of user categories changed,  if custom columns have come or
 
219
    # gone, or if columns have been hidden or restored, we must rebuild the
 
220
    # model. Reason: it is much easier than reconstructing the browser tree.
 
221
    def set_new_model(self):
 
222
        try:
 
223
            self._model = TagsModel(self.db, parent=self,
 
224
                                    hidden_categories=self.hidden_categories,
 
225
                                    search_restriction=self.search_restriction)
 
226
            self.setModel(self._model)
 
227
        except:
 
228
            # The DB must be gone. Set the model to None and hope that someone
 
229
            # will call set_database later. I don't know if this in fact works
 
230
            self._model = None
 
231
            self.setModel(None)
 
232
    # }}}
 
233
 
 
234
class TagTreeItem(object): # {{{
78
235
 
79
236
    CATEGORY = 0
80
237
    TAG      = 1
81
238
    ROOT     = 2
82
239
 
83
 
    def __init__(self, data=None, tag=None, category_icon=None, icon_map=None, parent=None):
 
240
    def __init__(self, data=None, category_icon=None, icon_map=None,
 
241
                 parent=None, tooltip=None, category_key=None):
84
242
        self.parent = parent
85
243
        self.children = []
86
244
        if self.parent is not None:
95
253
            self.bold_font = QFont()
96
254
            self.bold_font.setBold(True)
97
255
            self.bold_font = QVariant(self.bold_font)
 
256
            self.category_key = category_key
98
257
        elif self.type == self.TAG:
99
 
            self.tag, self.icon_map = data, list(map(QVariant, icon_map))
 
258
            icon_map[0] = data.icon
 
259
            self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
 
260
        self.tooltip = tooltip
100
261
 
101
262
    def __str__(self):
102
263
        if self.type == self.ROOT:
103
264
            return 'ROOT'
104
265
        if self.type == self.CATEGORY:
105
 
            return 'CATEGORY:'+self.name+':%d'%len(self.children)
 
266
            return 'CATEGORY:'+str(QVariant.toString(self.name))+':%d'%len(self.children)
106
267
        return 'TAG:'+self.tag.name
107
268
 
108
269
    def row(self):
123
284
 
124
285
    def category_data(self, role):
125
286
        if role == Qt.DisplayRole:
126
 
            return self.name
 
287
            return QVariant(self.py_name + ' [%d]'%len(self.children))
127
288
        if role == Qt.DecorationRole:
128
289
            return self.icon
129
290
        if role == Qt.FontRole:
130
291
            return self.bold_font
 
292
        if role == Qt.ToolTipRole and self.tooltip is not None:
 
293
            return QVariant(self.tooltip)
131
294
        return NONE
132
295
 
133
296
    def tag_data(self, role):
136
299
                return QVariant('%s'%(self.tag.name))
137
300
            else:
138
301
                return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
 
302
        if role == Qt.EditRole:
 
303
            return QVariant(self.tag.name)
139
304
        if role == Qt.DecorationRole:
140
 
            return self.icon_map[self.tag.state]
141
 
        if role == Qt.ToolTipRole and self.tag.tooltip:
 
305
            return self.icon_state_map[self.tag.state]
 
306
        if role == Qt.ToolTipRole and self.tag.tooltip is not None:
142
307
            return QVariant(self.tag.tooltip)
143
308
        return NONE
144
309
 
146
311
        if self.type == self.TAG:
147
312
            self.tag.state = (self.tag.state + 1)%3
148
313
 
149
 
 
150
 
class TagsModel(QAbstractItemModel):
151
 
    categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags'), _('Searches')]
152
 
    row_map    = ['author', 'series', 'format', 'publisher', 'news', 'tag', 'search']
153
 
 
154
 
    def __init__(self, db, parent=None):
 
314
    # }}}
 
315
 
 
316
class TagsModel(QAbstractItemModel): # {{{
 
317
 
 
318
    def __init__(self, db, parent, hidden_categories=None, search_restriction=None):
155
319
        QAbstractItemModel.__init__(self, parent)
156
 
        self.cmap = tuple(map(QIcon, [I('user_profile.svg'),
157
 
                I('series.svg'), I('book.svg'), I('publisher.png'),
158
 
                I('news.svg'), I('tags.svg'), I('search.svg')]))
159
 
        self.icon_map = [QIcon(), QIcon(I('plus.svg')),
160
 
                QIcon(I('minus.svg'))]
 
320
 
 
321
        # must do this here because 'QPixmap: Must construct a QApplication
 
322
        # before a QPaintDevice'. The ':' in front avoids polluting either the
 
323
        # user-defined categories (':' at end) or columns namespaces (no ':').
 
324
        self.category_icon_map = TagsIcons({
 
325
                    'authors'   : QIcon(I('user_profile.svg')),
 
326
                    'series'    : QIcon(I('series.svg')),
 
327
                    'formats'   : QIcon(I('book.svg')),
 
328
                    'publisher' : QIcon(I('publisher.png')),
 
329
                    'rating'    : QIcon(I('star.png')),
 
330
                    'news'      : QIcon(I('news.svg')),
 
331
                    'tags'      : QIcon(I('tags.svg')),
 
332
                    ':custom'   : QIcon(I('column.svg')),
 
333
                    ':user'     : QIcon(I('drawer.svg')),
 
334
                    'search'    : QIcon(I('search.svg'))})
 
335
 
 
336
        self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
161
337
        self.db = db
162
 
        self.ignore_next_search = 0
 
338
        self.tags_view = parent
 
339
        self.hidden_categories = hidden_categories
 
340
        self.search_restriction = search_restriction
 
341
        self.row_map = []
 
342
 
 
343
        # get_node_tree cannot return None here, because row_map is empty
 
344
        data = self.get_node_tree(config['sort_by_popularity'])
163
345
        self.root_item = TagTreeItem()
164
 
        data = self.db.get_categories(config['sort_by_popularity'])
165
 
        data['search'] = self.get_search_nodes()
166
 
 
167
346
        for i, r in enumerate(self.row_map):
 
347
            if self.hidden_categories and self.categories[i] in self.hidden_categories:
 
348
                continue
 
349
            if self.db.field_metadata[r]['kind'] != 'user':
 
350
                tt = _('The lookup/search name is "{0}"').format(r)
 
351
            else:
 
352
                tt = ''
168
353
            c = TagTreeItem(parent=self.root_item,
169
 
                    data=self.categories[i], category_icon=self.cmap[i])
 
354
                    data=self.categories[i],
 
355
                    category_icon=self.category_icon_map[r],
 
356
                    tooltip=tt, category_key=r)
170
357
            for tag in data[r]:
171
 
                TagTreeItem(parent=c, data=tag, icon_map=self.icon_map)
172
 
 
173
 
 
174
 
    def get_search_nodes(self):
175
 
        l = []
176
 
        for i in saved_searches.names():
177
 
            l.append(Tag(i, tooltip=saved_searches.lookup(i)))
178
 
        return l
 
358
                TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
 
359
 
 
360
    def set_search_restriction(self, s):
 
361
        self.search_restriction = s
 
362
 
 
363
    def get_node_tree(self, sort):
 
364
        old_row_map = self.row_map[:]
 
365
        self.row_map = []
 
366
        self.categories = []
 
367
 
 
368
        # Reconstruct the user categories, putting them into metadata
 
369
        tb_cats = self.db.field_metadata
 
370
        for k in tb_cats.keys():
 
371
            if tb_cats[k]['kind'] in ['user', 'search']:
 
372
                del tb_cats[k]
 
373
        for user_cat in sorted(prefs['user_categories'].keys()):
 
374
            cat_name = user_cat+':' # add the ':' to avoid name collision
 
375
            tb_cats.add_user_category(label=cat_name, name=user_cat)
 
376
        if len(saved_searches.names()):
 
377
            tb_cats.add_search_category(label='search', name=_('Searches'))
 
378
 
 
379
        # Now get the categories
 
380
        if self.search_restriction:
 
381
            data = self.db.get_categories(sort_on_count=sort,
 
382
                        icon_map=self.category_icon_map,
 
383
                        ids=self.db.search('', return_matches=True))
 
384
        else:
 
385
            data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
 
386
 
 
387
        tb_categories = self.db.field_metadata
 
388
        for category in tb_categories:
 
389
            if category in data: # The search category can come and go
 
390
                self.row_map.append(category)
 
391
                self.categories.append(tb_categories[category]['name'])
 
392
        if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map):
 
393
            # A category has been added or removed. We must force a rebuild of
 
394
            # the model
 
395
            return None
 
396
        return data
179
397
 
180
398
    def refresh(self):
181
 
        data = self.db.get_categories(config['sort_by_popularity'])
182
 
        data['search'] = self.get_search_nodes()
 
399
        data = self.get_node_tree(config['sort_by_popularity']) # get category data
 
400
        if data is None:
 
401
            return False
 
402
        row_index = -1
183
403
        for i, r in enumerate(self.row_map):
184
 
            category = self.root_item.children[i]
 
404
            if self.hidden_categories and self.categories[i] in self.hidden_categories:
 
405
                continue
 
406
            row_index += 1
 
407
            category = self.root_item.children[row_index]
185
408
            names = [t.tag.name for t in category.children]
186
409
            states = [t.tag.state for t in category.children]
187
410
            state_map = dict(izip(names, states))
188
 
            category_index = self.index(i, 0, QModelIndex())
 
411
            category_index = self.index(row_index, 0, QModelIndex())
189
412
            if len(category.children) > 0:
190
413
                self.beginRemoveRows(category_index, 0,
191
414
                        len(category.children)-1)
194
417
            if len(data[r]) > 0:
195
418
                self.beginInsertRows(category_index, 0, len(data[r])-1)
196
419
                for tag in data[r]:
197
 
                    if r == 'author':
198
 
                        tag.name = tag.name.replace('|', ',')
199
420
                    tag.state = state_map.get(tag.name, 0)
200
 
                    t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_map)
 
421
                    t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
201
422
                self.endInsertRows()
 
423
        return True
202
424
 
203
425
    def columnCount(self, parent):
204
426
        return 1
209
431
        item = index.internalPointer()
210
432
        return item.data(role)
211
433
 
 
434
    def setData(self, index, value, role=Qt.EditRole):
 
435
        if not index.isValid():
 
436
            return NONE
 
437
        # set up to position at the category label
 
438
        path = self.path_for_index(self.parent(index))
 
439
        val = unicode(value.toString())
 
440
        if not val:
 
441
            error_dialog(self.tags_view, _('Item is blank'),
 
442
                        _('An item cannot be set to nothing. Delete it instead.')).exec_()
 
443
            return False
 
444
        item = index.internalPointer()
 
445
        key = item.parent.category_key
 
446
        # make certain we know about the item's category
 
447
        if key not in self.db.field_metadata:
 
448
            return
 
449
        if key == 'search':
 
450
            if val in saved_searches.names():
 
451
                error_dialog(self.tags_view, _('Duplicate search name'),
 
452
                    _('The saved search name %s is already used.')%val).exec_()
 
453
                return False
 
454
            saved_searches.rename(unicode(item.data(role).toString()), val)
 
455
            self.tags_view.search_item_renamed.emit()
 
456
        else:
 
457
            if key == 'series':
 
458
                self.db.rename_series(item.tag.id, val)
 
459
            elif key == 'publisher':
 
460
                self.db.rename_publisher(item.tag.id, val)
 
461
            elif key == 'tags':
 
462
                self.db.rename_tag(item.tag.id, val)
 
463
            elif key == 'authors':
 
464
                self.db.rename_author(item.tag.id, val)
 
465
            elif self.db.field_metadata[key]['is_custom']:
 
466
                self.db.rename_custom_item(item.tag.id, val,
 
467
                                    label=self.db.field_metadata[key]['label'])
 
468
            self.tags_view.tag_item_renamed.emit()
 
469
        item.tag.name = val
 
470
        self.refresh() # Should work, because no categories can have disappeared
 
471
        if path:
 
472
            idx = self.index_for_path(path)
 
473
            if idx.isValid():
 
474
                self.tags_view.setCurrentIndex(idx)
 
475
                self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter)
 
476
        return True
 
477
 
212
478
    def headerData(self, *args):
213
479
        return NONE
214
480
 
215
481
    def flags(self, *args):
216
 
        return Qt.ItemIsEnabled|Qt.ItemIsSelectable
 
482
        return Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable
217
483
 
218
484
    def path_for_index(self, index):
219
485
        ans = []
273
539
        return len(parent_item.children)
274
540
 
275
541
    def reset_all_states(self, except_=None):
 
542
        update_list = []
276
543
        for i in xrange(self.rowCount(QModelIndex())):
277
544
            category_index = self.index(i, 0, QModelIndex())
278
545
            for j in xrange(self.rowCount(category_index)):
279
546
                tag_index = self.index(j, 0, category_index)
280
547
                tag_item = tag_index.internalPointer()
281
 
                if tag_item is except_:
 
548
                tag = tag_item.tag
 
549
                if tag is except_:
 
550
                    self.dataChanged.emit(tag_index, tag_index)
282
551
                    continue
283
 
                tag = tag_item.tag
284
 
                if tag.state != 0:
 
552
                if tag.state != 0 or tag in update_list:
285
553
                    tag.state = 0
286
 
                    self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
287
 
                            tag_index, tag_index)
 
554
                    update_list.append(tag)
 
555
                    self.dataChanged.emit(tag_index, tag_index)
288
556
 
289
557
    def clear_state(self):
290
558
        self.reset_all_states()
291
559
 
292
 
    def reinit(self, *args, **kwargs):
293
 
        if self.ignore_next_search == 0:
294
 
            self.reset_all_states()
295
 
        else:
296
 
            self.ignore_next_search -= 1
297
 
 
298
560
    def toggle(self, index, exclusive):
299
561
        if not index.isValid(): return False
300
562
        item = index.internalPointer()
301
563
        if item.type == TagTreeItem.TAG:
 
564
            item.toggle()
302
565
            if exclusive:
303
 
                self.reset_all_states(except_=item)
304
 
            item.toggle()
305
 
            self.ignore_next_search = 2
306
 
            self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index)
 
566
                self.reset_all_states(except_=item.tag)
 
567
            self.dataChanged.emit(index, index)
307
568
            return True
308
569
        return False
309
570
 
310
571
    def tokens(self):
311
572
        ans = []
 
573
        tags_seen = set()
 
574
        row_index = -1
312
575
        for i, key in enumerate(self.row_map):
313
 
            category_item = self.root_item.children[i]
 
576
            if self.hidden_categories and self.categories[i] in self.hidden_categories:
 
577
                continue
 
578
            row_index += 1
 
579
            if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
 
580
                continue
 
581
            category_item = self.root_item.children[row_index]
314
582
            for tag_item in category_item.children:
315
583
                tag = tag_item.tag
316
 
                category = key if key != 'news' else 'tag'
317
584
                if tag.state > 0:
318
585
                    prefix = ' not ' if tag.state == 2 else ''
319
 
                    ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
 
586
                    category = key if key != 'news' else 'tag'
 
587
                    if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
 
588
                        ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
 
589
                    else:
 
590
                        if category == 'tags':
 
591
                            if tag.name in tags_seen:
 
592
                                continue
 
593
                            tags_seen.add(tag.name)
 
594
                        ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
320
595
        return ans
321
596
 
 
597
    # }}}
 
598
 
 
599
class TagBrowserMixin(object): # {{{
 
600
 
 
601
    def __init__(self, db):
 
602
        self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
 
603
        self.tags_view.set_database(self.library_view.model().db,
 
604
                self.tag_match, self.popularity)
 
605
        self.tags_view.tags_marked.connect(self.search.search_from_tags)
 
606
        self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
 
607
        self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
 
608
        self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
 
609
        self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
 
610
        self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
 
611
        self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
 
612
        self.edit_categories.clicked.connect(lambda x:
 
613
                self.do_user_categories_edit())
 
614
 
 
615
    def do_user_categories_edit(self, on_category=None):
 
616
        d = TagCategories(self, self.library_view.model().db, on_category)
 
617
        d.exec_()
 
618
        if d.result() == d.Accepted:
 
619
            self.tags_view.set_new_model()
 
620
            self.tags_view.recount()
 
621
 
 
622
    def do_tags_list_edit(self, tag, category):
 
623
        d = TagListEditor(self, self.library_view.model().db, tag, category)
 
624
        d.exec_()
 
625
        if d.result() == d.Accepted:
 
626
            # Clean up everything, as information could have changed for many books.
 
627
            self.library_view.model().refresh()
 
628
            self.tags_view.set_new_model()
 
629
            self.tags_view.recount()
 
630
            self.saved_search.clear_to_help()
 
631
            self.search.clear_to_help()
 
632
 
 
633
    def do_tag_item_renamed(self):
 
634
        # Clean up library view and search
 
635
        self.library_view.model().refresh()
 
636
        self.saved_search.clear_to_help()
 
637
        self.search.clear_to_help()
 
638
 
 
639
# }}}
 
640
 
 
641
class TagBrowserWidget(QWidget): # {{{
 
642
 
 
643
    def __init__(self, parent):
 
644
        QWidget.__init__(self, parent)
 
645
        self._layout = QVBoxLayout()
 
646
        self.setLayout(self._layout)
 
647
 
 
648
        parent.tags_view = TagsView(parent)
 
649
        self._layout.addWidget(parent.tags_view)
 
650
 
 
651
        parent.popularity = QCheckBox(parent)
 
652
        parent.popularity.setText(_('Sort by &popularity'))
 
653
        self._layout.addWidget(parent.popularity)
 
654
 
 
655
        parent.tag_match = QComboBox(parent)
 
656
        for x in (_('Match any'), _('Match all')):
 
657
            parent.tag_match.addItem(x)
 
658
        parent.tag_match.setCurrentIndex(0)
 
659
        self._layout.addWidget(parent.tag_match)
 
660
 
 
661
        parent.edit_categories = QPushButton(_('Manage &user categories'), parent)
 
662
        self._layout.addWidget(parent.edit_categories)
 
663
 
 
664
 
 
665
# }}}
322
666