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