#!/usr/pkg/bin/python3.13


__license__   = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import os
from collections import OrderedDict
from functools import partial

from qt.core import (Qt, QComboBox, QLabel, QSpinBox, QDoubleSpinBox,
        QDateTime, QGroupBox, QVBoxLayout, QSizePolicy, QGridLayout, QUrl,
        QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, QLineEdit,
        QMessageBox, QToolButton, QPlainTextEdit, QApplication, QStyle, QDialog)

from calibre.utils.date import qt_to_dt, now, as_local_time, as_utc, internal_iso_format_string
from calibre.gui2.complete2 import EditWithComplete as EWC
from calibre.gui2.comments_editor import Editor as CommentsEditor
from calibre.gui2 import UNDEFINED_QDATETIME, error_dialog, elided_text, gprefs
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key
from calibre.library.comments import comments_to_html
from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox
from calibre.gui2.widgets2 import RatingEditor, DateTimeEdit as DateTimeEditBase


class EditWithComplete(EWC):

    def __init__(self, *a, **kw):
        super().__init__(*a, **kw)
        self.set_clear_button_enabled(False)


def safe_disconnect(signal):
    try:
        signal.disconnect()
    except Exception:
        pass


def label_string(txt):
    if txt:
        try:
            if txt[0].isalnum():
                return '&' + txt
        except:
            pass
    return txt


def get_tooltip(col_metadata, add_index=False):
    key = col_metadata['label'] + ('_index' if add_index else '')
    label = col_metadata['name'] + (_(' index') if add_index else '')
    description = col_metadata.get('display', {}).get('description', '')
    return '{} (#{}){} {}'.format(
                  label, key, ':' if description else '', description).strip()


class Base:

    def __init__(self, db, col_id, parent=None):
        self.db, self.col_id = db, col_id
        self.book_id = None
        self.col_metadata = db.custom_column_num_map[col_id]
        self.initial_val = self.widgets = None
        self.signals_to_disconnect = []
        self.setup_ui(parent)
        description = get_tooltip(self.col_metadata)
        try:
            self.widgets[0].setToolTip(description)
            self.widgets[1].setToolTip(description)
        except:
            try:
                self.widgets[1].setToolTip(description)
            except:
                pass

    def finish_ui_setup(self, parent, edit_widget):
        self.was_none = False
        w = QWidget(parent)
        self.widgets.append(w)
        l = QHBoxLayout()
        l.setContentsMargins(0, 0, 0, 0)
        w.setLayout(l)
        self.editor = editor = edit_widget(parent)
        l.addWidget(editor)
        self.clear_button = QToolButton(parent)
        self.clear_button.setIcon(QIcon(I('trash.png')))
        self.clear_button.clicked.connect(self.set_to_undefined)
        self.clear_button.setToolTip(_('Clear {0}').format(self.col_metadata['name']))
        l.addWidget(self.clear_button)

    def initialize(self, book_id):
        self.book_id = book_id
        val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
        val = self.normalize_db_val(val)
        self.setter(val)
        self.initial_val = self.current_val  # self.current_val might be different from val thanks to normalization

    @property
    def current_val(self):
        return self.normalize_ui_val(self.gui_val)

    @property
    def gui_val(self):
        return self.getter()

    def commit(self, book_id, notify=False):
        val = self.current_val
        if val != self.initial_val:
            return self.db.set_custom(book_id, val, num=self.col_id,
                            notify=notify, commit=False, allow_case_change=True)
        else:
            return set()

    def apply_to_metadata(self, mi):
        mi.set('#' + self.col_metadata['label'], self.current_val)

    def normalize_db_val(self, val):
        return val

    def normalize_ui_val(self, val):
        return val

    def break_cycles(self):
        self.db = self.widgets = self.initial_val = None
        for signal in self.signals_to_disconnect:
            safe_disconnect(signal)
        self.signals_to_disconnect = []

    def connect_data_changed(self, slot):
        pass


class SimpleText(Base):

    def setup_ui(self, parent):
        self.editor = QLineEdit(parent)
        self.widgets = [QLabel(label_string(self.col_metadata['name']), parent),
                        self.editor]
        self.editor.setClearButtonEnabled(True)

    def setter(self, val):
        self.editor.setText(str(val or ''))

    def getter(self):
        return self.editor.text().strip()

    def connect_data_changed(self, slot):
        self.editor.textChanged.connect(slot)
        self.signals_to_disconnect.append(self.editor.textChanged)


class LongText(Base):

    def setup_ui(self, parent):
        self._box = QGroupBox(parent)
        self._box.setTitle(label_string(self.col_metadata['name']))
        self._layout = QVBoxLayout()
        self._tb = QPlainTextEdit(self._box)
        self._tb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
        self._layout.addWidget(self._tb)
        self._box.setLayout(self._layout)
        self.widgets = [self._box]

    def setter(self, val):
        self._tb.setPlainText(str(val or ''))

    def getter(self):
        return self._tb.toPlainText()

    def connect_data_changed(self, slot):
        self._tb.textChanged.connect(slot)
        self.signals_to_disconnect.append(self._tb.textChanged)


