From 845b90e865ecc837b168f80591d8da59eee07e46 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 26 Jan 2013 15:25:17 +0100 Subject: [PATCH 1/6] Use coloring rules to change cells from text to icons --- src/calibre/gui2/library/models.py | 72 +++++++++- src/calibre/gui2/preferences/coloring.py | 175 +++++++++++++++++------ src/calibre/library/database2.py | 11 ++ 3 files changed, 207 insertions(+), 51 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index ba37e2c875..2cb14812fe 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -24,7 +24,7 @@ from calibre.db.search import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH from calibre.library.caches import (MetadataBackup, force_to_bool) from calibre.library.save_to_disk import find_plugboard from calibre import strftime, isbytestring -from calibre.constants import filesystem_encoding, DEBUG +from calibre.constants import filesystem_encoding, DEBUG, config_dir from calibre.gui2.library import DEFAULT_SORT from calibre.utils.localization import calibre_langcode_to_name from calibre.library.coloring import color_row_key @@ -70,6 +70,30 @@ class ColumnColor(object): pass +class ColumnIcon(object): + + def __init__(self): + self.mi = None + + def __call__(self, id_, key, fmt, kind, db, formatter, icon_cache): + dex = key+kind + if id_ in icon_cache and dex in icon_cache[id_]: + self.mi = None + return icon_cache[id_][dex] + try: + if self.mi is None: + self.mi = db.get_metadata(id_, index_is_id=True) + icon = formatter.safe_format(fmt, self.mi, '', self.mi) + if icon: + d = os.path.join(config_dir, 'cc_icons', icon) + if (os.path.exists(d)): + icon = QIcon(d) + icon_cache[id_][dex] = icon + self.mi = None + return icon + except: + pass + class BooksModel(QAbstractTableModel): # {{{ about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted') @@ -98,6 +122,7 @@ class BooksModel(QAbstractTableModel): # {{{ QAbstractTableModel.__init__(self, parent) self.db = None self.column_color = ColumnColor() + self.column_icon = ColumnIcon() self.book_on_device = None self.editable_cols = ['title', 'authors', 'rating', 'publisher', 'tags', 'series', 'timestamp', 'pubdate', @@ -110,6 +135,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.headers = {} self.alignment_map = {} self.color_cache = defaultdict(dict) + self.icon_cache = defaultdict(dict) self.color_row_fmt_cache = None self.buffer_size = buffer self.metadata_backup = None @@ -196,6 +222,7 @@ class BooksModel(QAbstractTableModel): # {{{ def refresh_ids(self, ids, current_row=-1): self.color_cache = defaultdict(dict) + self.icon_cache = defaultdict(dict) self.color_row_fmt_cache = None rows = self.db.refresh_ids(ids) if rows: @@ -203,6 +230,7 @@ class BooksModel(QAbstractTableModel): # {{{ def refresh_rows(self, rows, current_row=-1): self.color_cache = defaultdict(dict) + self.icon_cache = defaultdict(dict) self.color_row_fmt_cache = None for row in rows: if row == current_row: @@ -235,6 +263,7 @@ class BooksModel(QAbstractTableModel): # {{{ def count_changed(self, *args): self.color_cache = defaultdict(dict) + self.icon_cache = defaultdict(dict) self.color_row_fmt_cache = None self.count_changed_signal.emit(self.db.count()) @@ -367,6 +396,7 @@ class BooksModel(QAbstractTableModel): # {{{ def reset(self): self.color_cache = defaultdict(dict) + self.icon_cache = defaultdict(dict) self.color_row_fmt_cache = None QAbstractTableModel.reset(self) @@ -750,7 +780,18 @@ class BooksModel(QAbstractTableModel): # {{{ # we will get asked to display columns we don't know about. Must test for this. if col >= len(self.column_to_dc_map): return NONE - if role in (Qt.DisplayRole, Qt.EditRole, Qt.ToolTipRole): + if role == Qt.DisplayRole: + key = self.column_map[col] + id_ = self.id(index) + self.column_icon.mi = None + for kind, k, fmt in self.db.prefs['column_color_rules']: + if k == key and kind == 'icon_only': + ccicon = self.column_icon(id_, key, fmt, kind, self.db, + self.formatter, self.icon_cache) + if ccicon is not None: + return NONE + return self.column_to_dc_map[col](index.row()) + elif role in (Qt.EditRole, Qt.ToolTipRole): return self.column_to_dc_map[col](index.row()) elif role == Qt.BackgroundRole: if self.id(index) in self.ids_to_highlight_set: @@ -761,11 +802,12 @@ class BooksModel(QAbstractTableModel): # {{{ self.column_color.mi = None if self.color_row_fmt_cache is None: - self.color_row_fmt_cache = tuple(fmt for key, fmt in - self.db.prefs['column_color_rules'] if key == color_row_key) + self.color_row_fmt_cache = tuple(fmt for kind, key, fmt in + self.db.prefs['column_color_rules'] if kind == 'color' and + key == color_row_key) - for k, fmt in self.db.prefs['column_color_rules']: - if k == key: + for kind, k, fmt in self.db.prefs['column_color_rules']: + if k == key and kind == 'color': ccol = self.column_color(id_, key, fmt, self.db, self.formatter, self.color_cache, self.colors) if ccol is not None: @@ -796,7 +838,23 @@ class BooksModel(QAbstractTableModel): # {{{ return NONE elif role == Qt.DecorationRole: if self.column_to_dc_decorator_map[col] is not None: - return self.column_to_dc_decorator_map[index.column()](index.row()) + ccicon = self.column_to_dc_decorator_map[index.column()](index.row()) + if ccicon != NONE: + return ccicon + + key = self.column_map[col] + id_ = self.id(index) + self.column_icon.mi = None + need_icon_with_text = False + for kind, k, fmt in self.db.prefs['column_color_rules']: + if k == key and kind in ('icon', 'icon_only'): + need_icon_with_text = True + ccicon = self.column_icon(id_, key, fmt, kind, self.db, + self.formatter, self.icon_cache) + if ccicon is not None: + return ccicon + if need_icon_with_text: + return self.bool_blank_icon elif role == Qt.TextAlignmentRole: cname = self.column_map[index.column()] ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname, diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py index 72fe4fb12b..a2441f7d0f 100644 --- a/src/calibre/gui2/preferences/coloring.py +++ b/src/calibre/gui2/preferences/coloring.py @@ -7,15 +7,18 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import os + from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, QSize, QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon, QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox, QToolButton, QListView, QAbstractListModel, pyqtSignal, QSizePolicy, QSpacerItem, - QApplication) + QApplication, QHBoxLayout) -from calibre import prepare_string_for_xml +from calibre import prepare_string_for_xml, sanitize_file_name_unicode +from calibre.constants import config_dir from calibre.utils.icu import sort_key -from calibre.gui2 import error_dialog +from calibre.gui2 import error_dialog, choose_files, pixmap_to_data from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.metadata.single_download import RichTextDelegate from calibre.library.coloring import (Rule, conditionable_columns, @@ -257,52 +260,67 @@ class RuleEditor(QDialog): # {{{ self.l1 = l1 = QLabel(_('Create a coloring rule by' ' filling in the boxes below')) - l.addWidget(l1, 0, 0, 1, 5) + l.addWidget(l1, 0, 0, 1, 8) self.f1 = QFrame(self) self.f1.setFrameShape(QFrame.HLine) - l.addWidget(self.f1, 1, 0, 1, 5) + l.addWidget(self.f1, 1, 0, 1, 8) - self.l2 = l2 = QLabel(_('Set the color of the column:')) + self.l2 = l2 = QLabel(_('Set the')) l.addWidget(l2, 2, 0) - self.column_box = QComboBox(self) - l.addWidget(self.column_box, 2, 1) + self.kind_box = QComboBox(self) + self.kind_box.addItem(_('color'), 'color') + self.kind_box.addItem(_('icon'), 'icon') + self.kind_box.addItem(_('icon with no text'), 'icon_only') + l.addWidget(self.kind_box, 2, 1) - self.l3 = l3 = QLabel(_('to')) + self.l3 = l3 = QLabel(_('of the column:')) l.addWidget(l3, 2, 2) + self.column_box = QComboBox(self) + l.addWidget(self.column_box, 2, 3) + + self.l4 = l4 = QLabel(_('to')) + l.addWidget(l4, 2, 4) + self.color_box = QComboBox(self) self.color_label = QLabel('Sample text Sample text') self.color_label.setTextFormat(Qt.RichText) - l.addWidget(self.color_box, 2, 3) - l.addWidget(self.color_label, 2, 4) - l.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding), 2, 5) + l.addWidget(self.color_box, 2, 5) + l.addWidget(self.color_label, 2, 6) - self.l4 = l4 = QLabel( + self.filename_box = QLabel() + l.addWidget(self.filename_box, 2, 5) + self.filename_button = QPushButton(_('Choose icon')) + l.addWidget(self.filename_button, 2, 6) + + l.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding), 2, 7) + + self.l5 = l5 = QLabel( _('Only if the following conditions are all satisfied:')) - l.addWidget(l4, 3, 0, 1, 6) + l.addWidget(l5, 3, 0, 1, 7) self.scroll_area = sa = QScrollArea(self) sa.setMinimumHeight(300) sa.setMinimumWidth(950) sa.setWidgetResizable(True) - l.addWidget(sa, 4, 0, 1, 6) + l.addWidget(sa, 4, 0, 1, 8) self.add_button = b = QPushButton(QIcon(I('plus.png')), _('Add another condition')) - l.addWidget(b, 5, 0, 1, 6) + l.addWidget(b, 5, 0, 1, 8) b.clicked.connect(self.add_blank_condition) - self.l5 = l5 = QLabel(_('You can disable a condition by' + self.l6 = l6 = QLabel(_('You can disable a condition by' ' blanking all of its boxes')) - l.addWidget(l5, 6, 0, 1, 6) + l.addWidget(l6, 6, 0, 1, 8) self.bb = bb = QDialogButtonBox( QDialogButtonBox.Ok|QDialogButtonBox.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) - l.addWidget(bb, 7, 0, 1, 6) + l.addWidget(bb, 7, 0, 1, 8) self.conditions_widget = QWidget(self) sa.setWidget(self.conditions_widget) @@ -326,8 +344,12 @@ class RuleEditor(QDialog): # {{{ self.update_color_label() self.color_box.currentIndexChanged.connect(self.update_color_label) + self.kind_box.currentIndexChanged[int].connect(self.kind_index_changed) + self.filename_button.clicked.connect(self.filename_button_clicked) self.resize(self.sizeHint()) + self.icon_path = None + def update_color_label(self): pal = QApplication.palette() bg1 = unicode(pal.color(pal.Base).name()) @@ -338,13 +360,58 @@ class RuleEditor(QDialog): # {{{  {st}  '''.format(c=c, bg1=bg1, bg2=bg2, st=_('Sample Text'))) + def kind_index_changed(self, dex): + if dex != 0: + self.color_label.setVisible(False) + self.color_box.setVisible(False) + self.filename_box.setVisible(True) + self.filename_button.setVisible(True) + else: + self.color_label.setVisible(True) + self.color_box.setVisible(True) + self.filename_box.setVisible(False) + self.filename_button.setVisible(False) + + def filename_button_clicked(self): + try: + path = choose_files(self, 'choose_category_icon', + _('Select Icon'), filters=[ + ('Images', ['png', 'gif', 'jpg', 'jpeg'])], + all_files=False, select_only_single_file=True) + if path: + self.icon_path = path[0] + self.filename_box.setText( + sanitize_file_name_unicode( + os.path.splitext( + os.path.basename(self.icon_path))[0]+'.png')) + self.filename_box.adjustSize() + else: + self.icon_path = '' + except: + import traceback + traceback.print_exc() + return def add_blank_condition(self): c = ConditionEditor(self.fm, parent=self.conditions_widget) self.conditions.append(c) self.conditions_widget.layout().addWidget(c) - def apply_rule(self, col, rule): + def apply_rule(self, kind, col, rule): + if kind == 'color': + self.kind_box.setCurrentIndex(0) + self.filename_box.setVisible(False) + self.filename_button.setVisible(False) + if rule.color: + idx = self.color_box.findText(rule.color) + if idx >= 0: + self.color_box.setCurrentIndex(idx) + else: + self.kind_box.setCurrentIndex(1 if kind == 'icon' else 2) + self.color_box.setVisible(False) + self.color_label.setVisible(False) + self.filename_box.setText(rule.color) + for i in range(self.column_box.count()): c = unicode(self.column_box.itemData(i).toString()) if col == c: @@ -354,6 +421,8 @@ class RuleEditor(QDialog): # {{{ idx = self.color_box.findText(rule.color) if idx >= 0: self.color_box.setCurrentIndex(idx) + self.filename_box.setText(rule.color) + for c in rule.conditions: ce = ConditionEditor(self.fm, parent=self.conditions_widget) self.conditions.append(ce) @@ -366,6 +435,20 @@ class RuleEditor(QDialog): # {{{ def accept(self): + if self.kind_box.currentIndex() != 0: + path = self.icon_path + if path: + try: + fname = unicode(self.filename_box.text()) + p = QIcon(path).pixmap(QSize(128, 128)) + d = os.path.join(config_dir, 'cc_icons') + if not os.path.exists(d): + os.makedirs(d) + with open(os.path.join(d, fname), 'wb') as f: + f.write(pixmap_to_data(p, format='PNG')) + except: + import traceback + traceback.print_exc() if self.validate(): QDialog.accept(self) @@ -393,7 +476,11 @@ class RuleEditor(QDialog): # {{{ @property def rule(self): r = Rule(self.fm) - r.color = unicode(self.color_box.currentText()) + kind = unicode(self.kind_box.itemData(self.kind_box.currentIndex()).toString()) + if kind != 'color': + r.color = unicode(self.filename_box.text()) + else: + r.color = unicode(self.color_box.currentText()) idx = self.column_box.currentIndex() col = unicode(self.column_box.itemData(idx).toString()) for c in self.conditions: @@ -401,7 +488,7 @@ class RuleEditor(QDialog): # {{{ if condition is not None: r.add_condition(*condition) - return col, r + return kind, col, r # }}} class RulesModel(QAbstractListModel): # {{{ @@ -412,13 +499,13 @@ class RulesModel(QAbstractListModel): # {{{ self.fm = fm rules = list(prefs['column_color_rules']) self.rules = [] - for col, template in rules: + for kind, col, template in rules: if col not in self.fm: continue try: rule = rule_from_template(self.fm, template) except: rule = template - self.rules.append((col, rule)) + self.rules.append((kind, col, rule)) def rowCount(self, *args): return len(self.rules) @@ -426,7 +513,7 @@ class RulesModel(QAbstractListModel): # {{{ def data(self, index, role): row = index.row() try: - col, rule = self.rules[row] + kind, col, rule = self.rules[row] except: return None if role == Qt.DisplayRole: @@ -434,17 +521,17 @@ class RulesModel(QAbstractListModel): # {{{ col = all_columns_string else: col = self.fm[col]['name'] - return self.rule_to_html(col, rule) + return self.rule_to_html(kind, col, rule) if role == Qt.UserRole: - return (col, rule) + return (kind, col, rule) - def add_rule(self, col, rule): - self.rules.append((col, rule)) + def add_rule(self, kind, col, rule): + self.rules.append((kind, col, rule)) self.reset() return self.index(len(self.rules)-1) - def replace_rule(self, index, col, r): - self.rules[index.row()] = (col, r) + def replace_rule(self, index, kind, col, r): + self.rules[index.row()] = (kind, col, r) self.dataChanged.emit(index, index) def remove_rule(self, index): @@ -453,11 +540,11 @@ class RulesModel(QAbstractListModel): # {{{ def commit(self, prefs): rules = [] - for col, r in self.rules: + for kind, col, r in self.rules: if isinstance(r, Rule): r = r.template if r is not None: - rules.append((col, r)) + rules.append((kind, col, r)) prefs['column_color_rules'] = rules def move(self, idx, delta): @@ -475,7 +562,7 @@ class RulesModel(QAbstractListModel): # {{{ self.rules = [] self.reset() - def rule_to_html(self, col, rule): + def rule_to_html(self, kind, col, rule): if not isinstance(rule, Rule): return _('''

Advanced Rule for column %(col)s: @@ -483,10 +570,10 @@ class RulesModel(QAbstractListModel): # {{{ ''')%dict(col=col, rule=prepare_string_for_xml(rule)) conditions = [self.condition_to_html(c) for c in rule.conditions] return _('''\ -

Set the color of %(col)s to %(color)s if the following +

Set the %(kind)s of %(col)s to %(color)s if the following conditions are met:

- ''') % dict(col=col, color=rule.color, rule=''.join(conditions)) + ''') % dict(kind=kind, col=col, color=rule.color, rule=''.join(conditions)) def condition_to_html(self, condition): c, a, v = condition @@ -567,9 +654,9 @@ class EditRules(QWidget): # {{{ def _add_rule(self, dlg): if dlg.exec_() == dlg.Accepted: - col, r = dlg.rule - if r and col: - idx = self.model.add_rule(col, r) + kind, col, r = dlg.rule + if kind and r and col: + idx = self.model.add_rule(kind, col, r) self.rules_view.scrollTo(idx) self.changed.emit() @@ -584,18 +671,18 @@ class EditRules(QWidget): # {{{ def edit_rule(self, index): try: - col, rule = self.model.data(index, Qt.UserRole) + kind, col, rule = self.model.data(index, Qt.UserRole) except: return if isinstance(rule, Rule): d = RuleEditor(self.model.fm) - d.apply_rule(col, rule) + d.apply_rule(kind, col, rule) else: d = TemplateDialog(self, rule, mi=self.mi, fm=self.fm, color_field=col) if d.exec_() == d.Accepted: - col, r = d.rule - if r is not None and col: - self.model.replace_rule(index, col, r) + kind, col, r = d.rule + if kind and r is not None and col: + self.model.replace_rule(index, kind, col, r) self.rules_view.scrollTo(index) self.changed.emit() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index d93833ae9c..8ecc560122 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -253,6 +253,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if old_rules: self.prefs['column_color_rules'] += old_rules + new_rules = [] + must_save_new_rules = False + for tup in self.prefs['column_color_rules']: + if len(tup) == 2: + must_save_new_rules = True; + new_rules.append( ('color', tup[0], tup[1]) ) + else: + new_rules.append(tup) + if must_save_new_rules: + self.prefs['column_color_rules'] = new_rules + # Migrate saved search and user categories to db preference scheme def migrate_preference(key, default): oldval = prefs[key] From aa3f6e1211a2f2e92a5e68d61bb77bf864ad6eb3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 26 Jan 2013 15:58:25 +0100 Subject: [PATCH 2/6] Fix caching of icons and translate operations back to plain text. --- src/calibre/gui2/library/models.py | 10 +++++++--- src/calibre/gui2/preferences/coloring.py | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 2cb14812fe..219c0be9ff 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -786,10 +786,11 @@ class BooksModel(QAbstractTableModel): # {{{ self.column_icon.mi = None for kind, k, fmt in self.db.prefs['column_color_rules']: if k == key and kind == 'icon_only': - ccicon = self.column_icon(id_, key, fmt, kind, self.db, + ccicon = self.column_icon(id_, key, fmt, 'icon_only', self.db, self.formatter, self.icon_cache) if ccicon is not None: return NONE + self.icon_cache[id_][key+'icon_only'] = None return self.column_to_dc_map[col](index.row()) elif role in (Qt.EditRole, Qt.ToolTipRole): return self.column_to_dc_map[col](index.row()) @@ -848,13 +849,16 @@ class BooksModel(QAbstractTableModel): # {{{ need_icon_with_text = False for kind, k, fmt in self.db.prefs['column_color_rules']: if k == key and kind in ('icon', 'icon_only'): - need_icon_with_text = True - ccicon = self.column_icon(id_, key, fmt, kind, self.db, + if kind == 'icon': + need_icon_with_text = True + ccicon = self.column_icon(id_, key, fmt, 'icon', self.db, self.formatter, self.icon_cache) if ccicon is not None: return ccicon if need_icon_with_text: + self.icon_cache[id_][key+'icon'] = self.bool_blank_icon return self.bool_blank_icon + self.icon_cache[id_][key+'icon'] = None elif role == Qt.TextAlignmentRole: cname = self.column_map[index.column()] ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname, diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py index a2441f7d0f..1ee9b15e10 100644 --- a/src/calibre/gui2/preferences/coloring.py +++ b/src/calibre/gui2/preferences/coloring.py @@ -28,6 +28,10 @@ from calibre.utils.icu import lower all_columns_string = _('All Columns') +rule_kinds = [(_('color'), 'color'), + (_('icon with text'), 'icon'), + (_('icon with no text'), 'icon_only') ] + class ConditionEditor(QWidget): # {{{ ACTION_MAP = { @@ -270,9 +274,8 @@ class RuleEditor(QDialog): # {{{ l.addWidget(l2, 2, 0) self.kind_box = QComboBox(self) - self.kind_box.addItem(_('color'), 'color') - self.kind_box.addItem(_('icon'), 'icon') - self.kind_box.addItem(_('icon with no text'), 'icon_only') + for tt, t in rule_kinds: + self.kind_box.addItem(tt, t) l.addWidget(self.kind_box, 2, 1) self.l3 = l3 = QLabel(_('of the column:')) @@ -569,11 +572,18 @@ class RulesModel(QAbstractListModel): # {{{
%(rule)s
''')%dict(col=col, rule=prepare_string_for_xml(rule)) conditions = [self.condition_to_html(c) for c in rule.conditions] + + trans_kind = 'not found' + for tt, t in rule_kinds: + if kind == t: + trans_kind = tt + break + return _('''\

Set the %(kind)s of %(col)s to %(color)s if the following conditions are met:

- ''') % dict(kind=kind, col=col, color=rule.color, rule=''.join(conditions)) + ''') % dict(kind=trans_kind, col=col, color=rule.color, rule=''.join(conditions)) def condition_to_html(self, condition): c, a, v = condition From 06ca24abd18a2d192e080bb33e4ff2c20291e5d7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 27 Jan 2013 09:24:01 +0100 Subject: [PATCH 3/6] Make the "Was" column in the "manage tags" editor read-only --- src/calibre/gui2/dialogs/tag_list_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 298641a9df..4397ad7d28 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -136,7 +136,7 @@ class TagListEditor(QDialog, Ui_TagListEditor): item.setFlags (item.flags() & ~Qt.ItemIsSelectable) self.table.setItem(row, 1, item) item = QTableWidgetItem('') - item.setFlags (item.flags() & ~Qt.ItemIsSelectable) + item.setFlags (item.flags() & ~(Qt.ItemIsSelectable|Qt.ItemIsEditable)) self.table.setItem(row, 2, item) # Scroll to the selected item if there is one From 316a9539d8098cdaf0639cc701ecf2d824a22a1c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 27 Jan 2013 09:48:49 +0100 Subject: [PATCH 4/6] Add the full item name to the tool tip of a leaf item displayed in the tag browser --- src/calibre/library/database2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 8ecc560122..a0fe04c801 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1892,7 +1892,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # icon_map is not None if get_categories is to store an icon and # possibly a tooltip in the tag structure. icon = None - tooltip = '(' + category + ')' label = tb_cats.key_to_label(category) if icon_map: if not tb_cats.is_custom_field(category): @@ -1946,7 +1945,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): is_editable = category not in ['news', 'rating', 'languages'] categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id, avg=avgr(r), sort=r.s, icon=icon, - tooltip=tooltip, category=category, + tooltip=u'({0}:{1})'.format(category, + formatter(r.n)), + category=category, id_set=r.id_set, is_editable=is_editable, use_sort_as_name=use_sort_as_name) for r in items] From e758b9fe426494ea5bb86bc58326178852361a89 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 27 Jan 2013 22:36:41 +0100 Subject: [PATCH 5/6] Put column icons into separate rules and a separate preference. --- src/calibre/gui2/library/models.py | 65 +++++---- src/calibre/gui2/preferences/coloring.py | 169 ++++++++++++---------- src/calibre/gui2/preferences/look_feel.py | 11 +- src/calibre/library/database2.py | 12 +- 4 files changed, 138 insertions(+), 119 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 219c0be9ff..a32c3f1cbc 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -781,16 +781,18 @@ class BooksModel(QAbstractTableModel): # {{{ if col >= len(self.column_to_dc_map): return NONE if role == Qt.DisplayRole: - key = self.column_map[col] - id_ = self.id(index) - self.column_icon.mi = None - for kind, k, fmt in self.db.prefs['column_color_rules']: - if k == key and kind == 'icon_only': - ccicon = self.column_icon(id_, key, fmt, 'icon_only', self.db, - self.formatter, self.icon_cache) - if ccicon is not None: - return NONE - self.icon_cache[id_][key+'icon_only'] = None + rules = self.db.prefs['column_icon_rules'] + if rules: + key = self.column_map[col] + id_ = self.id(index) + self.column_icon.mi = None + for kind, k, fmt in rules: + if k == key and kind == 'icon_only': + ccicon = self.column_icon(id_, key, fmt, 'icon_only', self.db, + self.formatter, self.icon_cache) + if ccicon is not None: + return NONE + self.icon_cache[id_][key+'icon_only'] = None return self.column_to_dc_map[col](index.row()) elif role in (Qt.EditRole, Qt.ToolTipRole): return self.column_to_dc_map[col](index.row()) @@ -803,12 +805,11 @@ class BooksModel(QAbstractTableModel): # {{{ self.column_color.mi = None if self.color_row_fmt_cache is None: - self.color_row_fmt_cache = tuple(fmt for kind, key, fmt in - self.db.prefs['column_color_rules'] if kind == 'color' and - key == color_row_key) + self.color_row_fmt_cache = tuple(fmt for key, fmt in + self.db.prefs['column_color_rules'] if key == color_row_key) - for kind, k, fmt in self.db.prefs['column_color_rules']: - if k == key and kind == 'color': + for k, fmt in self.db.prefs['column_color_rules']: + if k == key: ccol = self.column_color(id_, key, fmt, self.db, self.formatter, self.color_cache, self.colors) if ccol is not None: @@ -843,22 +844,24 @@ class BooksModel(QAbstractTableModel): # {{{ if ccicon != NONE: return ccicon - key = self.column_map[col] - id_ = self.id(index) - self.column_icon.mi = None - need_icon_with_text = False - for kind, k, fmt in self.db.prefs['column_color_rules']: - if k == key and kind in ('icon', 'icon_only'): - if kind == 'icon': - need_icon_with_text = True - ccicon = self.column_icon(id_, key, fmt, 'icon', self.db, - self.formatter, self.icon_cache) - if ccicon is not None: - return ccicon - if need_icon_with_text: - self.icon_cache[id_][key+'icon'] = self.bool_blank_icon - return self.bool_blank_icon - self.icon_cache[id_][key+'icon'] = None + rules = self.db.prefs['column_icon_rules'] + if rules: + key = self.column_map[col] + id_ = self.id(index) + self.column_icon.mi = None + need_icon_with_text = False + for kind, k, fmt in rules: + if k == key and kind in ('icon', 'icon_only'): + if kind == 'icon': + need_icon_with_text = True + ccicon = self.column_icon(id_, key, fmt, 'icon', self.db, + self.formatter, self.icon_cache) + if ccicon is not None: + return ccicon + if need_icon_with_text: + self.icon_cache[id_][key+'icon'] = self.bool_blank_icon + return self.bool_blank_icon + self.icon_cache[id_][key+'icon'] = None elif role == Qt.TextAlignmentRole: cname = self.column_map[index.column()] ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname, diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py index 1ee9b15e10..19a113a78a 100644 --- a/src/calibre/gui2/preferences/coloring.py +++ b/src/calibre/gui2/preferences/coloring.py @@ -13,7 +13,7 @@ from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, QSize, QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon, QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox, QToolButton, QListView, QAbstractListModel, pyqtSignal, QSizePolicy, QSpacerItem, - QApplication, QHBoxLayout) + QApplication) from calibre import prepare_string_for_xml, sanitize_file_name_unicode from calibre.constants import config_dir @@ -28,9 +28,8 @@ from calibre.utils.icu import lower all_columns_string = _('All Columns') -rule_kinds = [(_('color'), 'color'), - (_('icon with text'), 'icon'), - (_('icon with no text'), 'icon_only') ] +icon_rule_kinds = [(_('icon with text'), 'icon'), + (_('icon with no text'), 'icon_only') ] class ConditionEditor(QWidget): # {{{ @@ -214,8 +213,6 @@ class ConditionEditor(QWidget): # {{{ col = self.current_col if not col: return - m = self.fm[col] - dt = m['datatype'] action = self.current_action if not action: return @@ -252,7 +249,7 @@ class ConditionEditor(QWidget): # {{{ class RuleEditor(QDialog): # {{{ - def __init__(self, fm, parent=None): + def __init__(self, fm, pref_name, parent=None): QDialog.__init__(self, parent) self.fm = fm @@ -273,10 +270,15 @@ class RuleEditor(QDialog): # {{{ self.l2 = l2 = QLabel(_('Set the')) l.addWidget(l2, 2, 0) - self.kind_box = QComboBox(self) - for tt, t in rule_kinds: - self.kind_box.addItem(tt, t) - l.addWidget(self.kind_box, 2, 1) + if pref_name == 'column_color_rules': + self.rule_kind = 'color' + l.addWidget(QLabel(_('color'))) + else: + self.rule_kind = 'icon' + self.kind_box = QComboBox(self) + for tt, t in icon_rule_kinds: + self.kind_box.addItem(tt, t) + l.addWidget(self.kind_box, 2, 1) self.l3 = l3 = QLabel(_('of the column:')) l.addWidget(l3, 2, 2) @@ -287,16 +289,17 @@ class RuleEditor(QDialog): # {{{ self.l4 = l4 = QLabel(_('to')) l.addWidget(l4, 2, 4) - self.color_box = QComboBox(self) - self.color_label = QLabel('Sample text Sample text') - self.color_label.setTextFormat(Qt.RichText) - l.addWidget(self.color_box, 2, 5) - l.addWidget(self.color_label, 2, 6) - - self.filename_box = QLabel() - l.addWidget(self.filename_box, 2, 5) - self.filename_button = QPushButton(_('Choose icon')) - l.addWidget(self.filename_button, 2, 6) + if self.rule_kind == 'color': + self.color_box = QComboBox(self) + self.color_label = QLabel('Sample text Sample text') + self.color_label.setTextFormat(Qt.RichText) + l.addWidget(self.color_box, 2, 5) + l.addWidget(self.color_label, 2, 6) + else: + self.filename_box = QLabel() + l.addWidget(self.filename_box, 2, 5) + self.filename_button = QPushButton(_('Choose icon')) + l.addWidget(self.filename_button, 2, 6) l.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding), 2, 7) @@ -331,24 +334,28 @@ class RuleEditor(QDialog): # {{{ self.conditions_widget.layout().setAlignment(Qt.AlignTop) self.conditions = [] - for b in (self.column_box, self.color_box): - b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon) - b.setMinimumContentsLength(15) + if self.rule_kind == 'color': + for b in (self.column_box, self.color_box): + b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon) + b.setMinimumContentsLength(15) for key in sorted(displayable_columns(fm), key=lambda(k): sort_key(fm[k]['name']) if k != color_row_key else 0): + if key == color_row_key and self.rule_kind != 'color': + continue name = all_columns_string if key == color_row_key else fm[key]['name'] if name: self.column_box.addItem(name, key) self.column_box.setCurrentIndex(0) - self.color_box.addItems(QColor.colorNames()) - self.color_box.setCurrentIndex(0) + if self.rule_kind == 'color': + self.color_box.addItems(QColor.colorNames()) + self.color_box.setCurrentIndex(0) + self.update_color_label() + self.color_box.currentIndexChanged.connect(self.update_color_label) + else: + self.filename_button.clicked.connect(self.filename_button_clicked) - self.update_color_label() - self.color_box.currentIndexChanged.connect(self.update_color_label) - self.kind_box.currentIndexChanged[int].connect(self.kind_index_changed) - self.filename_button.clicked.connect(self.filename_button_clicked) self.resize(self.sizeHint()) self.icon_path = None @@ -363,18 +370,6 @@ class RuleEditor(QDialog): # {{{  {st}  '''.format(c=c, bg1=bg1, bg2=bg2, st=_('Sample Text'))) - def kind_index_changed(self, dex): - if dex != 0: - self.color_label.setVisible(False) - self.color_box.setVisible(False) - self.filename_box.setVisible(True) - self.filename_button.setVisible(True) - else: - self.color_label.setVisible(True) - self.color_box.setVisible(True) - self.filename_box.setVisible(False) - self.filename_button.setVisible(False) - def filename_button_clicked(self): try: path = choose_files(self, 'choose_category_icon', @@ -402,17 +397,12 @@ class RuleEditor(QDialog): # {{{ def apply_rule(self, kind, col, rule): if kind == 'color': - self.kind_box.setCurrentIndex(0) - self.filename_box.setVisible(False) - self.filename_button.setVisible(False) if rule.color: idx = self.color_box.findText(rule.color) if idx >= 0: self.color_box.setCurrentIndex(idx) else: - self.kind_box.setCurrentIndex(1 if kind == 'icon' else 2) - self.color_box.setVisible(False) - self.color_label.setVisible(False) + self.kind_box.setCurrentIndex(0 if kind == 'icon' else 1) self.filename_box.setText(rule.color) for i in range(self.column_box.count()): @@ -421,10 +411,12 @@ class RuleEditor(QDialog): # {{{ self.column_box.setCurrentIndex(i) break if rule.color: - idx = self.color_box.findText(rule.color) - if idx >= 0: - self.color_box.setCurrentIndex(idx) - self.filename_box.setText(rule.color) + if kind == 'color': + idx = self.color_box.findText(rule.color) + if idx >= 0: + self.color_box.setCurrentIndex(idx) + else: + self.filename_box.setText(rule.color) for c in rule.conditions: ce = ConditionEditor(self.fm, parent=self.conditions_widget) @@ -438,7 +430,7 @@ class RuleEditor(QDialog): # {{{ def accept(self): - if self.kind_box.currentIndex() != 0: + if self.rule_kind != 'color': path = self.icon_path if path: try: @@ -479,8 +471,7 @@ class RuleEditor(QDialog): # {{{ @property def rule(self): r = Rule(self.fm) - kind = unicode(self.kind_box.itemData(self.kind_box.currentIndex()).toString()) - if kind != 'color': + if self.rule_kind != 'color': r.color = unicode(self.filename_box.text()) else: r.color = unicode(self.color_box.currentText()) @@ -490,25 +481,44 @@ class RuleEditor(QDialog): # {{{ condition = c.condition if condition is not None: r.add_condition(*condition) + if self.rule_kind == 'icon': + kind = unicode(self.kind_box.itemData( + self.kind_box.currentIndex()).toString()) + else: + kind = 'color' return kind, col, r # }}} class RulesModel(QAbstractListModel): # {{{ - def __init__(self, prefs, fm, parent=None): + def __init__(self, prefs, fm, pref_name, parent=None): QAbstractListModel.__init__(self, parent) self.fm = fm - rules = list(prefs['column_color_rules']) - self.rules = [] - for kind, col, template in rules: - if col not in self.fm: continue - try: - rule = rule_from_template(self.fm, template) - except: - rule = template - self.rules.append((kind, col, rule)) + self.pref_name = pref_name + if pref_name == 'column_color_rules': + self.rule_kind = 'color' + rules = list(prefs[pref_name]) + self.rules = [] + for col, template in rules: + if col not in self.fm and col != color_row_key: continue + try: + rule = rule_from_template(self.fm, template) + except: + rule = template + self.rules.append(('color', col, rule)) + else: + self.rule_kind = 'icon' + rules = list(prefs[pref_name]) + self.rules = [] + for kind, col, template in rules: + if col not in self.fm and col != color_row_key: continue + try: + rule = rule_from_template(self.fm, template) + except: + rule = template + self.rules.append((kind, col, rule)) def rowCount(self, *args): return len(self.rules) @@ -547,8 +557,11 @@ class RulesModel(QAbstractListModel): # {{{ if isinstance(r, Rule): r = r.template if r is not None: - rules.append((kind, col, r)) - prefs['column_color_rules'] = rules + if kind == 'color': + rules.append((col, r)) + else: + rules.append((kind, col, r)) + prefs[self.pref_name] = rules def move(self, idx, delta): row = idx.row() + delta @@ -574,10 +587,13 @@ class RulesModel(QAbstractListModel): # {{{ conditions = [self.condition_to_html(c) for c in rule.conditions] trans_kind = 'not found' - for tt, t in rule_kinds: - if kind == t: - trans_kind = tt - break + if kind == 'color': + trans_kind = _('color') + else: + for tt, t in icon_rule_kinds: + if kind == t: + trans_kind = tt + break return _('''\

Set the %(kind)s of %(col)s to %(color)s if the following @@ -656,8 +672,9 @@ class EditRules(QWidget): # {{{ b.clicked.connect(self.add_advanced) l.addWidget(b, 3, 0, 1, 2) - def initialize(self, fm, prefs, mi): - self.model = RulesModel(prefs, fm) + def initialize(self, fm, prefs, mi, pref_name): + self.pref_name = pref_name + self.model = RulesModel(prefs, fm, self.pref_name) self.rules_view.setModel(self.model) self.fm = fm self.mi = mi @@ -671,7 +688,7 @@ class EditRules(QWidget): # {{{ self.changed.emit() def add_rule(self): - d = RuleEditor(self.model.fm) + d = RuleEditor(self.model.fm, self.pref_name) d.add_blank_condition() self._add_rule(d) @@ -685,7 +702,7 @@ class EditRules(QWidget): # {{{ except: return if isinstance(rule, Rule): - d = RuleEditor(self.model.fm) + d = RuleEditor(self.model.fm, self.pref_name) d.apply_rule(kind, col, rule) else: d = TemplateDialog(self, rule, mi=self.mi, fm=self.fm, color_field=col) @@ -748,7 +765,7 @@ if __name__ == '__main__': db = db() if True: - d = RuleEditor(db.field_metadata) + d = RuleEditor(db.field_metadata, 'column_color_rules') d.add_blank_condition() d.exec_() diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 754ac6c6ce..52ca95cdc5 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -181,6 +181,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.edit_rules.changed.connect(self.changed_signal) self.tabWidget.addTab(self.edit_rules, QIcon(I('format-fill-color.png')), _('Column coloring')) + + self.icon_rules = EditRules(self.tabWidget) + self.icon_rules.changed.connect(self.changed_signal) + self.tabWidget.addTab(self.icon_rules, + QIcon(I('format-fill-color.png')), _('Column icons')) + self.tabWidget.setCurrentIndex(0) keys = [QKeySequence('F11', QKeySequence.PortableText), QKeySequence( 'Ctrl+Shift+F', QKeySequence.PortableText)] @@ -203,7 +209,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): mi = db.get_metadata(idx, index_is_id=False) except: mi=None - self.edit_rules.initialize(db.field_metadata, db.prefs, mi) + self.edit_rules.initialize(db.field_metadata, db.prefs, mi, 'column_color_rules') + self.icon_rules.initialize(db.field_metadata, db.prefs, mi, 'column_icon_rules') def restore_defaults(self): ConfigWidgetBase.restore_defaults(self) @@ -214,6 +221,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.update_font_display() self.display_model.restore_defaults() self.edit_rules.clear() + self.icon_rules.clear() self.changed_signal.emit() def build_font_obj(self): @@ -273,6 +281,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): rr = True self.display_model.commit() self.edit_rules.commit(self.gui.current_db.prefs) + self.icon_rules.commit(self.gui.current_db.prefs) return rr def refresh_gui(self, gui): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index a0fe04c801..1397d0ca51 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -211,6 +211,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): defs['gui_restriction'] = defs['cs_restriction'] = '' defs['categories_using_hierarchy'] = [] defs['column_color_rules'] = [] + defs['column_icon_rules'] = [] defs['grouped_search_make_user_categories'] = [] defs['similar_authors_search_key'] = 'authors' defs['similar_authors_match_kind'] = 'match_any' @@ -253,17 +254,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if old_rules: self.prefs['column_color_rules'] += old_rules - new_rules = [] - must_save_new_rules = False - for tup in self.prefs['column_color_rules']: - if len(tup) == 2: - must_save_new_rules = True; - new_rules.append( ('color', tup[0], tup[1]) ) - else: - new_rules.append(tup) - if must_save_new_rules: - self.prefs['column_color_rules'] = new_rules - # Migrate saved search and user categories to db preference scheme def migrate_preference(key, default): oldval = prefs[key] From 31f7b12b028a89b68291517b056a78b54933a345 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 28 Jan 2013 10:08:03 +0100 Subject: [PATCH 6/6] Finish column icon stuff --- resources/images/icon_choose.png | Bin 0 -> 15025 bytes src/calibre/gui2/library/models.py | 84 ++++++++++++---------- src/calibre/gui2/preferences/coloring.py | 39 ++++++---- src/calibre/gui2/preferences/look_feel.py | 2 +- 4 files changed, 75 insertions(+), 50 deletions(-) create mode 100644 resources/images/icon_choose.png diff --git a/resources/images/icon_choose.png b/resources/images/icon_choose.png new file mode 100644 index 0000000000000000000000000000000000000000..5a34aca1dfcffec7cca6b9f06eef5e1107110be6 GIT binary patch literal 15025 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 waCEk?wep2l;NWKE'+_( - 'You can control the color of columns in the' - ' book list by creating "rules" that tell calibre' - ' what color to use. Click the Add Rule button below' - ' to get started.

