diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 86c6778191..5dd71ad696 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -13,7 +13,7 @@ from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer from calibre.gui2 import error_dialog, Dispatcher, question_dialog from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog from calibre.gui2.dialogs.confirm_delete import confirm -from calibre.gui2.dialogs.tag_list_editor import TagListEditor +from calibre.gui2.dialogs.device_category_editor import DeviceCategoryEditor from calibre.gui2.actions import InterfaceAction from calibre.ebooks.metadata import authors_to_string from calibre.utils.icu import sort_key @@ -441,7 +441,7 @@ class EditMetadataAction(InterfaceAction): def edit_device_collections(self, view, oncard=None): model = view.model() result = model.get_collections_with_ids() - d = TagListEditor(self.gui, tag_to_match=None, data=result, key=sort_key) + d = DeviceCategoryEditor(self.gui, tag_to_match=None, data=result, key=sort_key) d.exec_() if d.result() == d.Accepted: to_rename = d.to_rename # dict of new text to old ids diff --git a/src/calibre/gui2/dialogs/device_category_editor.py b/src/calibre/gui2/dialogs/device_category_editor.py new file mode 100644 index 0000000000..5e3267f9c4 --- /dev/null +++ b/src/calibre/gui2/dialogs/device_category_editor.py @@ -0,0 +1,124 @@ +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal ' + +from PyQt4.QtCore import Qt, QString +from PyQt4.QtGui import QDialog, QListWidgetItem + +from calibre.gui2.dialogs.device_category_editor_ui import Ui_DeviceCategoryEditor +from calibre.gui2 import question_dialog, error_dialog + +class ListWidgetItem(QListWidgetItem): + + def __init__(self, txt): + QListWidgetItem.__init__(self, txt) + self.initial_value = QString(txt) + self.current_value = QString(txt) + self.previous_value = QString(txt) + + def data(self, role): + if role == Qt.DisplayRole: + if self.initial_value != self.current_value: + return _('%(curr)s (was %(initial)s)')%dict( + curr=self.current_value, initial=self.initial_value) + else: + return self.current_value + elif role == Qt.EditRole: + return self.current_value + else: + return QListWidgetItem.data(self, role) + + def setData(self, role, data): + if role == Qt.EditRole: + self.previous_value = self.current_value + self.current_value = data.toString() + QListWidgetItem.setData(self, role, data) + + def text(self): + return self.current_value + + def initial_text(self): + return self.initial_value + + def previous_text(self): + return self.previous_value + + def setText(self, txt): + self.current_value = txt + QListWidgetItem.setText(txt) + +class DeviceCategoryEditor(QDialog, Ui_DeviceCategoryEditor): + + def __init__(self, window, tag_to_match, data, key): + QDialog.__init__(self, window) + Ui_DeviceCategoryEditor.__init__(self) + self.setupUi(self) + # Remove help icon on title bar + icon = self.windowIcon() + self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) + self.setWindowIcon(icon) + + self.to_rename = {} + self.to_delete = set([]) + self.original_names = {} + self.all_tags = {} + + for k,v in data: + self.all_tags[v] = k + self.original_names[k] = v + for tag in sorted(self.all_tags.keys(), key=key): + item = ListWidgetItem(tag) + item.setData(Qt.UserRole, self.all_tags[tag]) + item.setFlags (item.flags() | Qt.ItemIsEditable) + self.available_tags.addItem(item) + + if tag_to_match is not None: + items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly) + if len(items) == 1: + self.available_tags.setCurrentItem(items[0]) + + self.delete_button.clicked.connect(self.delete_tags) + self.rename_button.clicked.connect(self.rename_tag) + self.available_tags.itemDoubleClicked.connect(self._rename_tag) + self.available_tags.itemChanged.connect(self.finish_editing) + + def finish_editing(self, item): + if not item.text(): + error_dialog(self, _('Item is blank'), + _('An item cannot be set to nothing. Delete it instead.')).exec_() + item.setText(item.previous_text()) + return + if item.text() != item.initial_text(): + id_ = item.data(Qt.UserRole).toInt()[0] + self.to_rename[id_] = unicode(item.text()) + + def rename_tag(self): + item = self.available_tags.currentItem() + self._rename_tag(item) + + def _rename_tag(self, item): + if item is None: + error_dialog(self, _('No item selected'), + _('You must select one item from the list of Available items.')).exec_() + return + self.available_tags.editItem(item) + + def delete_tags(self): + deletes = self.available_tags.selectedItems() + if not deletes: + error_dialog(self, _('No items selected'), + _('You must select at least one items from the list.')).exec_() + return + ct = ', '.join([unicode(item.text()) for item in deletes]) + if not question_dialog(self, _('Are your sure?'), + '

'+_('Are you certain you want to delete the following items?')+'
'+ct): + return + row = self.available_tags.row(deletes[0]) + for item in deletes: + (id,ign) = item.data(Qt.UserRole).toInt() + self.to_delete.add(id) + self.available_tags.takeItem(self.available_tags.row(item)) + + if row >= self.available_tags.count(): + row = self.available_tags.count() - 1 + if row >= 0: + self.available_tags.scrollToItem(self.available_tags.item(row)) diff --git a/src/calibre/gui2/dialogs/device_category_editor.ui b/src/calibre/gui2/dialogs/device_category_editor.ui new file mode 100644 index 0000000000..501cc4cf5a --- /dev/null +++ b/src/calibre/gui2/dialogs/device_category_editor.ui @@ -0,0 +1,166 @@ + + + DeviceCategoryEditor + + + + 0 + 0 + 397 + 335 + + + + Category Editor + + + + :/images/chapters.png:/images/chapters.png + + + + + + + + + + Items in use + + + available_tags + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Delete item from database. This will unapply the item from all books and then remove it from the database. + + + ... + + + + :/images/trash.png:/images/trash.png + + + + 32 + 32 + + + + + + + + Rename the item in every book where it is used. + + + ... + + + + :/images/edit_input.png:/images/edit_input.png + + + + 32 + 32 + + + + Ctrl+S + + + + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + TagListEditor + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + TagListEditor + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 22aad5ff61..d1945f7085 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -1,16 +1,16 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -from PyQt4.QtCore import Qt, QString -from PyQt4.QtGui import QDialog, QListWidgetItem +from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QIcon, QByteArray, QString) from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor -from calibre.gui2 import question_dialog, error_dialog +from calibre.gui2 import question_dialog, error_dialog, gprefs +from calibre.utils.icu import sort_key -class ListWidgetItem(QListWidgetItem): +class NameTableWidgetItem(QTableWidgetItem): def __init__(self, txt): - QListWidgetItem.__init__(self, txt) + QTableWidgetItem.__init__(self, txt) self.initial_value = QString(txt) self.current_value = QString(txt) self.previous_value = QString(txt) @@ -25,13 +25,13 @@ class ListWidgetItem(QListWidgetItem): elif role == Qt.EditRole: return self.current_value else: - return QListWidgetItem.data(self, role) + return QTableWidgetItem.data(self, role) def setData(self, role, data): if role == Qt.EditRole: self.previous_value = self.current_value self.current_value = data.toString() - QListWidgetItem.setData(self, role, data) + QTableWidgetItem.setData(self, role, data) def text(self): return self.current_value @@ -44,42 +44,127 @@ class ListWidgetItem(QListWidgetItem): def setText(self, txt): self.current_value = txt - QListWidgetItem.setText(txt) + QTableWidgetItem.setText(txt) + + def __ge__(self, other): + return sort_key(unicode(self.text())) >= sort_key(unicode(other.text())) + + def __lt__(self, other): + return sort_key(unicode(self.text())) < sort_key(unicode(other.text())) + +class CountTableWidgetItem(QTableWidgetItem): + + def __init__(self, count): + QTableWidgetItem.__init__(self, str(count)) + self._count = count + + def __ge__(self, other): + return self._count >= other._count + + def __lt__(self, other): + return self._count < other._count + class TagListEditor(QDialog, Ui_TagListEditor): - def __init__(self, window, tag_to_match, data, key): + def __init__(self, window, cat_name, tag_to_match, data, sorter): QDialog.__init__(self, window) Ui_TagListEditor.__init__(self) self.setupUi(self) + + t = self.windowTitle() + self.setWindowTitle(t + ' (' + cat_name + ')') # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) + try: + self.table_column_widths = \ + gprefs.get('tag_list_editor_table_widths', None) + geom = gprefs.get('tag_list_editor_dialog_geometry', bytearray('')) + self.restoreGeometry(QByteArray(geom)) + except: + pass + self.to_rename = {} self.to_delete = set([]) self.original_names = {} self.all_tags = {} + self.counts = {} - for k,v in data: + for k,v,count in data: self.all_tags[v] = k + self.counts[v] = count self.original_names[k] = v - for tag in sorted(self.all_tags.keys(), key=key): - item = ListWidgetItem(tag) + + # Set up the column headings + self.down_arrow_icon = QIcon(I('arrow-down.png')) + self.up_arrow_icon = QIcon(I('arrow-up.png')) + self.blank_icon = QIcon(I('blank.png')) + + self.table.setColumnCount(2) + self.name_col = QTableWidgetItem(_('Tag')) + self.table.setHorizontalHeaderItem(0, self.name_col) + self.name_col.setIcon(self.up_arrow_icon) + self.count_col = QTableWidgetItem(_('Count')) + self.table.setHorizontalHeaderItem(1, self.count_col) + self.count_col.setIcon(self.blank_icon) + + # Capture clicks on the horizontal header to sort the table columns + hh = self.table.horizontalHeader(); + hh.setClickable(True) + hh.sectionClicked.connect(self.header_clicked) + hh.sectionResized.connect(self.table_column_resized) + self.name_order = 0 + self.count_order = 1 + + # Add the data + select_item = None + self.table.setRowCount(len(self.all_tags)) + for row,tag in enumerate(sorted(self.all_tags.keys(), key=sorter)): + item = NameTableWidgetItem(tag) item.setData(Qt.UserRole, self.all_tags[tag]) item.setFlags (item.flags() | Qt.ItemIsEditable) - self.available_tags.addItem(item) + self.table.setItem(row, 0, item) + if tag == tag_to_match: + select_item = item + item = CountTableWidgetItem(self.counts[tag]) + item.setFlags (item.flags() & ~Qt.ItemIsSelectable) + self.table.setItem(row, 1, item) - if tag_to_match is not None: - items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly) - if len(items) == 1: - self.available_tags.setCurrentItem(items[0]) + if select_item is not None: + self.table.setCurrentItem(select_item) self.delete_button.clicked.connect(self.delete_tags) self.rename_button.clicked.connect(self.rename_tag) - self.available_tags.itemDoubleClicked.connect(self._rename_tag) - self.available_tags.itemChanged.connect(self.finish_editing) + self.table.itemDoubleClicked.connect(self._rename_tag) + self.table.itemChanged.connect(self.finish_editing) + + def table_column_resized(self, col, old, new): + self.save_state() + + def save_state(self): + self.table_column_widths = [] + for c in range(0, self.table.columnCount()): + self.table_column_widths.append(self.table.columnWidth(c)) + gprefs['tag_list_editor_table_widths'] = self.table_column_widths + gprefs['tag_list_editor_dialog_geometry'] = bytearray(self.saveGeometry()) + + def resizeEvent(self, *args): + QDialog.resizeEvent(self, *args) + if self.table_column_widths is not None: + for c,w in enumerate(self.table_column_widths): + self.table.setColumnWidth(c, w) + else: + # the vertical scroll bar might not be rendered, so might not yet + # have a width. Assume 25. Not a problem because user-changed column + # widths will be remembered + w = self.table.width() - 25 - self.table.verticalHeader().width() + w /= self.table.columnCount() + for c in range(0, self.table.columnCount()): + self.table.setColumnWidth(c, w) + self.save_state() def finish_editing(self, item): if not item.text(): @@ -92,7 +177,7 @@ class TagListEditor(QDialog, Ui_TagListEditor): self.to_rename[id_] = unicode(item.text()) def rename_tag(self): - item = self.available_tags.currentItem() + item = self.table.item(self.table.currentRow(), 0) self._rename_tag(item) def _rename_tag(self, item): @@ -100,10 +185,10 @@ class TagListEditor(QDialog, Ui_TagListEditor): error_dialog(self, _('No item selected'), _('You must select one item from the list of Available items.')).exec_() return - self.available_tags.editItem(item) + self.table.editItem(item) def delete_tags(self): - deletes = self.available_tags.selectedItems() + deletes = self.table.selectedItems() if not deletes: error_dialog(self, _('No items selected'), _('You must select at least one items from the list.')).exec_() @@ -112,13 +197,37 @@ class TagListEditor(QDialog, Ui_TagListEditor): if not question_dialog(self, _('Are your sure?'), '

'+_('Are you certain you want to delete the following items?')+'
'+ct): return - row = self.available_tags.row(deletes[0]) + row = self.table.row(deletes[0]) for item in deletes: (id,ign) = item.data(Qt.UserRole).toInt() self.to_delete.add(id) - self.available_tags.takeItem(self.available_tags.row(item)) + self.table.removeRow(self.table.row(item)) - if row >= self.available_tags.count(): - row = self.available_tags.count() - 1 + if row >= self.table.rowCount(): + row = self.table.rowCount() - 1 if row >= 0: - self.available_tags.scrollToItem(self.available_tags.item(row)) + self.table.scrollToItem(self.table.item(row, 0)) + + def header_clicked(self, idx): + if idx == 0: + self.do_sort_by_name() + else: + self.do_sort_by_count() + + def do_sort_by_name(self): + self.name_order = 1 if self.name_order == 0 else 0 + self.table.sortByColumn(0, self.name_order) + self.name_col.setIcon(self.down_arrow_icon if self.name_order + else self.up_arrow_icon) + self.count_col.setIcon(self.blank_icon) + + def do_sort_by_count (self): + self.count_order = 1 if self.count_order == 0 else 0 + self.table.sortByColumn(1, self.count_order) + self.count_col.setIcon(self.down_arrow_icon if self.count_order + else self.up_arrow_icon) + self.name_col.setIcon(self.blank_icon) + + def accepted(self): + self.save_state() + self.accept() diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui index 9c8231dd91..ccc404bf9c 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.ui +++ b/src/calibre/gui2/dialogs/tag_list_editor.ui @@ -20,33 +20,6 @@ - - - - - - Items in use - - - available_tags - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - @@ -97,7 +70,7 @@ - + true diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py index 390273e5b2..dc6238ae92 100644 --- a/src/calibre/gui2/tag_browser/ui.py +++ b/src/calibre/gui2/tag_browser/ui.py @@ -197,25 +197,19 @@ class TagBrowserMixin(object): # {{{ dialog will position the editor on that item. ''' db=self.library_view.model().db - if category == 'tags': - result = db.get_tags_with_ids() - key = sort_key - elif category == 'series': - result = db.get_series_with_ids() + cats = db.get_categories(sort='name', icon_map=None) + if category in cats: + result = [(t.id, t.name, t.count) for t in cats[category]] + else: + return + + if category == 'series': key = lambda x:sort_key(title_sort(x)) - elif category == 'publisher': - result = db.get_publishers_with_ids() - key = sort_key - else: # should be a custom field - cc_label = None - if category in db.field_metadata: - cc_label = db.field_metadata[category]['label'] - result = db.get_custom_items_with_ids(label=cc_label) - else: - result = [] + else: key = sort_key - d = TagListEditor(self, tag_to_match=tag, data=result, key=key) + d = TagListEditor(self, cat_name=db.field_metadata[category]['name'], + tag_to_match=tag, data=result, sorter=key) d.exec_() if d.result() == d.Accepted: to_rename = d.to_rename # dict of old id to new name @@ -232,7 +226,8 @@ class TagBrowserMixin(object): # {{{ elif category == 'publisher': rename_func = db.rename_publisher delete_func = db.delete_publisher_using_id - else: + else: # must be custom + cc_label = db.field_metadata[category]['label'] rename_func = partial(db.rename_custom_item, label=cc_label) delete_func = partial(db.delete_custom_item_using_id, label=cc_label) m = self.tags_view.model() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 7e44ccc8bc..fff2d2189c 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1810,6 +1810,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for t in categories[sc]: user_categories[c].append([t.name, sc, 0]) + gst_icon = icon_map['gst'] if icon_map else None for user_cat in sorted(user_categories.keys(), key=sort_key): items = [] names_seen = {} @@ -1825,7 +1826,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): t.tooltip = t.tooltip.replace(')', ', ' + label + ')') else: t = copy.copy(taglist[label][n]) - t.icon = icon_map['gst'] + t.icon = gst_icon names_seen[t.name] = t items.append(t) else: