mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Bug 1900921: various problems with enumerated types.
Enhancement: add an editor to change the list of permitted values.
This commit is contained in:
parent
bfe4260897
commit
5226feb900
184
src/calibre/gui2/dialogs/enum_values_edit.py
Normal file
184
src/calibre/gui2/dialogs/enum_values_edit.py
Normal 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)
|
||||
|
||||
|
@ -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,6 +381,9 @@ 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
|
||||
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)
|
||||
@ -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)
|
||||
|
||||
|
@ -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'])
|
||||
|
@ -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
|
||||
|
@ -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,6 +629,7 @@ 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 fm['datatype'] != 'enumeration':
|
||||
if self.model().get_in_vl():
|
||||
self.context_menu.addAction(self.rename_icon,
|
||||
_('Rename %s in Virtual library')%display_name(tag),
|
||||
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user