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.constants import preferred_encoding
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 @@ class CollectionsBookList(BookList):
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 @@ import sys
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 @@ class Comments(Base):
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 @@ class Comments(Base):
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.dialogs.book_info_ui import Ui_BookInfo
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 @@ class BookInfo(QDialog, Ui_BookInfo):
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 @@ __license__ = 'GPL v3'
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 @@ class CommentsDialog(QDialog, Ui_CommentsDialog):
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">
@ -21,7 +21,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPlainTextEdit" name="textbox"/>
<widget class="Editor" name="textbox" native="true"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
@ -35,6 +35,13 @@
</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 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
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 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
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 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
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 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
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 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
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 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
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 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \
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.config import tweaks
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 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{
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 @@ class CcTemplateDelegate(QStyledItemDelegate): # {{{
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 @@ class BooksModel(QAbstractTableModel): # {{{
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 @@ class _Parser(object):
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 @@ class _Parser(object):
}
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 @@ class _Parser(object):
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 @@ class _Parser(object):
# 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,6 +422,9 @@ class TemplateFormatter(string.Formatter):
self.kwargs = kwargs
self.book = book
self.composite_values = {}
if fmt.startswith('program:'):
ans = self._eval_program(None, fmt[8:])
else:
try:
ans = self.vformat(fmt, [], kwargs).strip()
except Exception, e:
@ -425,7 +433,7 @@ class TemplateFormatter(string.Formatter):
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 @@ class ValidateFormat(TemplateFormatter):
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()