Sync to trunk.

This commit is contained in:
John Schember 2010-12-26 20:48:00 -05:00
commit e548421835
14 changed files with 301 additions and 91 deletions

View file

@ -14,22 +14,7 @@
from calibre import isbytestring, force_unicode
from calibre.utils.config import prefs, tweaks
from calibre.utils.icu import strcmp
from calibre.utils.formatter import TemplateFormatter
class SafeFormat(TemplateFormatter):
'''
Provides a format function that substitutes '' for any missing value
'''
def get_value(self, key, args, kwargs):
try:
if key in kwargs:
return kwargs[key]
return key
except:
return key
safe_formatter = SafeFormat()
from calibre.utils.formatter import eval_formatter
class Book(Metadata):
def __init__(self, prefix, lpath, size=None, other=None):
@ -131,10 +116,10 @@ def compute_category_name(self, field_key, field_value, field_meta):
field_name = field_meta['name']
else:
field_name = ''
cat_name = safe_formatter.safe_format(
cat_name = eval_formatter.safe_format(
fmt=tweaks['sony_collection_name_template'],
kwargs={'category':field_name, 'value':field_value},
error_value='', book=None)
error_value='GET_CATEGORY', book=None)
return cat_name.strip()
def get_collections(self, collection_attributes):

View file

@ -44,7 +44,10 @@ def render_rows(data):
key = key.decode(preferred_encoding, 'replace')
if isinstance(txt, str):
txt = txt.decode(preferred_encoding, 'replace')
if '</font>' not in txt:
if key.endswith(u':html'):
key = key[:-5]
txt = comments_to_html(txt)
elif '</font>' not in txt:
txt = prepare_string_for_xml(txt)
if 'id' in data:
if key == _('Path'):

View file

@ -9,15 +9,17 @@
from functools import partial
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \
QDate, QGroupBox, QVBoxLayout, QSizePolicy, \
QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \
QPushButton
from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
from calibre.gui2.comments_editor import Editor as CommentsEditor
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key
from calibre.library.comments import comments_to_html
class Base(object):
@ -186,9 +188,9 @@ def setup_ui(self, parent):
self._box = QGroupBox(parent)
self._box.setTitle('&'+self.col_metadata['name'])
self._layout = QVBoxLayout()
self._tb = QPlainTextEdit(self._box)
self._tb = CommentsEditor(self._box)
self._tb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
self._tb.setTabChangesFocus(True)
#self._tb.setTabChangesFocus(True)
self._layout.addWidget(self._tb)
self._box.setLayout(self._layout)
self.widgets = [self._box]
@ -196,10 +198,10 @@ def setup_ui(self, parent):
def setter(self, val):
if val is None:
val = ''
self._tb.setPlainText(val)
self._tb.html = comments_to_html(val)
def getter(self):
val = unicode(self._tb.toPlainText()).strip()
val = unicode(self._tb.html).strip()
if not val:
val = None
return val

View file

@ -12,6 +12,7 @@
from calibre.gui2 import dynamic, open_local_file
from calibre import fit_image
from calibre.library.comments import comments_to_html
from calibre.utils.icu import sort_key
class BookInfo(QDialog, Ui_BookInfo):
@ -130,9 +131,12 @@ def refresh(self, row):
for f in formats:
f = f.strip()
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
for key in info.keys():
for key in sorted(info.keys(), key=sort_key):
if key == 'id': continue
txt = info[key]
if key.endswith(':html'):
key = key[:-5]
txt = comments_to_html(txt)
if key != _('Path'):
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)

View file

@ -5,6 +5,7 @@
from PyQt4.Qt import Qt, QDialog, QDialogButtonBox
from calibre.gui2.dialogs.comments_dialog_ui import Ui_CommentsDialog
from calibre.library.comments import comments_to_html
class CommentsDialog(QDialog, Ui_CommentsDialog):
@ -18,8 +19,8 @@ def __init__(self, parent, text):
self.setWindowIcon(icon)
if text is not None:
self.textbox.setPlainText(text)
self.textbox.setTabChangesFocus(True)
self.textbox.html = comments_to_html(text)
# self.textbox.setTabChangesFocus(True)
self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK'))
self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel'))

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>336</width>
<height>235</height>
<width>400</width>
<height>400</height>
</rect>
</property>
<property name="sizePolicy">
@ -19,22 +19,29 @@
<property name="windowTitle">
<string>Edit Comments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPlainTextEdit" name="textbox"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="Editor" name="textbox" native="true"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Editor</class>
<extends>QWidget</extends>
<header>calibre/gui2/comments_editor.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>