class Bool(Base):

    def setup_ui(self, parent):
        name = self.col_metadata['name']
        self.widgets = [QLabel(label_string(name), parent)]
        w = QWidget(parent)
        self.widgets.append(w)

        l = QHBoxLayout()
        l.setContentsMargins(0, 0, 0, 0)
        w.setLayout(l)
        self.combobox = QComboBox(parent)
        l.addWidget(self.combobox)

        c = QToolButton(parent)
        c.setIcon(QIcon(I('ok.png')))
        c.setToolTip(_('Set {} to yes').format(name))
        l.addWidget(c)
        c.clicked.connect(self.set_to_yes)

        c = QToolButton(parent)
        c.setIcon(QIcon(I('list_remove.png')))
        c.setToolTip(_('Set {} to no').format(name))
        l.addWidget(c)
        c.clicked.connect(self.set_to_no)

        if self.db.new_api.pref('bools_are_tristate'):
            c = QToolButton(parent)
            c.setIcon(QIcon(I('trash.png')))
            c.setToolTip(_('Clear {}').format(name))
            l.addWidget(c)
            c.clicked.connect(self.set_to_cleared)

        w = self.combobox
        items = [_('Yes'), _('No'), _('Undefined')]
        icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
        if not self.db.new_api.pref('bools_are_tristate'):
            items = items[:-1]
            icons = icons[:-1]
        for icon, text in zip(icons, items):
            w.addItem(QIcon(icon), text)

    def setter(self, val):
        val = {None: 2, False: 1, True: 0}[val]
        if not self.db.new_api.pref('bools_are_tristate') and val == 2:
            val = 1
        self.combobox.setCurrentIndex(val)

    def getter(self):
        val = self.combobox.currentIndex()
        return {2: None, 1: False, 0: True}[val]

    def set_to_yes(self):
        self.combobox.setCurrentIndex(0)

    def set_to_no(self):
        self.combobox.setCurrentIndex(1)

    def set_to_cleared(self):
        self.combobox.setCurrentIndex(2)

    def connect_data_changed(self, slot):
        self.combobox.currentTextChanged.connect(slot)
        self.signals_to_disconnect.append(self.combobox.currentTextChanged)


class Int(Base):

    def setup_ui(self, parent):
        self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
        self.finish_ui_setup(parent, ClearingSpinBox)
        self.editor.setRange(-1000000, 100000000)

    def finish_ui_setup(self, parent, edit_widget):
        Base.finish_ui_setup(self, parent, edit_widget)
        self.editor.setSpecialValueText(_('Undefined'))
        self.editor.setSingleStep(1)
        self.editor.valueChanged.connect(self.valueChanged)

    def setter(self, val):
        if val is None:
            val = self.editor.minimum()
        self.editor.setValue(val)
        self.was_none = val == self.editor.minimum()

    def getter(self):
        val = self.editor.value()
        if val == self.editor.minimum():
            val = None
        return val

    def valueChanged(self, to_what):
        if self.was_none and to_what == -999999:
            self.setter(0)
        self.was_none = to_what == self.editor.minimum()

    def connect_data_changed(self, slot):
        self.editor.valueChanged.connect(slot)
        self.signals_to_disconnect.append(self.editor.valueChanged)

    def set_to_undefined(self):
        self.editor.setValue(-1000000)


class Float(Int):

    def setup_ui(self, parent):
        self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
        self.finish_ui_setup(parent, ClearingDoubleSpinBox)
        self.editor.setRange(-1000000., float(100000000))
        self.editor.setDecimals(int(self.col_metadata['display'].get('decimals', 2)))


class Rating(Base):

    def setup_ui(self, parent):
        allow_half_stars = self.col_metadata['display'].get('allow_half_stars', False)
        self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
        self.finish_ui_setup(parent, partial(RatingEditor, is_half_star=allow_half_stars))

    def set_to_undefined(self):
        self.editor.setCurrentIndex(0)

    def setter(self, val):
        val = max(0, min(int(val or 0), 10))
        self.editor.rating_value = val

    def getter(self):
        return self.editor.rating_value or None

    def connect_data_changed(self, slot):
        self.editor.currentTextChanged.connect(slot)
        self.signals_to_disconnect.append(self.editor.currentTextChanged)


class DateTimeEdit(DateTimeEditBase):

    def focusInEvent(self, x):
        self.setSpecialValueText('')
        DateTimeEditBase.focusInEvent(self, x)

    def focusOutEvent(self, x):
        self.setSpecialValueText(_('Undefined'))
        DateTimeEditBase.focusOutEvent(self, x)

    def set_to_today(self):
        self.setDateTime(now())

    def set_to_clear(self):
        self.setDateTime(now())
        self.setDateTime(UNDEFINED_QDATETIME)


class DateTime(Base):

    def setup_ui(self, parent):
        cm = self.col_metadata
        self.widgets = [QLabel(label_string(cm['name']), parent)]
        w = QWidget(parent)
        self.widgets.append(w)
        l = QHBoxLayout()
        l.setContentsMargins(0, 0, 0, 0)
        w.setLayout(l)
        self.dte = dte = DateTimeEdit(parent)
        format_ = cm['display'].get('date_format','')
        if not format_:
            format_ = 'dd MMM yyyy hh:mm'
        elif format_ == 'iso':
            format_ = internal_iso_format_string()
        dte.setDisplayFormat(format_)
        dte.setCalendarPopup(True)
        dte.setMinimumDateTime(UNDEFINED_QDATETIME)
        dte.setSpecialValueText(_('Undefined'))
        l.addWidget(dte)

        self.today_button = QToolButton(parent)
        self.today_button.setText(_('Today'))
        self.today_button.clicked.connect(dte.set_to_today)
        l.addWidget(self.today_button)

        self.clear_button = QToolButton(parent)
        self.clear_button.setIcon(QIcon(I('trash.png')))
        self.clear_button.clicked.connect(dte.set_to_clear)
        self.clear_button.setToolTip(_('Clear {0}').format(self.col_metadata['name']))
        l.addWidget(self.clear_button)

    def setter(self, val):
        if val is None:
            val = self.dte.minimumDateTime()
        else:
            val = QDateTime(val)
        self.dte.setDateTime(val)

    def getter(self):
        val = self.dte.dateTime()
        if val <= UNDEFINED_QDATETIME:
            val = None
        else:
            val = qt_to_dt(val)
        return val

    def normalize_db_val(self, val):
        return as_local_time(val) if val is not None else None

    def normalize_ui_val(self, val):
        return as_utc(val) if val is not None else None

    def connect_data_changed(self, slot):
        self.dte.dateTimeChanged.connect(slot)
        self.signals_to_disconnect.append(self.dte.dateTimeChanged)


class Comments(Base):

    def setup_ui(self, parent):
        self._box = QGroupBox(parent)
        self._box.setTitle(label_string(self.col_metadata['name']))
        self._layout = QVBoxLayout()
        self._tb = CommentsEditor(self._box, toolbar_prefs_name='metadata-comments-editor-widget-hidden-toolbars')
        self._tb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
        # self._tb.setTabChangesFocus(True)
        self._layout.addWidget(self._tb)
        self._box.setLayout(self._layout)
        self.widgets = [self._box]

    def initialize(self, book_id):
        path = self.db.abspath(book_id, index_is_id=True)
        if path:
            self._tb.set_base_url(QUrl.fromLocalFile(os.path.join(path, 'metadata.html')))
        return Base.initialize(self, book_id)

    def setter(self, val):
        if not val or not val.strip():
            val = ''
        else:
            val = comments_to_html(val)
        self._tb.html = val
        self._tb.wyswyg_dirtied()

    def getter(self):
        val = str(self._tb.html).strip()
        if not val:
            val = None
        return val

    @property
    def tab(self):
        return self._tb.tab

    @tab.setter
    def tab(self, val):
        self._tb.tab = val

    def connect_data_changed(self, slot):
        self._tb.data_changed.connect(slot)
        self.signals_to_disconnect.append(self._tb.data_changed)


class MultipleWidget(QWidget):

    def __init__(self, parent):
        QWidget.__init__(self, parent)
        layout = QHBoxLayout()
        layout.setSpacing(5)
        layout.setContentsMargins(0, 0, 0, 0)

        self.tags_box = EditWithComplete(parent)
        layout.addWidget(self.tags_box, stretch=1000)
        self.editor_button = QToolButton(self)
        self.editor_button.setToolTip(_('Open Item editor. If CTRL or SHIFT is pressed, open Manage items'))
        self.editor_button.setIcon(QIcon(I('chapters.png')))
        layout.addWidget(self.editor_button)
        self.setLayout(layout)

    def get_editor_button(self):
        return self.editor_button

    def update_items_cache(self, values):
        self.tags_box.update_items_cache(values)

    def clear(self):
        self.tags_box.clear()

    def setEditText(self):
        self.tags_box.setEditText()

    def addItem(self, itm):
        self.tags_box.addItem(itm)

    def set_separator(self, sep):
        self.tags_box.set_separator(sep)

    def set_add_separator(self, sep):
        self.tags_box.set_add_separator(sep)

    def set_space_before_sep(self, v):
        self.tags_box.set_space_before_sep(v)

    def setSizePolicy(self, v1, v2):
        self.tags_box.setSizePolicy(v1, v2)

    def setText(self, v):
        self.tags_box.setText(v)

    def text(self):
        return self.tags_box.text()


def _save_dialog(parent, title, msg, det_msg=''):
    d = QMessageBox(parent)
    d.setWindowTitle(title)
    d.setText(msg)
    d.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel)
    return d.exec()


class Text(Base):

    def setup_ui(self, parent):
        self.sep = self.col_metadata['multiple_seps']
        self.key = self.db.field_metadata.label_to_key(self.col_metadata['label'],
                                                       prefer_custom=True)
        self.parent = parent

        if self.col_metadata['is_multiple']:
            w = MultipleWidget(parent)
            w.set_separator(self.sep['ui_to_list'])
            if self.sep['ui_to_list'] == '&':
                w.set_space_before_sep(True)
                w.set_add_separator(tweaks['authors_completer_append_separator'])
            w.get_editor_button().clicked.connect(self.edit)
            w.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
            self.set_to_undefined = w.clear
        else:
            w = EditWithComplete(parent)
            w.set_separator(None)
            w.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
            w.setMinimumContentsLength(25)
            self.set_to_undefined = w.clearEditText
        self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
        self.finish_ui_setup(parent, lambda parent: w)

    def initialize(self, book_id):
        values = list(self.db.all_custom(num=self.col_id))
        values.sort(key=sort_key)
        self.book_id = book_id
        self.editor.clear()
        self.editor.update_items_cache(values)
        val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
        if isinstance(val, list):
            if not self.col_metadata.get('display', {}).get('is_names', False):
                val.sort(key=sort_key)
        val = self.normalize_db_val(val)

        if self.col_metadata['is_multiple']:
            self.setter(val)
        else:
            self.editor.setText(val)
        self.initial_val = self.current_val

    def setter(self, val):
        if self.col_metadata['is_multiple']:
            if not val:
                val = []
            self.editor.setText(self.sep['list_to_ui'].join(val))

    def getter(self):
        if self.col_metadata['is_multiple']:
            val = str(self.editor.text()).strip()
            ans = [x.strip() for x in val.split(self.sep['ui_to_list']) if x.strip()]
            if not ans:
                ans = None
            return ans
        val = str(self.editor.currentText()).strip()
        if not val:
            val = None
        return val

    def edit(self):
        ctrl_or_shift_pressed = (QApplication.keyboardModifiers() &
                (Qt.KeyboardModifier.ControlModifier + Qt.KeyboardModifier.ShiftModifier))
        if (self.getter() != self.initial_val and (self.getter() or self.initial_val)):
            d = _save_dialog(self.parent, _('Values changed'),
                    _('You have changed the values. In order to use this '
                       'editor, you must either discard or apply these '
                       'changes. Apply changes?'))
            if d == QMessageBox.StandardButton.Cancel:
                return
            if d == QMessageBox.StandardButton.Yes:
                self.commit(self.book_id)
                self.db.commit()
                self.initial_val = self.current_val
            else:
                self.setter(self.initial_val)
        if ctrl_or_shift_pressed:
            from calibre.gui2.ui import get_gui
            get_gui().do_tags_list_edit(None, self.key)
            self.initialize(self.book_id)
        else:
            d = TagEditor(self.parent, self.db, self.book_id, self.key)
            if d.exec() == QDialog.DialogCode.Accepted:
                self.setter(d.tags)

    def connect_data_changed(self, slot):
        if self.col_metadata['is_multiple']:
            s = self.editor.tags_box.currentTextChanged
        else:
            s = self.editor.currentTextChanged
        s.connect(slot)
        self.signals_to_disconnect.append(s)


