Bug 1900921: various problems with enumerated types.

Enhancement: add an editor to change the list of permitted values.
This commit is contained in:
Charles Haley 2020-10-22 16:24:52 +01:00
parent bfe4260897
commit 5226feb900
5 changed files with 246 additions and 27 deletions

View File

@ -0,0 +1,184 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# License: GPLv3 Copyright: 2020, Charles Haley
from PyQt5.Qt import (QDialog, QColor, QDialogButtonBox, QHeaderView,
QApplication, QGridLayout, QTableWidget,
QTableWidgetItem, QVBoxLayout, QToolButton, QIcon,
QAbstractItemView, QComboBox)
from calibre.gui2 import error_dialog, gprefs
from polyglot.builtins import unicode_type
class EnumValuesEdit(QDialog):
def __init__(self, parent, db, key):
QDialog.__init__(self, parent)
geom = gprefs.get('enum-values-edit-geometry', None)
if geom is not None:
QApplication.instance().safe_restore_geometry(self, geom)
self.setWindowTitle(_('Edit permissible values for {0}').format(key))
self.db = db
l = QGridLayout()
bbox = QVBoxLayout()
bbox.addStretch(10)
self.del_button = QToolButton()
self.del_button.setIcon(QIcon(I('trash.png')))
self.ins_button = QToolButton()
self.ins_button.setIcon(QIcon(I('plus.png')))
self.move_up_button= QToolButton()
self.move_up_button.setIcon(QIcon(I('arrow-up.png')))
self.move_down_button= QToolButton()
self.move_down_button.setIcon(QIcon(I('arrow-down.png')))
bbox.addWidget(self.del_button)
bbox.addStretch(1)
bbox.addWidget(self.ins_button)
bbox.addStretch(1)
bbox.addWidget(self.move_up_button)
bbox.addStretch(1)
bbox.addWidget(self.move_down_button)
bbox.addStretch(10)
l.addItem(bbox, 0, 0)
self.del_button.clicked.connect(self.del_line)
self.all_colors = {unicode_type(s) for s in list(QColor.colorNames())}
tl = QVBoxLayout()
l.addItem(tl, 0, 1)
self.table = t = QTableWidget(parent)
t.setColumnCount(2)
t.setRowCount(1)
t.setHorizontalHeaderLabels([_('Value'), _('Color')])
t.setSelectionMode(QAbstractItemView.SingleSelection)
tl.addWidget(t)
self.fm = fm = db.field_metadata[key]
permitted_values = fm.get('display', {}).get('enum_values', '')
colors = fm.get('display', {}).get('enum_colors', '')
t.setRowCount(len(permitted_values))
for i,v in enumerate(permitted_values):
t.setItem(i, 0, QTableWidgetItem(v))
c = self.make_color_combobox(i, -1)
if colors:
c.setCurrentIndex(c.findText(colors[i]))
else:
c.setCurrentIndex(0)
t.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.setLayout(l)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
l.addWidget(bb, 1, 0, 1, 2)
self.ins_button.clicked.connect(self.ins_button_clicked)
self.move_down_button.clicked.connect(self.move_down_clicked)
self.move_up_button.clicked.connect(self.move_up_clicked)
def make_color_combobox(self, row, dex):
c = QComboBox(self)
c.addItem('')
c.addItems(QColor.colorNames())
self.table.setCellWidget(row, 1, c)
if dex >= 0:
c.setCurrentIndex(dex)
return c
def move_up_clicked(self):
row = self.table.currentRow()
if row < 0:
error_dialog(self, _('Select a cell'),
_('Select a cell before clicking the button'), show=True)
return
if row == 0:
return
self.move_row(row, -1)
def move_row(self, row, direction):
t = self.table.item(row, 0).text()
c = self.table.cellWidget(row, 1).currentIndex()
self.table.removeRow(row)
row += direction
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(t))
self.make_color_combobox(row, c)
self.table.setCurrentCell(row, 0)
def move_down_clicked(self):
row = self.table.currentRow()
if row < 0:
error_dialog(self, _('Select a cell'),
_('Select a cell before clicking the button'), show=True)
return
if row >= self.table.rowCount() - 1:
return
self.move_row(row, 1)
def del_line(self):
print(self.table.currentRow())
if self.table.currentRow() >= 0:
self.table.removeRow(self.table.currentRow())
def ins_button_clicked(self):
row = self.table.currentRow()
if row < 0:
error_dialog(self, _('Select a cell'),
_('Select a cell before clicking the button'), show=True)
return
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem())
c = QComboBox(self)
c.addItem('')
c.addItems(QColor.colorNames())
self.table.setCellWidget(row, 1, c)
def save_geometry(self):
gprefs.set('enum-values-edit-geometry', bytearray(self.saveGeometry()))
def accept(self):
disp = self.fm['display']
values = []
colors = []
for i in range(0, self.table.rowCount()):
v = unicode_type(self.table.item(i, 0).text())
if not v:
error_dialog(self, _('Empty value'),
_('Empty values are not allowed'), show=True)
return
values.append(v)
c = unicode_type(self.table.cellWidget(i, 1).currentText())
if c:
colors.append(c)
l_lower = [v.lower() for v in values]
for i,v in enumerate(l_lower):
if v in l_lower[i+1:]:
error_dialog(self, _('Duplicate value'),
_('The value "{0}" is in the list more than '
'once, perhaps with different case').format(values[i]),
show=True)
return
if colors and len(colors) != len(values):
error_dialog(self, _('Invalid colors specification'), _(
'Either all values or no values must have colors'), show=True)
return
disp['enum_values'] = values
disp['enum_colors'] = colors
self.db.set_custom_column_metadata(self.fm['colnum'], display=disp,
update_last_modified=True)
self.save_geometry()
return QDialog.accept(self)
def reject(self):
return QDialog.reject(self)