View file

@ -417,6 +417,8 @@ def prepare_search_and_replace(self):
self.multiple_separator.setFixedWidth(30)
self.multiple_separator.setText(' ::: ')
self.multiple_separator.textChanged.connect(self.s_r_separator_changed)
self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed)
self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed)
def s_r_get_field(self, mi, field):
if field:
@ -439,6 +441,9 @@ def s_r_get_field(self, mi, field):
val = []
return val
def s_r_display_bounds_changed(self, i):
self.s_r_search_field_changed(self.search_field.currentIndex())
def s_r_template_changed(self):
self.s_r_search_field_changed(self.search_field.currentIndex())
@ -454,6 +459,9 @@ def s_r_search_field_changed(self, idx):
mi = self.db.get_metadata(self.ids[i], index_is_id=True)
src = unicode(self.search_field.currentText())
t = self.s_r_get_field(mi, src)
if len(t) > 1:
t = t[self.starting_from.value()-1:
self.starting_from.value()-1 + self.results_count.value()]
w.setText(unicode(self.multiple_separator.text()).join(t))
if self.search_mode.currentIndex() == 0:
@ -466,12 +474,8 @@ def s_r_destination_field_changed(self, txt):
txt = unicode(txt)
if not txt:
txt = unicode(self.search_field.currentText())
self.comma_separated.setEnabled(True)
if txt and txt in self.writable_fields:
self.destination_field_fm = self.db.metadata_for_field(txt)
if self.destination_field_fm['is_multiple']:
self.comma_separated.setEnabled(False)
self.comma_separated.setChecked(True)
self.s_r_paint_results(None)
def s_r_search_mode_changed(self, val):
@ -542,6 +546,22 @@ def s_r_do_destination(self, mi, val):
dest = src
dest_mode = self.replace_mode.currentIndex()
if self.destination_field_fm['is_multiple']:
if self.comma_separated.isChecked():
if dest == 'authors':
splitter = ' & '
else:
splitter = ','
res = []
for v in val:
for x in v.split(splitter):
if x.strip():
res.append(x.strip())
val = res
else:
val = [v.replace(',', '') for v in val]
if dest_mode != 0:
dest_val = mi.get(dest, '')
if dest_val is None:
@ -602,8 +622,9 @@ def s_r_paint_results(self, txt):
try:
result = self.s_r_do_regexp(mi)
t = self.s_r_do_destination(mi, result)
if len(result) > 1 and self.destination_field_fm is not None and \
self.destination_field_fm['is_multiple']:
if len(t) > 1 and self.destination_field_fm['is_multiple']:
t = t[self.starting_from.value()-1:
self.starting_from.value()-1 + self.results_count.value()]
t = unicode(self.multiple_separator.text()).join(t)
else:
t = self.s_r_replace_mode_separator().join(t)

View file