You can change an existing rule by double' - ' clicking it.')) + self.l1 = l1 = QLabel('') l1.setWordWrap(True) l.addWidget(l1, 0, 0, 1, 2) @@ -678,6 +678,21 @@ class EditRules(QWidget): # {{{ self.rules_view.setModel(self.model) self.fm = fm self.mi = mi + if pref_name == 'column_color_rules': + self.l1.setText('

'+_( + 'You can control the color of columns in the' + ' book list by creating "rules" that tell calibre' + ' what color to use. Click the Add Rule button below' + ' to get started.

You can change an existing rule by' + ' double clicking it.')) + else: + self.l1.setText('

'+_( + 'You can add icons to columns in the' + ' book list by creating "rules" that tell calibre' + ' what icon to use. Click the Add Rule button below' + ' to get started.

You can change an existing rule by' + ' double clicking it.')) + self.add_advanced_button.setVisible(False) def _add_rule(self, dlg): if dlg.exec_() == dlg.Accepted: diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 52ca95cdc5..b3423e5a8c 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -185,7 +185,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.icon_rules = EditRules(self.tabWidget) self.icon_rules.changed.connect(self.changed_signal) self.tabWidget.addTab(self.icon_rules, - QIcon(I('format-fill-color.png')), _('Column icons')) + QIcon(I('icon_choose.png')), _('Column icons')) self.tabWidget.setCurrentIndex(0) keys = [QKeySequence('F11', QKeySequence.PortableText), QKeySequence(