KG updates

This commit is contained in:
GRiker 2011-05-31 05:07:42 -06:00
commit e2f8169871
16 changed files with 653 additions and 232 deletions

View File

@ -0,0 +1,43 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Rasmus Lauritsen <rasmus at lauritsen.info>'
'''
aoh.dk
'''
from calibre.web.feeds.news import BasicNewsRecipe
class aoh_dk(BasicNewsRecipe):
title = 'Alt om Herning'
__author__ = 'Rasmus Lauritsen'
description = 'Nyheder fra Herning om omegn'
publisher = 'Mediehuset Herning Folkeblad'
category = 'news, local, Denmark'
oldest_article = 14
max_articles_per_feed = 50
no_stylesheets = True
delay = 1
encoding = 'utf8'
use_embedded_content = False
language = 'da'
extra_css = """ body{font-family: Verdana,Arial,sans-serif }
img{margin-bottom: 0.4em}
.txtContent,.stamp{font-size: small}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
feeds = [(u'All news', u'http://aoh.dk/rss.xml')]
keep_only_tags = [
dict(name='h1')
,dict(name='span', attrs={'class':['frontpage_body']})
]
remove_tags = [
dict(name=['object','link'])
]

View File

@ -71,7 +71,7 @@ class Mediapart(BasicNewsRecipe):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None: if self.username is not None and self.password is not None:
br.open('http://www.mediapart.fr/') br.open('http://www.mediapart.fr/')
br.select_form(nr=1) br.select_form(nr=0)
br['name'] = self.username br['name'] = self.username
br['pass'] = self.password br['pass'] = self.password
br.submit() br.submit()

29
recipes/metro_uk.recipe Normal file
View File

@ -0,0 +1,29 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1306097511(BasicNewsRecipe):
title = u'Metro UK'
no_stylesheets = True
oldest_article = 1
max_articles_per_feed = 200
__author__ = 'Dave Asbury'
language = 'en_GB'
simultaneous_downloads= 3
masthead_url = 'http://e-edition.metro.co.uk/images/metro_logo.gif'
keep_only_tags = [
dict(attrs={'class':['img-cnt figure']}),
dict(attrs={'class':['art-img']}),
dict(name='h1'),
dict(name='h2', attrs={'class':'h2'}),
dict(name='div', attrs={'class':'art-lft'})
]
remove_tags = [dict(name='div', attrs={'class':[ 'metroCommentFormWrap',
'commentForm', 'metroCommentInnerWrap',
'art-rgt','pluck-app pluck-comm','news m12 clrd clr-l p5t', 'flt-r' ]})]
feeds = [
(u'News', u'http://www.metro.co.uk/rss/news/'), (u'Money', u'http://www.metro.co.uk/rss/money/'), (u'Sport', u'http://www.metro.co.uk/rss/sport/'), (u'Film', u'http://www.metro.co.uk/rss/metrolife/film/'), (u'Music', u'http://www.metro.co.uk/rss/metrolife/music/'), (u'TV', u'http://www.metro.co.uk/rss/tv/'), (u'Showbiz', u'http://www.metro.co.uk/rss/showbiz/'), (u'Weird News', u'http://www.metro.co.uk/rss/weird/'), (u'Travel', u'http://www.metro.co.uk/rss/travel/'), (u'Lifestyle', u'http://www.metro.co.uk/rss/lifestyle/'), (u'Books', u'http://www.metro.co.uk/rss/lifestyle/books/'), (u'Food', u'http://www.metro.co.uk/rss/lifestyle/restaurants/')]

64
recipes/version2.recipe Normal file
View File

@ -0,0 +1,64 @@
import re
__license__ = 'GPL v3'
__copyright__ = '2011, Rasmus Lauritsen <rasmus at lauritsen.info>'
'''
version2.dk
'''
from calibre.web.feeds.news import BasicNewsRecipe
class version2(BasicNewsRecipe):
title = 'Version2.dk'
__author__ = 'Rasmus Lauritsen'
description = 'IT News'
publisher = 'version2.dk'
category = 'news, IT, hardware, software, Denmark'
oldest_article = 14
max_articles_per_feed = 50
no_stylesheets = True
remove_empty_feeds = True
use_embedded_content = False
encoding = 'iso-8859-1'
language = 'da'
extra_css = """
body {font-family: "Verdana",Times,serif}
.articleauthor{color: #9F9F9F;
font-family: Arial, sans-serif;
font-size: small;
text-transform: uppercase}
.rubric,.dd,h6#credit{color: #CD0021;
font-family: Arial, sans-serif;
font-size: small;
text-transform: uppercase}
.descender:first-letter{display: inline; font-size: xx-large; font-weight: bold}
.dd,h6#credit{color: gray}
.c{display: block}
.caption,h2#articleintro{font-style: italic}
.caption{font-size: small}
"""
preprocess_regexps = [ (re.compile(r'</?a[^>]*>'),lambda match: ''),
(re.compile(r'<span[^>]*article-link-id.*?<br\s*\/?><br\s*\/?>'), 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')
]

View File

@ -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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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" "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"
} }

View File

@ -74,7 +74,7 @@ class Metadata(object):
Metadata from custom columns should be accessed via the get() method, Metadata from custom columns should be accessed via the get() method,
passing in the lookup name for the column, for example: "#mytags". 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. This object also has functions to format fields into strings.
@ -105,7 +105,7 @@ class Metadata(object):
def is_null(self, field): 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 'null' means it is unknown or evaluates to False. So a title of
_('Unknown') is null or a language of 'und' is null. _('Unknown') is null or a language of 'und' is null.

View File

@ -152,7 +152,8 @@ class DeleteAction(InterfaceAction):
if not ids: if not ids:
return return
fmts = self._get_selected_formats( fmts = self._get_selected_formats(
'<p>'+_('Choose formats <b>not</b> to be deleted'), ids) '<p>'+_('Choose formats <b>not</b> to be deleted.<p>Note that '
'this will never remove all formats from a book.'), ids)
if fmts is None: if fmts is None:
return return
for id in ids: for id in ids:
@ -161,9 +162,12 @@ class DeleteAction(InterfaceAction):
continue continue
bfmts = set([x.lower() for x in bfmts.split(',')]) bfmts = set([x.lower() for x in bfmts.split(',')])
rfmts = bfmts - set(fmts) rfmts = bfmts - set(fmts)
for fmt in rfmts: if bfmts - rfmts:
self.gui.library_view.model().db.remove_format(id, fmt, # Do not delete if it will leave the book with no
index_is_id=True, notify=False) # 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().refresh_ids(ids)
self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
self.gui.library_view.currentIndex()) self.gui.library_view.currentIndex())

View File

@ -44,7 +44,7 @@ class SelectFormats(QDialog):
self.setLayout(self._l) self.setLayout(self._l)
self.setWindowTitle(_('Choose formats')) self.setWindowTitle(_('Choose formats'))
self._m = QLabel(msg) self._m = QLabel(msg)
self._m.setWordWrap = True self._m.setWordWrap(True)
self._l.addWidget(self._m) self._l.addWidget(self._m)
self.formats = Formats(fmt_list) self.formats = Formats(fmt_list)
self.fview = QListView(self) self.fview = QListView(self)

View File

@ -5,12 +5,17 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import (QLineEdit, QDialog, QGridLayout, QLabel, QCheckBox, from functools import partial
QDialogButtonBox, QColor, QComboBox, QIcon) from collections import defaultdict
from PyQt4.Qt import (Qt, QLineEdit, QDialog, QGridLayout, QLabel, QCheckBox,
QIcon, QDialogButtonBox, QColor, QComboBox, QPushButton)
from calibre.ebooks.metadata.book.base import composite_formatter
from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.complete import MultiCompleteLineEdit from calibre.gui2.complete import MultiCompleteLineEdit
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.utils.icu import sort_key
class TemplateLineEditor(QLineEdit): class TemplateLineEditor(QLineEdit):
@ -22,114 +27,235 @@ class TemplateLineEditor(QLineEdit):
QLineEdit.__init__(self, parent) QLineEdit.__init__(self, parent)
self.tags = None self.tags = None
self.mi = None self.mi = None
self.txt = None
def set_mi(self, mi): def set_mi(self, mi):
self.mi = mi self.mi = mi
def set_tags(self, tags): def set_db(self, db):
self.tags = tags self.db = db
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
menu = self.createStandardContextMenu() menu = self.createStandardContextMenu()
menu.addSeparator() menu.addSeparator()
action_clear_field = menu.addAction(_('Remove any template from the box'))
action_clear_field.triggered.connect(self.clear_field)
action_open_editor = menu.addAction(_('Open Template Editor')) action_open_editor = menu.addAction(_('Open Template Editor'))
action_open_editor.triggered.connect(self.open_editor) action_open_editor.triggered.connect(self.open_editor)
if self.tags:
action_tag_wizard = menu.addAction(_('Open Tag Wizard'))
action_tag_wizard.triggered.connect(self.tag_wizard)
menu.exec_(event.globalPos()) menu.exec_(event.globalPos())
def clear_field(self):
self.setText('')
self.txt = None
self.setReadOnly(False)
self.setStyleSheet('TemplateLineEditor { color: black }')
def open_editor(self): def open_editor(self):
t = TemplateDialog(self, self.text(), self.mi) if self.txt:
t = TemplateDialog(self, self.txt, self.mi)
else:
t = TemplateDialog(self, self.text(), self.mi)
t.setWindowTitle(_('Edit template')) t.setWindowTitle(_('Edit template'))
if t.exec_(): if t.exec_():
self.txt = None
self.setText(t.textbox.toPlainText()) self.setText(t.textbox.toPlainText())
def enable_wizard_button(self, txt):
if not txt or txt.startswith('program:\n#tag wizard'):
return True
return False
def setText(self, txt):
txt = unicode(txt)
if txt and txt.startswith('program:\n#tag wizard'):
self.txt = txt
self.setReadOnly(True)
QLineEdit.setText(self, '')
QLineEdit.setText(self, _('Template generated by the wizard'))
self.setStyleSheet('TemplateLineEditor { color: gray }')
else:
QLineEdit.setText(self, txt)
def tag_wizard(self): def tag_wizard(self):
txt = unicode(self.text()) txt = unicode(self.text())
if txt and not txt.startswith('program:\n#tag wizard'): if txt and not self.txt:
error_dialog(self, _('Invalid text'), error_dialog(self, _('Invalid text'),
_('The text in the box was not generated by this wizard'), _('The text in the box was not generated by this wizard'),
show=True, show_copy_button=False) show=True, show_copy_button=False)
return return
d = TagWizard(self, self.tags, unicode(self.text())) d = TagWizard(self, self.db, unicode(self.txt), self.mi)
if d.exec_(): if d.exec_():
self.setText(d.template) self.setText(d.template)
def text(self):
if self.txt:
return self.txt
return QLineEdit.text(self)
class TagWizard(QDialog): class TagWizard(QDialog):
def __init__(self, parent, tags, txt): def __init__(self, parent, db, txt, mi):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setWindowTitle(_('Tag Wizard')) self.setWindowTitle(_('Coloring Wizard'))
self.setWindowIcon(QIcon(I('wizard.png'))) self.setWindowIcon(QIcon(I('wizard.png')))
self.tags = tags self.mi = mi
self.columns = []
self.completion_values = defaultdict(dict)
for k in db.all_field_keys():
m = db.metadata_for_field(k)
if m['datatype'] in ('text', 'enumeration', 'series') and \
m['is_category'] and k not in ('identifiers'):
self.columns.append(k)
if m['is_custom']:
self.completion_values[k]['v'] = db.all_custom(m['label'])
elif k == 'tags':
self.completion_values[k]['v'] = db.all_tags()
elif k == 'formats':
self.completion_values[k]['v'] = db.all_formats()
else:
if k in ('publisher'):
ck = k + 's'
else:
ck = k
f = getattr(db, 'all_' + ck, None)
if f:
if k == 'authors':
self.completion_values[k]['v'] = [v[1].\
replace('|', ',') for v in f()]
else:
self.completion_values[k]['v'] = [v[1] for v in f()]
if k in self.completion_values:
if k == 'authors':
self.completion_values[k]['m'] = None
else:
self.completion_values[k]['m'] = m['is_multiple']
self.columns.sort(key=sort_key)
self.columns.insert(0, '')
l = QGridLayout() l = QGridLayout()
self.setLayout(l) self.setLayout(l)
l.setColumnStretch(0, 1) l.setColumnStretch(2, 10)
l.setColumnMinimumWidth(0, 300) l.setColumnMinimumWidth(3, 300)
h = QLabel(_('Tags (see the popup help for more information)'))
h = QLabel(_('And'))
h.setToolTip('<p>' + h.setToolTip('<p>' +
_('You can enter more than one tag per box, separated by commas. ' _('Set this box to indicate that the two conditions must both '
'The comparison ignores letter case.<br>' 'be true to return the "color if value found". For example, you '
'A tag value can be a regular expression. Check the box to turn ' '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('<p>' +
_('Set this box to indicate that the value must <b>not</b> 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('<p>' +
_('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.<br>'
'A value can be a regular expression. Check the box to turn '
'them on. When using regular expressions, note that the wizard ' 'them on. When using regular expressions, note that the wizard '
'puts anchors (^ and $) around the expression, so you ' 'puts anchors (^ and $) around the expression, so you '
'must ensure your expression matches from the beginning ' 'must ensure your expression matches from the beginning '
'to the end of the tag.<br>' 'to the end of the column you are checking.<br>'
'Regular expression examples:') + '<ul>' + 'Regular expression examples:') + '<ul>' +
_('<li><code><b>.*</b></code> matches any tag. No empty tags are ' _('<li><code><b>.*</b></code> matches anything in the column. No '
'checked, so you don\'t need to worry about empty strings</li>' 'empty values are checked, so you don\'t need to worry about '
'<li><code><b>A.*</b></code> matches any tag beginning with A</li>' 'empty strings</li>'
'<li><code><b>.*mystery.*</b></code> matches any tag containing ' '<li><code><b>A.*</b></code> matches anything beginning with A</li>'
'<li><code><b>.*mystery.*</b></code> matches anything containing '
'the word "mystery"</li>') + '</ul></p>') 'the word "mystery"</li>') + '</ul></p>')
l.addWidget(h , 0, 0, 1, 1) l.addWidget(h , 0, 3, 1, 1)
c = QLabel(_('is RE')) c = QLabel(_('is RE'))
c.setToolTip('<p>' + c.setToolTip('<p>' +
_('Check this box if the tag box contains regular expressions') + '</p>') _('Check this box if the values box contains regular expressions') + '</p>')
l.addWidget(c, 0, 1, 1, 1) l.addWidget(c, 0, 4, 1, 1)
c = QLabel(_('Color if tag found')) c = QLabel(_('Color if value found'))
c.setToolTip('<p>' + c.setToolTip('<p>' +
_('At least one of the two color boxes must have a value. Leave ' _('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 ' '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 ' 'line in this wizard. If both boxes are filled in, the rest of '
'the lines in this wizard will be ignored.') + '</p>') 'the lines in this wizard will be ignored.') + '</p>')
l.addWidget(c, 0, 2, 1, 1) l.addWidget(c, 0, 5, 1, 1)
c = QLabel(_('Color if tag not found')) c = QLabel(_('Color if value not found'))
c.setToolTip('<p>' + c.setToolTip('<p>' +
_('This box is usually filled in only on the last test. If it is ' _('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.') + '</p>') 'must be empty or all the rest of the tests will be ignored.') + '</p>')
l.addWidget(c, 0, 3, 1, 1) l.addWidget(c, 0, 6, 1, 1)
self.andboxes = []
self.notboxes = []
self.tagboxes = [] self.tagboxes = []
self.colorboxes = [] self.colorboxes = []
self.nfcolorboxes = [] self.nfcolorboxes = []
self.reboxes = [] self.reboxes = []
self.colboxes = []
self.colors = [unicode(s) for s in list(QColor.colorNames())] self.colors = [unicode(s) for s in list(QColor.colorNames())]
self.colors.insert(0, '') 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 = MultiCompleteLineEdit(self)
tb.set_separator(', ') tb.set_separator(', ')
tb.update_items_cache(self.tags)
self.tagboxes.append(tb) 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) w = QCheckBox(self)
self.reboxes.append(w) self.reboxes.append(w)
l.addWidget(w, i+1, 1, 1, 1) l.addWidget(w, i, 4, 1, 1)
w = QComboBox(self) w = QComboBox(self)
w.addItems(self.colors) w.addItems(self.colors)
self.colorboxes.append(w) self.colorboxes.append(w)
l.addWidget(w, i+1, 2, 1, 1) l.addWidget(w, i, 5, 1, 1)
w = QComboBox(self) w = QComboBox(self)
w.addItems(self.colors) w.addItems(self.colors)
self.nfcolorboxes.append(w) self.nfcolorboxes.append(w)
l.addWidget(w, i+1, 3, 1, 1) l.addWidget(w, i, 6, 1, 1)
if txt: if txt:
lines = txt.split('\n')[3:] lines = txt.split('\n')[3:]
@ -141,39 +267,105 @@ class TagWizard(QDialog):
t, c = vals t, c = vals
nc = '' nc = ''
re = False re = False
f = 'tags'
a = False
n = False
else: else:
t,c,nc,re = vals t,c,f,nc,re,a,n = vals
try: try:
self.colorboxes[i].setCurrentIndex(self.colorboxes[i].findText(c)) self.colboxes[i].setCurrentIndex(self.colboxes[i].findText(f))
self.nfcolorboxes[i].setCurrentIndex(self.nfcolorboxes[i].findText(nc)) self.colorboxes[i].setCurrentIndex(
self.colorboxes[i].findText(c))
self.nfcolorboxes[i].setCurrentIndex(
self.nfcolorboxes[i].findText(nc))
self.tagboxes[i].setText(t) self.tagboxes[i].setText(t)
self.reboxes[i].setChecked(re == '2') self.reboxes[i].setChecked(re == '2')
self.andboxes[i].setChecked(a == '2')
self.notboxes[i].setChecked(n == '2')
i += 1
except: except:
pass 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) 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.accepted.connect(self.accepted)
bb.rejected.connect(self.reject) bb.rejected.connect(self.reject)
self.template = '' 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" res = ("program:\n#tag wizard -- do not directly edit\n"
" t = field('tags');\n first_non_empty(\n") " first_non_empty(\n")
lines = [] lines = []
for tb, cb, nfcb, reb in zip(self.tagboxes, self.colorboxes, was_and = False
self.nfcolorboxes, self.reboxes): had_line = False
tags = [t.strip() for t in unicode(tb.text()).split(',') if t.strip()]
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() c = unicode(cb.currentText()).strip()
nfc = unicode(nfcb.currentText()).strip() nfc = unicode(nfcb.currentText()).strip()
re = reb.checkState() re = reb.checkState()
if re == 2: a = ab.checkState()
tags = '$|^'.join(tags) n = nb.checkState()
line += 1
if n == 2:
tval = ''
fval = '1'
else: else:
tags = ','.join(tags) tval = '1'
if not tags or not (c or nfc): fval = ''
continue
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: if c not in self.colors:
error_dialog(self, _('Invalid color'), error_dialog(self, _('Invalid color'),
_('The color {0} is not valid').format(c), _('The color {0} is not valid').format(c),
@ -184,25 +376,69 @@ class TagWizard(QDialog):
_('The color {0} is not valid').format(nfc), _('The color {0} is not valid').format(nfc),
show=True, show_copy_button=False) show=True, show_copy_button=False)
return False return False
if re == 2:
lines.append(" in_list(t, ',', '^{0}$', '{1}', '{2}')".\ if not was_and:
format(tags, c, nfc)) if had_line:
lines[-1] += ','
had_line = True
lines.append(" test(and(")
else: else:
lines.append(" str_in_list(t, ',', '{0}', '{1}', '{2}')".\ lines[-1] += ','
format(tags, c, nfc))
res += ',\n'.join(lines) 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' res += ')\n'
self.template = res self.template = res
res = '' res = ''
for tb, cb, nfcb, reb in zip(self.tagboxes, self.colorboxes, for tb, cb, fb, nfcb, reb, ab, nb in zip(
self.nfcolorboxes, self.reboxes): self.tagboxes, self.colorboxes, self.colboxes,
self.nfcolorboxes, self.reboxes, self.andboxes, self.notboxes):
t = unicode(tb.text()).strip() t = unicode(tb.text()).strip()
if t.endswith(','): if t.endswith(','):
t = t[:-1] t = t[:-1]
c = unicode(cb.currentText()).strip() c = unicode(cb.currentText()).strip()
f = unicode(fb.currentText())
nfc = unicode(nfcb.currentText()).strip() nfc = unicode(nfcb.currentText()).strip()
re = unicode(reb.checkState()) re = unicode(reb.checkState())
if t and c: a = unicode(ab.checkState())
res += '#' + t + ':|:' + c + ':|:' + nfc + ':|:' + re + '\n' 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.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 = ''

View File

@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en'
import shutil, functools, re, os, traceback import shutil, functools, re, os, traceback
from contextlib import closing from contextlib import closing
from collections import defaultdict
from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage,
QModelIndex, QVariant, QDate, QColor) QModelIndex, QVariant, QDate, QColor)
@ -87,6 +88,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.column_map = [] self.column_map = []
self.headers = {} self.headers = {}
self.alignment_map = {} self.alignment_map = {}
self.color_cache = defaultdict(dict)
self.buffer_size = buffer self.buffer_size = buffer
self.metadata_backup = None self.metadata_backup = None
self.bool_yes_icon = QIcon(I('ok.png')) self.bool_yes_icon = QIcon(I('ok.png'))
@ -97,7 +99,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.ids_to_highlight_set = set() self.ids_to_highlight_set = set()
self.current_highlighted_idx = None self.current_highlighted_idx = None
self.highlight_only = False self.highlight_only = False
self.column_color_map = {} self.column_color_list = []
self.colors = [unicode(c) for c in QColor.colorNames()] self.colors = [unicode(c) for c in QColor.colorNames()]
self.read_config() self.read_config()
@ -172,11 +174,13 @@ class BooksModel(QAbstractTableModel): # {{{
def refresh_ids(self, ids, current_row=-1): def refresh_ids(self, ids, current_row=-1):
self.color_cache = defaultdict(dict)
rows = self.db.refresh_ids(ids) rows = self.db.refresh_ids(ids)
if rows: if rows:
self.refresh_rows(rows, current_row=current_row) self.refresh_rows(rows, current_row=current_row)
def refresh_rows(self, rows, current_row=-1): def refresh_rows(self, rows, current_row=-1):
self.color_cache = defaultdict(dict)
for row in rows: for row in rows:
if row == current_row: if row == current_row:
self.new_bookdisplay_data.emit( self.new_bookdisplay_data.emit(
@ -206,6 +210,7 @@ class BooksModel(QAbstractTableModel): # {{{
return ret return ret
def count_changed(self, *args): def count_changed(self, *args):
self.color_cache = defaultdict(dict)
self.count_changed_signal.emit(self.db.count()) self.count_changed_signal.emit(self.db.count())
def row_indices(self, index): def row_indices(self, index):
@ -336,6 +341,10 @@ class BooksModel(QAbstractTableModel): # {{{
self.db.refresh(field=None) self.db.refresh(field=None)
self.resort(reset=reset) self.resort(reset=reset)
def reset(self):
self.color_cache = defaultdict(dict)
QAbstractTableModel.reset(self)
def resort(self, reset=True): def resort(self, reset=True):
if not self.db: if not self.db:
return return
@ -537,12 +546,12 @@ class BooksModel(QAbstractTableModel): # {{{
return img return img
def set_color_templates(self, reset=True): 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): for i in range(1,self.db.column_color_count+1):
name = self.db.prefs.get('column_color_name_'+str(i)) name = self.db.prefs.get('column_color_name_'+str(i))
if name: if name:
self.column_color_map[name] = \ self.column_color_list.append((name,
self.db.prefs.get('column_color_template_'+str(i)) self.db.prefs.get('column_color_template_'+str(i))))
if reset: if reset:
self.reset() self.reset()
@ -717,18 +726,25 @@ class BooksModel(QAbstractTableModel): # {{{
return QVariant(QColor('lightgreen')) return QVariant(QColor('lightgreen'))
elif role == Qt.ForegroundRole: elif role == Qt.ForegroundRole:
key = self.column_map[col] 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) mi = self.db.get_metadata(self.id(index), index_is_id=True)
fmt = self.column_color_map[key]
try: try:
color = composite_formatter.safe_format(fmt, mi, '', mi) color = composite_formatter.safe_format(fmt, mi, '', mi)
if color in self.colors: if color in self.colors:
color = QColor(color) color = QColor(color)
if color.isValid(): if color.isValid():
return QVariant(color) color = QVariant(color)
self.color_cache[id_][key] = color
return color
except: except:
return NONE return NONE
elif self.is_custom_column(key) and \ if self.is_custom_column(key) and \
self.custom_columns[key]['datatype'] == 'enumeration': self.custom_columns[key]['datatype'] == 'enumeration':
cc = self.custom_columns[self.column_map[col]]['display'] cc = self.custom_columns[self.column_map[col]]['display']
colors = cc.get('enum_colors', []) colors = cc.get('enum_colors', [])

View File

@ -5,12 +5,15 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from functools import partial
from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog,
QAbstractListModel, Qt, QColor) QAbstractListModel, Qt, QColor, QIcon, QToolButton, QComboBox)
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs, qt_app from calibre.gui2 import config, gprefs, qt_app
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from calibre.utils.localization import (available_translations, from calibre.utils.localization import (available_translations,
get_language, get_lang) get_language, get_lang)
from calibre.utils.config import prefs from calibre.utils.config import prefs
@ -167,14 +170,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
'<a href="http://manual.calibre-ebook.com/template_lang.html">' '<a href="http://manual.calibre-ebook.com/template_lang.html">'
'tutorial</a> on using templates.') + 'tutorial</a> on using templates.') +
'</p><p>' + '</p><p>' +
_('If you want to color a field based on tags, then click the ' _('If you want to color a field based on contents of columns, '
'button next to an empty line to open the tags wizard. ' '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 ' 'It will build a template for you. You can later edit that '
'template with the same wizard. If you edit it by hand, the ' 'template with the same wizard. This is by far the easiest '
'wizard might not work or might restore old values.') + 'way to specify a template.') +
'</p><p>' + '</p><p>' +
_('The template must evaluate to one of the color names shown ' _('If you manually construct a template, then the template must '
'below. You can use any legal template expression. ' '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 ' 'For example, you can set the title to always display in '
'green using the template "green" (without the quotes). ' 'green using the template "green" (without the quotes). '
'To show the title in the color named in the custom column ' '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 ' 'of values", it is often easier to specify the '
'colors in the column definition dialog. There you can ' 'colors in the column definition dialog. There you can '
'provide a color for each value without using a template.')+ '</p>') 'provide a color for each value without using a template.')+ '</p>')
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 = db.field_metadata.displayable_field_keys()
choices.sort(key=sort_key) choices.sort(key=sort_key)
choices.insert(0, '') choices.insert(0, '')
self.column_color_count = db.column_color_count+1 self.column_color_count = db.column_color_count+1
tags = db.all_tags()
mi=None mi=None
try: try:
@ -213,17 +222,58 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
except: except:
pass pass
l = self.column_color_layout
for i in range(1, self.column_color_count): 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_name_'+str(i), db.prefs, choices=choices)
r('column_color_template_'+str(i), db.prefs) r('column_color_template_'+str(i), db.prefs)
tpl = getattr(self, 'opt_column_color_template_'+str(i)) txt = db.prefs.get('column_color_template_'+str(i), None)
tpl.set_tags(tags)
tpl.set_mi(mi) wtb.setEnabled(tpl.enable_wizard_button(txt))
toolbutton = getattr(self, 'opt_column_color_wizard_'+str(i)) ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt)
toolbutton.clicked.connect(tpl.tag_wizard)
all_colors = [unicode(s) for s in list(QColor.colorNames())] all_colors = [unicode(s) for s in list(QColor.colorNames())]
self.colors_box.setText(', '.join(all_colors)) 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): def initialize(self):
ConfigWidgetBase.initialize(self) ConfigWidgetBase.initialize(self)
font = gprefs['font'] font = gprefs['font']

View File

@ -416,114 +416,95 @@ then the tags will be displayed each on their own line.</string>
<string>Column Coloring</string> <string>Column Coloring</string>
</attribute> </attribute>
<layout class="QGridLayout" name="column_color_layout"> <layout class="QGridLayout" name="column_color_layout">
<item row="1" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Column to color</string> <string>Column to color</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="0" column="3">
<widget class="QLabel" name="label"> <layout class="QHBoxLayout">
<property name="text"> <item>
<string>Color selection template</string> <widget class="QLabel" name="label">
</property> <property name="text">
</widget> <string>Color selection template</string>
</item> </property>
<item row="2" column="0"> <property name="sizePolicy">
<widget class="QComboBox" name="opt_column_color_name_1"/> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
</item> <horstretch>10</horstretch>
<item row="2" column="1"> <verstretch>0</verstretch>
<widget class="TemplateLineEditor" name="opt_column_color_template_1"/> </sizepolicy>
</item> </property>
<item row="2" column="2"> </widget>
<widget class="QToolButton" name="opt_column_color_wizard_1"> </item>
<property name="icon"> <item>
<iconset resource="../../../../resources/images.qrc"> <widget class="QLabel" name="label">
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset> <property name="text">
</property> <string>The template wizard is easiest to use</string>
<property name="toolTip"> </property>
<string>Open the tags wizard.</string> </widget>
</property> </item>
</widget> <item>
</item> <widget class="QPushButton" name="color_help_button">
<item row="3" column="0"> <property name="text">
<widget class="QComboBox" name="opt_column_color_name_2"/> <string>Show/hide help text</string>
</item> </property>
<item row="3" column="1"> </widget>
<widget class="TemplateLineEditor" name="opt_column_color_template_2"/> </item>
</item> <item>
<item row="3" column="2"> <widget class="QPushButton" name="colors_button">
<widget class="QToolButton" name="opt_column_color_wizard_2"> <property name="text">
<property name="icon"> <string>Show/hide colors</string>
<iconset resource="../../../../resources/images.qrc"> </property>
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset> </widget>
</property> </item>
<property name="toolTip"> </layout>
<string>Open the tags wizard.</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QComboBox" name="opt_column_color_name_3"/>
</item>
<item row="4" column="1">
<widget class="TemplateLineEditor" name="opt_column_color_template_3"/>
</item>
<item row="4" column="2">
<widget class="QToolButton" name="opt_column_color_wizard_3">
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset>
</property>
<property name="toolTip">
<string>Open the tags wizard.</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QComboBox" name="opt_column_color_name_4"/>
</item>
<item row="5" column="1">
<widget class="TemplateLineEditor" name="opt_column_color_template_4"/>
</item>
<item row="5" column="2">
<widget class="QToolButton" name="opt_column_color_wizard_4">
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset>
</property>
<property name="toolTip">
<string>Open the tags wizard.</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QComboBox" name="opt_column_color_name_5"/>
</item>
<item row="6" column="1">
<widget class="TemplateLineEditor" name="opt_column_color_template_5"/>
</item>
<item row="6" column="2">
<widget class="QToolButton" name="opt_column_color_wizard_5">
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset>
</property>
<property name="toolTip">
<string>Open the tags wizard.</string>
</property>
</widget>
</item> </item>
<item row="20" column="0"> <item row="20" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="colors_label">
<property name="text"> <property name="text">
<string>Color names</string> <string>Color names</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0" colspan="3"> <item row="21" column="0" colspan="8">
<widget class="QScrollArea" name="scrollArea"> <widget class="QScrollArea" name="colors_scrollArea">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>300</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>687</width>
<height>61</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="colors_box">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="30" column="0" colspan="8">
<widget class="QScrollArea" name="color_help_scrollArea">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@ -534,7 +515,7 @@ then the tags will be displayed each on their own line.</string>
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignCenter</set> <set>Qt::AlignLeft|Qt::AlignTop</set>
</property> </property>
<widget class="QWidget" name="scrollAreaWidgetContents"> <widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry"> <property name="geometry">
@ -560,37 +541,24 @@ then the tags will be displayed each on their own line.</string>
</widget> </widget>
</widget> </widget>
</item> </item>
<item row="21" column="0" colspan="3"> <item row="40" column="0">
<widget class="QScrollArea" name="scrollArea_2"> <spacer>
<property name="maximumSize"> <property name="sizePolicy">
<sizepolicy vsizetype="Expanding" hsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size> <size>
<width>16777215</width> <width>0</width>
<height>120</height> <height>0</height>
</size> </size>
</property> </property>
<property name="widgetResizable"> </spacer>
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>687</width>
<height>61</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="colors_box">
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>

View File

@ -352,7 +352,7 @@ The syntax for searching for dates is::
If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy
locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009. Some locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009. Some
special date strings are available. The string ``today`` translates to today's date, whatever it is. The special date strings are available. The string ``today`` translates to today's date, whatever it is. The
strings `yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare strings ``yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare
to a date some number of days ago, for example: date:>10daysago, date:<=45daysago. to a date some number of days ago, for example: date:>10daysago, date:<=45daysago.
You can search for books that have a format of a certain size like this:: You can search for books that have a format of a certain size like this::

View File

@ -122,7 +122,7 @@ The functions available are:
* ``uppercase()`` -- return the value of the field in upper case. * ``uppercase()`` -- return the value of the field in upper case.
* ``titlecase()`` -- return the value of the field in title case. * ``titlecase()`` -- return the value of the field in title case.
* ``capitalize()`` -- return the value with the first letter upper case and the rest lower case. * ``capitalize()`` -- return the value with the first letter upper case and the rest lower case.
* ``contains(pattern, text if match, text if not match`` -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`. * ``contains(pattern, text if match, text if not match)`` -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`.
* ``count(separator)`` -- interprets the value as a list of items separated by `separator`, returning the number of items in the list. Most lists use a comma as the separator, but authors uses an ampersand. Examples: `{tags:count(,)}`, `{authors:count(&)}` * ``count(separator)`` -- interprets the value as a list of items separated by `separator`, returning the number of items in the list. Most lists use a comma as the separator, but authors uses an ampersand. Examples: `{tags:count(,)}`, `{authors:count(&)}`
* ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`. * ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`.
* ``in_list(separator, pattern, found_val, not_found_val)`` -- interpret the field as a list of items separated by `separator`, comparing the `pattern` against each value in the list. If the pattern matches a value, return `found_val`, otherwise return `not_found_val`. * ``in_list(separator, pattern, found_val, not_found_val)`` -- interpret the field as a list of items separated by `separator`, comparing the `pattern` against each value in the list. If the pattern matches a value, return `found_val`, otherwise return `not_found_val`.

View File

@ -331,9 +331,10 @@ class BuiltinInList(BuiltinFormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv): def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):
l = [v.strip() for v in val.split(sep) if v.strip()] l = [v.strip() for v in val.split(sep) if v.strip()]
for v in l: if l:
if re.search(pat, v, flags=re.I): for v in l:
return fv if re.search(pat, v, flags=re.I):
return fv
return nfv return nfv
class BuiltinStrInList(BuiltinFormatterFunction): class BuiltinStrInList(BuiltinFormatterFunction):
@ -349,10 +350,11 @@ class BuiltinStrInList(BuiltinFormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, val, sep, str, fv, nfv): def evaluate(self, formatter, kwargs, mi, locals, val, sep, str, fv, nfv):
l = [v.strip() for v in val.split(sep) if v.strip()] l = [v.strip() for v in val.split(sep) if v.strip()]
c = [v.strip() for v in str.split(sep) if v.strip()] c = [v.strip() for v in str.split(sep) if v.strip()]
for v in l: if l:
for t in c: for v in l:
if strcmp(t, v) == 0: for t in c:
return fv if strcmp(t, v) == 0:
return fv
return nfv return nfv
class BuiltinRe(BuiltinFormatterFunction): class BuiltinRe(BuiltinFormatterFunction):

View File

@ -1123,6 +1123,13 @@ class ZipFile:
targetpath = os.sep.join(components) targetpath = os.sep.join(components)
with open(targetpath, 'wb') as target: with open(targetpath, 'wb') as target:
shutil.copyfileobj(source, target) shutil.copyfileobj(source, target)
# Kovid: Try to preserve the timestamps in the ZIP file
try:
mtime = time.localtime()
mtime = time.mktime(member.date_time + (0, 0) + (mtime.tm_isdst,))
os.utime(targetpath, (mtime, mtime))
except:
pass
self.extract_mapping[member.filename] = targetpath self.extract_mapping[member.filename] = targetpath
return targetpath return targetpath