diff --git a/recipes/alt_om_herning.recipe b/recipes/alt_om_herning.recipe
new file mode 100644
index 0000000000..c60d142a85
--- /dev/null
+++ b/recipes/alt_om_herning.recipe
@@ -0,0 +1,43 @@
+__license__ = 'GPL v3'
+__copyright__ = '2011, Rasmus Lauritsen '+_('Choose formats not to be deleted'), ids)
+ ' '+_('Choose formats not to be deleted. Note that '
+ 'this will never remove all formats from a book.'), ids)
if fmts is None:
return
for id in ids:
@@ -161,9 +162,12 @@ class DeleteAction(InterfaceAction):
continue
bfmts = set([x.lower() for x in bfmts.split(',')])
rfmts = bfmts - set(fmts)
- for fmt in rfmts:
- self.gui.library_view.model().db.remove_format(id, fmt,
- index_is_id=True, notify=False)
+ if bfmts - rfmts:
+ # Do not delete if it will leave the book with no
+ # formats
+ for fmt in rfmts:
+ self.gui.library_view.model().db.remove_format(id, fmt,
+ index_is_id=True, notify=False)
self.gui.library_view.model().refresh_ids(ids)
self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
self.gui.library_view.currentIndex())
diff --git a/src/calibre/gui2/dialogs/select_formats.py b/src/calibre/gui2/dialogs/select_formats.py
index 5934c8c0f9..aea56ad196 100644
--- a/src/calibre/gui2/dialogs/select_formats.py
+++ b/src/calibre/gui2/dialogs/select_formats.py
@@ -44,7 +44,7 @@ class SelectFormats(QDialog):
self.setLayout(self._l)
self.setWindowTitle(_('Choose formats'))
self._m = QLabel(msg)
- self._m.setWordWrap = True
+ self._m.setWordWrap(True)
self._l.addWidget(self._m)
self.formats = Formats(fmt_list)
self.fview = QListView(self)
diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py
index 3d199b156c..b24328f0ad 100644
--- a/src/calibre/gui2/dialogs/template_line_editor.py
+++ b/src/calibre/gui2/dialogs/template_line_editor.py
@@ -5,12 +5,17 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal ' +
- _('You can enter more than one tag per box, separated by commas. '
- 'The comparison ignores letter case. ' +
+ _('Set this box to indicate that the value must not match '
+ 'to return the "color if value found". For example, you '
+ 'can check if a tag does not exist by entering that tag '
+ 'and checking this box. You can check if tags are empty by '
+ 'checking this box, entering .* (period asterisk) for the text, '
+ 'then checking the RE box. The .* regular expression matches '
+ 'anything, so if this box is checked, it matches nothing. '
+ 'This box is particularly useful when using the AND box.'))
+ h.setAlignment(Qt.AlignCenter)
+ l.addWidget(h, 0, 2, 1, 1)
+
+ h = QLabel(_('Values (see the popup help for more information)'))
+ h.setAlignment(Qt.AlignCenter)
+ h.setToolTip(' ' +
+ _('You can enter more than one value per box, separated by commas. '
+ 'The comparison ignores letter case. Special note: you can '
+ 'enter at most one author. ' +
- _('Check this box if the tag box contains regular expressions') + '
'), lambda match: '')]
+
+ keep_only_tags = [dict(name='div', attrs={'class':'article'})]
+
+ remove_tags = [
+ dict(name='p',attrs={'class':'meta links'}),
+ dict(name='div',attrs={'class':'float-right'}),
+ dict(name='span',attrs={'class':'article-link-id'})
+ ]
+
+ feeds = [
+ (u'Seneste nyheder' , u'http://www.version2.dk/feeds/nyheder')
+ ,(u'Forretningssoftware' , u'http://www.version2.dk/feeds/forretningssoftware')
+ ,(u'Internet & styresystemer' , u'http://www.version2.dk/feeds/styresystemer')
+ ,(u'It-arkitektur' , u'http://www.version2.dk/feeds/it-arkitektur')
+ ,(u'It-styring & outsourcing' , u'http://www.version2.dk/feeds/it-styring')
+ ,(u'Job & karriere' , u'http://www.version2.dk/feeds/karriere')
+ ,(u'Mobil it & tele' , u'http://www.version2.dk/feeds/tele')
+ ,(u'Server/storage & netværk' , u'http://www.version2.dk/feeds/server-storage')
+ ,(u'Sikkerhed' , u'http://www.version2.dk/feeds/sikkerhed')
+ ,(u'Softwareudvikling' , u'http://www.version2.dk/feeds/softwareudvikling')
+ ]
diff --git a/resources/template-functions.json b/resources/template-functions.json
index 9fdc1782ea..606b102d41 100644
--- a/resources/template-functions.json
+++ b/resources/template-functions.json
@@ -1,26 +1,27 @@
{
"and": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if not args[i]:\n return ''\n i += 1\n return '1'\n",
- "contains": "def evaluate(self, formatter, kwargs, mi, locals,\n val, test, value_if_present, value_if_not):\n if re.search(test, val):\n return value_if_present\n else:\n return value_if_not\n",
+ "contains": "def evaluate(self, formatter, kwargs, mi, locals,\n val, test, value_if_present, value_if_not):\n if re.search(test, val, flags=re.I):\n return value_if_present\n else:\n return value_if_not\n",
"divide": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x / y)\n",
"uppercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.upper()\n",
"strcat": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n res = ''\n for i in range(0, len(args)):\n res += args[i]\n return res\n",
- "in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n for v in l:\n if re.search(pat, v):\n return fv\n return nfv\n",
+ "in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n if l:\n for v in l:\n if re.search(pat, v, flags=re.I):\n return fv\n return nfv\n",
"multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n",
"ifempty": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):\n if val:\n return val\n else:\n return value_if_empty\n",
"booksize": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.book_size is not None:\n try:\n return str(mi.book_size)\n except:\n pass\n return ''\n",
"select": "def evaluate(self, formatter, kwargs, mi, locals, val, key):\n if not val:\n return ''\n vals = [v.strip() for v in val.split(',')]\n for v in vals:\n if v.startswith(key+':'):\n return v[len(key)+1:]\n return ''\n",
"strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n",
"first_non_empty": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return args[i]\n i += 1\n return ''\n",
- "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val)\n",
+ "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val, flags=re.I)\n",
"subtract": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x - y)\n",
"list_item": "def evaluate(self, formatter, kwargs, mi, locals, val, index, sep):\n if not val:\n return ''\n index = int(index)\n val = val.split(sep)\n try:\n return val[index]\n except:\n return ''\n",
"shorten": "def evaluate(self, formatter, kwargs, mi, locals,\n val, leading, center_string, trailing):\n l = max(0, int(leading))\n t = max(0, int(trailing))\n if len(val) > l + len(center_string) + t:\n return val[0:l] + center_string + ('' if t == 0 else val[-t:])\n else:\n return val\n",
"field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n",
"add": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x + y)\n",
- "lookup": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if len(args) == 2: # here for backwards compatibility\n if val:\n return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)\n else:\n return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)\n if (len(args) % 2) != 1:\n raise ValueError(_('lookup requires either 2 or an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)\n if re.search(args[i], val):\n return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)\n i += 2\n",
+ "lookup": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if len(args) == 2: # here for backwards compatibility\n if val:\n return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)\n else:\n return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)\n if (len(args) % 2) != 1:\n raise ValueError(_('lookup requires either 2 or an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)\n if re.search(args[i], val, flags=re.I):\n return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)\n i += 2\n",
"template": "def evaluate(self, formatter, kwargs, mi, locals, template):\n template = template.replace('[[', '{').replace(']]', '}')\n return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)\n",
"print": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n print args\n return None\n",
"merge_lists": "def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):\n l1 = [l.strip() for l in list1.split(separator) if l.strip()]\n l2 = [l.strip() for l in list2.split(separator) if l.strip()]\n lcl1 = set([icu_lower(l) for l in l1])\n res = []\n for i in l1:\n res.append(i)\n for i in l2:\n if icu_lower(i) not in lcl1:\n res.append(i)\n return ', '.join(sorted(res, key=sort_key))\n",
+ "str_in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, str, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n c = [v.strip() for v in str.split(sep) if v.strip()]\n if l:\n for v in l:\n for t in c:\n if strcmp(t, v) == 0:\n return fv\n return nfv\n",
"titlecase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return titlecase(val)\n",
"subitems": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n items = [v.strip() for v in val.split(',')]\n rv = set()\n for item in items:\n component = item.split('.')\n try:\n if ei == 0:\n rv.add('.'.join(component[si:]))\n else:\n rv.add('.'.join(component[si:ei]))\n except:\n pass\n return ', '.join(sorted(rv, key=sort_key))\n",
"sublist": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n val = val.split(sep)\n try:\n if ei == 0:\n return sep.join(val[si:])\n else:\n return sep.join(val[si:ei])\n except:\n return ''\n",
@@ -32,9 +33,10 @@
"count": "def evaluate(self, formatter, kwargs, mi, locals, val, sep):\n return unicode(len(val.split(sep)))\n",
"lowercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.lower()\n",
"substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n",
- "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n",
- "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n",
"or": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n",
+ "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val, flags=re.I):\n return args[i+1]\n i += 2\n",
+ "ondevice": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.ondevice_col:\n return _('Yes')\n return ''\n",
+ "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n",
"raw_field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return unicode(getattr(mi, name, None))\n",
"cmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n x = float(x if x else 0)\n y = float(y if y else 0)\n if x < y:\n return lt\n if x == y:\n return eq\n return gt\n"
}
\ No newline at end of file
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 3e2201f6a4..69407dcb2e 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -74,7 +74,7 @@ class Metadata(object):
Metadata from custom columns should be accessed via the get() method,
passing in the lookup name for the column, for example: "#mytags".
- Use the :meth:`is_null` method to test if a filed is null.
+ Use the :meth:`is_null` method to test if a field is null.
This object also has functions to format fields into strings.
@@ -105,7 +105,7 @@ class Metadata(object):
def is_null(self, field):
'''
- Return True if the value of filed is null in this object.
+ Return True if the value of field is null in this object.
'null' means it is unknown or evaluates to False. So a title of
_('Unknown') is null or a language of 'und' is null.
diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py
index 718f0737b3..43465512e0 100644
--- a/src/calibre/gui2/actions/delete.py
+++ b/src/calibre/gui2/actions/delete.py
@@ -152,7 +152,8 @@ class DeleteAction(InterfaceAction):
if not ids:
return
fmts = self._get_selected_formats(
- '
'
- 'A tag value can be a regular expression. Check the box to turn '
+ _('Set this box to indicate that the two conditions must both '
+ 'be true to return the "color if value found". For example, you '
+ 'can check if two tags are present, if the book has a tag '
+ 'and a #read custom column is checked, or if a book has '
+ 'some tag and has a particular format.'))
+ l.addWidget(h, 0, 0, 1, 1)
+
+ h = QLabel(_('Column'))
+ h.setAlignment(Qt.AlignCenter)
+ l.addWidget(h, 0, 1, 1, 1)
+
+ h = QLabel(_('Not'))
+ h.setToolTip('
'
+ 'A value can be a regular expression. Check the box to turn '
'them on. When using regular expressions, note that the wizard '
'puts anchors (^ and $) around the expression, so you '
'must ensure your expression matches from the beginning '
- 'to the end of the tag.
'
+ 'to the end of the column you are checking.
'
'Regular expression examples:') + '' +
- _('
.*
matches any tag. No empty tags are '
- 'checked, so you don\'t need to worry about empty stringsA.*
matches any tag beginning with A.*mystery.*
matches any tag containing '
+ _('.*
matches anything in the column. No '
+ 'empty values are checked, so you don\'t need to worry about '
+ 'empty stringsA.*
matches anything beginning with A.*mystery.*
matches anything containing '
'the word "mystery"
' + _('At least one of the two color boxes must have a value. Leave ' 'one color box empty if you want the template to use the next ' 'line in this wizard. If both boxes are filled in, the rest of ' 'the lines in this wizard will be ignored.') + '
') - l.addWidget(c, 0, 2, 1, 1) - c = QLabel(_('Color if tag not found')) + l.addWidget(c, 0, 5, 1, 1) + c = QLabel(_('Color if value not found')) c.setToolTip('' + _('This box is usually filled in only on the last test. If it is ' - 'filled in before the last test, then the color for tag found box ' + 'filled in before the last test, then the color for value found box ' 'must be empty or all the rest of the tests will be ignored.') + '
') - l.addWidget(c, 0, 3, 1, 1) + l.addWidget(c, 0, 6, 1, 1) + + self.andboxes = [] + self.notboxes = [] self.tagboxes = [] self.colorboxes = [] self.nfcolorboxes = [] self.reboxes = [] + self.colboxes = [] self.colors = [unicode(s) for s in list(QColor.colorNames())] self.colors.insert(0, '') - for i in range(0, 10): + + maxlines = 10 + for i in range(1, maxlines+1): + ab = QCheckBox(self) + self.andboxes.append(ab) + if i != maxlines: + # let the last box float in space + l.addWidget(ab, i, 0, 2, 1) + ab.stateChanged.connect(partial(self.and_box_changed, line=i-1)) + else: + ab.setVisible(False) + + w = QComboBox(self) + w.addItems(self.columns) + l.addWidget(w, i, 1, 1, 1) + self.colboxes.append(w) + + nb = QCheckBox(self) + self.notboxes.append(nb) + l.addWidget(nb, i, 2, 1, 1) + tb = MultiCompleteLineEdit(self) tb.set_separator(', ') - tb.update_items_cache(self.tags) self.tagboxes.append(tb) - l.addWidget(tb, i+1, 0, 1, 1) + l.addWidget(tb, i, 3, 1, 1) + w.currentIndexChanged[str].connect(partial(self.column_changed, valbox=tb)) w = QCheckBox(self) self.reboxes.append(w) - l.addWidget(w, i+1, 1, 1, 1) + l.addWidget(w, i, 4, 1, 1) w = QComboBox(self) w.addItems(self.colors) self.colorboxes.append(w) - l.addWidget(w, i+1, 2, 1, 1) + l.addWidget(w, i, 5, 1, 1) w = QComboBox(self) w.addItems(self.colors) self.nfcolorboxes.append(w) - l.addWidget(w, i+1, 3, 1, 1) + l.addWidget(w, i, 6, 1, 1) if txt: lines = txt.split('\n')[3:] @@ -141,39 +267,105 @@ class TagWizard(QDialog): t, c = vals nc = '' re = False + f = 'tags' + a = False + n = False else: - t,c,nc,re = vals + t,c,f,nc,re,a,n = vals try: - self.colorboxes[i].setCurrentIndex(self.colorboxes[i].findText(c)) - self.nfcolorboxes[i].setCurrentIndex(self.nfcolorboxes[i].findText(nc)) + self.colboxes[i].setCurrentIndex(self.colboxes[i].findText(f)) + self.colorboxes[i].setCurrentIndex( + self.colorboxes[i].findText(c)) + self.nfcolorboxes[i].setCurrentIndex( + self.nfcolorboxes[i].findText(nc)) self.tagboxes[i].setText(t) self.reboxes[i].setChecked(re == '2') + self.andboxes[i].setChecked(a == '2') + self.notboxes[i].setChecked(n == '2') + i += 1 except: pass - i += 1 + + w = QLabel(_('Preview')) + l.addWidget(w, 99, 1, 1, 1) + w = self.test_box = QLineEdit(self) + w.setReadOnly(True) + l.addWidget(w, 99, 3, 1, 1) + w = QPushButton(_('Test')) + l.addWidget(w, 99, 5, 1, 1) + w.clicked.connect(self.preview) bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel, parent=self) - l.addWidget(bb, 100, 2, 1, 2) + l.addWidget(bb, 100, 3, 1, 2) bb.accepted.connect(self.accepted) bb.rejected.connect(self.reject) self.template = '' - def accepted(self): + def preview(self): + if not self.generate_program(): + return + t = composite_formatter.safe_format(self.template, self.mi, + _('EXCEPTION'), self.mi) + self.test_box.setText(t) + + def column_changed(self, s, valbox=None): + k = unicode(s) + if k in self.completion_values: + valbox.update_items_cache(self.completion_values[k]['v']) + if self.completion_values[k]['m']: + valbox.set_separator(', ') + else: + valbox.set_separator(None) + else: + valbox.update_items_cache([]) + valbox.set_separator(None) + + def generate_program(self): res = ("program:\n#tag wizard -- do not directly edit\n" - " t = field('tags');\n first_non_empty(\n") + " first_non_empty(\n") lines = [] - for tb, cb, nfcb, reb in zip(self.tagboxes, self.colorboxes, - self.nfcolorboxes, self.reboxes): - tags = [t.strip() for t in unicode(tb.text()).split(',') if t.strip()] + was_and = False + had_line = False + + line = 0 + for tb, cb, fb, nfcb, reb, ab, nb in zip( + self.tagboxes, self.colorboxes, self.colboxes, + self.nfcolorboxes, self.reboxes, self.andboxes, self.notboxes): + f = unicode(fb.currentText()) + if not f: + continue + m = self.completion_values[f]['m'] c = unicode(cb.currentText()).strip() nfc = unicode(nfcb.currentText()).strip() re = reb.checkState() - if re == 2: - tags = '$|^'.join(tags) + a = ab.checkState() + n = nb.checkState() + line += 1 + + if n == 2: + tval = '' + fval = '1' else: - tags = ','.join(tags) - if not tags or not (c or nfc): - continue + tval = '1' + fval = '' + + if m: + tags = [t.strip() for t in unicode(tb.text()).split(',') if t.strip()] + if re == 2: + tags = '$|^'.join(tags) + else: + tags = ','.join(tags) + else: + tags = unicode(tb.text()).strip() + if f == 'authors': + tags.replace(',', '|') + + if (tags or f) and not (tags and f and (a == 2 or c)): + error_dialog(self, _('Invalid line'), + _('Line number {0} is not valid').format(line), + show=True, show_copy_button=False) + return False + if c not in self.colors: error_dialog(self, _('Invalid color'), _('The color {0} is not valid').format(c), @@ -184,25 +376,69 @@ class TagWizard(QDialog): _('The color {0} is not valid').format(nfc), show=True, show_copy_button=False) return False - if re == 2: - lines.append(" in_list(t, ',', '^{0}$', '{1}', '{2}')".\ - format(tags, c, nfc)) + + if not was_and: + if had_line: + lines[-1] += ',' + had_line = True + lines.append(" test(and(") else: - lines.append(" str_in_list(t, ',', '{0}', '{1}', '{2}')".\ - format(tags, c, nfc)) - res += ',\n'.join(lines) + lines[-1] += ',' + + if re == 2: + if m: + lines.append(" in_list(field('{1}'), ',', '^{0}$', '{2}', '{3}')".\ + format(tags, f, tval, fval)) + else: + lines.append(" contains(field('{1}'), '{0}', '{2}', '{3}')".\ + format(tags, f, tval, fval)) + else: + if m: + lines.append(" str_in_list(field('{1}'), ',', '{0}', '{2}', '{3}')".\ + format(tags, f, tval, fval)) + else: + lines.append(" strcmp(field('{1}'), '{0}', '{3}', '{2}', '{3}')".\ + format(tags, f, tval, fval)) + if a == 2: + was_and = True + else: + was_and = False + lines.append(" ), '{0}', '{1}')".format(c, nfc)) + + res += '\n'.join(lines) res += ')\n' self.template = res res = '' - for tb, cb, nfcb, reb in zip(self.tagboxes, self.colorboxes, - self.nfcolorboxes, self.reboxes): + for tb, cb, fb, nfcb, reb, ab, nb in zip( + self.tagboxes, self.colorboxes, self.colboxes, + self.nfcolorboxes, self.reboxes, self.andboxes, self.notboxes): t = unicode(tb.text()).strip() if t.endswith(','): t = t[:-1] c = unicode(cb.currentText()).strip() + f = unicode(fb.currentText()) nfc = unicode(nfcb.currentText()).strip() re = unicode(reb.checkState()) - if t and c: - res += '#' + t + ':|:' + c + ':|:' + nfc + ':|:' + re + '\n' + a = unicode(ab.checkState()) + n = unicode(nb.checkState()) + if f and t and (a == '2' or c): + res += '#' + t + ':|:' + c + ':|:' + f + ':|:' + \ + nfc + ':|:' + re + ':|:' + a + ':|:' + n + '\n' self.template += res - self.accept() + return True + + def and_box_changed(self, state, line=None): + if state == 2: + self.colorboxes[line].setCurrentIndex(0) + self.colorboxes[line].setEnabled(False) + self.nfcolorboxes[line].setCurrentIndex(0) + self.nfcolorboxes[line].setEnabled(False) + else: + self.colorboxes[line].setEnabled(True) + self.nfcolorboxes[line].setEnabled(True) + + def accepted(self): + if self.generate_program(): + self.accept() + else: + self.template = '' diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 554b104c34..d79c92befa 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' import shutil, functools, re, os, traceback from contextlib import closing +from collections import defaultdict from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, QModelIndex, QVariant, QDate, QColor) @@ -87,6 +88,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.column_map = [] self.headers = {} self.alignment_map = {} + self.color_cache = defaultdict(dict) self.buffer_size = buffer self.metadata_backup = None self.bool_yes_icon = QIcon(I('ok.png')) @@ -97,7 +99,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.ids_to_highlight_set = set() self.current_highlighted_idx = None self.highlight_only = False - self.column_color_map = {} + self.column_color_list = [] self.colors = [unicode(c) for c in QColor.colorNames()] self.read_config() @@ -172,11 +174,13 @@ class BooksModel(QAbstractTableModel): # {{{ def refresh_ids(self, ids, current_row=-1): + self.color_cache = defaultdict(dict) rows = self.db.refresh_ids(ids) if rows: self.refresh_rows(rows, current_row=current_row) def refresh_rows(self, rows, current_row=-1): + self.color_cache = defaultdict(dict) for row in rows: if row == current_row: self.new_bookdisplay_data.emit( @@ -206,6 +210,7 @@ class BooksModel(QAbstractTableModel): # {{{ return ret def count_changed(self, *args): + self.color_cache = defaultdict(dict) self.count_changed_signal.emit(self.db.count()) def row_indices(self, index): @@ -336,6 +341,10 @@ class BooksModel(QAbstractTableModel): # {{{ self.db.refresh(field=None) self.resort(reset=reset) + def reset(self): + self.color_cache = defaultdict(dict) + QAbstractTableModel.reset(self) + def resort(self, reset=True): if not self.db: return @@ -537,12 +546,12 @@ class BooksModel(QAbstractTableModel): # {{{ return img def set_color_templates(self, reset=True): - self.column_color_map = {} + self.column_color_list = [] for i in range(1,self.db.column_color_count+1): name = self.db.prefs.get('column_color_name_'+str(i)) if name: - self.column_color_map[name] = \ - self.db.prefs.get('column_color_template_'+str(i)) + self.column_color_list.append((name, + self.db.prefs.get('column_color_template_'+str(i)))) if reset: self.reset() @@ -717,18 +726,25 @@ class BooksModel(QAbstractTableModel): # {{{ return QVariant(QColor('lightgreen')) elif role == Qt.ForegroundRole: key = self.column_map[col] - if key in self.column_color_map: + for k,fmt in self.column_color_list: + if k != key: + continue + id_ = self.id(index) + if id_ in self.color_cache: + if key in self.color_cache[id_]: + return self.color_cache[id_][key] mi = self.db.get_metadata(self.id(index), index_is_id=True) - fmt = self.column_color_map[key] try: color = composite_formatter.safe_format(fmt, mi, '', mi) if color in self.colors: color = QColor(color) if color.isValid(): - return QVariant(color) + color = QVariant(color) + self.color_cache[id_][key] = color + return color except: return NONE - elif self.is_custom_column(key) and \ + if self.is_custom_column(key) and \ self.custom_columns[key]['datatype'] == 'enumeration': cc = self.custom_columns[self.column_map[col]]['display'] colors = cc.get('enum_colors', []) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 862636cb04..7a8c1fb69c 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -5,12 +5,15 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal' + - _('If you want to color a field based on tags, then click the ' - 'button next to an empty line to open the tags wizard. ' + _('If you want to color a field based on contents of columns, ' + 'then click the button next to an empty line to open the wizard. ' 'It will build a template for you. You can later edit that ' - 'template with the same wizard. If you edit it by hand, the ' - 'wizard might not work or might restore old values.') + + 'template with the same wizard. This is by far the easiest ' + 'way to specify a template.') + '
' + - _('The template must evaluate to one of the color names shown ' - 'below. You can use any legal template expression. ' + _('If you manually construct a template, then the template must ' + 'evaluate to a valid color name shown in the color names box.' + 'You can use any legal template expression. ' 'For example, you can set the title to always display in ' 'green using the template "green" (without the quotes). ' 'To show the title in the color named in the custom column ' @@ -200,11 +204,16 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): 'of values", it is often easier to specify the ' 'colors in the column definition dialog. There you can ' 'provide a color for each value without using a template.')+ '
') + self.color_help_scrollArea.setVisible(False) + self.color_help_button.clicked.connect(self.change_help_text) + self.colors_scrollArea.setVisible(False) + self.colors_label.setVisible(False) + self.colors_button.clicked.connect(self.change_colors_text) + choices = db.field_metadata.displayable_field_keys() choices.sort(key=sort_key) choices.insert(0, '') self.column_color_count = db.column_color_count+1 - tags = db.all_tags() mi=None try: @@ -213,17 +222,58 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): except: pass + l = self.column_color_layout for i in range(1, self.column_color_count): + ccn = QComboBox(parent=self) + setattr(self, 'opt_column_color_name_'+str(i), ccn) + l.addWidget(ccn, i, 0, 1, 1) + + wtb = QToolButton(parent=self) + setattr(self, 'opt_column_color_wizard_'+str(i), wtb) + wtb.setIcon(QIcon(I('wizard.png'))) + l.addWidget(wtb, i, 1, 1, 1) + + ttb = QToolButton(parent=self) + setattr(self, 'opt_column_color_tpledit_'+str(i), ttb) + ttb.setIcon(QIcon(I('edit_input.png'))) + l.addWidget(ttb, i, 2, 1, 1) + + tpl = TemplateLineEditor(parent=self) + setattr(self, 'opt_column_color_template_'+str(i), tpl) + tpl.textChanged.connect(partial(self.tpl_edit_text_changed, ctrl=i)) + tpl.set_db(db) + tpl.set_mi(mi) + l.addWidget(tpl, i, 3, 1, 1) + + wtb.clicked.connect(tpl.tag_wizard) + ttb.clicked.connect(tpl.open_editor) + r('column_color_name_'+str(i), db.prefs, choices=choices) r('column_color_template_'+str(i), db.prefs) - tpl = getattr(self, 'opt_column_color_template_'+str(i)) - tpl.set_tags(tags) - tpl.set_mi(mi) - toolbutton = getattr(self, 'opt_column_color_wizard_'+str(i)) - toolbutton.clicked.connect(tpl.tag_wizard) + txt = db.prefs.get('column_color_template_'+str(i), None) + + wtb.setEnabled(tpl.enable_wizard_button(txt)) + ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt) + all_colors = [unicode(s) for s in list(QColor.colorNames())] self.colors_box.setText(', '.join(all_colors)) + def change_help_text(self): + self.color_help_scrollArea.setVisible(not self.color_help_scrollArea.isVisible()) + + def change_colors_text(self): + self.colors_scrollArea.setVisible(not self.colors_scrollArea.isVisible()) + self.colors_label.setVisible(not self.colors_label.isVisible()) + + def tpl_edit_text_changed(self, ign, ctrl=None): + tpl = getattr(self, 'opt_column_color_template_'+str(ctrl)) + txt = unicode(tpl.text()) + wtb = getattr(self, 'opt_column_color_wizard_'+str(ctrl)) + ttb = getattr(self, 'opt_column_color_tpledit_'+str(ctrl)) + wtb.setEnabled(tpl.enable_wizard_button(txt)) + ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt) + tpl.setFocus() + def initialize(self): ConfigWidgetBase.initialize(self) font = gprefs['font'] diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index fe6134f235..def1bdd41c 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -416,114 +416,95 @@ then the tags will be displayed each on their own line.