class Series(Base):

    def setup_ui(self, parent):
        w = EditWithComplete(parent)
        w.set_separator(None)
        w.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
        w.setMinimumContentsLength(25)
        self.name_widget = w
        self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
        self.finish_ui_setup(parent, lambda parent: w)
        w.editTextChanged.connect(self.series_changed)

        w = QLabel(label_string(self.col_metadata['name'])+_(' index'), parent)
        w.setToolTip(get_tooltip(self.col_metadata, add_index=True))
        self.widgets.append(w)
        w = QDoubleSpinBox(parent)
        w.setRange(-10000., float(100000000))
        w.setDecimals(2)
        w.setSingleStep(1)
        self.idx_widget=w
        w.setToolTip(get_tooltip(self.col_metadata, add_index=True))
        self.widgets.append(w)

    def set_to_undefined(self):
        self.name_widget.clearEditText()
        self.idx_widget.setValue(1.0)

    def initialize(self, book_id):
        values = list(self.db.all_custom(num=self.col_id))
        values.sort(key=sort_key)
        val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
        s_index = self.db.get_custom_extra(book_id, num=self.col_id, index_is_id=True)
        try:
            s_index = float(s_index)
        except (ValueError, TypeError):
            s_index = 1.0
        self.idx_widget.setValue(s_index)
        val = self.normalize_db_val(val)
        self.name_widget.blockSignals(True)
        self.name_widget.update_items_cache(values)
        self.name_widget.setText(val)
        self.name_widget.blockSignals(False)
        self.initial_val, self.initial_index = self.current_val

    def getter(self):
        n = str(self.name_widget.currentText()).strip()
        i = self.idx_widget.value()
        return n, i

    def series_changed(self, val):
        val, s_index = self.gui_val
        if tweaks['series_index_auto_increment'] == 'no_change':
            pass
        elif tweaks['series_index_auto_increment'] == 'const':
            s_index = 1.0
        else:
            s_index = self.db.get_next_cc_series_num_for(val,
                                                     num=self.col_id)
        self.idx_widget.setValue(s_index)

    @property
    def current_val(self):
        val, s_index = self.gui_val
        val = self.normalize_ui_val(val)
        return val, s_index

    def commit(self, book_id, notify=False):
        val, s_index = self.current_val
        if val != self.initial_val or s_index != self.initial_index:
            if not val:
                val = s_index = None
            return self.db.set_custom(book_id, val, extra=s_index, num=self.col_id,
                               notify=notify, commit=False, allow_case_change=True)
        else:
            return set()

    def apply_to_metadata(self, mi):
        val, s_index = self.current_val
        mi.set('#' + self.col_metadata['label'], val, extra=s_index)

    def connect_data_changed(self, slot):
        for s in self.name_widget.editTextChanged, self.idx_widget.valueChanged:
            s.connect(slot)
            self.signals_to_disconnect.append(s)


class Enumeration(Base):

    def setup_ui(self, parent):
        self.parent = parent
        self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
        self.finish_ui_setup(parent, QComboBox)
        vals = self.col_metadata['display']['enum_values']
        self.editor.addItem('')
        for v in vals:
            self.editor.addItem(v)

    def initialize(self, book_id):
        val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
        val = self.normalize_db_val(val)
        idx = self.editor.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.editor.setCurrentIndex(idx)
        self.initial_val = self.current_val

    def setter(self, val):
        self.editor.setCurrentIndex(self.editor.findText(val))

    def getter(self):
        return str(self.editor.currentText())

    def normalize_db_val(self, val):
        if val is None:
            val = ''
        return val

    def normalize_ui_val(self, val):
        if not val:
            val = None
        return val

    def set_to_undefined(self):
        self.editor.setCurrentIndex(0)

    def connect_data_changed(self, slot):
        self.editor.currentIndexChanged.connect(slot)
        self.signals_to_disconnect.append(self.editor.currentIndexChanged)


def comments_factory(db, key, parent):
    fm = db.custom_column_num_map[key]
    ctype = fm.get('display', {}).get('interpret_as', 'html')
    if ctype == 'short-text':
        return SimpleText(db, key, parent)
    if ctype in ('long-text', 'markdown'):
        return LongText(db, key, parent)
    return Comments(db, key, parent)


widgets = {
        'bool' : Bool,
        'rating' : Rating,
        'int': Int,
        'float': Float,
        'datetime': DateTime,
        'text' : Text,
        'comments': comments_factory,
        'series': Series,
        'enumeration': Enumeration
}


def field_sort_key(y, fm=None):
    m1 = fm[y]
    name = icu_lower(m1['name'])
    n1 = 'zzzzz' + name if column_is_comments(y, fm) else name
    return sort_key(n1)


def column_is_comments(key, fm):
    return (fm[key]['datatype'] == 'comments' and
            fm[key].get('display', {}).get('interpret_as') != 'short-text')


def get_field_list(db, use_defaults=False, pref_data_override=None):
    fm = db.field_metadata
    fields = fm.custom_field_keys(include_composites=False)
    if pref_data_override is not None:
        displayable = pref_data_override
    else:
        displayable = db.prefs.get('edit_metadata_custom_columns_to_display', None)
    if use_defaults or displayable is None:
        fields.sort(key=partial(field_sort_key, fm=fm))
        return [(k, True) for k in fields]
    else:
        field_set = set(fields)
        result = OrderedDict({k:v for k,v in displayable if k in field_set})
        for k in fields:
            if k not in result:
                result[k] = True
        return [(k,v) for k,v in result.items()]


