mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
KG updates
This commit is contained in:
commit
5987a0eeae
@ -4,6 +4,7 @@ src/calibre/plugins
|
||||
resources/images.qrc
|
||||
src/calibre/manual/.build/
|
||||
src/calibre/manual/cli/
|
||||
src/calibre/manual/template_ref.rst
|
||||
build
|
||||
dist
|
||||
docs
|
||||
@ -31,4 +32,4 @@ nbproject/
|
||||
.pydevproject
|
||||
.settings/
|
||||
*.DS_Store
|
||||
calibre_plugins/
|
||||
calibre_plugins/
|
||||
|
@ -1227,6 +1227,15 @@ class StoreEHarlequinStore(StoreBase):
|
||||
formats = ['EPUB', 'PDF']
|
||||
affiliate = True
|
||||
|
||||
class StoreEpubBudStore(StoreBase):
|
||||
name = 'ePub Bud'
|
||||
description = 'Well, it\'s pretty much just "YouTube for Children\'s eBooks. A not-for-profit organization devoted to brining self published childrens books to the world.'
|
||||
actual_plugin = 'calibre.gui2.store.epubbud_plugin:EpubBudStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
formats = ['EPUB']
|
||||
|
||||
class StoreFeedbooksStore(StoreBase):
|
||||
name = 'Feedbooks'
|
||||
description = u'Feedbooks is a cloud publishing and distribution service, connected to a large ecosystem of reading systems and social networks. Provides a variety of genres from independent and classic books.'
|
||||
@ -1422,6 +1431,7 @@ plugins += [
|
||||
StoreEBookShoppeUKStore,
|
||||
StoreEPubBuyDEStore,
|
||||
StoreEHarlequinStore,
|
||||
StoreEpubBudStore,
|
||||
StoreFeedbooksStore,
|
||||
StoreFoylesUKStore,
|
||||
StoreGandalfStore,
|
||||
|
@ -408,6 +408,8 @@ def identify(log, abort, # {{{
|
||||
for f in plugin.prefs['ignore_fields']:
|
||||
if ':' not in f:
|
||||
setattr(result, f, getattr(dummy, f))
|
||||
if f == 'series':
|
||||
result.series_index = dummy.series_index
|
||||
result.relevance_in_source = i
|
||||
result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable
|
||||
and plugin.get_cached_cover_url(result.identifiers) is not
|
||||
|
@ -171,10 +171,9 @@ class ConvertAction(InterfaceAction):
|
||||
raise Exception(_('Empty output file, '
|
||||
'probably the conversion process crashed'))
|
||||
|
||||
data = open(temp_files[-1].name, 'rb')
|
||||
self.gui.library_view.model().db.add_format(book_id, \
|
||||
with open(temp_files[-1].name, 'rb') as data:
|
||||
self.gui.library_view.model().db.add_format(book_id, \
|
||||
fmt, data, index_is_id=True)
|
||||
data.close()
|
||||
self.gui.status_bar.show_message(job.description + \
|
||||
(' completed'), 2000)
|
||||
finally:
|
||||
|
@ -482,6 +482,8 @@ class EditMetadataAction(InterfaceAction):
|
||||
if mi.identifiers:
|
||||
idents.update(mi.identifiers)
|
||||
mi.identifiers = idents
|
||||
if mi.is_null('series'):
|
||||
mi.series_index = None
|
||||
db.set_metadata(i, mi, commit=False, set_title=set_title,
|
||||
set_authors=set_authors, notify=False)
|
||||
self.applied_ids.append(i)
|
||||
|
@ -99,8 +99,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.ids_to_highlight_set = set()
|
||||
self.current_highlighted_idx = None
|
||||
self.highlight_only = False
|
||||
self.column_color_list = []
|
||||
self.colors = [unicode(c) for c in QColor.colorNames()]
|
||||
self.colors = frozenset([unicode(c) for c in QColor.colorNames()])
|
||||
self.read_config()
|
||||
|
||||
def change_alignment(self, colname, alignment):
|
||||
@ -156,7 +155,6 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.headers[col] = self.custom_columns[col]['name']
|
||||
|
||||
self.build_data_convertors()
|
||||
self.set_color_templates(reset=False)
|
||||
self.reset()
|
||||
self.database_changed.emit(db)
|
||||
self.stop_metadata_backup()
|
||||
@ -545,16 +543,6 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
img = self.default_image
|
||||
return img
|
||||
|
||||
def set_color_templates(self, reset=True):
|
||||
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_list.append((name,
|
||||
self.db.prefs.get('column_color_template_'+str(i))))
|
||||
if reset:
|
||||
self.reset()
|
||||
|
||||
def build_data_convertors(self):
|
||||
def authors(r, idx=-1):
|
||||
au = self.db.data[r][idx]
|
||||
@ -726,14 +714,16 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
return QVariant(QColor('lightgreen'))
|
||||
elif role == Qt.ForegroundRole:
|
||||
key = self.column_map[col]
|
||||
for k,fmt in self.column_color_list:
|
||||
mi = None
|
||||
for k, fmt in self.db.prefs['column_color_rules']:
|
||||
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)
|
||||
if mi is None:
|
||||
mi = self.db.get_metadata(id_, index_is_id=True)
|
||||
try:
|
||||
color = composite_formatter.safe_format(fmt, mi, '', mi)
|
||||
if color in self.colors:
|
||||
@ -743,7 +733,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.color_cache[id_][key] = color
|
||||
return color
|
||||
except:
|
||||
return NONE
|
||||
continue
|
||||
if self.is_custom_column(key) and \
|
||||
self.custom_columns[key]['datatype'] == 'enumeration':
|
||||
cc = self.custom_columns[self.column_map[col]]['display']
|
||||
|
@ -35,8 +35,9 @@ from calibre import force_unicode
|
||||
|
||||
class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent=None, max_width=160):
|
||||
QStyledItemDelegate.__init__(self, parent)
|
||||
self.max_width = max_width
|
||||
|
||||
def to_doc(self, index):
|
||||
doc = QTextDocument()
|
||||
@ -46,8 +47,8 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
def sizeHint(self, option, index):
|
||||
doc = self.to_doc(index)
|
||||
ans = doc.size().toSize()
|
||||
if ans.width() > 150:
|
||||
ans.setWidth(160)
|
||||
if ans.width() > self.max_width - 10:
|
||||
ans.setWidth(self.max_width)
|
||||
ans.setHeight(ans.height()+10)
|
||||
return ans
|
||||
|
||||
|
596
src/calibre/gui2/preferences/coloring.py
Normal file
596
src/calibre/gui2/preferences/coloring.py
Normal file
@ -0,0 +1,596 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, QSize,
|
||||
QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon,
|
||||
QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox, QToolButton,
|
||||
QListView, QAbstractListModel, pyqtSignal)
|
||||
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
||||
from calibre.gui2.metadata.single_download import RichTextDelegate
|
||||
from calibre.library.coloring import (Rule, conditionable_columns,
|
||||
displayable_columns, rule_from_template)
|
||||
|
||||
class ConditionEditor(QWidget): # {{{
|
||||
|
||||
def __init__(self, fm, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.fm = fm
|
||||
|
||||
self.action_map = {
|
||||
'bool' : (
|
||||
(_('is true'), 'is true',),
|
||||
(_('is false'), 'is false'),
|
||||
(_('is undefined'), 'is undefined')
|
||||
),
|
||||
'int' : (
|
||||
(_('is equal to'), 'eq'),
|
||||
(_('is less than'), 'lt'),
|
||||
(_('is greater than'), 'gt')
|
||||
),
|
||||
'multiple' : (
|
||||
(_('has'), 'has'),
|
||||
(_('does not have'), 'does not have'),
|
||||
(_('has pattern'), 'has pattern'),
|
||||
(_('does not have pattern'), 'does not have pattern'),
|
||||
(_('is set'), 'is set'),
|
||||
(_('is not set'), 'is not set'),
|
||||
),
|
||||
'single' : (
|
||||
(_('is'), 'is'),
|
||||
(_('is not'), 'is not'),
|
||||
(_('matches pattern'), 'matches pattern'),
|
||||
(_('does not match pattern'), 'does not match pattern'),
|
||||
(_('is set'), 'is set'),
|
||||
(_('is not set'), 'is not set'),
|
||||
),
|
||||
}
|
||||
|
||||
for x in ('float', 'rating', 'datetime'):
|
||||
self.action_map[x] = self.action_map['int']
|
||||
|
||||
self.l = l = QGridLayout(self)
|
||||
self.setLayout(l)
|
||||
|
||||
self.l1 = l1 = QLabel(_('If the '))
|
||||
l.addWidget(l1, 0, 0)
|
||||
|
||||
self.column_box = QComboBox(self)
|
||||
l.addWidget(self.column_box, 0, 1)
|
||||
|
||||
self.l2 = l2 = QLabel(_(' column '))
|
||||
l.addWidget(l2, 0, 2)
|
||||
|
||||
self.action_box = QComboBox(self)
|
||||
l.addWidget(self.action_box, 0, 3)
|
||||
|
||||
self.l3 = l3 = QLabel(_(' the value '))
|
||||
l.addWidget(l3, 0, 4)
|
||||
|
||||
self.value_box = QLineEdit(self)
|
||||
l.addWidget(self.value_box, 0, 5)
|
||||
|
||||
self.column_box.addItem('', '')
|
||||
for key in sorted(
|
||||
conditionable_columns(fm),
|
||||
key=lambda x:sort_key(fm[x]['name'])):
|
||||
self.column_box.addItem(key, key)
|
||||
self.column_box.setCurrentIndex(0)
|
||||
|
||||
self.column_box.currentIndexChanged.connect(self.init_action_box)
|
||||
self.action_box.currentIndexChanged.connect(self.init_value_box)
|
||||
|
||||
for b in (self.column_box, self.action_box):
|
||||
b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
|
||||
b.setMinimumContentsLength(15)
|
||||
|
||||
@dynamic_property
|
||||
def current_col(self):
|
||||
def fget(self):
|
||||
idx = self.column_box.currentIndex()
|
||||
return unicode(self.column_box.itemData(idx).toString())
|
||||
def fset(self, val):
|
||||
for idx in range(self.column_box.count()):
|
||||
c = unicode(self.column_box.itemData(idx).toString())
|
||||
if c == val:
|
||||
self.column_box.setCurrentIndex(idx)
|
||||
return
|
||||
raise ValueError('Column %r not found'%val)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@dynamic_property
|
||||
def current_action(self):
|
||||
def fget(self):
|
||||
idx = self.action_box.currentIndex()
|
||||
return unicode(self.action_box.itemData(idx).toString())
|
||||
def fset(self, val):
|
||||
for idx in range(self.action_box.count()):
|
||||
c = unicode(self.action_box.itemData(idx).toString())
|
||||
if c == val:
|
||||
self.action_box.setCurrentIndex(idx)
|
||||
return
|
||||
raise ValueError('Action %r not valid for current column'%val)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@property
|
||||
def current_val(self):
|
||||
return unicode(self.value_box.text()).strip()
|
||||
|
||||
@dynamic_property
|
||||
def condition(self):
|
||||
|
||||
def fget(self):
|
||||
c, a, v = (self.current_col, self.current_action,
|
||||
self.current_val)
|
||||
if not c or not a:
|
||||
return None
|
||||
return (c, a, v)
|
||||
|
||||
def fset(self, condition):
|
||||
c, a, v = condition
|
||||
if not v:
|
||||
v = ''
|
||||
v = v.strip()
|
||||
self.current_col = c
|
||||
self.current_action = a
|
||||
self.value_box.setText(v)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def init_action_box(self):
|
||||
self.action_box.blockSignals(True)
|
||||
self.action_box.clear()
|
||||
self.action_box.addItem('', '')
|
||||
col = self.current_col
|
||||
m = self.fm[col]
|
||||
dt = m['datatype']
|
||||
if dt in self.action_map:
|
||||
actions = self.action_map[dt]
|
||||
else:
|
||||
k = 'multiple' if m['is_multiple'] else 'single'
|
||||
actions = self.action_map[k]
|
||||
|
||||
for text, key in actions:
|
||||
self.action_box.addItem(text, key)
|
||||
self.action_box.setCurrentIndex(0)
|
||||
self.action_box.blockSignals(False)
|
||||
self.init_value_box()
|
||||
|
||||
def init_value_box(self):
|
||||
self.value_box.setEnabled(True)
|
||||
self.value_box.setText('')
|
||||
self.value_box.setInputMask('')
|
||||
self.value_box.setValidator(None)
|
||||
col = self.current_col
|
||||
m = self.fm[col]
|
||||
dt = m['datatype']
|
||||
action = self.current_action
|
||||
if not col or not action:
|
||||
return
|
||||
tt = ''
|
||||
if dt in ('int', 'float', 'rating'):
|
||||
tt = _('Enter a number')
|
||||
v = QIntValidator if dt == 'int' else QDoubleValidator
|
||||
self.value_box.setValidator(v(self.value_box))
|
||||
elif dt == 'datetime':
|
||||
self.value_box.setInputMask('9999-99-99')
|
||||
tt = _('Enter a date in the format YYYY-MM-DD')
|
||||
else:
|
||||
tt = _('Enter a string')
|
||||
if 'pattern' in action:
|
||||
tt = _('Enter a regular expression')
|
||||
self.value_box.setToolTip(tt)
|
||||
if action in ('is set', 'is not set', 'is true', 'is false',
|
||||
'is undefined'):
|
||||
self.value_box.setEnabled(False)
|
||||
# }}}
|
||||
|
||||
class RuleEditor(QDialog): # {{{
|
||||
|
||||
def __init__(self, fm, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.fm = fm
|
||||
|
||||
self.setWindowIcon(QIcon(I('format-fill-color.png')))
|
||||
self.setWindowTitle(_('Create/edit a column coloring rule'))
|
||||
|
||||
self.l = l = QGridLayout(self)
|
||||
self.setLayout(l)
|
||||
|
||||
self.l1 = l1 = QLabel(_('Create a coloring rule by'
|
||||
' filling in the boxes below'))
|
||||
l.addWidget(l1, 0, 0, 1, 4)
|
||||
|
||||
self.f1 = QFrame(self)
|
||||
self.f1.setFrameShape(QFrame.HLine)
|
||||
l.addWidget(self.f1, 1, 0, 1, 4)
|
||||
|
||||
self.l2 = l2 = QLabel(_('Set the color of the column:'))
|
||||
l.addWidget(l2, 2, 0)
|
||||
|
||||
self.column_box = QComboBox(self)
|
||||
l.addWidget(self.column_box, 2, 1)
|
||||
|
||||
self.l3 = l3 = QLabel(_('to'))
|
||||
l3.setAlignment(Qt.AlignHCenter)
|
||||
l.addWidget(l3, 2, 2)
|
||||
|
||||
self.color_box = QComboBox(self)
|
||||
l.addWidget(self.color_box, 2, 3)
|
||||
|
||||
self.l4 = l4 = QLabel(
|
||||
_('Only if the following conditions are all satisfied:'))
|
||||
l4.setAlignment(Qt.AlignHCenter)
|
||||
l.addWidget(l4, 3, 0, 1, 4)
|
||||
|
||||
self.scroll_area = sa = QScrollArea(self)
|
||||
sa.setMinimumHeight(300)
|
||||
sa.setMinimumWidth(950)
|
||||
sa.setWidgetResizable(True)
|
||||
l.addWidget(sa, 4, 0, 1, 4)
|
||||
|
||||
self.add_button = b = QPushButton(QIcon(I('plus.png')),
|
||||
_('Add another condition'))
|
||||
l.addWidget(b, 5, 0, 1, 4)
|
||||
b.clicked.connect(self.add_blank_condition)
|
||||
|
||||
self.l5 = l5 = QLabel(_('You can disable a condition by'
|
||||
' blanking all of its boxes'))
|
||||
l.addWidget(l5, 6, 0, 1, 4)
|
||||
|
||||
self.bb = bb = QDialogButtonBox(
|
||||
QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
l.addWidget(bb, 7, 0, 1, 4)
|
||||
|
||||
self.conditions_widget = QWidget(self)
|
||||
sa.setWidget(self.conditions_widget)
|
||||
self.conditions_widget.setLayout(QVBoxLayout())
|
||||
self.conditions_widget.layout().setAlignment(Qt.AlignTop)
|
||||
self.conditions = []
|
||||
|
||||
for b in (self.column_box, self.color_box):
|
||||
b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
|
||||
b.setMinimumContentsLength(15)
|
||||
|
||||
for key in sorted(
|
||||
displayable_columns(fm),
|
||||
key=lambda x:sort_key(fm[x]['name'])):
|
||||
name = fm[key]['name']
|
||||
if name:
|
||||
self.column_box.addItem(key, key)
|
||||
self.column_box.setCurrentIndex(0)
|
||||
|
||||
self.color_box.addItems(QColor.colorNames())
|
||||
self.color_box.setCurrentIndex(0)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def add_blank_condition(self):
|
||||
c = ConditionEditor(self.fm, parent=self.conditions_widget)
|
||||
self.conditions.append(c)
|
||||
self.conditions_widget.layout().addWidget(c)
|
||||
|
||||
def apply_rule(self, col, rule):
|
||||
for i in range(self.column_box.count()):
|
||||
c = unicode(self.column_box.itemData(i).toString())
|
||||
if col == c:
|
||||
self.column_box.setCurrentIndex(i)
|
||||
break
|
||||
if rule.color:
|
||||
idx = self.color_box.findText(rule.color)
|
||||
if idx >= 0:
|
||||
self.color_box.setCurrentIndex(idx)
|
||||
for c in rule.conditions:
|
||||
ce = ConditionEditor(self.fm, parent=self.conditions_widget)
|
||||
self.conditions.append(ce)
|
||||
self.conditions_widget.layout().addWidget(ce)
|
||||
try:
|
||||
ce.condition = c
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def accept(self):
|
||||
if self.validate():
|
||||
QDialog.accept(self)
|
||||
|
||||
def validate(self):
|
||||
r = Rule(self.fm)
|
||||
for c in self.conditions:
|
||||
condition = c.condition
|
||||
if condition is not None:
|
||||
try:
|
||||
r.add_condition(*condition)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_dialog(self, _('Invalid condition'),
|
||||
_('One of the conditions for this rule is'
|
||||
' invalid: <b>%s</b>')%e,
|
||||
det_msg=traceback.format_exc(), show=True)
|
||||
return False
|
||||
if len(r.conditions) < 1:
|
||||
error_dialog(self, _('No conditions'),
|
||||
_('You must specify at least one non-empty condition'
|
||||
' for this rule'), show=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def rule(self):
|
||||
r = Rule(self.fm)
|
||||
r.color = unicode(self.color_box.currentText())
|
||||
idx = self.column_box.currentIndex()
|
||||
col = unicode(self.column_box.itemData(idx).toString())
|
||||
for c in self.conditions:
|
||||
condition = c.condition
|
||||
if condition is not None:
|
||||
r.add_condition(*condition)
|
||||
|
||||
return col, r
|
||||
# }}}
|
||||
|
||||
class RulesModel(QAbstractListModel): # {{{
|
||||
|
||||
def __init__(self, prefs, fm, parent=None):
|
||||
QAbstractListModel.__init__(self, parent)
|
||||
|
||||
self.fm = fm
|
||||
rules = list(prefs['column_color_rules'])
|
||||
self.rules = []
|
||||
for col, template in rules:
|
||||
try:
|
||||
rule = rule_from_template(self.fm, template)
|
||||
except:
|
||||
rule = template
|
||||
self.rules.append((col, rule))
|
||||
|
||||
def rowCount(self, *args):
|
||||
return len(self.rules)
|
||||
|
||||
def data(self, index, role):
|
||||
row = index.row()
|
||||
try:
|
||||
col, rule = self.rules[row]
|
||||
except:
|
||||
return None
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
return self.rule_to_html(col, rule)
|
||||
if role == Qt.UserRole:
|
||||
return (col, rule)
|
||||
|
||||
def add_rule(self, col, rule):
|
||||
self.rules.append((col, rule))
|
||||
self.reset()
|
||||
return self.index(len(self.rules)-1)
|
||||
|
||||
def replace_rule(self, index, col, r):
|
||||
self.rules[index.row()] = (col, r)
|
||||
self.dataChanged.emit(index, index)
|
||||
|
||||
def remove_rule(self, index):
|
||||
self.rules.remove(self.rules[index.row()])
|
||||
self.reset()
|
||||
|
||||
def commit(self, prefs):
|
||||
rules = []
|
||||
for col, r in self.rules:
|
||||
if isinstance(r, Rule):
|
||||
r = r.template
|
||||
if r is not None:
|
||||
rules.append((col, r))
|
||||
prefs['column_color_rules'] = rules
|
||||
|
||||
def move(self, idx, delta):
|
||||
row = idx.row() + delta
|
||||
if row >= 0 and row < len(self.rules):
|
||||
t = self.rules[row]
|
||||
self.rules[row] = self.rules[row-delta]
|
||||
self.rules[row-delta] = t
|
||||
self.dataChanged.emit(idx, idx)
|
||||
idx = self.index(row)
|
||||
self.dataChanged.emit(idx, idx)
|
||||
return idx
|
||||
|
||||
def clear(self):
|
||||
self.rules = []
|
||||
self.reset()
|
||||
|
||||
def rule_to_html(self, col, rule):
|
||||
if isinstance(rule, basestring):
|
||||
return _('''
|
||||
<p>Advanced Rule for column: %s
|
||||
<pre>%s</pre>
|
||||
''')%(col, rule)
|
||||
conditions = [self.condition_to_html(c) for c in rule.conditions]
|
||||
return _('''\
|
||||
<p>Set the color of <b>%s</b> to <b>%s</b> if the following
|
||||
conditions are met:</p>
|
||||
<ul>%s</ul>
|
||||
''') % (col, rule.color, ''.join(conditions))
|
||||
|
||||
def condition_to_html(self, condition):
|
||||
return (
|
||||
_('<li>If the <b>%s</b> column <b>%s</b> the value: <b>%s</b>') %
|
||||
tuple(condition))
|
||||
|
||||
# }}}
|
||||
|
||||
class EditRules(QWidget): # {{{
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
|
||||
self.l = l = QGridLayout(self)
|
||||
self.setLayout(l)
|
||||
|
||||
self.l1 = l1 = QLabel(_(
|
||||
'You can control the color of columns in the'
|
||||
' book list by creating "rules" that tell calibre'
|
||||
' what color to use. Click the Add Rule button below'
|
||||
' to get started. You can change an existing rule by double'
|
||||
' clicking it.'))
|
||||
l1.setWordWrap(True)
|
||||
l.addWidget(l1, 0, 0, 1, 2)
|
||||
|
||||
self.add_button = QPushButton(QIcon(I('plus.png')), _('Add Rule'),
|
||||
self)
|
||||
self.remove_button = QPushButton(QIcon(I('minus.png')),
|
||||
_('Remove Rule'), self)
|
||||
self.add_button.clicked.connect(self.add_rule)
|
||||
self.remove_button.clicked.connect(self.remove_rule)
|
||||
l.addWidget(self.add_button, 1, 0)
|
||||
l.addWidget(self.remove_button, 1, 1)
|
||||
|
||||
self.g = g = QGridLayout()
|
||||
self.rules_view = QListView(self)
|
||||
self.rules_view.doubleClicked.connect(self.edit_rule)
|
||||
self.rules_view.setSelectionMode(self.rules_view.SingleSelection)
|
||||
self.rules_view.setAlternatingRowColors(True)
|
||||
self.rtfd = RichTextDelegate(parent=self.rules_view, max_width=400)
|
||||
self.rules_view.setItemDelegate(self.rtfd)
|
||||
g.addWidget(self.rules_view, 0, 0, 2, 1)
|
||||
|
||||
self.up_button = b = QToolButton(self)
|
||||
b.setIcon(QIcon(I('arrow-up.png')))
|
||||
b.setToolTip(_('Move the selected rule up'))
|
||||
b.clicked.connect(self.move_up)
|
||||
g.addWidget(b, 0, 1, 1, 1, Qt.AlignTop)
|
||||
self.down_button = b = QToolButton(self)
|
||||
b.setIcon(QIcon(I('arrow-down.png')))
|
||||
b.setToolTip(_('Move the selected rule down'))
|
||||
b.clicked.connect(self.move_down)
|
||||
g.addWidget(b, 1, 1, 1, 1, Qt.AlignBottom)
|
||||
|
||||
l.addLayout(g, 2, 0, 1, 2)
|
||||
l.setRowStretch(2, 10)
|
||||
|
||||
self.add_advanced_button = b = QPushButton(QIcon(I('plus.png')),
|
||||
_('Add Advanced Rule'), self)
|
||||
b.clicked.connect(self.add_advanced)
|
||||
l.addWidget(b, 3, 0, 1, 2)
|
||||
|
||||
def initialize(self, fm, prefs):
|
||||
self.model = RulesModel(prefs, fm)
|
||||
self.rules_view.setModel(self.model)
|
||||
|
||||
def add_rule(self):
|
||||
d = RuleEditor(self.model.fm)
|
||||
d.add_blank_condition()
|
||||
if d.exec_() == d.Accepted:
|
||||
col, r = d.rule
|
||||
if r is not None and col:
|
||||
idx = self.model.add_rule(col, r)
|
||||
self.rules_view.scrollTo(idx)
|
||||
self.changed.emit()
|
||||
|
||||
def add_advanced(self):
|
||||
td = TemplateDialog(self, '', None)
|
||||
if td.exec_() == td.Accepted:
|
||||
self.changed.emit()
|
||||
pass # TODO
|
||||
|
||||
def edit_rule(self, index):
|
||||
try:
|
||||
col, rule = self.model.data(index, Qt.UserRole)
|
||||
except:
|
||||
return
|
||||
if isinstance(rule, Rule):
|
||||
d = RuleEditor(self.model.fm)
|
||||
d.apply_rule(col, rule)
|
||||
if d.exec_() == d.Accepted:
|
||||
col, r = d.rule
|
||||
if r is not None and col:
|
||||
self.model.replace_rule(index, col, r)
|
||||
self.rules_view.scrollTo(index)
|
||||
self.changed.emit()
|
||||
else:
|
||||
td = TemplateDialog(self, rule, None)
|
||||
if td.exec_() == td.Accepted:
|
||||
self.changed.emit()
|
||||
pass # TODO
|
||||
|
||||
def get_selected_row(self, txt):
|
||||
sm = self.rules_view.selectionModel()
|
||||
rows = list(sm.selectedRows())
|
||||
if not rows:
|
||||
error_dialog(self, _('No rule selected'),
|
||||
_('No rule selected for %s.')%txt, show=True)
|
||||
return None
|
||||
return rows[0]
|
||||
|
||||
def remove_rule(self):
|
||||
row = self.get_selected_row(_('removal'))
|
||||
if row is not None:
|
||||
self.model.remove_rule(row)
|
||||
self.changed.emit()
|
||||
|
||||
def move_up(self):
|
||||
idx = self.rules_view.currentIndex()
|
||||
if idx.isValid():
|
||||
idx = self.model.move(idx, -1)
|
||||
if idx is not None:
|
||||
sm = self.rules_view.selectionModel()
|
||||
sm.select(idx, sm.ClearAndSelect)
|
||||
self.rules_view.setCurrentIndex(idx)
|
||||
self.changed.emit()
|
||||
|
||||
def move_down(self):
|
||||
idx = self.rules_view.currentIndex()
|
||||
if idx.isValid():
|
||||
idx = self.model.move(idx, 1)
|
||||
if idx is not None:
|
||||
sm = self.rules_view.selectionModel()
|
||||
sm.select(idx, sm.ClearAndSelect)
|
||||
self.rules_view.setCurrentIndex(idx)
|
||||
self.changed.emit()
|
||||
|
||||
def clear(self):
|
||||
self.model.clear()
|
||||
self.changed.emit()
|
||||
|
||||
def commit(self, prefs):
|
||||
self.model.commit(prefs)
|
||||
|
||||
# }}}
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
|
||||
from calibre.library import db
|
||||
|
||||
db = db()
|
||||
|
||||
if False:
|
||||
d = RuleEditor(db.field_metadata)
|
||||
d.add_blank_condition()
|
||||
d.exec_()
|
||||
|
||||
col, r = d.rule
|
||||
|
||||
print ('Column to be colored:', col)
|
||||
print ('Template:')
|
||||
print (r.template)
|
||||
else:
|
||||
d = EditRules()
|
||||
d.resize(QSize(800, 600))
|
||||
d.initialize(db.field_metadata, db.prefs)
|
||||
d.show()
|
||||
app.exec_()
|
||||
d.commit(db.prefs)
|
||||
|
||||
|
@ -5,21 +5,19 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog,
|
||||
QAbstractListModel, Qt, QColor, QIcon, QToolButton, QComboBox)
|
||||
QAbstractListModel, Qt, QIcon)
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
|
||||
from calibre.gui2.preferences.look_feel_ui import Ui_Form
|
||||
from calibre.gui2 import config, gprefs, qt_app
|
||||
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
|
||||
from calibre.utils.localization import (available_translations,
|
||||
get_language, get_lang)
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.gui2 import NONE
|
||||
from calibre.gui2.book_details import get_field_list
|
||||
from calibre.gui2.preferences.coloring import EditRules
|
||||
|
||||
class DisplayedFields(QAbstractListModel): # {{{
|
||||
|
||||
@ -162,117 +160,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.df_up_button.clicked.connect(self.move_df_up)
|
||||
self.df_down_button.clicked.connect(self.move_df_down)
|
||||
|
||||
self.color_help_text.setText('<p>' +
|
||||
_('Here you can specify coloring rules for columns shown in the '
|
||||
'library view. Choose the column you wish to color, then '
|
||||
'supply a template that specifies the color to use based on '
|
||||
'the values in the column. There is a '
|
||||
'<a href="http://manual.calibre-ebook.com/template_lang.html">'
|
||||
'tutorial</a> on using templates.') +
|
||||
'</p><p>' +
|
||||
_('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. This is by far the easiest '
|
||||
'way to specify a template.') +
|
||||
'</p><p>' +
|
||||
_('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 '
|
||||
'#column, use "{#column}". To show the title in blue if the '
|
||||
'custom column #column contains the value "foo", in red if the '
|
||||
'column contains the value "bar", otherwise in black, use '
|
||||
'<pre>{#column:switch(foo,blue,bar,red,black)}</pre>'
|
||||
'To show the title in blue if the book has the exact tag '
|
||||
'"Science Fiction", red if the book has the exact tag '
|
||||
'"Mystery", or black if the book has neither tag, use'
|
||||
"<pre>program: \n"
|
||||
" t = field('tags'); \n"
|
||||
" first_non_empty(\n"
|
||||
" in_list(t, ',', '^Science Fiction$', 'blue', ''), \n"
|
||||
" in_list(t, ',', '^Mystery$', 'red', 'black'))</pre>"
|
||||
'To show the title in green if it has one format, blue if it '
|
||||
'two formats, and red if more, use'
|
||||
"<pre>program:cmp(count(field('formats'),','), 2, 'green', 'blue', 'red')</pre>") +
|
||||
'</p><p>' +
|
||||
_('You can access a multi-line template editor from the '
|
||||
'context menu (right-click).') + '</p><p>' +
|
||||
_('<b>Note:</b> if you want to color a "custom column with a fixed set '
|
||||
'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.')+ '</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.sort(key=sort_key)
|
||||
choices.insert(0, '')
|
||||
self.column_color_count = db.column_color_count+1
|
||||
|
||||
mi=None
|
||||
try:
|
||||
idx = gui.library_view.currentIndex().row()
|
||||
mi = db.get_metadata(idx, index_is_id=False)
|
||||
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)
|
||||
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()
|
||||
self.edit_rules = EditRules(self.tabWidget)
|
||||
self.edit_rules.changed.connect(self.changed_signal)
|
||||
self.tabWidget.addTab(self.edit_rules,
|
||||
QIcon(I('format-fill-color.png')), _('Column coloring'))
|
||||
self.tabWidget.setCurrentIndex(0)
|
||||
|
||||
def initialize(self):
|
||||
ConfigWidgetBase.initialize(self)
|
||||
@ -283,6 +175,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.current_font = self.initial_font = font
|
||||
self.update_font_display()
|
||||
self.display_model.initialize()
|
||||
db = self.gui.current_db
|
||||
self.edit_rules.initialize(db.field_metadata, db.prefs)
|
||||
|
||||
def restore_defaults(self):
|
||||
ConfigWidgetBase.restore_defaults(self)
|
||||
@ -292,6 +186,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.changed_signal.emit()
|
||||
self.update_font_display()
|
||||
self.display_model.restore_defaults()
|
||||
self.edit_rules.clear()
|
||||
self.changed_signal.emit()
|
||||
|
||||
def build_font_obj(self):
|
||||
@ -341,12 +236,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.changed_signal.emit()
|
||||
|
||||
def commit(self, *args):
|
||||
for i in range(1, self.column_color_count):
|
||||
col = getattr(self, 'opt_column_color_name_'+str(i))
|
||||
tpl = getattr(self, 'opt_column_color_template_'+str(i))
|
||||
if not col.currentIndex() or not unicode(tpl.text()).strip():
|
||||
col.setCurrentIndex(0)
|
||||
tpl.setText('')
|
||||
rr = ConfigWidgetBase.commit(self, *args)
|
||||
if self.current_font != self.initial_font:
|
||||
gprefs['font'] = (self.current_font[:4] if self.current_font else
|
||||
@ -356,10 +245,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
QApplication.setFont(self.font_display.font())
|
||||
rr = True
|
||||
self.display_model.commit()
|
||||
self.edit_rules.commit(self.gui.current_db.prefs)
|
||||
return rr
|
||||
|
||||
def refresh_gui(self, gui):
|
||||
gui.library_view.model().set_color_templates()
|
||||
gui.library_view.model().reset()
|
||||
self.update_font_display()
|
||||
gui.tags_view.reread_collapse_parameters()
|
||||
gui.library_view.refresh_book_details()
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>717</width>
|
||||
<width>820</width>
|
||||
<height>519</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -407,161 +407,6 @@ then the tags will be displayed each on their own line.</string>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_5">
|
||||
<attribute name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/format-fill-color.png</normaloff>:/images/format-fill-color.png</iconset>
|
||||
</attribute>
|
||||
<attribute name="title">
|
||||
<string>Column Coloring</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="column_color_layout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Column to color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Color selection template</string>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>The template wizard is easiest to use</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="color_help_button">
|
||||
<property name="text">
|
||||
<string>Show/hide help text</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="colors_button">
|
||||
<property name="text">
|
||||
<string>Show/hide colors</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="20" column="0">
|
||||
<widget class="QLabel" name="colors_label">
|
||||
<property name="text">
|
||||
<string>Color names</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="21" column="0" colspan="8">
|
||||
<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">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>687</width>
|
||||
<height>194</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="color_help_text">
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="40" column="0">
|
||||
<spacer>
|
||||
<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>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@ -572,11 +417,6 @@ then the tags will be displayed each on their own line.</string>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>calibre/gui2/complete.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>TemplateLineEditor</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>calibre/gui2/dialogs/template_line_editor.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
|
@ -7,7 +7,9 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import json, traceback
|
||||
|
||||
from calibre.gui2 import error_dialog
|
||||
from PyQt4.Qt import QDialogButtonBox
|
||||
|
||||
from calibre.gui2 import error_dialog, warning_dialog
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||
from calibre.gui2.preferences.template_functions_ui import Ui_Form
|
||||
from calibre.gui2.widgets import PythonHighlighter
|
||||
@ -152,10 +154,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
_('Name already used'), show=True)
|
||||
return
|
||||
if self.argument_count.value() == 0:
|
||||
error_dialog(self.gui, _('Template functions'),
|
||||
_('Argument count must be -1 or greater than zero'),
|
||||
show=True)
|
||||
return
|
||||
box = warning_dialog(self.gui, _('Template functions'),
|
||||
_('Argument count should be -1 or greater than zero.'
|
||||
'Setting it to zero means that this function cannot '
|
||||
'be used in single function mode.'), det_msg = '',
|
||||
show=False)
|
||||
box.bb.setStandardButtons(box.bb.standardButtons() | QDialogButtonBox.Cancel)
|
||||
box.det_msg_toggle.setVisible(False)
|
||||
if not box.exec_():
|
||||
return
|
||||
try:
|
||||
prog = unicode(self.program.toPlainText())
|
||||
cls = compile_user_function(name, unicode(self.documentation.toPlainText()),
|
||||
|
78
src/calibre/gui2/store/epubbud_plugin.py
Normal file
78
src/calibre/gui2/store/epubbud_plugin.py
Normal file
@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import urllib
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser, url_slash_cleaner
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
class EpubBudStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
url = 'http://epubbud.com/'
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
'''
|
||||
OPDS based search.
|
||||
|
||||
We really should get the catelog from http://pragprog.com/catalog.opds
|
||||
and look for the application/opensearchdescription+xml entry.
|
||||
Then get the opensearch description to get the search url and
|
||||
format. However, we are going to be lazy and hard code it.
|
||||
'''
|
||||
url = 'http://www.epubbud.com/search.php?format=atom&q=' + urllib.quote_plus(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
# Use html instead of etree as html allows us
|
||||
# to ignore the namespace easily.
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//entry'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//id/text()'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/thumbnail"]/@href'))
|
||||
|
||||
title = u''.join(data.xpath('.//title/text()'))
|
||||
author = u''.join(data.xpath('.//author/name/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = '$0.00'
|
||||
s.detail_item = id.strip()
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.formats = 'EPUB'
|
||||
|
||||
yield s
|
189
src/calibre/library/coloring.py
Normal file
189
src/calibre/library/coloring.py
Normal file
@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
from future_builtins import map
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import binascii, re, json
|
||||
from textwrap import dedent
|
||||
|
||||
class Rule(object): # {{{
|
||||
|
||||
SIGNATURE = '# BasicColorRule():'
|
||||
|
||||
def __init__(self, fm, color=None):
|
||||
self.color = color
|
||||
self.fm = fm
|
||||
self.conditions = []
|
||||
|
||||
def add_condition(self, col, action, val):
|
||||
if col not in self.fm:
|
||||
raise ValueError('%r is not a valid column name'%col)
|
||||
v = self.validate_condition(col, action, val)
|
||||
if v:
|
||||
raise ValueError(v)
|
||||
self.conditions.append((col, action, val))
|
||||
|
||||
def validate_condition(self, col, action, val):
|
||||
m = self.fm[col]
|
||||
dt = m['datatype']
|
||||
if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')):
|
||||
try:
|
||||
int(val) if dt == 'int' else float(val)
|
||||
except:
|
||||
return '%r is not a valid numerical value'%val
|
||||
|
||||
if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern'
|
||||
in action):
|
||||
try:
|
||||
re.compile(val)
|
||||
except:
|
||||
return '%r is not a valid regular expression'%val
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
args = (self.color, self.conditions)
|
||||
sig = json.dumps(args, ensure_ascii=False)
|
||||
return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8'))
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
if not self.color or not self.conditions:
|
||||
return None
|
||||
conditions = map(self.apply_condition, self.conditions)
|
||||
conditions = (',\n' + ' '*9).join(conditions)
|
||||
return dedent('''\
|
||||
program:
|
||||
{sig}
|
||||
test(and(
|
||||
{conditions}
|
||||
), '{color}', '');
|
||||
''').format(sig=self.signature, conditions=conditions,
|
||||
color=self.color)
|
||||
|
||||
def apply_condition(self, condition):
|
||||
col, action, val = condition
|
||||
m = self.fm[col]
|
||||
dt = m['datatype']
|
||||
|
||||
if dt == 'bool':
|
||||
return self.bool_condition(col, action, val)
|
||||
|
||||
if dt in ('int', 'float', 'rating'):
|
||||
return self.number_condition(col, action, val)
|
||||
|
||||
if dt == 'datetime':
|
||||
return self.date_condition(col, action, val)
|
||||
|
||||
if dt in ('comments', 'series', 'text', 'enumeration'):
|
||||
ism = m.get('is_multiple', False)
|
||||
if ism:
|
||||
return self.multiple_condition(col, action, val, ism)
|
||||
return self.text_condition(col, action, val)
|
||||
|
||||
def bool_condition(self, col, action, val):
|
||||
test = {'is true': 'True',
|
||||
'is false': 'False',
|
||||
'is undefined': 'None'}[action]
|
||||
return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col)
|
||||
|
||||
def number_condition(self, col, action, val):
|
||||
lt, eq, gt = {
|
||||
'eq': ('', '1', ''),
|
||||
'lt': ('1', '', ''),
|
||||
'gt': ('', '', '1')
|
||||
}[action]
|
||||
lt, eq, gt = '', '1', ''
|
||||
return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
|
||||
|
||||
def date_condition(self, col, action, val):
|
||||
lt, eq, gt = {
|
||||
'eq': ('', '1', ''),
|
||||
'lt': ('1', '', ''),
|
||||
'gt': ('', '', '1')
|
||||
}[action]
|
||||
return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
|
||||
val, lt, eq, gt)
|
||||
|
||||
def multiple_condition(self, col, action, val, sep):
|
||||
if action == 'is set':
|
||||
return "test('%s', '1', '')"%col
|
||||
if action == 'is not set':
|
||||
return "test('%s', '', '1')"%col
|
||||
if action == 'has':
|
||||
return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
|
||||
if action == 'does not have':
|
||||
return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
|
||||
if action == 'has pattern':
|
||||
return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
|
||||
if action == 'does not have pattern':
|
||||
return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
|
||||
|
||||
def text_condition(self, col, action, val):
|
||||
if action == 'is set':
|
||||
return "test('%s', '1', '')"%col
|
||||
if action == 'is not set':
|
||||
return "test('%s', '', '1')"%col
|
||||
if action == 'is':
|
||||
return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val)
|
||||
if action == 'is not':
|
||||
return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val)
|
||||
if action == 'matches pattern':
|
||||
return "contains(field('%s'), \"%s\", '1', '')"%(col, val)
|
||||
if action == 'does not match pattern':
|
||||
return "contains(field('%s'), \"%s\", '', '1')"%(col, val)
|
||||
|
||||
# }}}
|
||||
|
||||
def rule_from_template(fm, template):
|
||||
ok_lines = []
|
||||
for line in template.splitlines():
|
||||
if line.startswith(Rule.SIGNATURE):
|
||||
raw = line[len(Rule.SIGNATURE):].strip()
|
||||
try:
|
||||
color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8'))
|
||||
except:
|
||||
continue
|
||||
r = Rule(fm)
|
||||
r.color = color
|
||||
for c in conditions:
|
||||
try:
|
||||
r.add_condition(*c)
|
||||
except:
|
||||
continue
|
||||
if r.color and r.conditions:
|
||||
return r
|
||||
else:
|
||||
ok_lines.append(line)
|
||||
return '\n'.join(ok_lines)
|
||||
|
||||
def conditionable_columns(fm):
|
||||
for key in fm:
|
||||
m = fm[key]
|
||||
dt = m['datatype']
|
||||
if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series',
|
||||
'comments', 'text', 'enumeration', 'datetime'):
|
||||
yield key
|
||||
|
||||
def displayable_columns(fm):
|
||||
for key in fm.displayable_field_keys():
|
||||
if key not in ('sort', 'author_sort', 'comments', 'formats',
|
||||
'identifiers', 'path'):
|
||||
yield key
|
||||
|
||||
def migrate_old_rule(fm, template):
|
||||
if template.startswith('program:\n#tag wizard'):
|
||||
rules = []
|
||||
for line in template.splitlines():
|
||||
if line.startswith('#') and ':|:' in line:
|
||||
value, color = line[1:].split(':|:')
|
||||
r = Rule(fm, color=color)
|
||||
r.add_condition('tags', 'has', value)
|
||||
rules.append(r.template)
|
||||
return rules
|
||||
return [template]
|
||||
|
@ -211,10 +211,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
defs = self.prefs.defaults
|
||||
defs['gui_restriction'] = defs['cs_restriction'] = ''
|
||||
defs['categories_using_hierarchy'] = []
|
||||
self.column_color_count = 5
|
||||
for i in range(1,self.column_color_count+1):
|
||||
defs['column_color_name_'+str(i)] = ''
|
||||
defs['column_color_template_'+str(i)] = ''
|
||||
defs['column_color_rules'] = []
|
||||
|
||||
# Migrate the bool tristate tweak
|
||||
defs['bools_are_tristate'] = \
|
||||
@ -222,6 +219,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
if self.prefs.get('bools_are_tristate') is None:
|
||||
self.prefs.set('bools_are_tristate', defs['bools_are_tristate'])
|
||||
|
||||
# Migrate column coloring rules
|
||||
if self.prefs.get('column_color_name_1', None) is not None:
|
||||
from calibre.library.coloring import migrate_old_rule
|
||||
old_rules = []
|
||||
for i in range(1, 5):
|
||||
col = self.prefs.get('column_color_name_'+str(i), None)
|
||||
templ = self.prefs.get('column_color_template_'+str(i), None)
|
||||
if col and templ:
|
||||
try:
|
||||
del self.prefs['column_color_name_'+str(i)]
|
||||
rules = migrate_old_rule(self.field_metadata, templ)
|
||||
for templ in rules:
|
||||
old_rules.append((col, templ))
|
||||
except:
|
||||
pass
|
||||
if old_rules:
|
||||
self.prefs['column_color_rules'] += old_rules
|
||||
|
||||
# Migrate saved search and user categories to db preference scheme
|
||||
def migrate_preference(key, default):
|
||||
oldval = prefs[key]
|
||||
|
@ -240,11 +240,21 @@ def cli_docs(app):
|
||||
raw += '\n'+'\n'.join(lines)
|
||||
update_cli_doc(os.path.join('cli', cmd+'.rst'), raw, info)
|
||||
|
||||
def generate_docs(app):
|
||||
cli_docs(app)
|
||||
template_docs(app)
|
||||
|
||||
def template_docs(app):
|
||||
from template_ref_generate import generate_template_language_help
|
||||
info = app.builder.info
|
||||
raw = generate_template_language_help()
|
||||
update_cli_doc('template_ref.rst', raw, info)
|
||||
|
||||
def setup(app):
|
||||
app.add_config_value('epub_cover', None, False)
|
||||
app.add_builder(EPUBHelpBuilder)
|
||||
app.connect('doctree-read', substitute)
|
||||
app.connect('builder-inited', cli_docs)
|
||||
app.connect('builder-inited', generate_docs)
|
||||
app.connect('build-finished', finished)
|
||||
|
||||
def finished(app, exception):
|
||||
|
@ -1,266 +0,0 @@
|
||||
.. include:: global.rst
|
||||
|
||||
.. _templaterefcalibre:
|
||||
|
||||
Reference for all builtin template language functions
|
||||
========================================================
|
||||
|
||||
Here, we document all the builtin functions available in the |app| template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type.
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
:local:
|
||||
|
||||
.. module:: calibre.utils.formatter_functions
|
||||
|
||||
Get values from metadata
|
||||
--------------------------
|
||||
|
||||
field(name)
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinField
|
||||
|
||||
raw_field(name)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinRaw_field
|
||||
|
||||
booksize()
|
||||
^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinBooksize
|
||||
|
||||
format_date(val, format_string)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinFormatDate
|
||||
|
||||
ondevice()
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinOndevice
|
||||
|
||||
Arithmetic
|
||||
-------------
|
||||
|
||||
add(x, y)
|
||||
^^^^^^^^^^^^^
|
||||
.. autoclass:: BuiltinAdd
|
||||
|
||||
subtract(x, y)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSubtract
|
||||
|
||||
multiply(x, y)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinMultiply
|
||||
|
||||
divide(x, y)
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinDivide
|
||||
|
||||
Boolean
|
||||
------------
|
||||
|
||||
and(value1, value2, ...)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinAnd
|
||||
|
||||
or(value1, value2, ...)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinOr
|
||||
|
||||
not(value)
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinNot
|
||||
|
||||
If-then-else
|
||||
-----------------
|
||||
|
||||
contains(val, pattern, text if match, text if not match)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinContains
|
||||
|
||||
test(val, text if not empty, text if empty)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinTest
|
||||
|
||||
ifempty(val, text if empty)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinIfempty
|
||||
|
||||
Iterating over values
|
||||
------------------------
|
||||
|
||||
first_non_empty(value, value, ...)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinFirstNonEmpty
|
||||
|
||||
lookup(val, pattern, field, pattern, field, ..., else_field)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinLookup
|
||||
|
||||
switch(val, pattern, value, pattern, value, ..., else_value)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSwitch
|
||||
|
||||
List Lookup
|
||||
---------------
|
||||
|
||||
in_list(val, separator, pattern, found_val, not_found_val)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinInList
|
||||
|
||||
str_in_list(val, separator, string, found_val, not_found_val)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinStrInList
|
||||
|
||||
list_item(val, index, separator)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinListitem
|
||||
|
||||
select(val, key)
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSelect
|
||||
|
||||
|
||||
List Manipulation
|
||||
-------------------
|
||||
|
||||
count(val, separator)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinCount
|
||||
|
||||
merge_lists(list1, list2, separator)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinMergeLists
|
||||
|
||||
sublist(val, start_index, end_index, separator)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSublist
|
||||
|
||||
subitems(val, start_index, end_index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSubitems
|
||||
|
||||
Recursion
|
||||
-------------
|
||||
|
||||
eval(template)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinEval
|
||||
|
||||
template(x)
|
||||
^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinTemplate
|
||||
|
||||
Relational
|
||||
-----------
|
||||
|
||||
cmp(x, y, lt, eq, gt)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinCmp
|
||||
|
||||
strcmp(x, y, lt, eq, gt)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinStrcmp
|
||||
|
||||
String case changes
|
||||
---------------------
|
||||
|
||||
lowercase(val)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinLowercase
|
||||
|
||||
uppercase(val)
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinUppercase
|
||||
|
||||
titlecase(val)
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinTitlecase
|
||||
|
||||
capitalize(val)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinCapitalize
|
||||
|
||||
String Manipulation
|
||||
---------------------
|
||||
|
||||
re(val, pattern, replacement)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinRe
|
||||
|
||||
shorten(val, left chars, middle text, right chars)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinShorten
|
||||
|
||||
substr(str, start, end)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSubstr
|
||||
|
||||
|
||||
Other
|
||||
--------
|
||||
|
||||
assign(id, val)
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinAssign
|
||||
|
||||
print(a, b, ...)
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinPrint
|
||||
|
||||
|
||||
API of the Metadata objects
|
||||
----------------------------
|
||||
|
||||
The python implementation of the template functions is passed in a Metadata object. Knowing it's API is useful if you want to define your own template functions.
|
||||
|
||||
.. module:: calibre.ebooks.metadata.book.base
|
||||
|
||||
.. autoclass:: Metadata
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. data:: STANDARD_METADATA_FIELDS
|
||||
|
||||
The set of standard metadata fields.
|
||||
|
||||
.. literalinclude:: ../ebooks/metadata/book/__init__.py
|
||||
:lines: 7-
|
||||
|
92
src/calibre/manual/template_ref_generate.py
Normal file
92
src/calibre/manual/template_ref_generate.py
Normal file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
PREAMBLE = '''\
|
||||
.. include:: global.rst
|
||||
|
||||
.. _templaterefcalibre:
|
||||
|
||||
Reference for all builtin template language functions
|
||||
========================================================
|
||||
|
||||
Here, we document all the builtin functions available in the |app| template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type.
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
:local:
|
||||
|
||||
.. module:: calibre.utils.formatter_functions
|
||||
|
||||
'''
|
||||
|
||||
CATEGORY_TEMPLATE = '''\
|
||||
{category}
|
||||
{dashes}
|
||||
|
||||
'''
|
||||
|
||||
FUNCTION_TEMPLATE = '''\
|
||||
{fs}
|
||||
{hats}
|
||||
|
||||
.. autoclass:: {cn}
|
||||
|
||||
'''
|
||||
|
||||
POSTAMBLE = '''\
|
||||
|
||||
API of the Metadata objects
|
||||
----------------------------
|
||||
|
||||
The python implementation of the template functions is passed in a Metadata object. Knowing it's API is useful if you want to define your own template functions.
|
||||
|
||||
.. module:: calibre.ebooks.metadata.book.base
|
||||
|
||||
.. autoclass:: Metadata
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. data:: STANDARD_METADATA_FIELDS
|
||||
|
||||
The set of standard metadata fields.
|
||||
|
||||
.. literalinclude:: ../ebooks/metadata/book/__init__.py
|
||||
:lines: 7-
|
||||
'''
|
||||
|
||||
|
||||
def generate_template_language_help():
|
||||
from calibre.utils.formatter_functions import all_builtin_functions
|
||||
|
||||
funcs = defaultdict(dict)
|
||||
|
||||
for func in all_builtin_functions:
|
||||
class_name = func.__class__.__name__
|
||||
func_sig = getattr(func, 'doc')
|
||||
x = func_sig.find(' -- ')
|
||||
if x < 0:
|
||||
print 'No sig for ', class_name
|
||||
continue
|
||||
func_sig = func_sig[:x]
|
||||
func_cat = getattr(func, 'category')
|
||||
funcs[func_cat][func_sig] = class_name
|
||||
|
||||
output = PREAMBLE
|
||||
cats = sorted(funcs.keys())
|
||||
for cat in cats:
|
||||
output += CATEGORY_TEMPLATE.format(category=cat, dashes='-'*len(cat))
|
||||
entries = [k for k in sorted(funcs[cat].keys())]
|
||||
for entry in entries:
|
||||
output += FUNCTION_TEMPLATE.format(fs = entry, cn=funcs[cat][entry],
|
||||
hats='^'*len(entry))
|
||||
|
||||
output += POSTAMBLE
|
||||
return output
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate_template_language_help()
|
@ -57,6 +57,7 @@ class FormatterFunction(object):
|
||||
|
||||
doc = _('No documentation provided')
|
||||
name = 'no name provided'
|
||||
category = 'Unknown'
|
||||
arg_count = 0
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, *args):
|
||||
@ -87,6 +88,7 @@ class BuiltinFormatterFunction(FormatterFunction):
|
||||
class BuiltinStrcmp(BuiltinFormatterFunction):
|
||||
name = 'strcmp'
|
||||
arg_count = 5
|
||||
category = 'Relational'
|
||||
__doc__ = doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x '
|
||||
'and y as strings. Returns lt if x < y. Returns eq if x == y. '
|
||||
'Otherwise returns gt.')
|
||||
@ -101,6 +103,7 @@ class BuiltinStrcmp(BuiltinFormatterFunction):
|
||||
|
||||
class BuiltinCmp(BuiltinFormatterFunction):
|
||||
name = 'cmp'
|
||||
category = 'Relational'
|
||||
arg_count = 5
|
||||
__doc__ = doc = _('cmp(x, y, lt, eq, gt) -- compares x and y after converting both to '
|
||||
'numbers. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt.')
|
||||
@ -117,6 +120,7 @@ class BuiltinCmp(BuiltinFormatterFunction):
|
||||
class BuiltinStrcat(BuiltinFormatterFunction):
|
||||
name = 'strcat'
|
||||
arg_count = -1
|
||||
category = 'String Manipulation'
|
||||
__doc__ = doc = _('strcat(a, b, ...) -- can take any number of arguments. Returns a '
|
||||
'string formed by concatenating all the arguments')
|
||||
|
||||
@ -130,6 +134,7 @@ class BuiltinStrcat(BuiltinFormatterFunction):
|
||||
class BuiltinAdd(BuiltinFormatterFunction):
|
||||
name = 'add'
|
||||
arg_count = 2
|
||||
category = 'Arithmetic'
|
||||
__doc__ = doc = _('add(x, y) -- returns x + y. Throws an exception if either x or y are not numbers.')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, x, y):
|
||||
@ -140,6 +145,7 @@ class BuiltinAdd(BuiltinFormatterFunction):
|
||||
class BuiltinSubtract(BuiltinFormatterFunction):
|
||||
name = 'subtract'
|
||||
arg_count = 2
|
||||
category = 'Arithmetic'
|
||||
__doc__ = doc = _('subtract(x, y) -- returns x - y. Throws an exception if either x or y are not numbers.')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, x, y):
|
||||
@ -150,6 +156,7 @@ class BuiltinSubtract(BuiltinFormatterFunction):
|
||||
class BuiltinMultiply(BuiltinFormatterFunction):
|
||||
name = 'multiply'
|
||||
arg_count = 2
|
||||
category = 'Arithmetic'
|
||||
__doc__ = doc = _('multiply(x, y) -- returns x * y. Throws an exception if either x or y are not numbers.')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, x, y):
|
||||
@ -160,6 +167,7 @@ class BuiltinMultiply(BuiltinFormatterFunction):
|
||||
class BuiltinDivide(BuiltinFormatterFunction):
|
||||
name = 'divide'
|
||||
arg_count = 2
|
||||
category = 'Arithmetic'
|
||||
__doc__ = doc = _('divide(x, y) -- returns x / y. Throws an exception if either x or y are not numbers.')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, x, y):
|
||||
@ -170,6 +178,8 @@ class BuiltinDivide(BuiltinFormatterFunction):
|
||||
class BuiltinTemplate(BuiltinFormatterFunction):
|
||||
name = 'template'
|
||||
arg_count = 1
|
||||
category = 'Recursion'
|
||||
|
||||
__doc__ = doc = _('template(x) -- evaluates x as a template. The evaluation is done '
|
||||
'in its own context, meaning that variables are not shared between '
|
||||
'the caller and the template evaluation. Because the { and } '
|
||||
@ -185,6 +195,7 @@ class BuiltinTemplate(BuiltinFormatterFunction):
|
||||
class BuiltinEval(BuiltinFormatterFunction):
|
||||
name = 'eval'
|
||||
arg_count = 1
|
||||
category = 'Recursion'
|
||||
__doc__ = doc = _('eval(template) -- evaluates the template, passing the local '
|
||||
'variables (those \'assign\'ed to) instead of the book metadata. '
|
||||
' This permits using the template processor to construct complex '
|
||||
@ -198,6 +209,7 @@ class BuiltinEval(BuiltinFormatterFunction):
|
||||
class BuiltinAssign(BuiltinFormatterFunction):
|
||||
name = 'assign'
|
||||
arg_count = 2
|
||||
category = 'Other'
|
||||
__doc__ = doc = _('assign(id, val) -- assigns val to id, then returns val. '
|
||||
'id must be an identifier, not an expression')
|
||||
|
||||
@ -208,6 +220,7 @@ class BuiltinAssign(BuiltinFormatterFunction):
|
||||
class BuiltinPrint(BuiltinFormatterFunction):
|
||||
name = 'print'
|
||||
arg_count = -1
|
||||
category = 'Other'
|
||||
__doc__ = doc = _('print(a, b, ...) -- prints the arguments to standard output. '
|
||||
'Unless you start calibre from the command line (calibre-debug -g), '
|
||||
'the output will go to a black hole.')
|
||||
@ -219,14 +232,16 @@ class BuiltinPrint(BuiltinFormatterFunction):
|
||||
class BuiltinField(BuiltinFormatterFunction):
|
||||
name = 'field'
|
||||
arg_count = 1
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('field(name) -- returns the metadata field named by name')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, name):
|
||||
return formatter.get_value(name, [], kwargs)
|
||||
|
||||
class BuiltinRaw_field(BuiltinFormatterFunction):
|
||||
class BuiltinRawField(BuiltinFormatterFunction):
|
||||
name = 'raw_field'
|
||||
arg_count = 1
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('raw_field(name) -- returns the metadata field named by name '
|
||||
'without applying any formatting.')
|
||||
|
||||
@ -236,6 +251,7 @@ class BuiltinRaw_field(BuiltinFormatterFunction):
|
||||
class BuiltinSubstr(BuiltinFormatterFunction):
|
||||
name = 'substr'
|
||||
arg_count = 3
|
||||
category = 'String Manipulation'
|
||||
__doc__ = doc = _('substr(str, start, end) -- returns the start\'th through the end\'th '
|
||||
'characters of str. The first character in str is the zero\'th '
|
||||
'character. If end is negative, then it indicates that many '
|
||||
@ -249,6 +265,7 @@ class BuiltinSubstr(BuiltinFormatterFunction):
|
||||
class BuiltinLookup(BuiltinFormatterFunction):
|
||||
name = 'lookup'
|
||||
arg_count = -1
|
||||
category = 'Iterating over values'
|
||||
__doc__ = doc = _('lookup(val, pattern, field, pattern, field, ..., else_field) -- '
|
||||
'like switch, except the arguments are field (metadata) names, not '
|
||||
'text. The value of the appropriate field will be fetched and used. '
|
||||
@ -276,6 +293,7 @@ class BuiltinLookup(BuiltinFormatterFunction):
|
||||
class BuiltinTest(BuiltinFormatterFunction):
|
||||
name = 'test'
|
||||
arg_count = 3
|
||||
category = 'If-then-else'
|
||||
__doc__ = doc = _('test(val, text if not empty, text if empty) -- return `text if not '
|
||||
'empty` if the field is not empty, otherwise return `text if empty`')
|
||||
|
||||
@ -288,6 +306,7 @@ class BuiltinTest(BuiltinFormatterFunction):
|
||||
class BuiltinContains(BuiltinFormatterFunction):
|
||||
name = 'contains'
|
||||
arg_count = 4
|
||||
category = 'If-then-else'
|
||||
__doc__ = doc = _('contains(val, 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 '
|
||||
@ -303,6 +322,7 @@ class BuiltinContains(BuiltinFormatterFunction):
|
||||
class BuiltinSwitch(BuiltinFormatterFunction):
|
||||
name = 'switch'
|
||||
arg_count = -1
|
||||
category = 'Iterating over values'
|
||||
__doc__ = doc = _('switch(val, pattern, value, pattern, value, ..., else_value) -- '
|
||||
'for each `pattern, value` pair, checks if the field matches '
|
||||
'the regular expression `pattern` and if so, returns that '
|
||||
@ -323,6 +343,7 @@ class BuiltinSwitch(BuiltinFormatterFunction):
|
||||
class BuiltinInList(BuiltinFormatterFunction):
|
||||
name = 'in_list'
|
||||
arg_count = 5
|
||||
category = 'List Lookup'
|
||||
__doc__ = doc = _('in_list(val, separator, pattern, found_val, not_found_val) -- '
|
||||
'treat val as a list of items separated by separator, '
|
||||
'comparing the pattern against each value in the list. If the '
|
||||
@ -340,6 +361,8 @@ class BuiltinInList(BuiltinFormatterFunction):
|
||||
class BuiltinStrInList(BuiltinFormatterFunction):
|
||||
name = 'str_in_list'
|
||||
arg_count = 5
|
||||
category = 'List Lookup'
|
||||
category = 'Iterating over values'
|
||||
__doc__ = doc = _('str_in_list(val, separator, string, found_val, not_found_val) -- '
|
||||
'treat val as a list of items separated by separator, '
|
||||
'comparing the string against each value in the list. If the '
|
||||
@ -360,6 +383,7 @@ class BuiltinStrInList(BuiltinFormatterFunction):
|
||||
class BuiltinRe(BuiltinFormatterFunction):
|
||||
name = 're'
|
||||
arg_count = 3
|
||||
category = 'String Manipulation'
|
||||
__doc__ = doc = _('re(val, pattern, replacement) -- return the field after applying '
|
||||
'the regular expression. All instances of `pattern` are replaced '
|
||||
'with `replacement`. As in all of calibre, these are '
|
||||
@ -371,6 +395,7 @@ class BuiltinRe(BuiltinFormatterFunction):
|
||||
class BuiltinIfempty(BuiltinFormatterFunction):
|
||||
name = 'ifempty'
|
||||
arg_count = 2
|
||||
category = 'If-then-else'
|
||||
__doc__ = doc = _('ifempty(val, text if empty) -- return val if val is not empty, '
|
||||
'otherwise return `text if empty`')
|
||||
|
||||
@ -383,6 +408,7 @@ class BuiltinIfempty(BuiltinFormatterFunction):
|
||||
class BuiltinShorten(BuiltinFormatterFunction):
|
||||
name = 'shorten'
|
||||
arg_count = 4
|
||||
category = 'String Manipulation'
|
||||
__doc__ = doc = _('shorten(val, left chars, middle text, right chars) -- Return a '
|
||||
'shortened version of the field, consisting of `left chars` '
|
||||
'characters from the beginning of the field, followed by '
|
||||
@ -408,6 +434,7 @@ class BuiltinShorten(BuiltinFormatterFunction):
|
||||
class BuiltinCount(BuiltinFormatterFunction):
|
||||
name = 'count'
|
||||
arg_count = 2
|
||||
category = 'List Manipulation'
|
||||
__doc__ = doc = _('count(val, 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 '
|
||||
@ -419,6 +446,7 @@ class BuiltinCount(BuiltinFormatterFunction):
|
||||
class BuiltinListitem(BuiltinFormatterFunction):
|
||||
name = 'list_item'
|
||||
arg_count = 3
|
||||
category = 'List Lookup'
|
||||
__doc__ = doc = _('list_item(val, index, separator) -- interpret the value as a list of '
|
||||
'items separated by `separator`, returning the `index`th item. '
|
||||
'The first item is number zero. The last item can be returned '
|
||||
@ -439,6 +467,7 @@ class BuiltinListitem(BuiltinFormatterFunction):
|
||||
class BuiltinSelect(BuiltinFormatterFunction):
|
||||
name = 'select'
|
||||
arg_count = 2
|
||||
category = 'List Lookup'
|
||||
__doc__ = doc = _('select(val, key) -- interpret the value as a comma-separated list '
|
||||
'of items, with the items being "id:value". Find the pair with the'
|
||||
'id equal to key, and return the corresponding value.'
|
||||
@ -456,6 +485,7 @@ class BuiltinSelect(BuiltinFormatterFunction):
|
||||
class BuiltinSublist(BuiltinFormatterFunction):
|
||||
name = 'sublist'
|
||||
arg_count = 4
|
||||
category = 'List Manipulation'
|
||||
__doc__ = doc = _('sublist(val, start_index, end_index, separator) -- interpret the '
|
||||
'value as a list of items separated by `separator`, returning a '
|
||||
'new list made from the `start_index` to the `end_index` item. '
|
||||
@ -486,6 +516,7 @@ class BuiltinSublist(BuiltinFormatterFunction):
|
||||
class BuiltinSubitems(BuiltinFormatterFunction):
|
||||
name = 'subitems'
|
||||
arg_count = 3
|
||||
category = 'List Manipulation'
|
||||
__doc__ = doc = _('subitems(val, start_index, end_index) -- This function is used to '
|
||||
'break apart lists of items such as genres. It interprets the value '
|
||||
'as a comma-separated list of items, where each item is a period-'
|
||||
@ -523,6 +554,7 @@ class BuiltinSubitems(BuiltinFormatterFunction):
|
||||
class BuiltinFormatDate(BuiltinFormatterFunction):
|
||||
name = 'format_date'
|
||||
arg_count = 2
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('format_date(val, format_string) -- format the value, '
|
||||
'which must be a date, using the format_string, returning a string. '
|
||||
'The formatting codes are: '
|
||||
@ -551,6 +583,7 @@ class BuiltinFormatDate(BuiltinFormatterFunction):
|
||||
class BuiltinUppercase(BuiltinFormatterFunction):
|
||||
name = 'uppercase'
|
||||
arg_count = 1
|
||||
category = 'String case changes'
|
||||
__doc__ = doc = _('uppercase(val) -- return value of the field in upper case')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||
@ -559,6 +592,7 @@ class BuiltinUppercase(BuiltinFormatterFunction):
|
||||
class BuiltinLowercase(BuiltinFormatterFunction):
|
||||
name = 'lowercase'
|
||||
arg_count = 1
|
||||
category = 'String case changes'
|
||||
__doc__ = doc = _('lowercase(val) -- return value of the field in lower case')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||
@ -567,6 +601,7 @@ class BuiltinLowercase(BuiltinFormatterFunction):
|
||||
class BuiltinTitlecase(BuiltinFormatterFunction):
|
||||
name = 'titlecase'
|
||||
arg_count = 1
|
||||
category = 'String case changes'
|
||||
__doc__ = doc = _('titlecase(val) -- return value of the field in title case')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||
@ -575,6 +610,7 @@ class BuiltinTitlecase(BuiltinFormatterFunction):
|
||||
class BuiltinCapitalize(BuiltinFormatterFunction):
|
||||
name = 'capitalize'
|
||||
arg_count = 1
|
||||
category = 'String case changes'
|
||||
__doc__ = doc = _('capitalize(val) -- return value of the field capitalized')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||
@ -583,6 +619,7 @@ class BuiltinCapitalize(BuiltinFormatterFunction):
|
||||
class BuiltinBooksize(BuiltinFormatterFunction):
|
||||
name = 'booksize'
|
||||
arg_count = 0
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('booksize() -- return value of the size field')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals):
|
||||
@ -596,6 +633,7 @@ class BuiltinBooksize(BuiltinFormatterFunction):
|
||||
class BuiltinOndevice(BuiltinFormatterFunction):
|
||||
name = 'ondevice'
|
||||
arg_count = 0
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('ondevice() -- return Yes if ondevice is set, otherwise return '
|
||||
'the empty string')
|
||||
|
||||
@ -607,6 +645,7 @@ class BuiltinOndevice(BuiltinFormatterFunction):
|
||||
class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
|
||||
name = 'first_non_empty'
|
||||
arg_count = -1
|
||||
category = 'Iterating over values'
|
||||
__doc__ = doc = _('first_non_empty(value, value, ...) -- '
|
||||
'returns the first value that is not empty. If all values are '
|
||||
'empty, then the empty value is returned.'
|
||||
@ -623,6 +662,7 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
|
||||
class BuiltinAnd(BuiltinFormatterFunction):
|
||||
name = 'and'
|
||||
arg_count = -1
|
||||
category = 'Boolean'
|
||||
__doc__ = doc = _('and(value, value, ...) -- '
|
||||
'returns the string "1" if all values are not empty, otherwise '
|
||||
'returns the empty string. This function works well with test or '
|
||||
@ -639,6 +679,7 @@ class BuiltinAnd(BuiltinFormatterFunction):
|
||||
class BuiltinOr(BuiltinFormatterFunction):
|
||||
name = 'or'
|
||||
arg_count = -1
|
||||
category = 'Boolean'
|
||||
__doc__ = doc = _('or(value, value, ...) -- '
|
||||
'returns the string "1" if any value is not empty, otherwise '
|
||||
'returns the empty string. This function works well with test or '
|
||||
@ -655,6 +696,7 @@ class BuiltinOr(BuiltinFormatterFunction):
|
||||
class BuiltinNot(BuiltinFormatterFunction):
|
||||
name = 'not'
|
||||
arg_count = 1
|
||||
category = 'Boolean'
|
||||
__doc__ = doc = _('not(value) -- '
|
||||
'returns the string "1" if the value is empty, otherwise '
|
||||
'returns the empty string. This function works well with test or '
|
||||
@ -671,6 +713,7 @@ class BuiltinNot(BuiltinFormatterFunction):
|
||||
class BuiltinMergeLists(BuiltinFormatterFunction):
|
||||
name = 'merge_lists'
|
||||
arg_count = 3
|
||||
category = 'List Manipulation'
|
||||
__doc__ = doc = _('merge_lists(list1, list2, separator) -- '
|
||||
'return a list made by merging the items in list1 and list2, '
|
||||
'removing duplicate items using a case-insensitive compare. If '
|
||||
@ -716,7 +759,7 @@ builtin_not = BuiltinNot()
|
||||
builtin_ondevice = BuiltinOndevice()
|
||||
builtin_or = BuiltinOr()
|
||||
builtin_print = BuiltinPrint()
|
||||
builtin_raw_field = BuiltinRaw_field()
|
||||
builtin_raw_field = BuiltinRawField()
|
||||
builtin_re = BuiltinRe()
|
||||
builtin_select = BuiltinSelect()
|
||||
builtin_shorten = BuiltinShorten()
|
||||
|
Loading…
x
Reference in New Issue
Block a user