diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 053dd7a743..b0b68033bb 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -15,7 +15,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
-from calibre.gui2 import UNDEFINED_QDATE
+from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.utils.config import tweaks
class Base(object):
@@ -310,6 +310,43 @@ class Series(Base):
self.db.set_custom(book_id, val, extra=s_index,
num=self.col_id, notify=notify, commit=False)
+class Enumeration(Base):
+
+ def setup_ui(self, parent):
+ self.parent = parent
+ self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
+ QComboBox(parent)]
+ w = self.widgets[1]
+ vals = self.col_metadata['display']['enum_values']
+ for v in vals:
+ w.addItem(v)
+
+ def initialize(self, book_id):
+ val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
+ if val is None:
+ # This really shouldn't happen
+ val = self.col_metadata['display']['enum_values'][0]
+ self.initial_val = val
+ val = self.normalize_db_val(val)
+ idx = self.widgets[1].findText(val)
+ if idx < 0:
+ error_dialog(self.parent, '',
+ _('The enumeration "{0}" contains an invalid value '
+ 'that will be set to the default').format(
+ self.col_metadata['name']),
+ show=True, show_copy_button=False)
+
+ idx = 0
+ self.widgets[1].setCurrentIndex(idx)
+
+ def setter(self, val):
+ if val is None:
+ val = ''
+ self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
+
+ def getter(self):
+ return unicode(self.widgets[1].currentText())
+
widgets = {
'bool' : Bool,
'rating' : Rating,
@@ -319,6 +356,7 @@ widgets = {
'text' : Text,
'comments': Comments,
'series': Series,
+ 'enumeration': Enumeration
}
def field_sort(y, z, x=None):
@@ -551,6 +589,63 @@ class BulkSeries(BulkBase):
self.db.set_custom_bulk(book_ids, val, extras=extras,
num=self.col_id, notify=notify)
+class BulkEnumeration(BulkBase, Enumeration):
+
+ def get_initial_value(self, book_ids):
+ value = None
+ ret_value = None
+ dialog_shown = False
+ for book_id in book_ids:
+ val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
+ if val not in self.col_metadata['display']['enum_values']:
+ if not dialog_shown:
+ error_dialog(self.parent, '',
+ _('The enumeration "{0}" contains invalid values '
+ 'that will not appear in the list').format(
+ self.col_metadata['name']),
+ show=True, show_copy_button=False)
+ dialog_shown = True
+ ret_value = ' nochange '
+ elif value is not None and value != val:
+ ret_value = ' nochange '
+ value = val
+ if ret_value is None:
+ return value
+ return ret_value
+
+ def setup_ui(self, parent):
+ self.parent = parent
+ self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
+ QComboBox(parent)]
+ w = self.widgets[1]
+ vals = self.col_metadata['display']['enum_values']
+ w.addItem('Do Not Change')
+ for v in vals:
+ w.addItem(v)
+
+ def getter(self):
+ if self.widgets[1].currentIndex() == 0:
+ return ' nochange '
+ return unicode(self.widgets[1].currentText())
+
+ def setter(self, val):
+ if val == ' nochange ':
+ self.widgets[1].setCurrentIndex(0)
+ else:
+ self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
+
+ def commit(self, book_ids, notify=False):
+ val = self.gui_val
+ val = self.normalize_ui_val(val)
+ if val != self.initial_val and val != ' nochange ':
+ self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
+
+ def normalize_db_val(self, val):
+ if val is None:
+ # this really shouldn't happen
+ val = self.col_metadata['display']['enum_values'][0]
+ return val
+
class RemoveTags(QWidget):
def __init__(self, parent, values):
@@ -656,4 +751,5 @@ bulk_widgets = {
'datetime': BulkDateTime,
'text' : BulkText,
'series': BulkSeries,
+ 'enumeration': BulkEnumeration,
}
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py
index e30e0e16e1..7c6125d537 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.py
+++ b/src/calibre/gui2/dialogs/metadata_bulk.py
@@ -255,7 +255,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
fm = self.db.field_metadata
for f in fm:
if (f in ['author_sort'] or
- (fm[f]['datatype'] in ['text', 'series']
+ (fm[f]['datatype'] in ['text', 'series', 'enumeration']
and fm[f].get('search_terms', None)
and f not in ['formats', 'ondevice', 'sort'])):
self.all_fields.append(f)
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py
index f9ba612bc9..bb73a55fc9 100644
--- a/src/calibre/gui2/library/delegates.py
+++ b/src/calibre/gui2/library/delegates.py
@@ -254,6 +254,44 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
# }}}
+class CcEnumDelegate(QStyledItemDelegate): # {{{
+ '''
+ Delegate for text/int/float data.
+ '''
+
+ def createEditor(self, parent, option, index):
+ m = index.model()
+ col = m.column_map[index.column()]
+ editor = QComboBox(parent)
+ for v in m.custom_columns[col]['display']['enum_values']:
+ editor.addItem(v)
+ return editor
+
+ def setModelData(self, editor, model, index):
+ val = unicode(editor.currentText())
+ m = index.model()
+ col = m.column_map[index.column()]
+ if val not in m.custom_columns[col]['display']['enum_values']:
+ # This shouldn't happen ...
+ print 'shouldnt happen'
+ val = m.custom_columns[col]['display']['enum_values'][0]
+ model.setData(index, QVariant(val), Qt.EditRole)
+
+ def setEditorData(self, editor, index):
+ m = index.model()
+ val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
+ if val is None:
+ # This shouldn't happen
+ m = index.model()
+ col = m.column_map[index.column()]
+ val = m.custom_columns[col]['display']['enum_values'][0]
+ idx = editor.findText(val)
+ if idx < 0:
+ editor.setCurrentIndex(0)
+ else:
+ editor.setCurrentIndex(idx)
+# }}}
+
class CcCommentsDelegate(QStyledItemDelegate): # {{{
'''
Delegate for comments data.
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 4ef2c34c03..bee90fc44c 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -634,7 +634,7 @@ class BooksModel(QAbstractTableModel): # {{{
for col in self.custom_columns:
idx = self.custom_columns[col]['rec_index']
datatype = self.custom_columns[col]['datatype']
- if datatype in ('text', 'comments', 'composite'):
+ if datatype in ('text', 'comments', 'composite', 'enumeration'):
self.dc[col] = functools.partial(text_type, idx=idx,
mult=self.custom_columns[col]['is_multiple'])
elif datatype in ('int', 'float'):
@@ -722,7 +722,9 @@ class BooksModel(QAbstractTableModel): # {{{
if typ in ('text', 'comments'):
val = unicode(value.toString()).strip()
val = val if val else None
- if typ == 'bool':
+ elif typ == 'enumeration':
+ val = unicode(value.toString()).strip()
+ elif typ == 'bool':
val = value.toPyObject()
elif typ == 'rating':
val = value.toInt()[0]
diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py
index fadb8314e3..f724ca7b58 100644
--- a/src/calibre/gui2/library/views.py
+++ b/src/calibre/gui2/library/views.py
@@ -14,7 +14,8 @@ from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
- CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate
+ CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, \
+ CcEnumDelegate
from calibre.gui2.library.models import BooksModel, DeviceBooksModel
from calibre.utils.config import tweaks, prefs
from calibre.gui2 import error_dialog, gprefs
@@ -76,6 +77,7 @@ class BooksView(QTableView): # {{{
self.publisher_delegate = TextDelegate(self)
self.text_delegate = TextDelegate(self)
self.cc_text_delegate = CcTextDelegate(self)
+ self.cc_enum_delegate = CcEnumDelegate(self)
self.cc_bool_delegate = CcBoolDelegate(self)
self.cc_comments_delegate = CcCommentsDelegate(self)
self.cc_template_delegate = CcTemplateDelegate(self)
@@ -427,6 +429,8 @@ class BooksView(QTableView): # {{{
self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
elif cc['datatype'] == 'composite':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate)
+ elif cc['datatype'] == 'enumeration':
+ self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate)
else:
dattr = colhead+'_delegate'
delegate = colhead if hasattr(self, dattr) else 'text'
diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py
index ebf33784d4..419fed046d 100644
--- a/src/calibre/gui2/preferences/create_custom_column.py
+++ b/src/calibre/gui2/preferences/create_custom_column.py
@@ -27,18 +27,20 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
3:{'datatype':'series',
'text':_('Text column for keeping series-like information'),
'is_multiple':False},
- 4:{'datatype':'datetime',
+ 4:{'datatype':'enumeration',
+ 'text':_('Text, but with a fixed set of permitted values'), 'is_multiple':False},
+ 5:{'datatype':'datetime',
'text':_('Date'), 'is_multiple':False},
- 5:{'datatype':'float',
+ 6:{'datatype':'float',
'text':_('Floating point numbers'), 'is_multiple':False},
- 6:{'datatype':'int',
+ 7:{'datatype':'int',
'text':_('Integers'), 'is_multiple':False},
- 7:{'datatype':'rating',
+ 8:{'datatype':'rating',
'text':_('Ratings, shown with stars'),
'is_multiple':False},
- 8:{'datatype':'bool',
+ 9:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False},
- 9:{'datatype':'composite',
+ 10:{'datatype':'composite',
'text':_('Column built from other columns'), 'is_multiple':False},
}
@@ -59,6 +61,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.editing_col = editing
self.standard_colheads = standard_colheads
self.standard_colnames = standard_colnames
+ self.column_type_box.setMaxVisibleItems(len(self.column_types))
for t in self.column_types:
self.column_type_box.addItem(self.column_types[t]['text'])
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
@@ -91,6 +94,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.date_format_box.setText(c['display'].get('date_format', ''))
elif ct == 'composite':
self.composite_box.setText(c['display'].get('composite_template', ''))
+ elif ct == 'enumeration':
+ self.enum_box.setText(','.join(c['display'].get('enum_values', [])))
self.datatype_changed()
self.exec_()
@@ -103,7 +108,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime')
for x in ('box', 'default_label', 'label'):
getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
-
+ for x in ('box', 'default_label', 'label'):
+ getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
def accept(self):
col = unicode(self.column_name_box.text())
@@ -145,17 +151,27 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
return self.simple_error('', _('The heading %s is already used')%col_heading)
display_dict = {}
+
if col_type == 'datetime':
if self.date_format_box.text():
display_dict = {'date_format':unicode(self.date_format_box.text())}
else:
display_dict = {'date_format': None}
-
- if col_type == 'composite':
+ elif col_type == 'composite':
if not self.composite_box.text():
return self.simple_error('', _('You must enter a template for'
' composite columns'))
display_dict = {'composite_template':unicode(self.composite_box.text())}
+ elif col_type == 'enumeration':
+ if not self.enum_box.text():
+ return self.simple_error('', _('You must enter at least one'
+ ' value for enumeration columns'))
+ l = [v.strip() for v in unicode(self.enum_box.text()).split(',')]
+ for i in range(0, len(l)-1):
+ if l[i] in l[i+1:]:
+ return self.simple_error('', _('The value "{0}" is in the '
+ 'list more than once').format(l[i]))
+ display_dict = {'enum_values': l}
db = self.parent.gui.library_view.model().db
key = db.field_metadata.custom_field_prefix+col
diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui
index 640becca8c..360c1a4345 100644
--- a/src/calibre/gui2/preferences/create_custom_column.ui
+++ b/src/calibre/gui2/preferences/create_custom_column.ui
@@ -10,7 +10,7 @@
0
0
528
- 199
+ 212
@@ -24,7 +24,7 @@
-
-
+
QLayout::SetDefaultConstraint
@@ -56,7 +56,7 @@
- -
+
-
@@ -69,7 +69,7 @@
- -
+
-
Column heading in the library view and category name in the tag browser
@@ -86,7 +86,7 @@
- -
+
-
@@ -105,7 +105,7 @@
- -
+
-
-
@@ -147,7 +147,7 @@
- -
+
-
-
@@ -158,7 +158,7 @@
- <p>Field template. Uses the same syntax as save templates.
+ Field template. Uses the same syntax as save templates.
@@ -184,7 +184,7 @@
- -
+
-
Qt::Vertical
@@ -197,6 +197,46 @@
+ -
+
+
+ Values
+
+
+ enum_box
+
+
+
+ -
+
+
-
+
+
+ A comma-separated list of permitted values. You can specify
+empty values by entering only the comma. For example, the list
+',one,two,three' has 4 valid values, one of them empty. The first
+value in the list is the default.
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Default: (nothing)
+
+
+ The first value entered will be the default value for this enumeration
+
+
+
+
+
-
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 6148019906..8365cfe773 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -254,6 +254,12 @@ class ResultCache(SearchQueryParser): # {{{
if field_metadata[key]['datatype'] == 'composite':
self.composites.append((key, field_metadata[key]['rec_index']))
+ self.enumerations = []
+ for key in field_metadata:
+ if field_metadata[key]['datatype'] == 'enumeration':
+ self.enumerations.append((field_metadata[key]['display']['enum_values'][0],
+ field_metadata[key]['rec_index']))
+
def __getitem__(self, row):
return self._data[self._map_filtered[row]]
@@ -520,7 +526,7 @@ class ResultCache(SearchQueryParser): # {{{
if len(self.field_metadata[x]['search_terms']):
db_col[x] = self.field_metadata[x]['rec_index']
if self.field_metadata[x]['datatype'] not in \
- ['composite', 'text', 'comments', 'series']:
+ ['composite', 'text', 'comments', 'series', 'enumeration']:
exclude_fields.append(db_col[x])
col_datatype[db_col[x]] = self.field_metadata[x]['datatype']
is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple']
@@ -691,6 +697,10 @@ class ResultCache(SearchQueryParser): # {{{
mi = db.get_metadata(id, index_is_id=True)
for k,c in self.composites:
self._data[id][c] = mi.get(k, None)
+ if len(self.enumerations) > 0:
+ for v,c in self.enumerations:
+ if self._data[id][c] is None:
+ self._data[id][c] = v
except IndexError:
return None
try:
@@ -711,6 +721,10 @@ class ResultCache(SearchQueryParser): # {{{
mi = db.get_metadata(id, index_is_id=True)
for k,c in self.composites:
self._data[id][c] = mi.get(k)
+ if len(self.enumerations) > 0:
+ for v,c in self.self._data[id][c]:
+ if self._data[id][c] is None:
+ self._data[id][c] = v
self._map[0:0] = ids
self._map_filtered[0:0] = ids
@@ -740,6 +754,10 @@ class ResultCache(SearchQueryParser): # {{{
mi = db.get_metadata(item[0], index_is_id=True)
for k,c in self.composites:
item[c] = mi.get(k)
+ if len(self.enumerations) > 0:
+ for v,c in self.enumerations:
+ if item[c] is None:
+ item[c] = v
self._map = [i[0] for i in self._data if i is not None]
if field is not None:
@@ -828,7 +846,7 @@ class SortKeyGenerator(object):
sidx = record[sidx_fm['rec_index']]
val = (val, sidx)
- elif dt in ('text', 'comments', 'composite'):
+ elif dt in ('text', 'comments', 'composite', 'enumeration'):
if val is None:
val = ''
val = val.lower()
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index fdd78e89f8..dc3a67e860 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -18,7 +18,7 @@ from calibre.utils.date import parse_date
class CustomColumns(object):
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
- 'int', 'float', 'bool', 'series', 'composite'])
+ 'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
def custom_table_names(self, num):
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
@@ -144,7 +144,8 @@ class CustomColumns(object):
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
'datetime' : adapt_datetime,
'text':adapt_text,
- 'series':adapt_text
+ 'series':adapt_text,
+ 'enumeration': adapt_text
}
# Create Tag Browser categories for custom columns
@@ -176,6 +177,8 @@ class CustomColumns(object):
ans = ans.split('|') if ans else []
if data['display'].get('sort_alpha', False):
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
+ elif data['datatype'] == 'enumeration' and ans is None:
+ ans = data['display']['enum_values'][0]
return ans
def get_custom_extra(self, idx, label=None, num=None, index_is_id=False):
@@ -439,6 +442,9 @@ class CustomColumns(object):
val = self.custom_data_adapters[data['datatype']](val, data)
if data['normalized']:
+ if data['datatype'] == 'enumeration' and \
+ val not in data['display']['enum_values']:
+ return None
if not append or not data['is_multiple']:
self.conn.execute('DELETE FROM %s WHERE book=?'%lt, (id_,))
self.conn.execute(
@@ -558,7 +564,7 @@ class CustomColumns(object):
if datatype in ('rating', 'int'):
dt = 'INT'
- elif datatype in ('text', 'comments', 'series', 'composite'):
+ elif datatype in ('text', 'comments', 'series', 'composite', 'enumeration'):
dt = 'TEXT'
elif datatype in ('float',):
dt = 'REAL'
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index 9217aca566..1be6604d5d 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -83,7 +83,7 @@ class FieldMetadata(dict):
'''
VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
- 'int', 'float', 'bool', 'series', 'composite'])
+ 'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
# Builtin metadata {{{
diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py
index 8aa76b2643..7a86447090 100644
--- a/src/calibre/library/sqlite.py
+++ b/src/calibre/library/sqlite.py
@@ -115,7 +115,6 @@ def pynocase(one, two, encoding='utf-8'):
pass
return cmp(one.lower(), two.lower())
-
def load_c_extensions(conn, debug=DEBUG):
try:
conn.enable_load_extension(True)