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)