View File

@ -7,7 +7,7 @@ from functools import partial
from PyQt5.Qt import (Qt, QDialog, QTableWidgetItem, QIcon, QByteArray, QSize,
QDialogButtonBox, QTableWidget, QItemDelegate, QApplication,
pyqtSignal, QAction, QFrame, QLabel, QTimer, QMenu)
pyqtSignal, QAction, QFrame, QLabel, QTimer, QMenu, QColor)
from calibre.gui2.actions.show_quickview import get_quickview_action_plugin
from calibre.gui2.complete2 import EditWithComplete
@ -140,7 +140,7 @@ class EditColumnDelegate(QItemDelegate):
class TagListEditor(QDialog, Ui_TagListEditor):
def __init__(self, window, cat_name, tag_to_match, get_book_ids, sorter,
ttm_is_first_letter=False, category=None):
ttm_is_first_letter=False, category=None, fm=None):
QDialog.__init__(self, window)
Ui_TagListEditor.__init__(self)
self.setupUi(self)
@ -250,6 +250,12 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.resize(self.sizeHint()+QSize(150, 100))
except:
pass
self.is_enumerated = False
if fm:
if fm['datatype'] == 'enumeration':
self.is_enumerated = True
self.enum_permitted_values = fm.get('display', {}).get('enum_values', None)
# Add the data
self.search_item_row = -1
self.fill_in_table(None, tag_to_match, ttm_is_first_letter)
@ -375,7 +381,10 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.all_tags[v] = {'key': k, 'count': count, 'cur_name': v,
'is_deleted': k in self.to_delete}
self.original_names[k] = v
self.edit_delegate.set_completion_data(self.original_names.values())
if self.is_enumerated:
self.edit_delegate.set_completion_data(self.enum_permitted_values)
else:
self.edit_delegate.set_completion_data(self.original_names.values())
self.ordered_tags = sorted(self.all_tags.keys(), key=self.sorter)
if tags is None:
@ -403,6 +412,12 @@ class TagListEditor(QDialog, Ui_TagListEditor):
item.setText(self.to_rename[_id])
else:
item.setText(tag)
if self.is_enumerated and unicode_type(item.text()) not in self.enum_permitted_values:
item.setBackground(QColor('#FF2400'))
item.setToolTip('<p>' +
_("This is not one of this column's permitted "
"values ({0})").format(', '.join(self.enum_permitted_values))
+ '</p>')
item.setFlags(item.flags() | Qt.ItemIsSelectable | Qt.ItemIsEditable)
self.table.setItem(row, 0, item)
if select_item is None:
@ -498,13 +513,24 @@ class TagListEditor(QDialog, Ui_TagListEditor):
edited_item.setText(self.text_before_editing)
self.table.blockSignals(False)
return
new_text = unicode_type(edited_item.text())
if self.is_enumerated and new_text not in self.enum_permitted_values:
error_dialog(self, _('Item is not a permitted value'), '<p>' + _(
"This column has a fixed set of permitted values. The entered "
"text must be one of ({0}).").format(', '.join(self.enum_permitted_values)) +
'</p>', show=True)
self.table.blockSignals(True)
edited_item.setText(self.text_before_editing)
self.table.blockSignals(False)
return
items = self.table.selectedItems()
self.table.blockSignals(True)
for item in items:
id_ = int(item.data(Qt.UserRole))
self.to_rename[id_] = unicode_type(edited_item.text())
self.to_rename[id_] = new_text
orig = self.table.item(item.row(), 2)
item.setText(edited_item.text())
item.setText(new_text)
orig.setData(Qt.DisplayRole, item.initial_text())
self.table.blockSignals(False)

