mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
Fixes #2091139 [Request for Feature: Reordering Columns in Custom Column Editor](https://bugs.launchpad.net/calibre/+bug/2091139) Fixes #2091189 [Enumerated columns: Item count in permissible values dialog](https://bugs.launchpad.net/calibre/+bug/2091189)
This commit is contained in:
commit
14e1025a71
@ -18,6 +18,19 @@ from qt.core import (
|
||||
)
|
||||
|
||||
from calibre.gui2 import error_dialog, gprefs
|
||||
from calibre.utils.localization import ngettext
|
||||
|
||||
|
||||
class CountTableWidgetItem(QTableWidgetItem):
|
||||
|
||||
def __init__(self, count):
|
||||
QTableWidgetItem.__init__(self, str(count) if count is not None else '0')
|
||||
self.setTextAlignment(Qt.AlignmentFlag.AlignRight|Qt.AlignmentFlag.AlignVCenter)
|
||||
self.setFlags(self.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
|
||||
self._count = count
|
||||
|
||||
def set_count(self, count):
|
||||
self.setText(str(count) if count is not None else '0')
|
||||
|
||||
|
||||
class EnumValuesEdit(QDialog):
|
||||
@ -33,7 +46,8 @@ class EnumValuesEdit(QDialog):
|
||||
bbox.addStretch(10)
|
||||
self.del_button = QToolButton()
|
||||
self.del_button.setIcon(QIcon.ic('trash.png'))
|
||||
self.del_button.setToolTip(_('Remove the currently selected value'))
|
||||
self.del_button.setToolTip(_('Remove the currently selected value. Only '
|
||||
'values with a count of zero can be removed'))
|
||||
self.ins_button = QToolButton()
|
||||
self.ins_button.setIcon(QIcon.ic('plus.png'))
|
||||
self.ins_button.setToolTip(_('Add a new permissible value'))
|
||||
@ -58,12 +72,15 @@ class EnumValuesEdit(QDialog):
|
||||
tl = QVBoxLayout()
|
||||
l.addItem(tl, 0, 1)
|
||||
self.table = t = QTableWidget(parent)
|
||||
t.setColumnCount(2)
|
||||
t.setColumnCount(3)
|
||||
t.setRowCount(1)
|
||||
t.setHorizontalHeaderLabels([_('Value'), _('Color')])
|
||||
t.setHorizontalHeaderLabels([_('Value'), _('Color'), _('Count')])
|
||||
t.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
tl.addWidget(t)
|
||||
|
||||
counts = self.db.new_api.get_usage_count_by_id(key)
|
||||
self.name_to_count = {self.db.new_api.get_item_name(key, item_id):count for item_id,count in counts.items()}
|
||||
|
||||
self.key = key
|
||||
self.fm = fm = db.field_metadata[key]
|
||||
permitted_values = fm.get('display', {}).get('enum_values', '')
|
||||
@ -76,6 +93,7 @@ class EnumValuesEdit(QDialog):
|
||||
c.setCurrentIndex(c.findText(colors[i]))
|
||||
else:
|
||||
c.setCurrentIndex(0)
|
||||
self.make_count_item(i, v)
|
||||
|
||||
t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
@ -86,11 +104,19 @@ class EnumValuesEdit(QDialog):
|
||||
bb.rejected.connect(self.reject)
|
||||
l.addWidget(bb, 1, 0, 1, 2)
|
||||
|
||||
self.table.cellChanged.connect(self.cell_changed)
|
||||
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)
|
||||
self.restore_geometry(gprefs, 'enum-values-edit-geometry')
|
||||
|
||||
def cell_changed(self, row, col):
|
||||
if col == 0:
|
||||
item = self.table.item(row, 2)
|
||||
if item is not None and self.table.item(row, 0) is not None:
|
||||
count = self.name_to_count.get(self.table.item(row, 0).text())
|
||||
item.set_count(count)
|
||||
|
||||
def sizeHint(self):
|
||||
sz = QDialog.sizeHint(self)
|
||||
sz.setWidth(max(sz.width(), 600))
|
||||
@ -115,6 +141,10 @@ class EnumValuesEdit(QDialog):
|
||||
c.setCurrentIndex(dex)
|
||||
return c
|
||||
|
||||
def make_count_item(self, row, txt):
|
||||
it = CountTableWidgetItem(self.name_to_count.get(txt))
|
||||
self.table.setItem(row, 2, it)
|
||||
|
||||
def move_up_clicked(self):
|
||||
row = self.table.currentRow()
|
||||
if row < 0:
|
||||
@ -128,11 +158,13 @@ class EnumValuesEdit(QDialog):
|
||||
def move_row(self, row, direction):
|
||||
t = self.table.takeItem(row, 0)
|
||||
c = self.table.cellWidget(row, 1).currentIndex()
|
||||
count = self.table.takeItem(row, 2)
|
||||
self.table.removeRow(row)
|
||||
row += direction
|
||||
self.table.insertRow(row)
|
||||
self.table.setItem(row, 0, t)
|
||||
self.make_color_combobox(row, c)
|
||||
self.table.setItem(row, 2, count)
|
||||
self.table.setCurrentCell(row, 0)
|
||||
|
||||
def move_down_clicked(self):
|
||||
@ -146,7 +178,18 @@ class EnumValuesEdit(QDialog):
|
||||
self.move_row(row, 1)
|
||||
|
||||
def del_line(self):
|
||||
if self.table.currentRow() >= 0:
|
||||
row = self.table.currentRow()
|
||||
if row >= 0:
|
||||
txt = self.table.item(row, 0).text()
|
||||
count = self.name_to_count.get(txt, 0)
|
||||
if count > 0:
|
||||
error_dialog(self,
|
||||
_('Cannot remove value "{}"').format(txt),
|
||||
ngettext('The value "{0}" is used in {1} book and cannot be removed.',
|
||||
'The value "{0}" is used in {1} books and cannot be removed.',
|
||||
count).format(txt, count),
|
||||
show=True)
|
||||
return
|
||||
self.table.removeRow(self.table.currentRow())
|
||||
|
||||
def ins_button_clicked(self):
|
||||
@ -158,6 +201,7 @@ class EnumValuesEdit(QDialog):
|
||||
self.table.insertRow(row)
|
||||
self.make_name_item(row, '')
|
||||
self.make_color_combobox(row, -1)
|
||||
self.make_count_item(row, '')
|
||||
|
||||
def save_geometry(self):
|
||||
super().save_geometry(gprefs, 'enum-values-edit-geometry')
|
||||
|
@ -19,6 +19,16 @@ from calibre.gui2.preferences.create_custom_column import CreateCustomColumn
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
ORDER_COLUMN = 0
|
||||
HEADER_COLUMN = 1
|
||||
KEY_COLUMN = 2
|
||||
TYPE_COLUMN = 3
|
||||
DESCRIPTION_COLUMN = 4
|
||||
STATUS_COLUMN = 5
|
||||
|
||||
column_headings = (_('Order'), _('Column header'), _('Lookup name'),
|
||||
_('Type'), _('Description'), _('Status'))
|
||||
|
||||
restart_critical = True
|
||||
|
||||
def genesis(self, gui):
|
||||
@ -46,6 +56,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
signal.connect(self.columns_changed)
|
||||
self.show_all_button.clicked.connect(self.show_all)
|
||||
self.hide_all_button.clicked.connect(self.hide_all)
|
||||
self.column_positions = None
|
||||
|
||||
def initialize(self):
|
||||
ConfigWidgetBase.initialize(self)
|
||||
@ -57,10 +68,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.changed_signal.emit()
|
||||
|
||||
def commit(self):
|
||||
widths = []
|
||||
for i in range(0, self.opt_columns.columnCount()):
|
||||
widths.append(self.opt_columns.columnWidth(i))
|
||||
gprefs.set('custcol-prefs-table-geometry', widths)
|
||||
rr = ConfigWidgetBase.commit(self)
|
||||
return self.apply_custom_column_changes() or rr
|
||||
|
||||
@ -79,13 +86,24 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.field_metadata = db.field_metadata
|
||||
|
||||
self.opt_columns.setColumnCount(6)
|
||||
self.opt_columns.setHorizontalHeaderItem(0, QTableWidgetItem(_('Order')))
|
||||
self.opt_columns.setHorizontalHeaderItem(1, QTableWidgetItem(_('Column header')))
|
||||
self.opt_columns.setHorizontalHeaderItem(2, QTableWidgetItem(_('Lookup name')))
|
||||
self.opt_columns.setHorizontalHeaderItem(3, QTableWidgetItem(_('Type')))
|
||||
self.opt_columns.setHorizontalHeaderItem(4, QTableWidgetItem(_('Description')))
|
||||
self.opt_columns.setHorizontalHeaderItem(5, QTableWidgetItem(_('Status')))
|
||||
self.opt_columns.horizontalHeader().sectionClicked.connect(self.table_sorted)
|
||||
# Set up the columns in logical index order
|
||||
for p in range(0, len(self.column_headings)):
|
||||
self.opt_columns.setHorizontalHeaderItem(p, QTableWidgetItem(self.column_headings[p]))
|
||||
|
||||
# Now reorder the columns into the desired visual order. Note: ignore
|
||||
# visual order when looking at items. Qt automatically maps the visual
|
||||
# order onto the logical order.
|
||||
self.column_positions = gprefs.get('custcol-prefs-column_order', [0, 1, 2, 3, 4, 5])
|
||||
header = self.opt_columns.horizontalHeader()
|
||||
for dvi,li in enumerate(self.column_positions):
|
||||
cvi = header.visualIndex(li)
|
||||
if cvi != dvi:
|
||||
header.moveSection(cvi, dvi)
|
||||
header.sectionClicked.connect(self.table_sorted)
|
||||
header.setSectionsMovable(True)
|
||||
header.setFirstSectionMovable(False)
|
||||
header.sectionMoved.connect(self.header_moved)
|
||||
header.sectionResized.connect(self.save_geometry)
|
||||
self.opt_columns.verticalHeader().hide()
|
||||
|
||||
self.opt_columns.setRowCount(len(colmap))
|
||||
@ -104,6 +122,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.set_up_down_enabled(self.opt_columns.currentItem(), None)
|
||||
self.opt_columns.blockSignals(False)
|
||||
|
||||
def header_moved(self, log_index, old_v_index, new_v_index):
|
||||
self.column_positions = []
|
||||
for vi in range(0, self.opt_columns.columnCount()):
|
||||
self.column_positions.append(self.opt_columns.horizontalHeader().logicalIndex(vi))
|
||||
self.save_geometry()
|
||||
|
||||
def set_up_down_enabled(self, current_item, _):
|
||||
h = self.opt_columns.horizontalHeader()
|
||||
row = current_item.row()
|
||||
@ -130,7 +154,16 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
def row_double_clicked(self, r, c):
|
||||
self.edit_custcol()
|
||||
|
||||
def save_geometry(self):
|
||||
# Save both the column widths and the column order
|
||||
widths = []
|
||||
for i in range(0, self.opt_columns.columnCount()):
|
||||
widths.append(self.opt_columns.columnWidth(i))
|
||||
gprefs.set('custcol-prefs-table-geometry', widths)
|
||||
gprefs.set('custcol-prefs-column_order', self.column_positions)
|
||||
|
||||
def restore_geometry(self):
|
||||
# restore the column widths. Order is done when the table is created.
|
||||
geom = gprefs.get('custcol-prefs-table-geometry', None)
|
||||
if geom is not None and len(geom) == self.opt_columns.columnCount():
|
||||
with suppress(Exception):
|
||||
@ -141,14 +174,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
def hide_all(self):
|
||||
for row in range(self.opt_columns.rowCount()):
|
||||
item = self.opt_columns.item(row, 0)
|
||||
item = self.opt_columns.item(row, self.ORDER_COLUMN)
|
||||
if item.checkState() != Qt.CheckState.PartiallyChecked:
|
||||
item.setCheckState(Qt.CheckState.Unchecked)
|
||||
self.changed_signal.emit()
|
||||
|
||||
def show_all(self):
|
||||
for row in range(self.opt_columns.rowCount()):
|
||||
item = self.opt_columns.item(row, 0)
|
||||
item = self.opt_columns.item(row, self.ORDER_COLUMN)
|
||||
if item.checkState() != Qt.CheckState.PartiallyChecked:
|
||||
item.setCheckState(Qt.CheckState.Checked)
|
||||
self.changed_signal.emit()
|
||||
@ -169,7 +202,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
item.setToolTip(str(order))
|
||||
item.setData(Qt.ItemDataRole.UserRole, key)
|
||||
item.setFlags(flags)
|
||||
self.opt_columns.setItem(row, 0, item)
|
||||
self.opt_columns.setItem(row, self.ORDER_COLUMN, item)
|
||||
|
||||
flags |= Qt.ItemFlag.ItemIsUserCheckable
|
||||
if key == 'ondevice':
|
||||
@ -182,17 +215,20 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
else:
|
||||
item.setCheckState(force_checked_to)
|
||||
|
||||
# The columns are added in logical index order, not visual index order,
|
||||
# so we can process them without a loop
|
||||
|
||||
item = QTableWidgetItem(cc['name'])
|
||||
item.setToolTip(cc['name'])
|
||||
item.setFlags(flags)
|
||||
if self.is_custom_key(key):
|
||||
item.setData(Qt.ItemDataRole.DecorationRole, (QIcon.ic('column.png')))
|
||||
self.opt_columns.setItem(row, 1, item)
|
||||
self.opt_columns.setItem(row, self.HEADER_COLUMN, item)
|
||||
|
||||
item = QTableWidgetItem(key)
|
||||
item.setToolTip(key)
|
||||
item.setFlags(flags)
|
||||
self.opt_columns.setItem(row, 2, item)
|
||||
self.opt_columns.setItem(row, self.KEY_COLUMN, item)
|
||||
|
||||
if key == 'title':
|
||||
coltype = _('Text')
|
||||
@ -210,13 +246,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
item = QTableWidgetItem(coltype)
|
||||
item.setToolTip(coltype)
|
||||
item.setFlags(flags)
|
||||
self.opt_columns.setItem(row, 3, item)
|
||||
self.opt_columns.setItem(row, self.TYPE_COLUMN, item)
|
||||
|
||||
desc = cc['display'].get('description', "")
|
||||
item = QTableWidgetItem(desc)
|
||||
item.setToolTip(desc)
|
||||
item.setFlags(flags)
|
||||
self.opt_columns.setItem(row, 4, item)
|
||||
self.opt_columns.setItem(row, self.DESCRIPTION_COLUMN, item)
|
||||
|
||||
if '*deleted' in cc:
|
||||
col_status = _('Deleted column. Double-click to undelete it')
|
||||
@ -231,13 +267,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
item = QTableWidgetItem(col_status)
|
||||
item.setToolTip(col_status)
|
||||
item.setFlags(flags)
|
||||
self.opt_columns.setItem(row, 5, item)
|
||||
self.opt_columns.setItem(row, self.STATUS_COLUMN, item)
|
||||
|
||||
self.opt_columns.setSortingEnabled(True)
|
||||
|
||||
def recreate_row(self, row):
|
||||
checked = self.opt_columns.item(row, 0).checkState()
|
||||
title = self.opt_columns.item(row, 2).text()
|
||||
self.setup_row(row, title, row, force_checked_to=checked)
|
||||
checked = self.opt_columns.item(row, self.ORDER_COLUMN).checkState()
|
||||
# Again, use the logical index, not the visual index
|
||||
key = self.opt_columns.item(row, self.KEY_COLUMN).text()
|
||||
self.setup_row(row, key, row, force_checked_to=checked)
|
||||
|
||||
def get_move_count(self):
|
||||
mods = QApplication.keyboardModifiers()
|
||||
@ -297,7 +335,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
if row < 0:
|
||||
return error_dialog(self, '', _('You must select a column to delete it'),
|
||||
show=True)
|
||||
key = str(self.opt_columns.item(row, 0).data(Qt.ItemDataRole.UserRole) or '')
|
||||
key = str(self.opt_columns.item(row, self.ORDER_COLUMN).data(Qt.ItemDataRole.UserRole) or '')
|
||||
if key not in self.custcols:
|
||||
return error_dialog(self, '',
|
||||
_('The selected column is not a custom column'), show=True)
|
||||
@ -340,13 +378,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
return key.startswith('#')
|
||||
|
||||
def column_order_val(self, row):
|
||||
return int(self.opt_columns.item(row, 0).text())
|
||||
return int(self.opt_columns.item(row, self.ORDER_COLUMN).text())
|
||||
|
||||
def edit_custcol(self):
|
||||
model = self.gui.library_view.model()
|
||||
row = self.opt_columns.currentRow()
|
||||
try:
|
||||
key = str(self.opt_columns.item(row, 0).data(Qt.ItemDataRole.UserRole))
|
||||
key = str(self.opt_columns.item(row, self.ORDER_COLUMN).data(Qt.ItemDataRole.UserRole))
|
||||
if key not in self.custcols:
|
||||
return error_dialog(self, '',
|
||||
_('The selected column is not a user-defined column'),
|
||||
@ -382,14 +420,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
model = self.gui.library_view.model()
|
||||
db = model.db
|
||||
self.opt_columns.sortItems(0, Qt.SortOrder.AscendingOrder)
|
||||
config_cols = [str(self.opt_columns.item(i, 0).data(Qt.ItemDataRole.UserRole) or '')
|
||||
config_cols = [str(self.opt_columns.item(i, self.ORDER_COLUMN).data(Qt.ItemDataRole.UserRole) or '')
|
||||
for i in range(self.opt_columns.rowCount())]
|
||||
if not config_cols:
|
||||
config_cols = ['title']
|
||||
removed_cols = set(model.column_map) - set(config_cols)
|
||||
hidden_cols = {str(self.opt_columns.item(i, 0).data(Qt.ItemDataRole.UserRole) or '')
|
||||
hidden_cols = {str(self.opt_columns.item(i, self.ORDER_COLUMN).data(Qt.ItemDataRole.UserRole) or '')
|
||||
for i in range(self.opt_columns.rowCount())
|
||||
if self.opt_columns.item(i, 0).checkState()==Qt.CheckState.Unchecked}
|
||||
if self.opt_columns.item(i, self.ORDER_COLUMN).checkState()==Qt.CheckState.Unchecked}
|
||||
hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols
|
||||
hidden_cols = list(hidden_cols.intersection(set(model.column_map)))
|
||||
if 'ondevice' in hidden_cols:
|
||||
|
Loading…
x
Reference in New Issue
Block a user