@ -658,11 +658,12 @@ If blank, the source field is used if the field is modifiable</string>
<item>
<widget class="QCheckBox" name="comma_separated">
<property name="toolTip">
<string>Specifies whether a comma should be put between values when copying from a
multiple-valued field to a single-valued field</string>
<string>Specifies whether result items should be split into multiple values or
left as single values. This option has the most effect when the source field is
not multiple and the destination field is multiple</string>
</property>
<property name="text">
<string>&amp;Use comma</string>
<string>Split &amp;result</string>
</property>
<property name="checked">
<bool>true</bool>
@ -684,28 +685,8 @@ multiple-valued field to a single-valued field</string>
</item>
</layout>
</item>
<item row="8" column="1">
<widget class="QLabel" name="xlabel_3">
<property name="text">
<string>Test text</string>
</property>
<property name="buddy">
<cstring>test_text</cstring>
</property>
</widget>
</item>
<item row="8" column="2">
<item row="8" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_21">
<item>
<widget class="QLabel" name="label_51">
<property name="text">
<string>Test result</string>
</property>
<property name="buddy">
<cstring>test_result</cstring>
</property>
</widget>
</item>
<item>
<spacer name="HSpacer_347">
<property name="orientation">
@ -719,10 +700,62 @@ multiple-valued field to a single-valued field</string>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="xlabel_412">
<property name="text">
<string>For multiple-valued fields, sho&amp;w</string>
</property>
<property name="buddy">
<cstring>results_count</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="results_count">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>999</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="xlabel_412">
<property name="text">
<string>values starting a&amp;t</string>
</property>
<property name="buddy">
<cstring>starting_from</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="starting_from">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="xlabel_41">
<property name="text">
<string>Multi&amp;ple separator:</string>
<string>with values separated b&amp;y</string>
</property>
<property name="buddy">
<cstring>multiple_separator</cstring>
@ -756,6 +789,20 @@ multiple-valued field to a single-valued field</string>
</rect>
</property>
<layout class="QGridLayout" name="testgrid">
<item row="7" column="1">
<widget class="QLabel" name="xlabel_3">
<property name="text">
<string>Test text</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QLabel" name="xlabel_3">
<property name="text">
<string>Test result</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_31">
<property name="text">
@ -857,6 +904,8 @@ multiple-valued field to a single-valued field</string>
<tabstop>destination_field</tabstop>
<tabstop>replace_mode</tabstop>
<tabstop>comma_separated</tabstop>
<tabstop>results_count</tabstop>
<tabstop>starting_from</tabstop>
<tabstop>multiple_separator</tabstop>
<tabstop>test_text</tabstop>
<tabstop>test_result</tabstop>

View file

@ -0,0 +1,25 @@
#!/usr/bin/env python
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import Qt, QDialog, QDialogButtonBox
from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog
class TemplateDialog(QDialog, Ui_TemplateDialog):
def __init__(self, parent, text):
QDialog.__init__(self, parent)
Ui_TemplateDialog.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
if text is not None:
self.textbox.setPlainText(text)
self.textbox.setTabChangesFocus(True)
self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK'))
self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel'))

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TemplateDialog</class>
<widget class="QDialog" name="TemplateDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>336</width>
<height>235</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Edit Comments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPlainTextEdit" name="textbox"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TemplateDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>229</x>
<y>211</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>234</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TemplateDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>297</x>
<y>217</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>234</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -13,7 +13,7 @@
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QIcon, QDoubleSpinBox, QVariant, QSpinBox, \
QStyledItemDelegate, QCompleter, \
QComboBox
QComboBox, QTextDocument
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
@ -22,6 +22,8 @@
from calibre.utils.formatter import validation_formatter
from calibre.utils.icu import sort_key
from calibre.gui2.dialogs.comments_dialog import CommentsDialog
from calibre.gui2.dialogs.template_dialog import TemplateDialog
class RatingDelegate(QStyledItemDelegate): # {{{
COLOR = QColor("blue")
@ -294,6 +296,24 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{
Delegate for comments data.
'''
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.document = QTextDocument()
def paint(self, painter, option, index):
style = self.parent().style()
self.document.setHtml(index.data(Qt.DisplayRole).toString())
painter.save()
if hasattr(QStyle, 'CE_ItemViewItem'):
style.drawControl(QStyle.CE_ItemViewItem, option,
painter, self.parent())
elif option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
painter.setClipRect(option.rect)
painter.translate(option.rect.topLeft())
self.document.drawContents(painter)
painter.restore()
def createEditor(self, parent, option, index):
m = index.model()
col = m.column_map[index.column()]
@ -301,11 +321,11 @@ def createEditor(self, parent, option, index):
editor = CommentsDialog(parent, text)
d = editor.exec_()
if d:
m.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole)
m.setData(index, QVariant(editor.textbox.html), Qt.EditRole)
return None
def setModelData(self, editor, model, index):
model.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole)
model.setData(index, QVariant(editor.textbox.html), Qt.EditRole)
# }}}
class CcBoolDelegate(QStyledItemDelegate): # {{{
@ -351,7 +371,7 @@ def __init__(self, parent):
def createEditor(self, parent, option, index):
m = index.model()
text = m.custom_columns[m.column_map[index.column()]]['display']['composite_template']
editor = CommentsDialog(parent, text)
editor = TemplateDialog(parent, text)
editor.setWindowTitle(_("Edit template"))
editor.textbox.setTabChangesFocus(False)
editor.textbox.setTabStopWidth(20)

View file

@ -334,6 +334,8 @@ def custom_keys_to_display():
if key not in cf_to_display:
continue
name, val = mi.format_field(key)
if mi.metadata_for_field(key)['datatype'] == 'comments':
name += ':html'
if val:
data[name] = val
return data

View file

@ -173,6 +173,8 @@ def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix):
extra.append('%s: %s<br />'%(xml(name), xml(format_tag_string(val, ',',
ignore_max=True,
no_tag_count=True))))
elif datatype == 'comments':
extra.append('%s: %s<br />'%(xml(name), comments_to_html(unicode(val))))
else:
extra.append('%s: %s<br />'%(xml(name), xml(unicode(val))))
comments = item[FM['comments']]

View file

@ -36,7 +36,7 @@ def _cmp(self, x, y, lt, eq, gt):
return gt
def _assign(self, target, value):
setattr(self, target, value)
self.variables[target] = value
return value
def _concat(self, *args):
@ -55,18 +55,23 @@ def _math(self, x, y, op=None):
}
x = float(x if x else 0)
y = float(y if y else 0)
return ops[op](x, y)
return unicode(ops[op](x, y))
def _template(self, template):
template = template.replace('[[', '{').replace(']]', '}')
return self.parent.safe_format(template, self.parent.kwargs, 'TEMPLATE',
self.parent.book)
def _eval(self, template):
template = template.replace('[[', '{').replace(']]', '}')
return eval_formatter.safe_format(template, self.variables, 'EVAL', None)
local_functions = {
'add' : (2, partial(_math, op='+')),
'assign' : (2, _assign),
'cmp' : (5, _cmp),
'divide' : (2, partial(_math, op='/')),
'eval' : (1, _eval),
'field' : (1, lambda s, x: s.parent.get_value(x, [], s.parent.kwargs)),
'multiply' : (2, partial(_math, op='*')),
'strcat' : (-1, _concat),
@ -82,7 +87,7 @@ def __init__(self, val, prog, parent):
if prog[1] != '':
self.error(_('failed to scan program. Invalid input {0}').format(prog[1]))
self.parent = parent
setattr(self, '$', val)
self.variables = {'$':val}
def error(self, message):
m = 'Formatter: ' + message + _(' near ')
@ -144,7 +149,7 @@ def expr(self):
# We have an identifier. Determine if it is a function
id = self.token()
if not self.token_op_is_a('('):
return getattr(self, id, _('unknown id ') + id)
return self.variables.get(id, _('unknown id ') + id)
# We have a function.
# Check if it is a known one. We do this here so error reporting is
# better, as it can identify the tokens near the problem.
@ -417,15 +422,18 @@ def safe_format(self, fmt, kwargs, error_value, book):
self.kwargs = kwargs
self.book = book
self.composite_values = {}
try:
ans = self.vformat(fmt, [], kwargs).strip()
except Exception, e:
if DEBUG:
traceback.print_exc()
ans = error_value + ' ' + e.message
if fmt.startswith('program:'):
ans = self._eval_program(None, fmt[8:])
else:
try:
ans = self.vformat(fmt, [], kwargs).strip()
except Exception, e:
if DEBUG:
traceback.print_exc()
ans = error_value + ' ' + e.message
return ans
class ValidateFormat(TemplateFormatter):
class ValidateFormatter(TemplateFormatter):
'''
Provides a format function that substitutes '' for any missing value
'''
@ -435,6 +443,14 @@ def get_value(self, key, args, kwargs):
def validate(self, x):
return self.vformat(x, [], {})
validation_formatter = ValidateFormat()
validation_formatter = ValidateFormatter()
class EvalFormatter(TemplateFormatter):
'''
A template formatter that uses a simple dict instead of an mi instance
'''
def get_value(self, key, args, kwargs):
return kwargs.get(key, _('No such variable ') + key)
eval_formatter = EvalFormatter()