diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index e4e5ff085a..162679b4fb 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -676,24 +676,48 @@ class CreateNewCustomColumn(object): """ Provide an API to create new custom columns. - Use the create_column() method to open a dialog to create a new custom - column with given lookup_name, column_heading, datatype, and is_multiple. - The lookup name must begin with a '#'. The datatype must be valid and - is_multiple must be valid for that datatype. The user cannot change - the datatype. + Usage: + from calibre.gui2.preferences.create_custom_column import CreateNewCustomColumn + creator = CreateNewCustomColumn(gui) + if creator.must_restart(): + ... + else: + result = creator.create_column(....) + if result[0] == creator.Result.COLUMN_ADDED: - If generate_unused_lookup_name is False then the provided lookup_name must - not already exist. If generate_unused_lookup_name is True then if necessary - the method will add the suffix '_n' to the provided lookup_name to allocate - an unused lookup_name, where 'n' is an integer. It could be that if a new - lookup name is generated then the user will be required to change the - column heading to make it is unique. + The parameter 'gui' passed when creating a class instance is the main + calibre gui (calibre.gui2.ui.get_gui()) + + Use the create_column(...) method to open a dialog to create a new custom + column with given lookup_name, column_heading, datatype, and is_multiple. + You can create as many columns as you wish with a single instance of the + CreateNewCustomColumn class. Subsequent class instances will refuse to + create columns until calibre is restarted, as will calibre Preferences. + + The lookup name must begin with a '#'. All remaining characters must be + lower case letters, digits or underscores. The character after the '#' must + be a letter. The lookup name must not end with the suffix '_index'. + + The datatype must be one of calibre's custom column types: 'bool', + 'comments', 'composite', 'datetime', 'enumeration', 'float', 'int', + 'rating', 'series', or 'text'. The datatype can't be changed in the dialog. + + is_multiple tells calibre that the column contains multiple values -- is + tags-like. The value True is allowed only for 'composite' and 'text' types. + + If generate_unused_lookup_name is False then the provided lookup_name and + column_heading must not already exist. If generate_unused_lookup_name is + True then if necessary the method will add the suffix '_n' to the provided + lookup_name to allocate an unused lookup_name, where 'n' is an integer. + The same processing is applied to column_heading to make it is unique, using + the same suffix used for the lookup name if possible. In either case the + user can change the column heading in the dialog. Set freeze_lookup_name to False if you want to allow the user choose a different lookup name. The user will not be allowed to choose the lookup - name of an existing column. The provided lookup_name either must not exist - or generate_unused_lookup_name must be True, regardless of the value of - freeze_lookup_name. + name of an existing column. The provided lookup_name and column_heading + either must not exist or generate_unused_lookup_name must be True, + regardless of the value of freeze_lookup_name. The 'display' parameter is used to pass item- and type-specific information for the column. It is a dict. The easiest way to see the current values for @@ -740,21 +764,14 @@ class CreateNewCustomColumn(object): You or the user must restart calibre for the column(s) to be actually added. + Result.EXCEPTION_RAISED is returned if the create dialog raises an exception. + This can happen if the display contains illegal values, for example a string + where a boolean is required. The string is the exception text. Run calibre + in debug mode to see the entire traceback. + The method returns Result.MUST_RESTART if further calibre configuration has been blocked. You can check for this situation in advance by calling must_restart(). - - The parameter 'gui' passed when creating a class instance is the main - calibre gui (calibre.gui2.ui.get_gui()) - - Usage: - from calibre.gui2.preferences.create_custom_column import CreateNewCustomColumn - creator = CreateNewCustomColumn(gui) - if creator.must_restart(): - ... - else: - result = creator.create_column(....) - if result[0] == creator.Result.COLUMN_ADDED: """ class Result(Enum): @@ -762,10 +779,12 @@ class CreateNewCustomColumn(object): CANCELED = 1 INVALID_KEY = 2 DUPLICATE_KEY = 3 - INVALID_TYPE = 4 - INVALID_IS_MULTIPLE = 5 - INVALID_DISPLAY = 6 - MUST_RESTART = 7 + DUPLICATE_HEADING = 4 + INVALID_TYPE = 5 + INVALID_IS_MULTIPLE = 6 + INVALID_DISPLAY = 7 + EXCEPTION_RAISED = 8 + MUST_RESTART = 9 def __init__(self, gui): self.gui = gui @@ -783,14 +802,28 @@ class CreateNewCustomColumn(object): return (self.Result.MUST_RESTART, _("You must restart calibre before making any more changes")) if not lookup_name.startswith('#'): return (self.Result.INVALID_KEY, _("The lookup name must begin with a '#'")) + suffix_number = 1 if lookup_name in self.custcols: if not generate_unused_lookup_name: return(self.Result.DUPLICATE_KEY, _("The custom column %s already exists") % lookup_name) - for i in range(1, 10000): - nk = '%s_%d'%(lookup_name, i) + for suffix_number in range(suffix_number, 100000): + nk = '%s_%d'%(lookup_name, suffix_number) if nk not in self.custcols: lookup_name = nk break + if column_heading: + headings = {v['name'] for v in self.custcols.values()} + if column_heading in headings: + if not generate_unused_lookup_name: + return(self.Result.DUPLICATE_HEADING, + _("The column heading %s already exists") % column_heading) + for i in range(suffix_number, 100000): + nh = '%s_%d'%(column_heading, i) + if nh not in headings: + column_heading = nh + break + else: + column_heading = lookup_name if datatype not in CreateCustomColumn.column_types_map: return(self.Result.INVALID_TYPE, _("The custom column type %s doesn't exist") % datatype) @@ -800,10 +833,6 @@ class CreateNewCustomColumn(object): if not isinstance(display, dict): return(self.Result.INVALID_DISPLAY, _("The display parameter must a python dict")) - - if not column_heading: - column_heading = lookup_name - self.key = lookup_name self.created_count += 1 self.custcols[lookup_name] = { 'label': lookup_name, @@ -814,18 +843,26 @@ class CreateNewCustomColumn(object): 'colnum': self.created_count, 'is_multiple': is_multiple, } - dialog = CreateCustomColumn(self.gui, self, lookup_name, self.gui.library_view.model().orig_headers, - freeze_lookup_name=freeze_lookup_name) - if dialog.result() == QDialog.DialogCode.Accepted and self.cc_column_key is not None: - cc = self.custcols[lookup_name] - self.db.create_custom_column( - label=cc['label'], - name=cc['name'], - datatype=cc['datatype'], - is_multiple=cc['is_multiple'], - display=cc['display']) - self.gui.must_restart_before_config = True - return ((self.Result.COLUMN_ADDED, self.cc_column_key)) + try: + dialog = CreateCustomColumn(self.gui, self, lookup_name, + self.gui.library_view.model().orig_headers, + freeze_lookup_name=freeze_lookup_name) + if dialog.result() == QDialog.DialogCode.Accepted and self.cc_column_key is not None: + cc = self.custcols[lookup_name] + self.db.create_custom_column( + label=cc['label'], + name=cc['name'], + datatype=cc['datatype'], + is_multiple=cc['is_multiple'], + display=cc['display']) + self.gui.must_restart_before_config = True + return (self.Result.COLUMN_ADDED, self.cc_column_key) + except Exception as e: + import traceback + traceback.print_exc() + self.custcols.pop(lookup_name, None) + return (self.Result.EXCEPTION_RAISED, str(e)) + self.custcols.pop(lookup_name, None) return (self.Result.CANCELED, _('Canceled')) def current_columns(self): @@ -847,7 +884,21 @@ class CreateNewCustomColumn(object): doesn't use. See calibre.library.field_metadata.add_custom_field() for the complete list. """ - return copy.deepcopy(self.custcols) #deepcopy to prevent users from changing it + # deepcopy to prevent users from changing it. The new MappingProxyType + # isn't enough because only the top-level dict is immutable, not the + # items in the dict. + return copy.deepcopy(self.custcols) + + + def current_headings(self): + """ + Return the currently defined column headings + + Return the column headings including the ones that haven't yet been + created. It is a dict. The key is the heading, the value is the lookup + name having that heading. + """ + return {v['name']:('#' + v['label']) for v in self.custcols.values()} def must_restart(self): """Return true if calibre must be restarted before new columns can be added."""