def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, parent=None):
    def widget_factory(typ, key):
        if bulk:
            w = bulk_widgets[typ](db, key, parent)
        else:
            w = widgets[typ](db, key, parent)
        if book_id is not None:
            w.initialize(book_id)
        return w
    fm = db.field_metadata

    # Get list of all non-composite custom fields. We must make widgets for these
    cols = [k[0] for k in get_field_list(db, use_defaults=db.prefs['edit_metadata_ignore_display_order']) if k[1]]
    # This deals with the historical behavior where comments fields go to the
    # bottom, starting on the left hand side. If a comment field is moved to
    # somewhere else then it isn't moved to either side.
    comments_at_end = 0
    for k in cols[::-1]:
        if not column_is_comments(k, fm):
            break
        comments_at_end += 1
    comments_not_at_end = len([k for k in cols if column_is_comments(k, fm)]) - comments_at_end

    count = len(cols)
    layout_rows_for_comments = 9
    if two_column:
        turnover_point = int(((count - comments_at_end + 1) +
                                int(comments_not_at_end*(layout_rows_for_comments-1)))/2)
    else:
        # Avoid problems with multi-line widgets
        turnover_point = count + 1000
    ans = []
    column = row = base_row = max_row = 0
    label_width = 0
    do_elision = gprefs['edit_metadata_elide_labels']
    elide_pos = gprefs['edit_metadata_elision_point']
    elide_pos = elide_pos if elide_pos in {'left', 'middle', 'right'} else 'right'
    # make room on the right side for the scrollbar
    sb_width = QApplication.instance().style().pixelMetric(QStyle.PixelMetric.PM_ScrollBarExtent)
    layout.setContentsMargins(0, 0, sb_width, 0)
    for key in cols:
        if not fm[key]['is_editable']:
            continue  # The job spy plugin can change is_editable
        dt = fm[key]['datatype']
        if dt == 'composite' or (bulk and dt == 'comments'):
            continue
        is_comments = column_is_comments(key, fm)
        w = widget_factory(dt, fm[key]['colnum'])
        ans.append(w)
        if two_column and is_comments:
            # Here for compatibility with old layout. Comments always started
            # in the left column
            comments_not_at_end -= 1
            # no special processing if the comment field was named in the tweak
            if comments_not_at_end < 0 and comments_at_end > 0:
                # Force a turnover, adding comments widgets below max_row.
                # Save the row to return to if we turn over again
                column = 0
                row = max_row
                base_row = row
                turnover_point = row + int((comments_at_end * layout_rows_for_comments)/2)
                comments_at_end = 0

        l = QGridLayout()
        if is_comments:
            layout.addLayout(l, row, column, layout_rows_for_comments, 1)
            layout.setColumnStretch(column, 100)
            row += layout_rows_for_comments
        else:
            layout.addLayout(l, row, column, 1, 1)
            layout.setColumnStretch(column, 100)
            row += 1
        for c in range(0, len(w.widgets), 2):
            if not is_comments:
                # Set the label column width to a fixed size. Elide labels that
                # don't fit
                wij = w.widgets[c]
                if label_width == 0:
                    font_metrics = wij.fontMetrics()
                    colon_width = font_metrics.width(':')
                    if bulk:
                        label_width = (font_metrics.averageCharWidth() *
                               gprefs['edit_metadata_bulk_cc_label_length']) - colon_width
                    else:
                        label_width = (font_metrics.averageCharWidth() *
                               gprefs['edit_metadata_single_cc_label_length']) - colon_width
                wij.setMaximumWidth(label_width)
                if c == 0:
                    wij.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred)
                    l.setColumnMinimumWidth(0, label_width)
                wij.setAlignment(Qt.AlignmentFlag.AlignRight|Qt.AlignmentFlag.AlignVCenter)
                t = str(wij.text())
                if t:
                    if do_elision:
                        wij.setText(elided_text(t, font=font_metrics,
                                            width=label_width, pos=elide_pos) + ':')
                    else:
                        wij.setText(t + ':')
                        wij.setWordWrap(True)
                wij.setBuddy(w.widgets[c+1])
                l.addWidget(wij, c, 0)
                l.addWidget(w.widgets[c+1], c, 1)
            else:
                l.addWidget(w.widgets[0], 0, 0, 1, 2)
        max_row = max(max_row, row)
        if row >= turnover_point:
            column = 1
            turnover_point = count + 1000
            row = base_row

    items = []
    if len(ans) > 0:
        items.append(QSpacerItem(10, 10, QSizePolicy.Policy.Minimum,
            QSizePolicy.Policy.Expanding))
        layout.addItem(items[-1], layout.rowCount(), 0, 1, 1)
        layout.setRowStretch(layout.rowCount()-1, 100)
    return ans, items


class BulkBase(Base):

    @property
    def gui_val(self):
        if not hasattr(self, '_cached_gui_val_'):
            self._cached_gui_val_ = self.getter()
        return self._cached_gui_val_

    def get_initial_value(self, book_ids):
        values = set()
        for book_id in book_ids:
            val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
            if isinstance(val, list):
                val = frozenset(val)
            values.add(val)
            if len(values) > 1:
                break
        ans = None
        if len(values) == 1:
            ans = next(iter(values))
        if isinstance(ans, frozenset):
            ans = list(ans)
        return ans

    def finish_ui_setup(self, parent, is_bool=False, add_edit_tags_button=(False,)):
        self.was_none = False
        l = self.widgets[1].layout()
        if not is_bool or self.bools_are_tristate:
            self.clear_button = QToolButton(parent)
            self.clear_button.setIcon(QIcon(I('trash.png')))
            self.clear_button.setToolTip(_('Clear {0}').format(self.col_metadata['name']))
            self.clear_button.clicked.connect(self.set_to_undefined)
            l.insertWidget(1, self.clear_button)
        if is_bool:
            self.set_no_button = QToolButton(parent)
            self.set_no_button.setIcon(QIcon(I('list_remove.png')))
            self.set_no_button.clicked.connect(lambda:self.main_widget.setCurrentIndex(1))
            self.set_no_button.setToolTip(_('Set {0} to No').format(self.col_metadata['name']))
            l.insertWidget(1, self.set_no_button)
            self.set_yes_button = QToolButton(parent)
            self.set_yes_button.setIcon(QIcon(I('ok.png')))
            self.set_yes_button.clicked.connect(lambda:self.main_widget.setCurrentIndex(0))
            self.set_yes_button.setToolTip(_('Set {0} to Yes').format(self.col_metadata['name']))
            l.insertWidget(1, self.set_yes_button)
        if add_edit_tags_button[0]:
            self.edit_tags_button = QToolButton(parent)
            self.edit_tags_button.setToolTip(_('Open Item editor'))
            self.edit_tags_button.setIcon(QIcon(I('chapters.png')))
            self.edit_tags_button.clicked.connect(add_edit_tags_button[1])
            l.insertWidget(1, self.edit_tags_button)
        l.insertStretch(2)

    def initialize(self, book_ids):
        self.initial_val = val = self.get_initial_value(book_ids)
        val = self.normalize_db_val(val)
        self.setter(val)

    def commit(self, book_ids, notify=False):
        if not self.a_c_checkbox.isChecked():
            return
        val = self.gui_val
        val = self.normalize_ui_val(val)
        self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)

    def make_widgets(self, parent, main_widget_class):
        w = QWidget(parent)
        self.widgets = [QLabel(label_string(self.col_metadata['name']), w), w]
        l = QHBoxLayout()
        l.setContentsMargins(0, 0, 0, 0)
        w.setLayout(l)
        self.main_widget = main_widget_class(w)
        l.addWidget(self.main_widget)
        l.setStretchFactor(self.main_widget, 10)
        self.a_c_checkbox = QCheckBox(_('Apply changes'), w)
        l.addWidget(self.a_c_checkbox)
        self.ignore_change_signals = True

        # connect to the various changed signals so we can auto-update the
        # apply changes checkbox
        if hasattr(self.main_widget, 'editTextChanged'):
            # editable combobox widgets
            self.main_widget.editTextChanged.connect(self.a_c_checkbox_changed)
        if hasattr(self.main_widget, 'textChanged'):
            # lineEdit widgets
            self.main_widget.textChanged.connect(self.a_c_checkbox_changed)
        if hasattr(self.main_widget, 'currentIndexChanged'):
            # combobox widgets
            self.main_widget.currentIndexChanged[int].connect(self.a_c_checkbox_changed)
        if hasattr(self.main_widget, 'valueChanged'):
            # spinbox widgets
            self.main_widget.valueChanged.connect(self.a_c_checkbox_changed)
        if hasattr(self.main_widget, 'dateTimeChanged'):
            # dateEdit widgets
            self.main_widget.dateTimeChanged.connect(self.a_c_checkbox_changed)

    def a_c_checkbox_changed(self):
        if not self.ignore_change_signals:
            self.a_c_checkbox.setChecked(True)


