home *** CD-ROM | disk | FTP | other *** search
Wrap
# Source Generated with Decompyle++ # File: in.pyc (Python 2.6) __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' from itertools import izip from functools import partial from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, QAbstractItemModel, QVariant, QModelIndex, QMenu, QPushButton, QWidget, QItemDelegate from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.utils.search_query_parser import saved_searches from calibre.gui2 import error_dialog from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog class TagDelegate(QItemDelegate): def paint(self, painter, option, index): item = index.internalPointer() if item.type != TagTreeItem.TAG: QItemDelegate.paint(self, painter, option, index) return None r = option.rect model = self.parent().model() icon = model.data(index, Qt.DecorationRole).toPyObject() painter.save() if item.tag.state != 0 and not config['show_avg_rating'] or item.tag.avg_rating is None: icon.paint(painter, r, Qt.AlignLeft) else: painter.setOpacity(0.3) icon.paint(painter, r, Qt.AlignLeft) painter.setOpacity(1) rating = item.tag.avg_rating painter.setClipRect(r.left(), r.bottom() - int(r.height() * (rating / 5)), r.width(), r.height()) icon.paint(painter, r, Qt.AlignLeft) painter.setClipRect(r) r.setLeft(r.left() + r.height() + 3) painter.drawText(r, Qt.AlignLeft | Qt.AlignVCenter, model.data(index, Qt.DisplayRole).toString()) painter.restore() class TagsView(QTreeView): refresh_required = pyqtSignal() tags_marked = pyqtSignal(object, object) user_category_edit = pyqtSignal(object) tag_list_edit = pyqtSignal(object, object) saved_search_edit = pyqtSignal(object) author_sort_edit = pyqtSignal(object, object) tag_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal() drag_drop_finished = pyqtSignal(object) def __init__(self, parent = None): QTreeView.__init__(self, parent = None) self.tag_match = None self.setUniformRowHeights(True) self.setCursor(Qt.PointingHandCursor) self.setIconSize(QSize(30, 30)) self.setTabKeyNavigation(True) self.setAlternatingRowColors(True) self.setAnimated(True) self.setHeaderHidden(True) self.setItemDelegate(TagDelegate(self)) self.made_connections = False self.setAcceptDrops(True) self.setDragDropMode(self.DropOnly) self.setDropIndicatorShown(True) self.setAutoExpandDelay(500) def set_database(self, db, tag_match, sort_by): self.hidden_categories = config['tag_browser_hidden_categories'] self._model = TagsModel(db, parent = self, hidden_categories = self.hidden_categories, search_restriction = None, drag_drop_finished = self.drag_drop_finished) self.sort_by = sort_by self.tag_match = tag_match self.db = db self.search_restriction = None self.setModel(self._model) self.setContextMenuPolicy(Qt.CustomContextMenu) pop = config['sort_tags_by'] self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop)) if not self.made_connections: self.clicked.connect(self.toggle) self.customContextMenuRequested.connect(self.show_context_menu) self.refresh_required.connect(self.recount, type = Qt.QueuedConnection) self.sort_by.currentIndexChanged.connect(self.sort_changed) self.made_connections = True db.add_listener(self.database_changed) def database_changed(self, event, ids): self.refresh_required.emit() def match_all(self): if self.tag_match: pass return self.tag_match.currentIndex() > 0 match_all = property(match_all) def sort_changed(self, pop): config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop]) self.recount() def set_search_restriction(self, s): if s: self.search_restriction = s else: self.search_restriction = None self.set_new_model() def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: QTreeView.mouseReleaseEvent(self, event) def mouseDoubleClickEvent(self, event): pass def toggle(self, index): modifiers = int(QApplication.keyboardModifiers()) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) if self._model.toggle(index, exclusive): self.tags_marked.emit(self._model.tokens(), self.match_all) def context_menu_handler(self, action = None, category = None, key = None, index = None): if not action: return None try: if action == 'edit_item': self.edit(index) return None if action == 'open_editor': self.tag_list_edit.emit(category, key) return None if action == 'manage_categories': self.user_category_edit.emit(category) return None if action == 'manage_searches': self.saved_search_edit.emit(category) return None if action == 'edit_author_sort': self.author_sort_edit.emit(self, index) return None if action == 'hide': self.hidden_categories.add(category) elif action == 'show': self.hidden_categories.discard(category) elif action == 'defaults': self.hidden_categories.clear() config.set('tag_browser_hidden_categories', self.hidden_categories) self.set_new_model() except: action return None def show_context_menu(self, point): index = self.indexAt(point) if not index.isValid(): return False item = index.internalPointer() tag_name = '' if item.type == TagTreeItem.TAG: tag_item = item tag_name = item.tag.name tag_id = item.tag.id item = item.parent if item.type == TagTreeItem.CATEGORY: category = unicode(item.name.toString()) key = item.category_key if key not in self.db.field_metadata: return True self.context_menu = QMenu(self) if tag_name: pass self.context_menu.addAction(_('Hide category %s') % category, partial(self.context_menu_handler, action = 'hide', category = category)) if self.hidden_categories: m = self.context_menu.addMenu(_('Show category')) for col in sorted(self.hidden_categories, cmp = (lambda x, y: cmp(x.lower(), y.lower()))): m.addAction(col, partial(self.context_menu_handler, action = 'show', category = col)) self.context_menu.addAction(_('Show all categories'), partial(self.context_menu_handler, action = 'defaults')) self.context_menu.addSeparator() if key in ('tags', 'publisher', 'series') or self.db.field_metadata[key]['is_custom']: self.context_menu.addAction(_('Manage %s') % category, partial(self.context_menu_handler, action = 'open_editor', category = tag_name, key = key)) elif key == 'authors': self.context_menu.addAction(_('Manage %s') % category, partial(self.context_menu_handler, action = 'edit_author_sort')) elif key == 'search': self.context_menu.addAction(_('Manage Saved Searches'), partial(self.context_menu_handler, action = 'manage_searches', category = tag_name)) self.context_menu.addSeparator() if category in self.db.prefs.get('user_categories', { }).keys(): self.context_menu.addAction(_('Manage User Categories'), partial(self.context_menu_handler, action = 'manage_categories', category = category)) else: self.context_menu.addAction(_('Manage User Categories'), partial(self.context_menu_handler, action = 'manage_categories', category = None)) self.context_menu.popup(self.mapToGlobal(point)) return True def clear(self): if self.model(): self.model().clear_state() def is_visible(self, idx): item = idx.internalPointer() if getattr(item, 'type', None) == TagTreeItem.TAG: idx = idx.parent() return self.isExpanded(idx) def recount(self, *args): ci = self.currentIndex() if not ci.isValid(): ci = self.indexAt(QPoint(10, 10)) path = None if self.is_visible(ci) else None try: if not self.model().refresh(): self.set_new_model() path = None except: pass if path: idx = self.model().index_for_path(path) if idx.isValid(): self.setCurrentIndex(idx) self.scrollTo(idx, QTreeView.PositionAtCenter) def set_new_model(self): try: self._model = TagsModel(self.db, parent = self, hidden_categories = self.hidden_categories, search_restriction = self.search_restriction, drag_drop_finished = self.drag_drop_finished) self.setModel(self._model) except: self._model = None self.setModel(None) class TagTreeItem(object): CATEGORY = 0 TAG = 1 ROOT = 2 def __init__(self, data = None, category_icon = None, icon_map = None, parent = None, tooltip = None, category_key = None): self.parent = parent self.children = [] if self.parent is not None: self.parent.append(self) if data is None: self.type = self.ROOT elif category_icon is None: pass self.type = 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) self.category_key = category_key elif self.type == self.TAG: icon_map[0] = data.icon self.tag = data self.icon_state_map = list(map(QVariant, icon_map)) self.tooltip = tooltip def __str__(self): if self.type == self.ROOT: return 'ROOT' if self.type == self.CATEGORY: return 'CATEGORY:' + str(QVariant.toString(self.name)) + ':%d' % len(self.children) return 'TAG:' + self.tag.name 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 role == Qt.UserRole: return self 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 QVariant(self.py_name + ' [%d]' % len(self.children)) if role == Qt.DecorationRole: return self.icon if role == Qt.FontRole: return self.bold_font if role == Qt.ToolTipRole and self.tooltip is not None: return QVariant(self.tooltip) return NONE def tag_data(self, role): if role == Qt.DisplayRole: if self.tag.count == 0: return QVariant('%s' % self.tag.name) return QVariant('[%d] %s' % (self.tag.count, self.tag.name)) role == Qt.DisplayRole if role == Qt.EditRole: return QVariant(self.tag.name) if role == Qt.DecorationRole: return self.icon_state_map[self.tag.state] if role == Qt.ToolTipRole and self.tag.tooltip is not None: return QVariant(self.tag.tooltip) return NONE def toggle(self): if self.type == self.TAG: self.tag.state = (self.tag.state + 1) % 3 class TagsModel(QAbstractItemModel): def __init__(self, db, parent, hidden_categories = None, search_restriction = None, drag_drop_finished = None): QAbstractItemModel.__init__(self, parent) iconmap = { } for key in category_icon_map: iconmap[key] = QIcon(I(category_icon_map[key])) self.category_icon_map = TagsIcons(iconmap) self.categories_with_ratings = [ 'authors', 'series', 'publisher', 'tags'] self.drag_drop_finished = drag_drop_finished self.icon_state_map = [ None, QIcon(I('plus.png')), QIcon(I('minus.png'))] self.db = db self.tags_view = parent self.hidden_categories = hidden_categories self.search_restriction = search_restriction self.row_map = [] data = self.get_node_tree(config['sort_tags_by']) self.root_item = TagTreeItem() for i, r in enumerate(self.row_map): if self.hidden_categories and self.categories[i] in self.hidden_categories: continue if self.db.field_metadata[r]['kind'] != 'user': tt = _('The lookup/search name is "{0}"').format(r) else: tt = '' c = TagTreeItem(parent = self.root_item, data = self.categories[i], category_icon = self.category_icon_map[r], tooltip = tt, category_key = r) for tag in data[r]: if r not in self.categories_with_ratings and not self.db.field_metadata[r]['is_custom'] and not (self.db.field_metadata[r]['kind'] == 'user'): tag.avg_rating = None TagTreeItem(parent = c, data = tag, icon_map = self.icon_state_map) def mimeTypes(self): return [ 'application/calibre+from_library'] def dropMimeData(self, md, action, row, column, parent): if not md.hasFormat('application/calibre+from_library') or action != Qt.CopyAction: return False idx = parent if idx.isValid(): node = self.data(idx, Qt.UserRole) if node.type == TagTreeItem.TAG: fm = self.db.metadata_for_field(node.tag.category) if (node.tag.category in ('tags', 'series', 'authors', 'rating', 'publisher') or fm['is_custom']) and fm['datatype'] in ('text', 'rating', 'series'): mime = 'application/calibre+from_library' ids = list(map(int, str(md.data(mime)).split())) self.handle_drop(node, ids) return True return False def handle_drop(self, on_node, ids): key = on_node.tag.category if key == 'authors' and len(ids) >= 5: if not confirm('<p>' + _('Changing the authors for several books can take a while. Are you sure?') + '</p>', 'tag_browser_drop_authors', self.parent()): return None elif len(ids) > 15: if not confirm('<p>' + _('Changing the metadata for that many books can take a while. Are you sure?') + '</p>', 'tag_browser_many_changes', self.parent()): return None fm = self.db.metadata_for_field(key) is_multiple = fm['is_multiple'] val = on_node.tag.name for id in ids: mi = self.db.get_metadata(id, index_is_id = True) set_authors = False mi.author_sort = None if key == 'authors': mi.authors = [ val] set_authors = True elif fm['datatype'] == 'rating': mi.set(key, len(val) * 2) elif fm['is_custom'] and fm['datatype'] == 'series': mi.set(key, val, extra = 1) elif is_multiple: new_val = mi.get(key, []) if val in new_val: continue new_val.append(val) mi.set(key, new_val) else: mi.set(key, val) self.db.set_metadata(id, mi, set_title = False, set_authors = set_authors, commit = False) self.db.commit() self.drag_drop_finished.emit(ids) def set_search_restriction(self, s): self.search_restriction = s def get_node_tree(self, sort): old_row_map = self.row_map[:] self.row_map = [] self.categories = [] self.db.field_metadata.remove_dynamic_categories() tb_cats = self.db.field_metadata for user_cat in sorted(self.db.prefs.get('user_categories', { }).keys()): cat_name = user_cat + ':' tb_cats.add_user_category(label = cat_name, name = user_cat) if len(saved_searches().names()): tb_cats.add_search_category(label = 'search', name = _('Searches')) if self.search_restriction: data = self.db.get_categories(sort = sort, icon_map = self.category_icon_map, ids = self.db.search('', return_matches = True)) else: data = self.db.get_categories(sort = sort, icon_map = self.category_icon_map) tb_categories = self.db.field_metadata for category in tb_categories: if category in data: self.row_map.append(category) self.categories.append(tb_categories[category]['name']) continue if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map): return None return data def refresh(self): data = self.get_node_tree(config['sort_tags_by']) if data is None: return False row_index = -1 for i, r in enumerate(self.row_map): if self.hidden_categories and self.categories[i] in self.hidden_categories: continue row_index += 1 category = self.root_item.children[row_index] 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(row_index, 0, QModelIndex()) 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_state_map) self.endInsertRows() continue None if r not in self.categories_with_ratings and not self.db.field_metadata[r]['is_custom'] and not (self.db.field_metadata[r]['kind'] == 'user') else [] return True 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 setData(self, index, value, role = Qt.EditRole): if not index.isValid(): return NONE path = self.path_for_index(self.parent(index)) val = unicode(value.toString()) if not val: error_dialog(self.tags_view, _('Item is blank'), _('An item cannot be set to nothing. Delete it instead.')).exec_() return False item = index.internalPointer() key = item.parent.category_key if key not in self.db.field_metadata: return False if key == 'search': if val in saved_searches().names(): error_dialog(self.tags_view, _('Duplicate search name'), _('The saved search name %s is already used.') % val).exec_() return False saved_searches().rename(unicode(item.data(role).toString()), val) item.tag.name = val self.tags_view.search_item_renamed.emit() elif key == 'series': self.db.rename_series(item.tag.id, val) elif key == 'publisher': self.db.rename_publisher(item.tag.id, val) elif key == 'tags': self.db.rename_tag(item.tag.id, val) elif key == 'authors': self.db.rename_author(item.tag.id, val) elif self.db.field_metadata[key]['is_custom']: self.db.rename_custom_item(item.tag.id, val, label = self.db.field_metadata[key]['label']) self.tags_view.tag_item_renamed.emit() item.tag.name = val self.refresh() if path: idx = self.index_for_path(path) if idx.isValid(): self.tags_view.setCurrentIndex(idx) self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter) return True def headerData(self, *args): return NONE def flags(self, index, *args): ans = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable if index.isValid(): node = self.data(index, Qt.UserRole) if node.type == TagTreeItem.TAG: fm = self.db.metadata_for_field(node.tag.category) if (node.tag.category in ('tags', 'series', 'authors', 'rating', 'publisher') or fm['is_custom']) and fm['datatype'] in ('text', 'rating', 'series'): ans |= Qt.ItemIsDropEnabled return ans def supportedDropActions(self): return Qt.CopyAction def path_for_index(self, index): ans = [] while index.isValid(): ans.append(index.row()) index = self.parent(index) ans.reverse() return ans def index_for_path(self, path): parent = QModelIndex() for i in path: parent = self.index(i, 0, parent) if not parent.isValid(): return QModelIndex() return parent 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() try: child_item = parent_item.children[row] except IndexError: return QModelIndex() 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 = getattr(child_item, 'parent', None) 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, except_ = None): update_list = [] for i in xrange(self.rowCount(QModelIndex())): category_index = self.index(i, 0, QModelIndex()) 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 is except_: self.dataChanged.emit(tag_index, tag_index) continue if tag.state != 0 or tag in update_list: tag.state = 0 update_list.append(tag) self.dataChanged.emit(tag_index, tag_index) continue def clear_state(self): self.reset_all_states() def toggle(self, index, exclusive): if not index.isValid(): return False item = index.internalPointer() if item.type == TagTreeItem.TAG: item.toggle() if exclusive: self.reset_all_states(except_ = item.tag) self.dataChanged.emit(index, index) return True return False def tokens(self): ans = [] tags_seen = set() row_index = -1 for i, key in enumerate(self.row_map): if self.hidden_categories and self.categories[i] in self.hidden_categories: continue row_index += 1 if key.endswith(':'): continue category_item = self.root_item.children[row_index] for tag_item in category_item.children: tag = tag_item.tag if tag.state > 0: prefix = None if tag.state == 2 else '' category = None if key != 'news' else 'tag' if tag.name and tag.name[0] == u'Γÿà': ans.append('%s%s:%s' % (prefix, category, len(tag.name))) elif category == 'tags': if tag.name in tags_seen: continue tags_seen.add(tag.name) ans.append('%s%s:"=%s"' % (prefix, category, tag.name)) continue return ans class TagBrowserMixin(object): def __init__(self, db): self.library_view.model().count_changed_signal.connect(self.tags_view.recount) self.tags_view.set_database(self.library_view.model().db, self.tag_match, self.sort_by) self.tags_view.tags_marked.connect(self.search.search_from_tags) self.tags_view.tags_marked.connect(self.saved_search.clear_to_help) self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) self.tags_view.user_category_edit.connect(self.do_user_categories_edit) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) self.tags_view.author_sort_edit.connect(self.do_author_sort_edit) self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) self.tags_view.search_item_renamed.connect(self.saved_searches_changed) self.tags_view.drag_drop_finished.connect(self.drag_drop_finished) (self.edit_categories.clicked.connect,)((lambda x: self.do_user_categories_edit())) def do_user_categories_edit(self, on_category = None): d = TagCategories(self, self.library_view.model().db, on_category) d.exec_() if d.result() == d.Accepted: self.tags_view.set_new_model() self.tags_view.recount() def do_tags_list_edit(self, tag, category): db = self.library_view.model().db if category == 'tags': result = db.get_tags_with_ids() compare = lambda x, y: cmp(x.lower(), y.lower()) elif category == 'series': result = db.get_series_with_ids() compare = lambda x, y: cmp(title_sort(x).lower(), title_sort(y).lower()) elif category == 'publisher': result = db.get_publishers_with_ids() compare = lambda x, y: cmp(x.lower(), y.lower()) else: cc_label = None if category in db.field_metadata: cc_label = db.field_metadata[category]['label'] result = self.db.get_custom_items_with_ids(label = cc_label) else: result = [] compare = lambda x, y: cmp(x.lower(), y.lower()) d = TagListEditor(self, tag_to_match = tag, data = result, compare = compare) d.exec_() if d.result() == d.Accepted: to_rename = d.to_rename to_delete = d.to_delete rename_func = None if category == 'tags': rename_func = db.rename_tag delete_func = db.delete_tag_using_id elif category == 'series': rename_func = db.rename_series delete_func = db.delete_series_using_id elif category == 'publisher': rename_func = db.rename_publisher delete_func = db.delete_publisher_using_id else: rename_func = partial(db.rename_custom_item, label = cc_label) delete_func = partial(db.delete_custom_item_using_id, label = cc_label) if rename_func: for item in to_delete: delete_func(item) for text in to_rename: for old_id in to_rename[text]: rename_func(old_id, new_name = unicode(text)) self.library_view.model().refresh() self.tags_view.set_new_model() self.tags_view.recount() self.saved_search.clear_to_help() self.search.clear_to_help() def do_tag_item_renamed(self): self.library_view.model().refresh() self.saved_search.clear_to_help() self.search.clear_to_help() def do_author_sort_edit(self, parent, id): db = self.library_view.model().db editor = EditAuthorsDialog(parent, db, id) d = editor.exec_() if d: for id, old_author, new_author, new_sort in editor.result: if old_author != new_author: id = db.rename_author(id, new_author) db.set_sort_field_for_author(id, unicode(new_sort)) self.library_view.model().refresh() self.tags_view.recount() def drag_drop_finished(self, ids): self.library_view.model().refresh_ids(ids) class TagBrowserWidget(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) self._layout = QVBoxLayout() self.setLayout(self._layout) parent.tags_view = TagsView(parent) self._layout.addWidget(parent.tags_view) parent.sort_by = QComboBox(parent) for x in (_('Sort by name'), _('Sort by popularity'), _('Sort by average rating')): parent.sort_by.addItem(x) parent.sort_by.setToolTip(_('Set the sort order for entries in the Tag Browser')) parent.sort_by.setStatusTip(parent.sort_by.toolTip()) parent.sort_by.setCurrentIndex(0) self._layout.addWidget(parent.sort_by) parent.tag_match = QComboBox(parent) for x in (_('Match any'), _('Match all')): parent.tag_match.addItem(x) parent.tag_match.setCurrentIndex(0) self._layout.addWidget(parent.tag_match) parent.tag_match.setToolTip(_('When selecting multiple entries in the Tag Browser match any or all of them')) parent.tag_match.setStatusTip(parent.tag_match.toolTip()) parent.edit_categories = QPushButton(_('Manage &user categories'), parent) self._layout.addWidget(parent.edit_categories) parent.edit_categories.setToolTip(_('Add your own categories to the Tag Browser')) parent.edit_categories.setStatusTip(parent.edit_categories.toolTip())