View File

@ -116,9 +116,6 @@ class CreateCustomColumn(QDialog):
self.column_type_box.addItem(self.column_types[t]['text'])
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
all_colors = [unicode_type(s) for s in list(QColor.colorNames())]
self.enum_colors_label.setToolTip('<p>' + ', '.join(all_colors) + '</p>')
if not self.editing_col:
self.datatype_changed()
self.exec_()
@ -203,6 +200,8 @@ class CreateCustomColumn(QDialog):
self.is_names.setChecked(c['display'].get('is_names', False))
self.description_box.setText(c['display'].get('description', ''))
all_colors = [unicode_type(s) for s in list(QColor.colorNames())]
self.enum_colors_label.setToolTip('<p>' + ', '.join(all_colors) + '</p>')
self.exec_()
def shortcut_activated(self, url): # {{{
@ -361,22 +360,16 @@ class CreateCustomColumn(QDialog):
self.comments_type_label = add_row(_('Interpret this column as:') + ' ', ct)
# Values for enum type
l = QGridLayout()
self.enum_box = eb = QLineEdit(self)
eb.setToolTip(_(
"A comma-separated list of permitted values. The empty value is always\n"
"included, and is the default. For example, the list 'one,two,three' has\n"
"four values, the first of them being the empty value."))
self.enum_default_label = la = QLabel(_("Values"))
la.setBuddy(eb)
l.addWidget(eb), l.addWidget(la, 0, 1)
self.enum_default_label = add_row(_("&Values"), eb)
self.enum_colors = ec = QLineEdit(self)
ec.setToolTip(_("A list of color names to use when displaying an item. The\n"
"list must be empty or contain a color for each value."))
self.enum_colors_label = la = QLabel(_('Colors'))
la.setBuddy(ec)
l.addWidget(ec), l.addWidget(la, 1, 1)
self.enum_label = add_row(_('&Values'), l)
self.enum_colors_label = add_row(_('Colors'), ec)
# Rating allow half stars
self.allow_half_stars = ahs = QCheckBox(_('Allow half stars'))
@ -482,7 +475,7 @@ class CreateCustomColumn(QDialog):
for x in ('box', 'default_label', 'label', 'sort_by', 'sort_by_label',
'make_category', 'contains_html'):
getattr(self, 'composite_'+x).setVisible(col_type in ['composite', '*composite'])
for x in ('box', 'default_label', 'label', 'colors', 'colors_label'):
for x in ('box', 'default_label', 'colors', 'colors_label'):
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
for x in ('value_label', 'value'):
getattr(self, 'default_'+x).setVisible(col_type not in ['composite', '*composite'])

View File