class BulkBool(BulkBase, Bool):

    def get_initial_value(self, book_ids):
        value = None
        for book_id in book_ids:
            val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
            if not self.db.new_api.pref('bools_are_tristate') and val is None:
                val = False
            if value is not None and value != val:
                return None
            value = val
        return value

    def setup_ui(self, parent):
        self.make_widgets(parent, QComboBox)
        items = [_('Yes'), _('No')]
        self.bools_are_tristate = self.db.new_api.pref('bools_are_tristate')
        if not self.bools_are_tristate:
            items.append('')
        else:
            items.append(_('Undefined'))
        icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
        self.main_widget.blockSignals(True)
        for icon, text in zip(icons, items):
            self.main_widget.addItem(QIcon(icon), text)
        self.main_widget.blockSignals(False)
        self.finish_ui_setup(parent, is_bool=True)

    def set_to_undefined(self):
        # Only called if bools are tristate
        self.main_widget.setCurrentIndex(2)

    def getter(self):
        val = self.main_widget.currentIndex()
        if not self.bools_are_tristate:
            return {2: False, 1: False, 0: True}[val]
        else:
            return {2: None, 1: False, 0: True}[val]

    def setter(self, val):
        val = {None: 2, False: 1, True: 0}[val]
        self.main_widget.setCurrentIndex(val)
        self.ignore_change_signals = False

    def commit(self, book_ids, notify=False):
        if not self.a_c_checkbox.isChecked():
            return
        val = self.gui_val
        val = self.normalize_ui_val(val)
        if not self.bools_are_tristate and val is None:
            val = False
        self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)

    def a_c_checkbox_changed(self):
        if not self.ignore_change_signals:
            if not self.bools_are_tristate and self.main_widget.currentIndex() == 2:
                self.a_c_checkbox.setChecked(False)
            else:
                self.a_c_checkbox.setChecked(True)


class BulkInt(BulkBase):

    def setup_ui(self, parent):
        self.make_widgets(parent, QSpinBox)
        self.main_widget.setRange(-1000000, 100000000)
        self.finish_ui_setup(parent)

    def finish_ui_setup(self, parent):
        BulkBase.finish_ui_setup(self, parent)
        self.main_widget.setSpecialValueText(_('Undefined'))
        self.main_widget.setSingleStep(1)
        self.main_widget.valueChanged.connect(self.valueChanged)

    def setter(self, val):
        if val is None:
            val = self.main_widget.minimum()
        self.main_widget.setValue(val)
        self.ignore_change_signals = False
        self.was_none = val == self.main_widget.minimum()

    def getter(self):
        val = self.main_widget.value()
        if val == self.main_widget.minimum():
            val = None
        return val

    def valueChanged(self, to_what):
        if self.was_none and to_what == -999999:
            self.setter(0)
        self.was_none = to_what == self.main_widget.minimum()

    def set_to_undefined(self):
        self.main_widget.setValue(-1000000)


class BulkFloat(BulkInt):

    def setup_ui(self, parent):
        self.make_widgets(parent, QDoubleSpinBox)
        self.main_widget.setRange(-1000000., float(100000000))
        self.main_widget.setDecimals(int(self.col_metadata['display'].get('decimals', 2)))
        self.finish_ui_setup(parent)

    def set_to_undefined(self):
        self.main_widget.setValue(-1000000.)


class BulkRating(BulkBase):

    def setup_ui(self, parent):
        allow_half_stars = self.col_metadata['display'].get('allow_half_stars', False)
        self.make_widgets(parent, partial(RatingEditor, is_half_star=allow_half_stars))
        self.finish_ui_setup(parent)

    def set_to_undefined(self):
        self.main_widget.setCurrentIndex(0)

    def setter(self, val):
        val = max(0, min(int(val or 0), 10))
        self.main_widget.rating_value = val
        self.ignore_change_signals = False

    def getter(self):
        return self.main_widget.rating_value or None


