diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 3db6e37eb0..ab185d8a6a 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -7,7 +7,7 @@ import json, os, traceback from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QSyntaxHighlighter, QFont, QRegExp, QApplication, QTextCharFormat, QColor, QCursor, - QIcon, QSize) + QIcon, QSize, QVariant) from calibre import sanitize_file_name_unicode from calibre.constants import config_dir @@ -19,7 +19,6 @@ from calibre.ebooks.metadata.book.formatter import SafeFormat from calibre.library.coloring import (displayable_columns, color_row_key) from calibre.gui2 import error_dialog, choose_files, pixmap_to_data - class ParenPosition: def __init__(self, block, pos, paren): @@ -247,9 +246,15 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.icon_file_names.append(icon_file) self.icon_file_names.sort(key=sort_key) self.update_filename_box() - self.icon_with_text.setChecked(True) - if icon_rule_kind == 'icon_only': - self.icon_without_text.setChecked(True) + + dex = 0 + from calibre.gui2.preferences.coloring import icon_rule_kinds + for i,tup in enumerate(icon_rule_kinds): + txt,val = tup + self.icon_kind.addItem(txt, userData=QVariant(val)) + if val == icon_rule_kind: + dex = i + self.icon_kind.setCurrentIndex(dex) self.icon_field.setCurrentIndex(self.icon_field.findData(icon_field_key)) if mi: @@ -410,7 +415,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.rule = (unicode(self.colored_field.itemData( self.colored_field.currentIndex()).toString()), txt) elif self.iconing: - rt = 'icon' if self.icon_with_text.isChecked() else 'icon_only' + rt = unicode(self.icon_kind.itemData(self.icon_kind.currentIndex()).toString()) self.rule = (rt, unicode(self.icon_field.itemData( self.icon_field.currentIndex()).toString()), diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui index db9cc16dd9..11dafa4627 100644 --- a/src/calibre/gui2/dialogs/template_dialog.ui +++ b/src/calibre/gui2/dialogs/template_dialog.ui @@ -69,33 +69,19 @@ - - - Kind - - - - - - icon with no text - - - - - - - icon with text - - - - - - - 100 - 0 - - - + + + + + Kind: + + + + + + + + diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 60768b481e..36bfc97c24 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -9,7 +9,7 @@ import functools, re, os, traceback, errno, time from collections import defaultdict, namedtuple from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, - QModelIndex, QVariant, QDateTime, QColor, QPixmap) + QModelIndex, QVariant, QDateTime, QColor, QPixmap, QPainter) from calibre.gui2 import NONE, error_dialog from calibre.utils.search_query_parser import ParseException @@ -61,7 +61,7 @@ class ColumnColor(object): # {{{ return color_cache[id_][key] try: if self.mi is None: - self.mi = db.get_metadata(id_, index_is_id=True) + self.mi = db.new_api.get_proxy_metadata(id_) color = self.formatter.safe_format(fmt, self.mi, '', self.mi) if color in self.colors: color = QColor(color) @@ -76,38 +76,68 @@ class ColumnColor(object): # {{{ class ColumnIcon(object): # {{{ - def __init__(self, formatter): + def __init__(self, formatter, model): self.mi = None self.formatter = formatter + self.model = model - def __call__(self, id_, key, fmt, kind, db, icon_cache, icon_bitmap_cache): - dex = key+kind - if id_ in icon_cache and dex in icon_cache[id_]: + def __call__(self, id_, key, fmts, cache_index, db, icon_cache, icon_bitmap_cache): + if id_ in icon_cache and cache_index in icon_cache[id_]: self.mi = None - return icon_cache[id_][dex] + return icon_cache[id_][cache_index] try: if self.mi is None: - self.mi = db.get_metadata(id_, index_is_id=True) - icon = self.formatter.safe_format(fmt, self.mi, '', self.mi) - if icon: - if icon in icon_bitmap_cache: - icon_bitmap = icon_bitmap_cache[icon] - icon_cache[id_][dex] = icon_bitmap - return icon_bitmap - d = os.path.join(config_dir, 'cc_icons', icon) - if (os.path.exists(d)): - icon_bitmap = QPixmap(d) - h = icon_bitmap.height() - w = icon_bitmap.width() - # If the image is landscape and width is more than 50% - # large than height, use the pixmap. This tells Qt to display - # the image full width. It might be clipped to row height. - if w < (3 * h)/2: - icon_bitmap = QIcon(icon_bitmap) - icon_cache[id_][dex] = icon_bitmap - icon_bitmap_cache[icon] = icon_bitmap - self.mi = None + self.mi = db.new_api.get_proxy_metadata(id_) + icons = [] + for kind, fmt in fmts: + rule_icons = self.formatter.safe_format(fmt, self.mi, '', self.mi) + if not rule_icons: + continue + icon_list = [ic.strip() for ic in rule_icons.split(':')] + if icon_list and not kind.endswith('_composed'): + icons = icon_list + break + else: + icons.extend(icon_list) + + if icons: + icon_string = ':'.join(icons) + if icon_string in icon_bitmap_cache: + icon_bitmap = icon_bitmap_cache[icon_string] + icon_cache[id_][cache_index] = icon_bitmap return icon_bitmap + + icon_bitmaps = [] + total_width = 0 + for icon in icons: + d = os.path.join(config_dir, 'cc_icons', icon) + if (os.path.exists(d)): + bm = QPixmap(d) + icon_bitmaps.append(bm) + total_width += bm.width() + if len(icon_bitmaps) > 1: + i = len(icon_bitmaps) + result = QPixmap((i * 128) + ((i-1)*2), 128) + result.fill(Qt.transparent) + painter = QPainter(result) + x = 0 + for bm in icon_bitmaps: + painter.drawPixmap(x, 0, bm) + x += bm.width() + 2 + painter.end() + else: + result = icon_bitmaps[0] + + # If the image height is less than the row height, leave it alone + # The -2 allows for a pixel above and below. Also ensure that + # it is always a bit positive + rh = max(2, self.model.row_height - 2) + if result.height() > rh: + result = result.scaledToHeight(rh, mode=Qt.SmoothTransformation) + icon_cache[id_][cache_index] = result + icon_bitmap_cache[icon_string] = result + self.mi = None + return result except: pass # }}} @@ -145,7 +175,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.colors = frozenset([unicode(c) for c in QColor.colorNames()]) self._clear_caches() self.column_color = ColumnColor(self.formatter, self.colors) - self.column_icon = ColumnIcon(self.formatter) + self.column_icon = ColumnIcon(self.formatter, self) self.book_on_device = None self.editable_cols = ['title', 'authors', 'rating', 'publisher', @@ -168,6 +198,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.ids_to_highlight_set = set() self.current_highlighted_idx = None self.highlight_only = False + self.row_height = 0 self.read_config() def _clear_caches(self): @@ -176,6 +207,9 @@ class BooksModel(QAbstractTableModel): # {{{ self.icon_bitmap_cache = {} self.color_row_fmt_cache = None + def set_row_height(self, height): + self.row_height = height + def change_alignment(self, colname, alignment): if colname in self.column_map and alignment in ('left', 'right', 'center'): old = self.alignment_map.get(colname, 'left') @@ -765,16 +799,21 @@ class BooksModel(QAbstractTableModel): # {{{ if rules: key = self.column_map[col] id_ = None + fmts = [] for kind, k, fmt in rules: - if k == key and kind == 'icon_only': + if k == key and kind in {'icon_only', 'icon_only_composed'}: if id_ is None: id_ = self.id(index) self.column_icon.mi = None - ccicon = self.column_icon(id_, key, fmt, 'icon_only', self.db, - self.icon_cache, self.icon_bitmap_cache) - if ccicon is not None: - return NONE - self.icon_cache[id_][key+'icon_only'] = None + fmts.append((kind, fmt)) + + if fmts: + cache_index = key + ':DisplayRole' + ccicon = self.column_icon(id_, key, fmts, cache_index, self.db, + self.icon_cache, self.icon_bitmap_cache) + if ccicon is not None: + return NONE + self.icon_cache[id_][cache_index] = 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()) @@ -831,21 +870,25 @@ class BooksModel(QAbstractTableModel): # {{{ key = self.column_map[col] id_ = None need_icon_with_text = False + fmts = [] for kind, k, fmt in rules: - if k == key and kind in ('icon', 'icon_only'): + if k == key and kind.startswith('icon'): if id_ is None: id_ = self.id(index) self.column_icon.mi = None - if kind == 'icon': + fmts.append((kind, fmt)) + if kind in ('icon', 'icon_composed'): need_icon_with_text = True - ccicon = self.column_icon(id_, key, fmt, kind, self.db, - self.icon_cache, self.icon_bitmap_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 + if fmts: + cache_index = key + ':DecorationRole' + ccicon = self.column_icon(id_, key, fmts, cache_index, self.db, + self.icon_cache, self.icon_bitmap_cache) + if ccicon is not None: + return ccicon + if need_icon_with_text: + self.icon_cache[id_][cache_index] = self.bool_blank_icon + return self.bool_blank_icon + self.icon_cache[id_][cache_index] = 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/library/views.py b/src/calibre/gui2/library/views.py index 6711da1d9b..034f543429 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -649,6 +649,7 @@ class BooksView(QTableView): # {{{ self.resizeRowToContents(0) self.verticalHeader().setDefaultSectionSize(self.rowHeight(0) + gprefs['extra_row_spacing']) + self._model.set_row_height(self.rowHeight(0)) self.row_sizing_done = True def resize_column_to_fit(self, column): diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py index a195c948e5..4d8dfce9c5 100644 --- a/src/calibre/gui2/preferences/coloring.py +++ b/src/calibre/gui2/preferences/coloring.py @@ -7,13 +7,13 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os +import os, textwrap 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, QStandardItem, QStandardItemModel, QCheckBox) from calibre import prepare_string_for_xml, sanitize_file_name_unicode from calibre.constants import config_dir @@ -29,7 +29,9 @@ from calibre.utils.icu import lower all_columns_string = _('All Columns') icon_rule_kinds = [(_('icon with text'), 'icon'), - (_('icon with no text'), 'icon_only') ] + (_('icon with no text'), 'icon_only'), + (_('composed icons w/text'), 'icon_composed'), + (_('composed icons w/no text'), 'icon_only_composed'),] class ConditionEditor(QWidget): # {{{ @@ -312,6 +314,10 @@ class RuleEditor(QDialog): # {{{ for tt, t in icon_rule_kinds: self.kind_box.addItem(tt, t) l.addWidget(self.kind_box, 2, 1) + self.kind_box.setToolTip(textwrap.fill(_( + 'If you choose composed icons and multiple rules match, then all the' + ' matching icons will be combined, otherwise the icon from the' + ' first rule to match will be used.'))) self.l3 = l3 = QLabel(_('of the column:')) l.addWidget(l3, 2, 2) @@ -331,7 +337,6 @@ class RuleEditor(QDialog): # {{{ l.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding), 2, 7) else: self.filename_box = QComboBox() - self.filename_box.setInsertPolicy(self.filename_box.InsertAlphabetically) d = os.path.join(config_dir, 'cc_icons') self.icon_file_names = [] if os.path.exists(d): @@ -341,9 +346,15 @@ class RuleEditor(QDialog): # {{{ if icon_file.endswith('.png'): self.icon_file_names.append(icon_file) self.icon_file_names.sort(key=sort_key) - self.update_filename_box() - l.addWidget(self.filename_box, 2, 5) + vb = QVBoxLayout() + self.multiple_icon_cb = QCheckBox(_('Choose more than one icon')) + vb.addWidget(self.multiple_icon_cb) + self.update_filename_box() + self.multiple_icon_cb.clicked.connect(self.multiple_box_clicked) + vb.addWidget(self.filename_box) + l.addLayout(vb, 2, 5) + self.filename_button = QPushButton(QIcon(I('document_open.png')), _('&Add icon')) l.addWidget(self.filename_button, 2, 6) @@ -401,18 +412,37 @@ class RuleEditor(QDialog): # {{{ self.update_color_label() self.color_box.currentIndexChanged.connect(self.update_color_label) else: + self.rule_icon_files = [] self.filename_button.clicked.connect(self.filename_button_clicked) self.resize(self.sizeHint()) + def multiple_box_clicked(self): + self.update_filename_box() + self.update_icon_filenames_in_box() + def update_filename_box(self): - self.filename_box.clear() + doing_multiple = self.multiple_icon_cb.isChecked() + + model = QStandardItemModel() + self.filename_box.setModel(model) self.icon_file_names.sort(key=sort_key) - self.filename_box.addItem('') - self.filename_box.addItems(self.icon_file_names) + if doing_multiple: + item = QStandardItem(_('Open to see checkboxes')) + else: + item = QStandardItem('') + model.appendRow(item) + for i,filename in enumerate(self.icon_file_names): + item = QStandardItem(filename) + if doing_multiple: + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled); + item.setData(Qt.Unchecked, Qt.CheckStateRole) + else: + item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable); icon = QIcon(os.path.join(config_dir, 'cc_icons', filename)) - self.filename_box.setItemIcon(i+1, icon) + item.setIcon(icon) + model.appendRow(item) def update_color_label(self): pal = QApplication.palette() @@ -432,9 +462,9 @@ class RuleEditor(QDialog): # {{{ all_files=False, select_only_single_file=True) if path: icon_path = path[0] - icon_name = sanitize_file_name_unicode( + icon_name = lower(sanitize_file_name_unicode( os.path.splitext( - os.path.basename(icon_path))[0]+'.png') + os.path.basename(icon_path))[0]+'.png')) if icon_name not in self.icon_file_names: self.icon_file_names.append(icon_name) self.update_filename_box() @@ -449,13 +479,47 @@ class RuleEditor(QDialog): # {{{ except: import traceback traceback.print_exc() - self.filename_box.setCurrentIndex(self.filename_box.findText(icon_name)) + if self.multiple_icon_cb.isChecked(): + if icon_name not in self.rule_icon_files: + self.rule_icon_files.append(icon_name) + self.update_icon_filenames_in_box() + else: + self.filename_box.setCurrentIndex(self.filename_box.findText(icon_name)) self.filename_box.adjustSize() except: import traceback traceback.print_exc() return + def get_filenames_from_box(self): + if self.multiple_icon_cb.isChecked(): + model = self.filename_box.model() + fnames = [] + for i in range(1, model.rowCount()): + item = model.item(i, 0) + if item.checkState() == Qt.Checked: + fnames.append(lower(unicode(item.text()))) + fname = ' : '.join(fnames) + else: + fname = lower(unicode(self.filename_box.currentText())) + return fname + + def update_icon_filenames_in_box(self): + if self.rule_icon_files: + if not self.multiple_icon_cb.isChecked(): + idx = self.filename_box.findText(self.rule_icon_files[0]) + if idx >= 0: + self.filename_box.setCurrentIndex(idx) + else: + self.filename_box.setCurrentIndex(0) + else: + model = self.filename_box.model() + for icon in self.rule_icon_files: + idx = self.filename_box.findText(icon) + if idx >= 0: + item = model.item(idx) + item.setCheckState(Qt.Checked) + def add_blank_condition(self): c = ConditionEditor(self.fm, parent=self.conditions_widget) self.conditions.append(c) @@ -468,13 +532,15 @@ class RuleEditor(QDialog): # {{{ if idx >= 0: self.color_box.setCurrentIndex(idx) else: - self.kind_box.setCurrentIndex(0 if kind == 'icon' else 1) - if rule.color: - idx = self.filename_box.findText(rule.color) - if idx >= 0: - self.filename_box.setCurrentIndex(idx) - else: - self.filename_box.setCurrentIndex(0) + for i,tup in enumerate(icon_rule_kinds): + if kind == tup[1]: + self.kind_box.setCurrentIndex(i) + break + self.rule_icon_files = [ic.strip() for ic in rule.color.split(':')] + if len(self.rule_icon_files) > 1: + self.multiple_icon_cb.setChecked(True) + self.update_filename_box() + self.update_icon_filenames_in_box() for i in range(self.column_box.count()): c = unicode(self.column_box.itemData(i).toString()) @@ -492,10 +558,9 @@ class RuleEditor(QDialog): # {{{ import traceback traceback.print_exc() - def accept(self): if self.rule_kind != 'color': - fname = lower(unicode(self.filename_box.currentText())) + fname = self.get_filenames_from_box() if not fname: error_dialog(self, _('No icon selected'), _('You must choose an icon for this rule'), show=True) @@ -528,7 +593,7 @@ class RuleEditor(QDialog): # {{{ def rule(self): r = Rule(self.fm) if self.rule_kind != 'color': - r.color = unicode(self.filename_box.currentText()) + r.color = self.get_filenames_from_box() else: r.color = unicode(self.color_box.currentText()) idx = self.column_box.currentIndex() @@ -635,6 +700,15 @@ class RulesModel(QAbstractListModel): # {{{ self.reset() def rule_to_html(self, kind, col, rule): + trans_kind = 'not found' + if kind == 'color': + trans_kind = _('color') + else: + for tt, t in icon_rule_kinds: + if kind == t: + trans_kind = tt + break + if not isinstance(rule, Rule): if kind == 'color': return _(''' @@ -646,21 +720,11 @@ class RulesModel(QAbstractListModel): # {{{

Advanced Rule: set %(typ)s for column %(col)s:

%(rule)s
''')%dict(col=col, - typ=icon_rule_kinds[0][0] - if kind == icon_rule_kinds[0][1] else icon_rule_kinds[1][0], + typ=trans_kind, rule=prepare_string_for_xml(rule)) conditions = [self.condition_to_html(c) for c in rule.conditions] - trans_kind = 'not found' - 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 conditions are met: