Tag browser: Move the preferences for controlling the order and display of categories in the Tag browser from Preferences->Tweaks to Preferences->Look & feel->Tag browser. Fixes #1987235 [Enhancement Request: Easier way to rearrange Tag Browser](https://bugs.launchpad.net/calibre/+bug/1987235)

Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
Kovid Goyal 2022-08-24 20:17:17 +05:30
commit fae1682ed8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 308 additions and 75 deletions

View File

@ -126,31 +126,6 @@ categories_collapsed_name_template = r'{first.sort:shorten(4,,0)} - {last.sort:s
categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}'
#: Control order of categories in the Tag browser
# Change the following dict to change the order that categories are displayed in
# the Tag browser. Items are named using their lookup name, and will be sorted
# using the number supplied. The lookup name '*' stands for all names that
# otherwise do not appear. Two names with the same value will be sorted
# using the default order, the one specified by tag_browser_category_default_sort.
# Example:
# tag_browser_category_order = {'series':1, 'tags':2, '*':3}
#
# results in the order series, tags, then everything else in default order.
# The tweak tag_browser_category_default_sort specifies the sort order before
# applying the category order from the dict. The allowed values are:
# tag_browser_category_default_sort = 'default' # The calibre default order
# tag_browser_category_default_sort = 'display_name' # Sort by the display name of the category
# tag_browser_category_default_sort = 'lookup_name' # Sort by the lookup name of the category
#
# In addition and if the category default sort is not 'default' you can specify
# whether the sort is ascending or descending. This is ignored if the sort is 'default'.
# tag_browser_category_default_sort_direction = 'ascending'
# tag_browser_category_default_sort_direction = 'descending'
tag_browser_category_order = {'*':1}
tag_browser_category_default_sort = 'default'
tag_browser_category_default_sort_direction = 'ascending'
#: Specify columns to sort the booklist by on startup
# Provide a set of columns to be sorted on when calibre starts.
# The argument is None if saved sort history is to be used

View File

@ -219,13 +219,14 @@ class IdLinksEditor(Dialog):
class DisplayedFields(QAbstractListModel): # {{{
def __init__(self, db, parent=None, pref_name=None):
def __init__(self, db, parent=None, pref_name=None, category_icons=None):
self.pref_name = pref_name or 'book_display_fields'
QAbstractListModel.__init__(self, parent)
self.fields = []
self.db = db
self.changed = False
self.category_icons = category_icons
def get_field_list(self, use_defaults=False):
return get_field_list(self.db.field_metadata, use_defaults=use_defaults, pref_name=self.pref_name)
@ -255,8 +256,13 @@ class DisplayedFields(QAbstractListModel): # {{{
return f'{name} ({field})'
if role == Qt.ItemDataRole.CheckStateRole:
return Qt.CheckState.Checked if visible else Qt.CheckState.Unchecked
if role == Qt.ItemDataRole.DecorationRole and field.startswith('#'):
return QIcon.ic('column.png')
if role == Qt.ItemDataRole.DecorationRole:
if self.category_icons:
icon = self.category_icons.get(field, None)
if icon is not None:
return icon
if field.startswith('#'):
return QIcon.ic('column.png')
return None
def toggle_all(self, show=True):
@ -356,6 +362,42 @@ class QVDisplayedFields(DisplayedFields): # {{{
# }}}
class TBDisplayedFields(DisplayedFields): # {{{
# The code in this class depends on the fact that the tag browser is
# initialized before this class is instantiated.
def __init__(self, db, parent=None, category_icons=None):
DisplayedFields.__init__(self, db, parent, category_icons=category_icons)
from calibre.gui2.ui import get_gui
self.gui = get_gui()
def initialize(self, use_defaults=False, pref_data_override=None):
tv = self.gui.tags_view
cat_ord = tv.model().get_ordered_categories(use_defaults=use_defaults,
pref_data_override=pref_data_override)
if use_defaults:
hc = []
elif pref_data_override:
hc = [k for k,v in pref_data_override if not v]
else:
hc = tv.hidden_categories
self.beginResetModel()
self.fields = [[x, x not in hc] for x in cat_ord]
self.endResetModel()
self.changed = True
def is_standard_category(self, key):
return self.gui.tags_view.model().is_standard_category(key)
def commit(self):
if self.changed:
self.db.new_api.set_pref('tag_browser_hidden_categories', [k for k,v in self.fields if not v])
self.db.new_api.set_pref('tag_browser_category_order', [k for k,v in self.fields])
self.gui.tags_view.model().set_hidden_categories({k for k,v in self.fields if not v})
# }}}
class Background(QWidget): # {{{
def __init__(self, parent):
@ -572,6 +614,17 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
connect_lambda(self.qv_down_button.clicked, self,
lambda self: move_field_down(self.qv_display_order, self.qv_display_model))
self.tb_display_model = TBDisplayedFields(self.gui.current_db,
self.tb_display_order,
category_icons=self.gui.tags_view.model().category_custom_icons)
self.tb_display_model.dataChanged.connect(self.changed_signal)
self.tb_display_order.setModel(self.tb_display_model)
self.tb_reset_layout_button.clicked.connect(self.tb_reset_layout)
self.tb_export_layout_button.clicked.connect(self.tb_export_layout)
self.tb_import_layout_button.clicked.connect(self.tb_import_layout)
self.tb_up_button.clicked.connect(self.tb_up_button_clicked)
self.tb_down_button.clicked.connect(self.tb_down_button_clicked)
self.edit_rules = EditRules(self.tabWidget)
self.edit_rules.changed.connect(self.changed_signal)
self.tabWidget.addTab(self.edit_rules,
@ -672,6 +725,59 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.em_display_model.initialize(use_defaults=True)
self.changed_signal.emit()
def tb_export_layout(self):
filename = choose_save_file(self, 'em_import_export_field_list',
_('Save column list to file'),
filters=[(_('Column list'), ['json'])])
if filename:
try:
with open(filename, 'w') as f:
json.dump(self.tb_display_model.fields, f, indent=1)
except Exception as err:
error_dialog(self, _('Export field layout'),
_('<p>Could not write field list. Error:<br>%s')%err, show=True)
def tb_import_layout(self):
filename = choose_files(self, 'em_import_export_field_list',
_('Load column list from file'),
filters=[(_('Column list'), ['json'])])
if filename:
try:
with open(filename[0]) as f:
fields = json.load(f)
self.tb_display_model.initialize(pref_data_override=fields)
self.changed_signal.emit()
except Exception as err:
error_dialog(self, _('Import layout'),
_('<p>Could not read field list. Error:<br>%s')%err, show=True)
def tb_reset_layout(self):
self.tb_display_model.initialize(use_defaults=True)
self.changed_signal.emit()
def tb_down_button_clicked(self):
idx = self.tb_display_order.currentIndex()
if idx.isValid():
row = idx.row()
model = self.tb_display_model
fields = model.fields
key = fields[row][0]
if not model.is_standard_category(key):
return
if row < len(fields) and model.is_standard_category(fields[row+1][0]):
move_field_down(self.tb_display_order, model)
def tb_up_button_clicked(self):
idx = self.tb_display_order.currentIndex()
if idx.isValid():
row = idx.row()
model = self.tb_display_model
fields = model.fields
key = fields[row][0]
if not model.is_standard_category(key):
return
move_field_up(self.tb_display_order, model)
def choose_icon_theme(self):
from calibre.gui2.icon_theme import ChooseTheme
d = ChooseTheme(self)
@ -732,6 +838,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.display_model.initialize()
self.em_display_model.initialize()
self.qv_display_model.initialize()
self.tb_display_model.initialize()
db = self.gui.current_db
mi = []
try:
@ -866,6 +973,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.display_model.commit()
self.em_display_model.commit()
self.qv_display_model.commit()
self.tb_display_model.commit()
self.edit_rules.commit(self.gui.current_db.prefs)
self.icon_rules.commit(self.gui.current_db.prefs)
self.grid_rules.commit(self.gui.current_db.prefs)

View File

@ -1175,7 +1175,119 @@ using the Tab key. The F2 (Edit) key will still open the template editor.&lt;/p&
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="1" column="1">
<item row="1" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select the categories to display in the Tag browser and their order</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0">
<widget class="QLabel">
<property name="text">
<string>User categories and Saved searches cannot be moved</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QToolButton" name="tb_down_button">
<property name="toolTip">
<string>Move down</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-down.png</normaloff>:/images/arrow-down.png</iconset>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="tb_up_button">
<property name="toolTip">
<string>Move up</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.png</normaloff>:/images/arrow-up.png</iconset>
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0" rowspan="3">
<widget class="QListView" name="tb_display_order">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="tb_reset_layout_button">
<property name="toolTip">
<string>&lt;p&gt;Click this button to reset the list to its default order.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Reset list</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tb_import_layout_button">
<property name="toolTip">
<string>&lt;p&gt;Click this button to set the list to one
previously exported. This could be useful if you have several libraries with
similar structure and you want to use the same column order for each one. Columns
in the imported list that aren't in the current library are ignored. Columns in
the library that are not in the imported list are put at the end and marked
as displayable.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Import list</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tb_export_layout_button">
<property name="toolTip">
<string>&lt;p&gt;Click this button to write the current display
settings to a file. This could be useful if you have several libraries with similar
structure and you want to use the same column order for each one.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Export list</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<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>
</layout>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="opt_tags_browser_partition_method">
<property name="toolTip">
<string>Choose how Tag browser subcategories are displayed when
@ -1186,7 +1298,7 @@ if you never want subcategories</string>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>&amp;Category partitioning method:</string>
@ -1196,7 +1308,7 @@ if you never want subcategories</string>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Co&amp;llapse when more items than:</string>
@ -1206,7 +1318,7 @@ if you never want subcategories</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QSpinBox" name="opt_tags_browser_collapse_at">
<property name="toolTip">
<string>If a Tag browser category has more than this number of items, it is divided
@ -1217,7 +1329,7 @@ up into subcategories. If the partition method is set to disable, this value is
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Combine letters &amp;when fewer items than:</string>
@ -1227,7 +1339,7 @@ up into subcategories. If the partition method is set to disable, this value is
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QSpinBox" name="opt_tags_browser_collapse_fl_at">
<property name="toolTip">
<string>If collapsing by first letter, combine adjacent letters together if
@ -1239,7 +1351,7 @@ not set to first letter, this value is ignored. Set to zero to disable.</string>
</property>
</widget>
</item>
<item row="5" column="0">
<item row="6" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Spacing between &amp;items:</string>
@ -1249,7 +1361,7 @@ not set to first letter, this value is ignored. Set to zero to disable.</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="6" column="1">
<widget class="QDoubleSpinBox" name="opt_tag_browser_item_padding">
<property name="toolTip">
<string>The spacing between consecutive items in the Tag browser. In units of (ex) which is the approximate height of the letter 'x' in the currently used font. </string>
@ -1271,7 +1383,7 @@ not set to first letter, this value is ignored. Set to zero to disable.</string>
</property>
</widget>
</item>
<item row="6" column="0">
<item row="7" column="0">
<widget class="QLabel" name="label_8111">
<property name="text">
<string>Categories &amp;not to partition:</string>
@ -1281,7 +1393,7 @@ not set to first letter, this value is ignored. Set to zero to disable.</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="7" column="1">
<widget class="EditWithComplete" name="opt_tag_browser_dont_collapse">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -1298,7 +1410,7 @@ a few top-level elements.</string>
</property>
</widget>
</item>
<item row="7" column="0">
<item row="8" column="0">
<widget class="QLabel" name="label_81">
<property name="text">
<string>C&amp;ategories with hierarchical items:</string>
@ -1308,7 +1420,7 @@ a few top-level elements.</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="8" column="1">
<widget class="EditWithComplete" name="opt_categories_using_hierarchy">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -1326,14 +1438,14 @@ then the tags will be displayed each on their own line.</string>
</property>
</widget>
</item>
<item row="8" column="0">
<item row="9" column="0">
<widget class="QCheckBox" name="opt_tag_browser_show_tooltips">
<property name="text">
<string>Show &amp;tooltips</string>
</property>
</widget>
</item>
<item row="9" column="0">
<item row="10" column="0">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text">
<string>Show &amp;average ratings</string>
@ -1343,7 +1455,7 @@ then the tags will be displayed each on their own line.</string>
</property>
</widget>
</item>
<item row="10" column="0" colspan="2">
<item row="11" column="0" colspan="2">
<widget class="QCheckBox" name="opt_tag_browser_show_counts">
<property name="toolTip">
<string>Show counts for items in the Tag browser. Such as the number of books
@ -1355,14 +1467,14 @@ see the counts by hovering your mouse over any item.</string>
</property>
</widget>
</item>
<item row="11" column="0" colspan="2">
<item row="12" column="0" colspan="2">
<widget class="QCheckBox" name="opt_tag_browser_old_look">
<property name="text">
<string>Use &amp;alternating row colors</string>
</property>
</widget>
</item>
<item row="12" column="0" colspan="2">
<item row="13" column="0" colspan="2">
<widget class="QCheckBox" name="opt_tag_browser_hide_empty_categories">
<property name="toolTip">
<string>When checked, calibre will automatically hide any category
@ -1375,7 +1487,7 @@ see the counts by hovering your mouse over any item.</string>
</property>
</widget>
</item>
<item row="13" column="0" colspan="2">
<item row="14" column="0" colspan="2">
<widget class="QCheckBox" name="opt_tag_browser_always_autocollapse">
<property name="toolTip">
<string>When checked, Find in the Tag browser will show all items
@ -1387,7 +1499,7 @@ see the counts by hovering your mouse over any item.</string>
</property>
</widget>
</item>
<item row="14" column="0" colspan="2">
<item row="15" column="0" colspan="2">
<widget class="QCheckBox" name="opt_tag_browser_allow_keyboard_focus">
<property name="toolTip">
<string>&lt;p&gt;When checked, the Tag browser can get keyboard focus, allowing
@ -1401,7 +1513,7 @@ using the mouse.&lt;/p&gt;</string>
</property>
</widget>
</item>
<item row="15" column="0" colspan="2">
<item row="16" column="0" colspan="2">
<widget class="QLabel" name="tb_focus_label">
<property name="styleSheet">
<string notr="true">margin-left: 1.5em</string>
@ -1414,7 +1526,7 @@ using the mouse.&lt;/p&gt;</string>
</property>
</widget>
</item>
</layout>
</layout>
</widget>
<widget class="QWidget" name="cover_browser_tab">
<attribute name="icon">

View File

@ -390,7 +390,7 @@ class TagsModel(QAbstractItemModel): # {{{
if hidden_cats is None:
hidden_cats = config['tag_browser_hidden_categories']
self.hidden_categories = set()
# strip out any non-existence field keys
# strip out any non-existent field keys
for cat in hidden_cats:
if cat in db.field_metadata:
self.hidden_categories.add(cat)
@ -402,6 +402,12 @@ class TagsModel(QAbstractItemModel): # {{{
self._run_rebuild()
self.endResetModel()
def set_hidden_categories(self, cats):
self.beginResetModel()
self.hidden_categories = cats
self._run_rebuild()
self.endResetModel()
def rebuild_node_tree(self, state_map={}):
if self._build_in_progress:
print('Tag browser build already in progress')
@ -1118,6 +1124,33 @@ class TagsModel(QAbstractItemModel): # {{{
return self.db.search('', return_matches=True, sort_results=False)
return None
def is_standard_category(self, key):
return not (key.startswith('@') or key == 'search')
def get_ordered_categories(self, use_defaults=False, pref_data_override=None):
if use_defaults:
tbo = []
elif pref_data_override:
tbo = [k for k,_ in pref_data_override]
else:
tbo = self.db.new_api.pref('tag_browser_category_order', [])
disp_cats = self.categories.keys()
cat_ord = []
# Do the standard categories first
# Verify all the columns in the pref are actually in the tag browser
for key in tbo:
if self.is_standard_category(key) and key in disp_cats:
cat_ord.append(key)
# Add any new standard cats to the order pref at the end of the list
for key in disp_cats:
if key not in cat_ord and self.is_standard_category(key):
cat_ord.append(key)
# Now add the non-standard cats (user cats and search)
for key in disp_cats:
if not self.is_standard_category(key):
cat_ord.append(key)
return cat_ord
def _get_category_nodes(self, sort):
'''
Called by __init__. Do not directly call this method.
@ -1157,32 +1190,37 @@ class TagsModel(QAbstractItemModel): # {{{
if category in data: # The search category can come and go
self.categories[category] = tb_categories[category]['name']
# Now build the list of fields in display order
order = tweaks.get('tag_browser_category_default_sort', None)
if order not in ('default', 'display_name', 'lookup_name'):
print('Tweak tag_browser_category_default_sort is not valid. Ignored')
order = 'default'
if order == 'default':
self.row_map = self.categories.keys()
# Now build the list of fields in display order. A lot of this is to
# maintain compatibility with the tweaks.
order_pref = self.db.new_api.pref('tag_browser_category_order', None)
if order_pref is not None:
# Keys are in order
self.row_map = self.get_ordered_categories()
else:
def key_func(val):
if order == 'display_name':
return icu_lower(self.db.field_metadata[val]['name'])
return icu_lower(val[1:] if val.startswith('#') or val.startswith('@') else val)
direction = tweaks.get('tag_browser_category_default_sort_direction', None)
if direction not in ('ascending', 'descending'):
print('Tweak tag_browser_category_default_sort_direction is not valid. Ignored')
direction = 'ascending'
self.row_map = sorted(self.categories, key=key_func, reverse=direction == 'descending')
try:
order = tweaks['tag_browser_category_order']
if not isinstance(order, dict):
raise TypeError()
except:
print('Tweak tag_browser_category_order is not valid. Ignored')
order = {'*': 100}
defvalue = order.get('*', 100)
self.row_map = sorted(self.row_map, key=lambda x: order.get(x, defvalue))
order = tweaks.get('tag_browser_category_default_sort', 'default')
self.row_map = list(self.categories.keys())
if order not in ('default', 'display_name', 'lookup_name'):
print('Tweak tag_browser_category_default_sort is not valid. Ignored')
order = 'default'
if order != 'default':
def key_func(val):
if order == 'display_name':
return icu_lower(self.db.field_metadata[val]['name'])
return icu_lower(val[1:] if val.startswith('#') or val.startswith('@') else val)
direction = tweaks.get('tag_browser_category_default_sort_direction', 'ascending')
if direction not in ('ascending', 'descending'):
print('Tweak tag_browser_category_default_sort_direction is not valid. Ignored')
direction = 'ascending'
self.row_map.sort(key=key_func, reverse=direction == 'descending')
try:
order = tweaks.get('tag_browser_category_order', {'*':1})
if not isinstance(order, dict):
raise TypeError()
except:
print('Tweak tag_browser_category_order is not valid. Ignored')
order = {'*': 1000}
defvalue = order.get('*', 1000)
self.row_map.sort(key=lambda x: order.get(x, defvalue))
return data
def set_categories_filter(self, txt):