class BulkDateTime(BulkBase):

    def setup_ui(self, parent):
        cm = self.col_metadata
        self.make_widgets(parent, DateTimeEdit)
        l = self.widgets[1].layout()
        self.today_button = QToolButton(parent)
        self.today_button.setText(_('Today'))
        l.insertWidget(1, self.today_button)
        self.clear_button = QToolButton(parent)
        self.clear_button.setIcon(QIcon(I('trash.png')))
        self.clear_button.setToolTip(_('Clear {0}').format(self.col_metadata['name']))
        l.insertWidget(2, self.clear_button)
        l.insertStretch(3)

        w = self.main_widget
        format_ = cm['display'].get('date_format','')
        if not format_:
            format_ = 'dd MMM yyyy'
        elif format_ == 'iso':
            format_ = internal_iso_format_string()
        w.setDisplayFormat(format_)
        w.setCalendarPopup(True)
        w.setMinimumDateTime(UNDEFINED_QDATETIME)
        w.setSpecialValueText(_('Undefined'))
        self.today_button.clicked.connect(w.set_to_today)
        self.clear_button.clicked.connect(w.set_to_clear)

    def setter(self, val):
        if val is None:
            val = self.main_widget.minimumDateTime()
        else:
            val = QDateTime(val)
        self.main_widget.setDateTime(val)
        self.ignore_change_signals = False

    def getter(self):
        val = self.main_widget.dateTime()
        if val <= UNDEFINED_QDATETIME:
            val = None
        else:
            val = qt_to_dt(val)
        return val

    def normalize_db_val(self, val):
        return as_local_time(val) if val is not None else None

    def normalize_ui_val(self, val):
        return as_utc(val) if val is not None else None


