Make the Manage Tags/Publishers/etc. dialog show a column with counts for each item, to easily sort by number of items

This commit is contained in:
Kovid Goyal 2011-07-31 19:13:43 -06:00
commit b0d4315372
8 changed files with 467 additions and 81 deletions

View File

@ -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

View File

@ -0,0 +1,124 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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 item from the list.')).exec_()
return
ct = ', '.join([unicode(item.text()) for item in deletes])
if not question_dialog(self, _('Are you sure?'),
'<p>'+_('Are you sure you want to delete the following items?')+'<br>'+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))

View File

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DeviceCategoryEditor</class>
<widget class="QDialog" name="DeviceCategoryEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>397</width>
<height>335</height>
</rect>
</property>
<property name="windowTitle">
<string>Category Editor</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Items in use</string>
</property>
<property name="buddy">
<cstring>available_tags</cstring>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QToolButton" name="delete_button">
<property name="toolTip">
<string>Delete item from database. This will unapply the item from all books and then remove it from the database.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="rename_button">
<property name="toolTip">
<string>Rename the item in every book where it is used.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/edit_input.png</normaloff>:/images/edit_input.png</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="available_tags">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>DeviceCategoryEditor</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>DeviceCategoryEditor</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,16 +1,17 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtCore import Qt, QString
from PyQt4.QtGui import QDialog, QListWidgetItem
from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QIcon, QByteArray,
QString, QSize)
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 +26,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 +45,138 @@ 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)
# Put the category name into the title bar
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)
# Get saved geometry info
try:
self.table_column_widths = \
gprefs.get('tag_list_editor_table_widths', None)
except:
pass
# initialization
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)
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])
# 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.ItemIsSelectable | Qt.ItemIsEditable)
self.table.setItem(row, 0, item)
if tag == tag_to_match:
select_item = item
item = CountTableWidgetItem(self.counts[tag])
# only the name column can be selected
item.setFlags (item.flags() & ~Qt.ItemIsSelectable)
self.table.setItem(row, 1, item)
# Scroll to the selected item if there is one
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)
self.buttonBox.accepted.connect(self.accepted)
try:
geom = gprefs.get('tag_list_editor_dialog_geometry', None)
if geom is not None:
self.restoreGeometry(QByteArray(geom))
else:
self.resize(self.sizeHint()+QSize(150, 100))
except:
pass
def table_column_resized(self, col, old, new):
self.table_column_widths = []
for c in range(0, self.table.columnCount()):
self.table_column_widths.append(self.table.columnWidth(c))
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)
def save_geometry(self):
gprefs['tag_list_editor_table_widths'] = self.table_column_widths
gprefs['tag_list_editor_dialog_geometry'] = bytearray(self.saveGeometry())
def finish_editing(self, item):
if not item.text():
@ -92,7 +189,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,25 +197,48 @@ 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_()
_('You must select at least one item from the list.')).exec_()
return
ct = ', '.join([unicode(item.text()) for item in deletes])
if not question_dialog(self, _('Are your sure?'),
'<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct):
if not question_dialog(self, _('Are you sure?'),
'<p>'+_('Are you sure you want to delete the following items?')+'<br>'+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_geometry()

View File

@ -20,33 +20,6 @@
<layout class="QGridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Items in use</string>
</property>
<property name="buddy">
<cstring>available_tags</cstring>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
@ -97,7 +70,7 @@
</layout>
</item>
<item>
<widget class="QListWidget" name="available_tags">
<widget class="QTableWidget" name="table">
<property name="alternatingRowColors">
<bool>true</bool>
</property>

View File

@ -500,6 +500,7 @@ class TagsModel(QAbstractItemModel): # {{{
if i < len(components)-1:
t = copy.copy(tag)
t.original_name = '.'.join(components[:i+1])
t.count = 0
if key != 'search':
# This 'manufactured' intermediate node can
# be searched, but cannot be edited.
@ -524,6 +525,12 @@ class TagsModel(QAbstractItemModel): # {{{
for category in self.category_nodes:
process_one_node(category, state_map.get(category.category_key, {}))
def get_category_editor_data(self, category):
for cat in self.root_item.children:
if cat.category_key == category:
return [(t.tag.id, t.tag.original_name, t.tag.count)
for t in cat.child_tags() if t.tag.count > 0]
# Drag'n Drop {{{
def mimeTypes(self):
return ["application/calibre+from_library",

View File

@ -196,26 +196,20 @@ class TagBrowserMixin(object): # {{{
Open the 'manage_X' dialog where X == category. If tag is not None, the
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()
tags_model = self.tags_view.model()
result = tags_model.get_category_editor_data(category)
if result is None:
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)
db=self.library_view.model().db
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()

View File

@ -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: