From a03dd04e222b1bf85ba0609261843d657d4f9cb3 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Thu, 25 Aug 2022 13:00:08 +0100 Subject: [PATCH 1/2] Prefs / Look & feel / Tag browser partition and hierarchy: change lists of keys to list widget with check boxes. NB: the partition list was a gpref, which was wrong. It is now a db pref. I added migration code to the tag browser. It is possible that the server code must be changed to get the db pref. I don't know how to do that, if it is needed. --- src/calibre/gui2/preferences/look_feel.py | 194 +++++++---- src/calibre/gui2/preferences/look_feel.ui | 394 ++++++++++++++-------- src/calibre/gui2/tag_browser/model.py | 13 +- 3 files changed, 391 insertions(+), 210 deletions(-) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index b723c450c7..7de3eb8fc8 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' import json from collections import defaultdict from threading import Thread +from functools import partial from qt.core import ( QApplication, QFont, QFontInfo, QFontDialog, QColorDialog, QPainter, QDialog, @@ -23,7 +24,7 @@ from calibre.ebooks.metadata.sources.prefs import msprefs from calibre.gui2.custom_column_widgets import get_field_list as em_get_field_list from calibre.gui2 import default_author_link, icon_resource_manager, choose_save_file, choose_files from calibre.gui2.dialogs.template_dialog import TemplateDialog -from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList +from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2 import config, gprefs, qt_app, open_local_file, question_dialog, error_dialog from calibre.utils.localization import (available_translations, @@ -377,24 +378,101 @@ class TBDisplayedFields(DisplayedFields): # {{{ pref_data_override=pref_data_override) if use_defaults: hc = [] + self.changed = True elif pref_data_override: hc = [k for k,v in pref_data_override if not v] + self.changed = True 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}) + self.db.prefs.set('tag_browser_hidden_categories', [k for k,v in self.fields if not v]) + self.db.prefs.set('tag_browser_category_order', [k for k,v in self.fields]) + self.gui.tags_view.model().reset_tag_browser_categories() +# }}} + + +class TBPartitionedFields(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 + cats = tv.model().categories + ans = [] + if use_defaults: + ans = [[k, True] for k in cats.keys()] + self.changed = True + elif pref_data_override: + po = {k:v for k,v in pref_data_override} + ans = [[k, po.get(k, True)] for k in cats.keys()] + self.changed = True + else: + # Check if setting not migrated yet + cats_to_partition = self.db.prefs.get('tag_browser_dont_collapse', + gprefs.get('tag_browser_dont_collapse')) + for key in cats: + ans.append([key, not key in cats_to_partition]) + self.beginResetModel() + self.fields = ans + self.endResetModel() + + def commit(self): + if self.changed: + # Migrate to a per-library setting + self.db.prefs.set('tag_browser_dont_collapse', [k for k,v in self.fields if not v]) + self.gui.tags_view.model().reset_tag_browser_categories() +# }}} + + +class TBHierarchicalFields(DisplayedFields): # {{{ + # The code in this class depends on the fact that the tag browser is + # initialized before this class is instantiated. + + cant_make_hierarical = {'authors', 'publisher', 'formats', 'news', + 'identifiers', 'languages', 'rating'} + + 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 + cats = [k for k in tv.model().categories.keys() if k not in self.cant_make_hierarical] + ans = [] + if use_defaults: + ans = [[k, False] for k in cats] + self.changed = True + elif pref_data_override: + ph = {k:v for k,v in pref_data_override} + ans = [[k, ph.get(k, False)] for k in cats] + self.changed = True + else: + hier_cats = self.db.prefs.get('categories_using_hierarchy') + for key in cats: + ans.append([key, key in hier_cats]) + self.beginResetModel() + self.fields = ans + self.endResetModel() + + def commit(self): + if self.changed: + self.db.prefs.set('categories_using_hierarchy', [k for k,v in self.fields if v]) + self.gui.tags_view.model().reset_tag_browser_categories() # }}} @@ -547,20 +625,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('tags_browser_collapse_at', gprefs) r('tags_browser_collapse_fl_at', gprefs) - choices = {k for k in db.field_metadata.all_field_keys() - if (db.field_metadata[k]['is_category'] and ( - db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration' - ]) and not db.field_metadata[k]['display'].get('is_names', False)) or ( - db.field_metadata[k]['datatype'] in ['composite' - ] and db.field_metadata[k]['display'].get('make_category', False))} - choices |= {'search'} - r('tag_browser_dont_collapse', gprefs, setting=CommaSeparatedList, - choices=sorted(choices, key=sort_key)) - - choices -= {'authors', 'publisher', 'formats', 'news', 'identifiers'} - r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList, - choices=sorted(choices, key=sort_key)) - fm = db.field_metadata choices = sorted(((fm[k]['name'], k) for k in fm.displayable_field_keys() if fm[k]['name']), key=lambda x:sort_key(x[0])) @@ -601,9 +665,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): lambda self: move_field_up(self.em_display_order, self.em_display_model)) connect_lambda(self.em_down_button.clicked, self, lambda self: move_field_down(self.em_display_order, self.em_display_model)) - self.em_export_layout_button.clicked.connect(self.em_export_layout) - self.em_import_layout_button.clicked.connect(self.em_import_layout) - self.em_reset_layout_button.clicked.connect(self.em_reset_layout) + self.em_export_layout_button.clicked.connect(partial(self.export_layout, + model=self.em_display_model)) + self.em_import_layout_button.clicked.connect(partial(self.import_layout, + model=self.em_display_model)) + self.em_reset_layout_button.clicked.connect(partial(self.reset_layout, + model=self.em_display_model)) self.qv_display_model = QVDisplayedFields(self.gui.current_db, self.qv_display_order) @@ -615,16 +682,43 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): 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_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_reset_layout_button.clicked.connect(partial(self.reset_layout, + model=self.tb_display_model)) + self.tb_export_layout_button.clicked.connect(partial(self.export_layout, + model=self.tb_display_model)) + self.tb_import_layout_button.clicked.connect(partial(self.import_layout, + model=self.tb_display_model)) self.tb_up_button.clicked.connect(self.tb_up_button_clicked) self.tb_down_button.clicked.connect(self.tb_down_button_clicked) + self.tb_categories_to_part_model = TBPartitionedFields(self.gui.current_db, + self.tb_cats_to_partition, + category_icons=self.gui.tags_view.model().category_custom_icons) + self.tb_categories_to_part_model.dataChanged.connect(self.changed_signal) + self.tb_cats_to_partition.setModel(self.tb_categories_to_part_model) + self.tb_partition_reset_button.clicked.connect(partial(self.reset_layout, + model=self.tb_categories_to_part_model)) + self.tb_partition_export_layout_button.clicked.connect(partial(self.export_layout, + model=self.tb_categories_to_part_model)) + self.tb_partition_import_layout_button.clicked.connect(partial(self.import_layout, + model=self.tb_categories_to_part_model)) + + self.tb_hierarchical_cats_model = TBHierarchicalFields(self.gui.current_db, + self.tb_hierarchical_cats, + category_icons=self.gui.tags_view.model().category_custom_icons) + self.tb_hierarchical_cats_model.dataChanged.connect(self.changed_signal) + self.tb_hierarchical_cats.setModel(self.tb_hierarchical_cats_model) + self.tb_hierarchy_reset_layout_button.clicked.connect(partial(self.reset_layout, + model=self.tb_hierarchical_cats_model)) + self.tb_hierarchy_export_layout_button.clicked.connect(partial(self.export_layout, + model=self.tb_hierarchical_cats_model)) + self.tb_hierarchy_import_layout_button.clicked.connect(partial(self.import_layout, + model=self.tb_hierarchical_cats_model)) + self.edit_rules = EditRules(self.tabWidget) self.edit_rules.changed.connect(self.changed_signal) self.tabWidget.addTab(self.edit_rules, @@ -695,19 +789,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.opt_color_palette.setEnabled(enabled) self.opt_color_palette_label.setEnabled(enabled) - def em_export_layout(self): + def export_layout(self, model=None): 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.em_display_model.fields, f, indent=1) + json.dump(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 em_import_layout(self): + def import_layout(self, model=None): filename = choose_files(self, 'em_import_export_field_list', _('Load column list from file'), filters=[(_('Column list'), ['json'])]) @@ -715,44 +809,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): try: with open(filename[0]) as f: fields = json.load(f) - self.em_display_model.initialize(pref_data_override=fields) + 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 em_reset_layout(self): - 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) + def reset_layout(self, model=None): + model.initialize(use_defaults=True) self.changed_signal.emit() def tb_down_button_clicked(self): @@ -839,6 +903,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.em_display_model.initialize() self.qv_display_model.initialize() self.tb_display_model.initialize() + self.tb_categories_to_part_model.initialize() + self.tb_hierarchical_cats_model.initialize() db = self.gui.current_db mi = [] try: @@ -974,6 +1040,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.em_display_model.commit() self.qv_display_model.commit() self.tb_display_model.commit() + self.tb_categories_to_part_model.commit() + self.tb_hierarchical_cats_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 c492cd7f74..d78efd6b9c 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -1156,17 +1156,10 @@ using the Tab key. The F2 (Edit) key will still open the template editor.</p& Select the categories to display in the Tag browser and their order - - - - User categories and Saved searches cannot be moved - - - - Move down + Move down. User categories and Saved searches cannot be moved @@ -1177,7 +1170,7 @@ using the Tab key. The F2 (Edit) key will still open the template editor.</p& - Move up + Move up. User categories and Saved searches cannot be moved @@ -1269,166 +1262,279 @@ structure and you want to use the same column order for each one.</p> - - - QFormLayout::ExpandingFieldsGrow - + - - - &Category partitioning method: + + + QFormLayout::ExpandingFieldsGrow - - opt_tags_browser_partition_method - - - - - - - Choose how Tag browser subcategories are displayed when + + + + &Category partitioning method: + + + opt_tags_browser_partition_method + + + + + + + Choose how Tag browser subcategories are displayed when there are more items than the limit. Select by first letter to see an A, B, C list. Choose partitioned to have a list of fixed-sized groups. Set to disabled if you never want subcategories - - + + + + + + + Spacing between &items: + + + opt_tag_browser_item_padding + + + + + + + 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. + + + ex + + + 1 + + + -1.000000000000000 + + + 2.000000000000000 + + + 0.100000000000000 + + + + + + + + + + + Combine letters &when fewer items than: + + + opt_tags_browser_collapse_fl_at + + + + + + + If collapsing by first letter, combine adjacent letters together if +there are fewer items under a letter than specified here. If the partition method is +not set to first letter, this value is ignored. Set to zero to disable. + + + 10000 + + + + + + + Co&llapse when more items than: + + + opt_tags_browser_collapse_at + + + + + + + If a Tag browser category has more than this number of items, it is divided +up into subcategories. If the partition method is set to disable, this value is ignored. + + + 10000 + + + + - + - Co&llapse when more items than: + Select categories to &partition: - opt_tags_browser_collapse_at + tb_cats_to_partition - - - - - If a Tag browser category has more than this number of items, it is divided -up into subcategories. If the partition method is set to disable, this value is ignored. - - - 10000 + <p>Check the box for categories that are to +be partitioned using the criteria above. Uncheck the box if you don't want to +partition a category even if the number of items is larger than +the value shown above. This option can be used to +avoid collapsing hierarchical categories that have only +a few top-level elements.</p> - - - Combine letters &when fewer items than: + + + + 0 + 200 + - - opt_tags_browser_collapse_fl_at - - - - - - - If collapsing by first letter, combine adjacent letters together if -there are fewer items under a letter than specified here. If the partition method is -not set to first letter, this value is ignored. Set to zero to disable. - - - 10000 + + 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 for each one.</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 for each one.</p> + + + Export list + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + - Spacing between &items: + Select categories with &hierarchical items: - opt_tag_browser_item_padding + tb_hierarchical_cats + + + <p>Check the box for an item if it is to be displayed as a +hierarchical tree in the Tag browser. For example, if you check +'tags' then tags of the form 'Mystery.English' +and 'Mystery.Thriller' will be displayed with English and Thriller +both under 'Mystery'. If 'tags' is not checked +then the tags will be displayed each on their own line.</p> + + + + + + + + 0 + 200 + + + + true - - - 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. - - - ex - - - 1 - - - -1.000000000000000 - - - 2.000000000000000 - - - 0.100000000000000 - - - - - - - Categories &not to partition: - - - opt_tag_browser_dont_collapse - - - - - - - - 0 - 0 - - - - A comma-separated list of categories that are not to -be partitioned even if the number of items is larger than -the value shown above. This option can be used to -avoid collapsing hierarchical categories that have only -a few top-level elements. - - + + + + + <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 for each one.</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 for each one.</p> + + + Export list + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - - C&ategories with hierarchical items: - - - opt_categories_using_hierarchy - - - - - - - - 0 - 0 - - - - A comma-separated list of categories in which items containing -periods are displayed in the Tag browser trees. For example, if -this box contains 'tags' then tags of the form 'Mystery.English' -and 'Mystery.Thriller' will be displayed with English and Thriller -both under 'Mystery'. If 'tags' is not in this box, -then the tags will be displayed each on their own line. - - - - - - - - Show &average ratings @@ -1438,14 +1544,14 @@ then the tags will be displayed each on their own line. - + Show &tooltips - + Show counts for items in the Tag browser. Such as the number of books @@ -1457,14 +1563,14 @@ see the counts by hovering your mouse over any item. - + Use &alternating row colors - + When checked, calibre will automatically hide any category @@ -1477,7 +1583,7 @@ see the counts by hovering your mouse over any item. - + When checked, Find in the Tag browser will show all items @@ -1489,7 +1595,7 @@ see the counts by hovering your mouse over any item. - + diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index d7f8af7b43..0d7e600725 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -402,9 +402,14 @@ class TagsModel(QAbstractItemModel): # {{{ self._run_rebuild() self.endResetModel() - def set_hidden_categories(self, cats): + def reset_tag_browser_categories(self): self.beginResetModel() - self.hidden_categories = cats + hidden_cats = self.db.new_api.pref('tag_browser_hidden_categories', {}) + self.hidden_categories = set() + # strip out any non-existent field keys + for cat in hidden_cats: + if cat in self.db.field_metadata: + self.hidden_categories.add(cat) self._run_rebuild() self.endResetModel() @@ -541,7 +546,9 @@ class TagsModel(QAbstractItemModel): # {{{ is_gst = category.is_gst if key not in data: return - if key in self.prefs['tag_browser_dont_collapse']: + # Use old pref if new one doesn't exist + if key in self.db.prefs.get('tag_browser_dont_collapse', + self.prefs['tag_browser_dont_collapse']): collapse_model = 'disable' cat_len = len(data[key]) if cat_len <= 0: From 9cdfd677243a2743332d79dab490d8db46a00145 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Thu, 25 Aug 2022 19:38:38 +0100 Subject: [PATCH 2/2] Minor typo: make "super-quote" always hyphenated --- manual/gui.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual/gui.rst b/manual/gui.rst index e07f31277a..8077fbbcf8 100644 --- a/manual/gui.rst +++ b/manual/gui.rst @@ -386,7 +386,7 @@ Two variants of equality searches are used for hierarchical items (e.g., A.B.C): *'Regular expression' searches* -Regular expression searches are indicated by prefixing the search string with a tilde (~). Any `Python-compatible regular expression `__ can be used. Backslashes used to escape special characters in regular expressions must be doubled because single backslashes will be removed during query parsing. For example, to match a literal parenthesis you must enter ``\\(`` or alternatively use `super quotes` (see below). Regular expression searches are 'contains' searches unless the expression is anchored. Character variants are significant: ``~e`` doesn't match ``é``. +Regular expression searches are indicated by prefixing the search string with a tilde (~). Any `Python-compatible regular expression `__ can be used. Backslashes used to escape special characters in regular expressions must be doubled because single backslashes will be removed during query parsing. For example, to match a literal parenthesis you must enter ``\\(`` or alternatively use `super-quotes` (see below). Regular expression searches are 'contains' searches unless the expression is anchored. Character variants are significant: ``~e`` doesn't match ``é``. *'Character variant' searches*