class BulkSeries(BulkBase):

    def setup_ui(self, parent):
        self.make_widgets(parent, EditWithComplete)
        values = self.all_values = list(self.db.all_custom(num=self.col_id))
        values.sort(key=sort_key)
        self.main_widget.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
        self.main_widget.setMinimumContentsLength(25)
        self.widgets.append(QLabel('', parent))
        w = QWidget(parent)
        layout = QHBoxLayout(w)
        layout.setContentsMargins(0, 0, 0, 0)
        self.remove_series = QCheckBox(parent)
        self.remove_series.setText(_('Clear series'))
        layout.addWidget(self.remove_series)
        self.idx_widget = QCheckBox(parent)
        self.idx_widget.setText(_('Automatically number books'))
        self.idx_widget.setToolTip('<p>' + _(
            'If not checked, the series number for the books will be set to 1. '
            'If checked, selected books will be automatically numbered, '
            'in the order you selected them. So if you selected '
            'Book A and then Book B, Book A will have series number 1 '
            'and Book B series number 2.') + '</p>')
        layout.addWidget(self.idx_widget)
        self.force_number = QCheckBox(parent)
        self.force_number.setText(_('Force numbers to start with '))
        self.force_number.setToolTip('<p>' + _(
            'Series will normally be renumbered from the highest '
            'number in the database for that series. Checking this '
            'box will tell calibre to start numbering from the value '
            'in the box') + '</p>')
        layout.addWidget(self.force_number)
        self.series_start_number = QDoubleSpinBox(parent)
        self.series_start_number.setMinimum(0.0)
        self.series_start_number.setMaximum(9999999.0)
        self.series_start_number.setProperty("value", 1.0)
        layout.addWidget(self.series_start_number)
        self.series_increment = QDoubleSpinBox(parent)
        self.series_increment.setMinimum(0.00)
        self.series_increment.setMaximum(99999.0)
        self.series_increment.setProperty("value", 1.0)
        self.series_increment.setToolTip('<p>' + _(
            'The amount by which to increment the series number '
            'for successive books. Only applicable when using '
            'force series numbers.') + '</p>')
        self.series_increment.setPrefix('+')
        layout.addWidget(self.series_increment)
        layout.addItem(QSpacerItem(20, 10, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
        self.widgets.append(w)
        self.idx_widget.stateChanged.connect(self.a_c_checkbox_changed)
        self.force_number.stateChanged.connect(self.a_c_checkbox_changed)
        self.series_start_number.valueChanged.connect(self.a_c_checkbox_changed)
        self.series_increment.valueChanged.connect(self.a_c_checkbox_changed)
        self.remove_series.stateChanged.connect(self.a_c_checkbox_changed)
        self.main_widget
        self.ignore_change_signals = False

    def a_c_checkbox_changed(self):
        def disable_numbering_checkboxes(idx_widget_enable):
            if idx_widget_enable:
                self.idx_widget.setEnabled(True)
            else:
                self.idx_widget.setChecked(False)
                self.idx_widget.setEnabled(False)
            self.force_number.setChecked(False)
            self.force_number.setEnabled(False)
            self.series_start_number.setEnabled(False)
            self.series_increment.setEnabled(False)

        if self.ignore_change_signals:
            return
        self.ignore_change_signals = True
        apply_changes = False
        if self.remove_series.isChecked():
            self.main_widget.setText('')
            self.main_widget.setEnabled(False)
            disable_numbering_checkboxes(idx_widget_enable=False)
            apply_changes = True
        elif self.main_widget.text():
            self.remove_series.setEnabled(False)
            self.idx_widget.setEnabled(True)
            apply_changes = True
        else:  # no text, no clear. Basically reinitialize
            self.main_widget.setEnabled(True)
            self.remove_series.setEnabled(True)
            disable_numbering_checkboxes(idx_widget_enable=False)
            apply_changes = False

        self.force_number.setEnabled(self.idx_widget.isChecked())
        self.series_start_number.setEnabled(self.force_number.isChecked())
        self.series_increment.setEnabled(self.force_number.isChecked())

        self.ignore_change_signals = False
        self.a_c_checkbox.setChecked(apply_changes)

    def initialize(self, book_id):
        self.idx_widget.setChecked(False)
        self.main_widget.set_separator(None)
        self.main_widget.update_items_cache(self.all_values)
        self.main_widget.setEditText('')
        self.a_c_checkbox.setChecked(False)

    def getter(self):
        n = str(self.main_widget.currentText()).strip()
        autonumber = self.idx_widget.checkState()
        force = self.force_number.checkState()
        start = self.series_start_number.value()
        remove = self.remove_series.checkState()
        increment = self.series_increment.value()
        return n, autonumber, force, start, remove, increment

    def commit(self, book_ids, notify=False):
        if not self.a_c_checkbox.isChecked():
            return
        val, update_indices, force_start, at_value, clear, increment = self.gui_val
        val = None if clear else self.normalize_ui_val(val)
        if clear or val != '':
            extras = []
            for book_id in book_ids:
                if clear:
                    extras.append(None)
                    continue
                if update_indices:
                    if force_start:
                        s_index = at_value
                        at_value += increment
                    elif tweaks['series_index_auto_increment'] != 'const':
                        s_index = self.db.get_next_cc_series_num_for(val, num=self.col_id)
                    else:
                        s_index = 1.0
                else:
                    s_index = self.db.get_custom_extra(book_id, num=self.col_id,
                                                       index_is_id=True)
                extras.append(s_index)
            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
        first = True
        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 and 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
            if first:
                value = val
                first = False
            elif value != val:
                value = None
        if not value:
            self.ignore_change_signals = False
        return value

    def setup_ui(self, parent):
        self.parent = parent
        self.make_widgets(parent, QComboBox)
        self.finish_ui_setup(parent)
        vals = self.col_metadata['display']['enum_values']
        self.main_widget.blockSignals(True)
        self.main_widget.addItem('')
        self.main_widget.addItems(vals)
        self.main_widget.blockSignals(False)

    def set_to_undefined(self):
        self.main_widget.setCurrentIndex(0)

    def getter(self):
        return str(self.main_widget.currentText())

    def setter(self, val):
        if val is None:
            self.main_widget.setCurrentIndex(0)
        else:
            self.main_widget.setCurrentIndex(self.main_widget.findText(val))
        self.ignore_change_signals = False


class RemoveTags(QWidget):

    def __init__(self, parent, values):
        QWidget.__init__(self, parent)
        layout = QHBoxLayout()
        layout.setSpacing(5)
        layout.setContentsMargins(0, 0, 0, 0)

        self.tags_box = EditWithComplete(parent)
        self.tags_box.update_items_cache(values)
        layout.addWidget(self.tags_box, stretch=3)
        self.remove_tags_button = QToolButton(parent)
        self.remove_tags_button.setToolTip(_('Open Item editor'))
        self.remove_tags_button.setIcon(QIcon(I('chapters.png')))
        layout.addWidget(self.remove_tags_button)
        self.checkbox = QCheckBox(_('Remove all tags'), parent)
        layout.addWidget(self.checkbox)
        layout.addStretch(1)
        self.setLayout(layout)
        self.checkbox.stateChanged[int].connect(self.box_touched)

    def box_touched(self, state):
        if state:
            self.tags_box.setText('')
            self.tags_box.setEnabled(False)
        else:
            self.tags_box.setEnabled(True)


class BulkText(BulkBase):

    def setup_ui(self, parent):
        values = self.all_values = list(self.db.all_custom(num=self.col_id))
        values.sort(key=sort_key)
        is_tags = False
        if self.col_metadata['is_multiple']:
            is_tags = not self.col_metadata['display'].get('is_names', False)
            self.make_widgets(parent, EditWithComplete)
            self.main_widget.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
            self.adding_widget = self.main_widget

            if is_tags:
                w = RemoveTags(parent, values)
                w.remove_tags_button.clicked.connect(self.edit_remove)
                l = QLabel(label_string(self.col_metadata['name'])+': ' +
                                           _('tags to remove'), parent)
                tt = get_tooltip(self.col_metadata) + ': ' + _('tags to remove')
                l.setToolTip(tt)
                self.widgets.append(l)
                w.setToolTip(tt)
                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, EditWithComplete)
            self.main_widget.set_separator(None)
            self.main_widget.setSizeAdjustPolicy(
                        QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
            self.main_widget.setMinimumContentsLength(25)
        self.ignore_change_signals = False
        self.parent = parent
        self.finish_ui_setup(parent, add_edit_tags_button=(is_tags,self.edit_add))

    def set_to_undefined(self):
        self.main_widget.clearEditText()

    def initialize(self, book_ids):
        self.main_widget.update_items_cache(self.all_values)
        if not self.col_metadata['is_multiple']:
            val = self.get_initial_value(book_ids)
            self.initial_val = val = self.normalize_db_val(val)
            self.ignore_change_signals = True
            self.main_widget.blockSignals(True)
            self.main_widget.setText(val)
            self.main_widget.blockSignals(False)
            self.ignore_change_signals = False

    def commit(self, book_ids, notify=False):
        if not self.a_c_checkbox.isChecked():
            return
        if self.col_metadata['is_multiple']:
            ism = self.col_metadata['multiple_seps']
            if self.col_metadata['display'].get('is_names', False):
                val = self.gui_val
                add = [v.strip() for v in val.split(ism['ui_to_list']) if v.strip()]
                self.db.set_custom_bulk(book_ids, add, num=self.col_id)
            else:
                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 = {v.strip() for v in txt.split(ism['ui_to_list'])}
                txt = adding
                if txt:
                    add = {v.strip() for v in txt.split(ism['ui_to_list'])}
                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)
            self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)

    def getter(self):
        if self.col_metadata['is_multiple']:
            if not self.col_metadata['display'].get('is_names', False):
                return self.removing_widget.checkbox.isChecked(), \
                        str(self.adding_widget.text()), \
                        str(self.removing_widget.tags_box.text())
            return str(self.adding_widget.text())
        val = str(self.main_widget.currentText()).strip()
        if not val:
            val = None
        return val

    def edit_remove(self):
        self.edit(widget=self.removing_widget.tags_box)

    def edit_add(self):
        self.edit(widget=self.main_widget)

    def edit(self, widget):
        if widget.text():
            d = _save_dialog(self.parent, _('Values changed'),
                    _('You have entered values. In order to use this '
                       'editor you must first discard them. '
                       'Discard the values?'))
            if d == QMessageBox.StandardButton.Cancel or d == QMessageBox.StandardButton.No:
                return
            widget.setText('')
        d = TagEditor(self.parent, self.db, key=('#'+self.col_metadata['label']))
        if d.exec() == QDialog.DialogCode.Accepted:
            val = d.tags
            if not val:
                val = []
            widget.setText(self.col_metadata['multiple_seps']['list_to_ui'].join(val))


bulk_widgets = {
        'bool' : BulkBool,
        'rating' : BulkRating,
        'int': BulkInt,
        'float': BulkFloat,
        'datetime': BulkDateTime,
        'text' : BulkText,
        'series': BulkSeries,
        'enumeration': BulkEnumeration,
}