@ -93,6 +93,7 @@ class TagBrowserMixin(object): # {{{
type=Qt.QueuedConnection)
self.tags_view.model().user_category_added.connect(self.user_categories_edited,
type=Qt.QueuedConnection)
self.tags_view.edit_enum_values.connect(self.edit_enum_values)
def user_categories_edited(self):
self.library_view.model().refresh()
@ -261,7 +262,8 @@ class TagBrowserMixin(object): # {{{
cat_name=db.field_metadata[category]['name'],
tag_to_match=tag,
get_book_ids=partial(self.get_book_ids, db=db, category=category),
sorter=key, ttm_is_first_letter=is_first_letter)
sorter=key, ttm_is_first_letter=is_first_letter,
fm=db.field_metadata[category])
d.exec_()
if d.result() == d.Accepted:
to_rename = d.to_rename # dict of old id to new name
@ -378,6 +380,11 @@ class TagBrowserMixin(object): # {{{
self.library_view.model().refresh_ids(set(changes), current_row=self.library_view.currentIndex().row())
self.tags_view.recount_with_position_based_index()
def edit_enum_values(self, parent, db, key):
from calibre.gui2.dialogs.enum_values_edit import EnumValuesEdit
d = EnumValuesEdit(parent, db, key)
d.exec_()
def do_tag_item_renamed(self):
# Clean up library view and search
# get information to redo the selection

View File

@ -157,6 +157,7 @@ class TagsView(QTreeView): # {{{
restriction_error = pyqtSignal()
tag_item_delete = pyqtSignal(object, object, object, object, object)
apply_tag_to_selected = pyqtSignal(object, object, object)
edit_enum_values = pyqtSignal(object, object, object)
def __init__(self, parent=None):
QTreeView.__init__(self, parent=None)
@ -550,6 +551,9 @@ class TagsView(QTreeView): # {{{
if item is not None:
self.apply_to_selected_books(item, True)
return
elif action == 'edit_enum':
self.edit_enum_values.emit(self, self.db, key)
return
self.db.new_api.set_pref('tag_browser_hidden_categories', list(self.hidden_categories))
if reset_filter_categories:
self._model.set_categories_filter(None)
@ -625,15 +629,16 @@ class TagsView(QTreeView): # {{{
# the possibility of renaming that item.
if tag.is_editable or tag.is_hierarchical:
# Add the 'rename' items to both interior and leaf nodes
if self.model().get_in_vl():
if fm['datatype'] != 'enumeration':
if self.model().get_in_vl():
self.context_menu.addAction(self.rename_icon,
_('Rename %s in Virtual library')%display_name(tag),
partial(self.context_menu_handler, action='edit_item_in_vl',
index=index, category=key))
self.context_menu.addAction(self.rename_icon,
_('Rename %s in Virtual library')%display_name(tag),
partial(self.context_menu_handler, action='edit_item_in_vl',
index=index, category=key))
self.context_menu.addAction(self.rename_icon,
_('Rename %s')%display_name(tag),
partial(self.context_menu_handler, action='edit_item_no_vl',
index=index, category=key))
_('Rename %s')%display_name(tag),
partial(self.context_menu_handler, action='edit_item_no_vl',
index=index, category=key))
if key in ('tags', 'series', 'publisher') or \
self._model.db.field_metadata.is_custom_field(key):
if self.model().get_in_vl():
@ -766,7 +771,7 @@ class TagsView(QTreeView): # {{{
# Offer specific editors for tags/series/publishers/saved searches
self.context_menu.addSeparator()
if key in ['tags', 'publisher', 'series'] or (
self.db.field_metadata[key]['is_custom'] and self.db.field_metadata[key]['datatype'] != 'composite'):
fm['is_custom'] and fm['datatype'] != 'composite'):
if tag_item.type == TagTreeItem.CATEGORY and tag_item.temporary:
self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='open_editor',
@ -777,6 +782,10 @@ class TagsView(QTreeView): # {{{
partial(self.context_menu_handler, action='open_editor',
category=tag.original_name if tag else None,
key=key))
if fm['datatype'] == 'enumeration':
self.context_menu.addAction(_('Edit permissable values for %s')%category,
partial(self.context_menu_handler, action='edit_enum',
key=key))
elif key == 'authors':
if tag_item.type == TagTreeItem.CATEGORY:
if tag_item.temporary: