diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 2bf23e4b82..91dcc29230 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -575,7 +575,10 @@ class Metadata(object): orig_res = res datatype = cmeta['datatype'] if datatype == 'text' and cmeta['is_multiple']: - res = u', '.join(sorted(res, key=sort_key)) + if cmeta['display'].get('is_names', False): + res = u' & '.join(res) + else: + res = u', '.join(sorted(res, key=sort_key)) elif datatype == 'series' and series_with_index: if self.get_extra(key) is not None: res = res + \ diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index beaca77a38..10602fb28c 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -226,10 +226,18 @@ class Comments(Base): class Text(Base): def setup_ui(self, parent): + if self.col_metadata['display'].get('is_names', False): + self.sep = u' & ' + else: + self.sep = u', ' values = self.all_values = list(self.db.all_custom(num=self.col_id)) values.sort(key=sort_key) if self.col_metadata['is_multiple']: w = MultiCompleteLineEdit(parent) + w.set_separator(self.sep.strip()) + if self.sep == u' & ': + w.set_space_before_sep(True) + w.set_add_separator(tweaks['authors_completer_append_separator']) w.update_items_cache(values) w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) else: @@ -261,12 +269,12 @@ class Text(Base): if self.col_metadata['is_multiple']: if not val: val = [] - self.widgets[1].setText(u', '.join(val)) + self.widgets[1].setText(self.sep.join(val)) def getter(self): if self.col_metadata['is_multiple']: val = unicode(self.widgets[1].text()).strip() - ans = [x.strip() for x in val.split(',') if x.strip()] + ans = [x.strip() for x in val.split(self.sep.strip()) if x.strip()] if not ans: ans = None return ans @@ -847,13 +855,20 @@ class BulkText(BulkBase): self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self.adding_widget = self.main_widget - w = RemoveTags(parent, values) - self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' + - _('tags to remove'), parent)) - self.widgets.append(w) - self.removing_widget = w - w.tags_box.textChanged.connect(self.a_c_checkbox_changed) - w.checkbox.stateChanged.connect(self.a_c_checkbox_changed) + if not self.col_metadata['display'].get('is_names', False): + w = RemoveTags(parent, values) + self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' + + _('tags to remove'), parent)) + self.widgets.append(w) + self.removing_widget = w + self.main_widget.set_separator(',') + w.tags_box.textChanged.connect(self.a_c_checkbox_changed) + w.checkbox.stateChanged.connect(self.a_c_checkbox_changed) + else: + self.main_widget.set_separator('&') + self.main_widget.set_space_before_sep(True) + self.main_widget.set_add_separator( + tweaks['authors_completer_append_separator']) else: self.make_widgets(parent, MultiCompleteComboBox) self.main_widget.set_separator(None) @@ -882,21 +897,26 @@ class BulkText(BulkBase): if not self.a_c_checkbox.isChecked(): return if self.col_metadata['is_multiple']: - remove_all, adding, rtext = self.gui_val - remove = set() - if remove_all: - remove = set(self.db.all_custom(num=self.col_id)) + if self.col_metadata['display'].get('is_names', False): + val = self.gui_val + add = [v.strip() for v in val.split('&') if v.strip()] + self.db.set_custom_bulk(book_ids, add, num=self.col_id) else: - txt = rtext + remove_all, adding, rtext = self.gui_val + remove = set() + if remove_all: + remove = set(self.db.all_custom(num=self.col_id)) + else: + txt = rtext + if txt: + remove = set([v.strip() for v in txt.split(',')]) + txt = adding if txt: - remove = set([v.strip() for v in txt.split(',')]) - txt = adding - if txt: - add = set([v.strip() for v in txt.split(',')]) - else: - add = set() - self.db.set_custom_bulk_multiple(book_ids, add=add, remove=remove, - num=self.col_id) + add = set([v.strip() for v in txt.split(',')]) + else: + add = set() + self.db.set_custom_bulk_multiple(book_ids, add=add, + remove=remove, num=self.col_id) else: val = self.gui_val val = self.normalize_ui_val(val) @@ -905,10 +925,11 @@ class BulkText(BulkBase): def getter(self): if self.col_metadata['is_multiple']: - return self.removing_widget.checkbox.isChecked(), \ - unicode(self.adding_widget.text()), \ - unicode(self.removing_widget.tags_box.text()) - + if not self.col_metadata['display'].get('is_names', False): + return self.removing_widget.checkbox.isChecked(), \ + unicode(self.adding_widget.text()), \ + unicode(self.removing_widget.tags_box.text()) + return unicode(self.adding_widget.text()) val = unicode(self.main_widget.currentText()).strip() if not val: val = None diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 9b25545252..4a9a320561 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -653,7 +653,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): if self.destination_field_fm['is_multiple']: if self.comma_separated.isChecked(): - if dest == 'authors': + if dest == 'authors' or \ + (self.destination_field_fm['is_custom'] and + self.destination_field_fm['datatype'] == 'text' and + self.destination_field_fm['display'].get('is_names', False)): splitter = ' & ' else: splitter = ',' diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index a200562ea9..c921ea125f 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -640,18 +640,18 @@ class BooksModel(QAbstractTableModel): # {{{ return self.bool_yes_icon return self.bool_blank_icon - def text_type(r, mult=False, idx=-1): + def text_type(r, mult=None, idx=-1): text = self.db.data[r][idx] - if text and mult: - return QVariant(', '.join(sorted(text.split('|'),key=sort_key))) + if text and mult is not None: + if mult: + return QVariant(u' & '.join(text.split('|'))) + return QVariant(u', '.join(sorted(text.split('|'),key=sort_key))) return QVariant(text) - def decorated_text_type(r, mult=False, idx=-1): + def decorated_text_type(r, idx=-1): text = self.db.data[r][idx] if force_to_bool(text) is not None: return None - if text and mult: - return QVariant(', '.join(sorted(text.split('|'),key=sort_key))) return QVariant(text) def number_type(r, idx=-1): @@ -659,7 +659,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.dc = { 'title' : functools.partial(text_type, - idx=self.db.field_metadata['title']['rec_index'], mult=False), + idx=self.db.field_metadata['title']['rec_index'], mult=None), 'authors' : functools.partial(authors, idx=self.db.field_metadata['authors']['rec_index']), 'size' : functools.partial(size, @@ -671,14 +671,14 @@ class BooksModel(QAbstractTableModel): # {{{ 'rating' : functools.partial(rating_type, idx=self.db.field_metadata['rating']['rec_index']), 'publisher': functools.partial(text_type, - idx=self.db.field_metadata['publisher']['rec_index'], mult=False), + idx=self.db.field_metadata['publisher']['rec_index'], mult=None), 'tags' : functools.partial(tags, idx=self.db.field_metadata['tags']['rec_index']), 'series' : functools.partial(series_type, idx=self.db.field_metadata['series']['rec_index'], siix=self.db.field_metadata['series_index']['rec_index']), 'ondevice' : functools.partial(text_type, - idx=self.db.field_metadata['ondevice']['rec_index'], mult=False), + idx=self.db.field_metadata['ondevice']['rec_index'], mult=None), } self.dc_decorator = { @@ -692,11 +692,12 @@ class BooksModel(QAbstractTableModel): # {{{ datatype = self.custom_columns[col]['datatype'] if datatype in ('text', 'comments', 'composite', 'enumeration'): mult=self.custom_columns[col]['is_multiple'] + if mult is not None: + mult = self.custom_columns[col]['display'].get('is_names', False) self.dc[col] = functools.partial(text_type, idx=idx, mult=mult) if datatype in ['text', 'composite', 'enumeration'] and not mult: if self.custom_columns[col]['display'].get('use_decorations', False): - self.dc[col] = functools.partial(decorated_text_type, - idx=idx, mult=mult) + self.dc[col] = functools.partial(decorated_text_type, idx=idx) self.dc_decorator[col] = functools.partial( bool_type_decorator, idx=idx, bool_cols_are_tristate= diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index c62936a46f..0cce33da9e 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -78,6 +78,7 @@ class BooksView(QTableView): # {{{ self.pubdate_delegate = PubDateDelegate(self) self.tags_delegate = CompleteDelegate(self, ',', 'all_tags') self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True) + self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True) self.series_delegate = TextDelegate(self) self.publisher_delegate = TextDelegate(self) self.text_delegate = TextDelegate(self) @@ -410,6 +411,7 @@ class BooksView(QTableView): # {{{ self.save_state() self._model.set_database(db) self.tags_delegate.set_database(db) + self.cc_names_delegate.set_database(db) self.authors_delegate.set_database(db) self.series_delegate.set_auto_complete_function(db.all_series) self.publisher_delegate.set_auto_complete_function(db.all_publishers) @@ -431,12 +433,17 @@ class BooksView(QTableView): # {{{ self.setItemDelegateForColumn(cm.index(colhead), delegate) elif cc['datatype'] == 'comments': self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate) - elif cc['datatype'] in ('text', 'series'): + elif cc['datatype'] == 'text': if cc['is_multiple']: - self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate) + if cc['display'].get('is_names', False): + self.setItemDelegateForColumn(cm.index(colhead), + self.cc_names_delegate) + else: + self.setItemDelegateForColumn(cm.index(colhead), + self.tags_delegate) else: self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) - elif cc['datatype'] in ('int', 'float'): + elif cc['datatype'] in ('series', 'int', 'float'): self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] == 'bool': self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate) diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index cee34f150e..e56f365b60 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -125,6 +125,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): self.datatype_changed() if ct in ['text', 'composite', 'enumeration']: self.use_decorations.setChecked(c['display'].get('use_decorations', False)) + elif ct == '*text': + self.is_names.setChecked(c['display'].get('is_names', False)) self.exec_() def shortcut_activated(self, url): @@ -167,6 +169,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): for x in ('box', 'default_label', 'label'): getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration') self.use_decorations.setVisible(col_type in ['text', 'composite', 'enumeration']) + self.is_names.setVisible(col_type == '*text') def accept(self): col = unicode(self.column_name_box.text()).strip() @@ -241,6 +244,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): return self.simple_error('', _('The value "{0}" is in the ' 'list more than once').format(l[i])) display_dict = {'enum_values': l} + elif col_type == 'text' and is_multiple: + display_dict = {'is_names': self.is_names.isChecked()} if col_type in ['text', 'composite', 'enumeration']: display_dict['use_decorations'] = self.use_decorations.checkState() diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui index 3290d3c846..16430375bd 100644 --- a/src/calibre/gui2/preferences/create_custom_column.ui +++ b/src/calibre/gui2/preferences/create_custom_column.ui @@ -120,6 +120,16 @@ Everything else will show nothing. + + + + Contains names + + + Check this box if this column contains names, like the authors column. + + + diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 206f2b97fb..a2d2236039 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -64,8 +64,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('tags_browser_collapse_at', gprefs) choices = set([k for k in db.field_metadata.all_field_keys() - if db.field_metadata[k]['is_category'] and - db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']]) + if db.field_metadata[k]['is_category'] and + (db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']) and + not db.field_metadata[k]['display'].get('is_names', False)]) choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers']) choices |= set(['search']) self.opt_categories_using_hierarchy.update_items_cache(choices) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 34fa3a8b10..6b1ce2f851 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -658,8 +658,7 @@ class TagTreeItem(object): # {{{ def tag_data(self, role): tag = self.tag - if tag.category == 'authors' and \ - tweaks['categories_use_field_for_author_name'] == 'author_sort': + if tag.use_sort_as_name: name = tag.sort tt_author = True else: @@ -1275,6 +1274,7 @@ class TagsModel(QAbstractItemModel): # {{{ if len(components) == 0 or '.'.join(components) != tag.original_name: components = [tag.original_name] if (not tag.is_hierarchical) and (in_uc or + (fm['is_custom'] and fm['display'].get('is_names', False)) or key in ['authors', 'publisher', 'news', 'formats', 'rating'] or key not in self.db.prefs.get('categories_using_hierarchy', []) or len(components) == 1): diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 19ef7e213c..e5864ceaaf 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -15,7 +15,7 @@ from calibre.utils.config import tweaks, prefs from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.pyparsing import ParseException -from calibre.ebooks.metadata import title_sort +from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre import prints @@ -1023,7 +1023,11 @@ class SortKeyGenerator(object): if val: sep = fm['is_multiple'] if sep: - val = sep.join(sorted(val.split(sep), + if fm['display'].get('is_names', False): + val = sep.join( + [author_to_author_sort(v) for v in val.split(sep)]) + else: + val = sep.join(sorted(val.split(sep), key=self.string_sort_key)) val = self.string_sort_key(val) diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index dec55f2b02..48960ac871 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -117,7 +117,7 @@ class CustomColumns(object): if x is None: return [] if isinstance(x, (str, unicode, bytes)): - x = x.split(',') + x = x.split('&' if d['display'].get('is_names', False) else',') x = [y.strip() for y in x if y.strip()] x = [y.decode(preferred_encoding, 'replace') if not isinstance(y, unicode) else y for y in x] @@ -482,8 +482,11 @@ class CustomColumns(object): set_val = val if data['is_multiple'] else [val] existing = getter() if not existing: - existing = [] - for x in set(set_val) - set(existing): + existing = set([]) + else: + existing = set(existing) + # preserve the order in set_val + for x in [v for v in set_val if v not in existing]: # normalized types are text and ratings, so we can do this check # to see if we need to re-add the value if not x: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index e751d4d522..b23c8ff4a4 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -48,7 +48,7 @@ class Tag(object): def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None, tooltip=None, icon=None, category=None, id_set=None, - is_editable = True, is_searchable=True): + is_editable = True, is_searchable=True, use_sort_as_name=False): self.name = self.original_name = name self.id = id self.count = count @@ -59,6 +59,7 @@ class Tag(object): self.id_set = id_set if id_set is not None else set([]) self.avg_rating = avg/2.0 if avg is not None else 0 self.sort = sort + self.use_sort_as_name = use_sort_as_name if self.avg_rating > 0: if tooltip: tooltip = tooltip + ': ' @@ -1323,6 +1324,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for l in list: (id, val) = (l[0], l[1]) tids[category][val] = (id, '{0:05.2f}'.format(val)) + elif cat['datatype'] == 'text' and cat['is_multiple'] and \ + cat['display'].get('is_names', False): + for l in list: + (id, val) = (l[0], l[1]) + tids[category][val] = (id, author_to_author_sort(val)) else: for l in list: (id, val) = (l[0], l[1]) @@ -1480,11 +1486,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): reverse=True items.sort(key=kf, reverse=reverse) + if tweaks['categories_use_field_for_author_name'] == 'author_sort' and\ + (category == 'authors' or + (cat['display'].get('is_names', False) and + cat['is_custom'] and cat['is_multiple'] and + cat['datatype'] == 'text')): + use_sort_as_name = True + else: + use_sort_as_name = False is_editable = category not in ['news', 'rating'] categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id, avg=avgr(r), sort=r.s, icon=icon, tooltip=tooltip, category=category, - id_set=r.id_set, is_editable=is_editable) + id_set=r.id_set, is_editable=is_editable, + use_sort_as_name=use_sort_as_name) for r in items] #print 'end phase "tags list":', time.clock() - last, 'seconds' diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index f1d9b9785c..895fbb06e9 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -15,7 +15,7 @@ from calibre import isbytestring, force_unicode, fit_image, \ prepare_string_for_xml from calibre.utils.ordered_dict import OrderedDict from calibre.utils.filenames import ascii_filename -from calibre.utils.config import prefs, tweaks +from calibre.utils.config import prefs from calibre.utils.icu import sort_key from calibre.utils.magick import Image from calibre.library.comments import comments_to_html @@ -155,8 +155,7 @@ def get_category_items(category, items, restriction, datatype, prefix): # {{{ '
{1}
' '
{2}
') rating, rstring = render_rating(i.avg_rating, prefix) - if i.category == 'authors' and \ - tweaks['categories_use_field_for_author_name'] == 'author_sort': + if i.use_sort_as_name: name = xml(i.sort) else: name = xml(i.name) @@ -696,7 +695,10 @@ class BrowseServer(object): xml(href, True), xml(val if len(dbtags) == 1 else tag.name), xml(key, True))) - join = ' & ' if key == 'authors' else ', ' + join = ' & ' if key == 'authors' or \ + (fm['is_custom'] and + fm['display'].get('is_names', False)) \ + else ', ' args[key] = join.join(vals) added_key = True if not added_key: diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index e7fdffbbbb..bdd35c16f1 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -22,7 +22,6 @@ from calibre.library.server.utils import format_tag_string, Offsets from calibre import guess_type, prepare_string_for_xml as xml from calibre.utils.icu import sort_key from calibre.utils.ordered_dict import OrderedDict -from calibre.utils.config import tweaks BASE_HREFS = { 0 : '/stanza', @@ -126,8 +125,7 @@ def CATALOG_ENTRY(item, item_kind, base_href, version, updated, count = (_('%d books') if item.count > 1 else _('%d book'))%item.count if ignore_count: count = '' - if item.category == 'authors' and \ - tweaks['categories_use_field_for_author_name'] == 'author_sort': + if item.use_sort_as_name: name = item.sort else: name = item.name