diff --git a/src/calibre/gui2/dialogs/enum_values_edit.py b/src/calibre/gui2/dialogs/enum_values_edit.py new file mode 100644 index 0000000000..24f9899592 --- /dev/null +++ b/src/calibre/gui2/dialogs/enum_values_edit.py @@ -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) + + diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 85d559c481..3656beebc6 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -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('
' + + _("This is not one of this column's permitted " + "values ({0})").format(', '.join(self.enum_permitted_values)) + + '
') 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'), '' + _( + "This column has a fixed set of permitted values. The entered " + "text must be one of ({0}).").format(', '.join(self.enum_permitted_values)) + + '
', 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) diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index 1ff57c821d..06fd195a50 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -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('' + ', '.join(all_colors) + '
') - 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('' + ', '.join(all_colors) + '
') 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']) diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py index c182b13626..a8c4127f4d 100644 --- a/src/calibre/gui2/tag_browser/ui.py +++ b/src/calibre/gui2/tag_browser/ui.py @@ -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 diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index 738e8cf925..0116c132b4 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -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: