From 1b1461f782632635b1ff126f10b7cd78c3c9f5b6 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Wed, 24 Aug 2022 11:38:21 +0100 Subject: [PATCH] Enhancement 1987235: Easier way to rearrange Tag Browser --- resources/default_tweaks.py | 25 ---- src/calibre/gui2/preferences/look_feel.py | 114 +++++++++++++++- src/calibre/gui2/preferences/look_feel.ui | 154 +++++++++++++++++++--- src/calibre/gui2/tag_browser/model.py | 90 +++++++++---- 4 files changed, 308 insertions(+), 75 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 437eb4834a..68846f1122 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -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 diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 7a7c92cfde..b723c450c7 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -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'), + _('

Could not write field list. Error:
%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'), + _('

Could not read field list. Error:
%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) diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 1ffeed4ce3..4dd4105739 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -1175,7 +1175,119 @@ using the Tab key. The F2 (Edit) key will still open the template editor.</p& QFormLayout::ExpandingFieldsGrow - + + + + Select the categories to display in the Tag browser and their order + + + + + + User categories and Saved searches cannot be moved + + + + + + + Move down + + + + :/images/arrow-down.png:/images/arrow-down.png + + + + + + + Move up + + + + :/images/arrow-up.png:/images/arrow-up.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + true + + + + + + + + + <p>Click this button to reset the list to its default order.</p> + + + Reset list + + + + + + + <p>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.</p> + + + Import list + + + + + + + <p>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.</p> + + + Export list + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Choose how Tag browser subcategories are displayed when @@ -1186,7 +1298,7 @@ if you never want subcategories - + &Category partitioning method: @@ -1196,7 +1308,7 @@ if you never want subcategories - + Co&llapse when more items than: @@ -1206,7 +1318,7 @@ if you never want subcategories - + 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 - + Combine letters &when fewer items than: @@ -1227,7 +1339,7 @@ up into subcategories. If the partition method is set to disable, this value is - + 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. - + Spacing between &items: @@ -1249,7 +1361,7 @@ not set to first letter, this value is ignored. Set to zero to disable. - + 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. @@ -1271,7 +1383,7 @@ not set to first letter, this value is ignored. Set to zero to disable. - + Categories &not to partition: @@ -1281,7 +1393,7 @@ not set to first letter, this value is ignored. Set to zero to disable. - + @@ -1298,7 +1410,7 @@ a few top-level elements. - + C&ategories with hierarchical items: @@ -1308,7 +1420,7 @@ a few top-level elements. - + @@ -1326,14 +1438,14 @@ then the tags will be displayed each on their own line. - + Show &tooltips - + Show &average ratings @@ -1343,7 +1455,7 @@ then the tags will be displayed each on their own line. - + 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. - + Use &alternating row colors - + When checked, calibre will automatically hide any category @@ -1375,7 +1487,7 @@ see the counts by hovering your mouse over any item. - + 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. - + <p>When checked, the Tag browser can get keyboard focus, allowing @@ -1401,7 +1513,7 @@ using the mouse.</p> - + margin-left: 1.5em @@ -1414,7 +1526,7 @@ using the mouse.</p> - + diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 329300ea5a..d7f8af7b43 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -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):