'
+__docformat__ = 'restructuredtext en'
+
+from functools import partial
+import json
+
+from qt.core import QListWidgetItem, Qt
+
+from calibre.gui2 import choose_files, choose_save_file, error_dialog, gprefs
+from calibre.gui2.preferences import ConfigTabWidget
+from calibre.gui2.preferences.look_feel_tabs import DisplayedFields
+from calibre.gui2.preferences.look_feel_tabs.tb_hierarchy_ui import Ui_Form
+
+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 (not k.startswith('@') and
+ 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') or ()
+ 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])
+# }}}
+
+
+
+class TbHierarchyTab(ConfigTabWidget, Ui_Form):
+
+ def genesis(self, gui):
+ self.gui = gui
+ self.tb_hierarchical_cats_model = TBHierarchicalFields(gui.current_db, self.tb_hierarchical_cats,
+ category_icons=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.fill_tb_search_order_box()
+ self.tb_search_order_up_button.clicked.connect(self.move_tb_search_up)
+ self.tb_search_order_down_button.clicked.connect(self.move_tb_search_down)
+ self.tb_search_order.set_movement_functions(self.move_tb_search_up, self.move_tb_search_down)
+ self.tb_search_order_reset_button.clicked.connect(self.reset_tb_search_order)
+
+ def initialize(self):
+ self.tb_hierarchical_cats_model.initialize()
+
+ def fill_tb_search_order_box(self):
+ # The tb_search_order is a directed graph of nodes with an arc to the next
+ # node in the sequence. Node 0 (zero) is the start node with the last node
+ # arcing back to node 0. This code linearizes the graph
+
+ choices = [(1, _('Search for books containing the current item')),
+ (2, _('Search for books containing the current item or its children')),
+ (3, _('Search for books not containing the current item')),
+ (4, _('Search for books not containing the current item or its children'))]
+ icon_map = self.gui.tags_view.model().icon_state_map
+
+ order = gprefs.get('tb_search_order')
+ self.tb_search_order.clear()
+ node = 0
+ while True:
+ v = order[str(node)]
+ if v == 0:
+ break
+ item = QListWidgetItem(icon_map[v], choices[v-1][1])
+ item.setData(Qt.ItemDataRole.UserRole, choices[v-1][0])
+ self.tb_search_order.addItem(item)
+ node = v
+
+ def move_tb_search_up(self):
+ idx = self.tb_search_order.currentRow()
+ if idx <= 0:
+ return
+ item = self.tb_search_order.takeItem(idx)
+ self.tb_search_order.insertItem(idx-1, item)
+ self.tb_search_order.setCurrentRow(idx-1)
+ self.changed_signal.emit()
+
+ def move_tb_search_down(self):
+ idx = self.tb_search_order.currentRow()
+ if idx < 0 or idx == 3:
+ return
+ item = self.tb_search_order.takeItem(idx)
+ self.tb_search_order.insertItem(idx+1, item)
+ self.tb_search_order.setCurrentRow(idx+1)
+ self.changed_signal.emit()
+
+ def tb_search_order_commit(self):
+ t = {}
+ # Walk the items in the list box building the (node -> node) graph of
+ # the option order
+ node = 0
+ for i in range(0, 4):
+ v = self.tb_search_order.item(i).data(Qt.ItemDataRole.UserRole)
+ # JSON dumps converts integer keys to strings, so do it explicitly
+ t[str(node)] = v
+ node = v
+ # Add the arc from the last node back to node 0
+ t[str(node)] = 0
+ gprefs.set('tb_search_order', t)
+
+ def reset_tb_search_order(self):
+ gprefs.set('tb_search_order', gprefs.defaults['tb_search_order'])
+ self.fill_tb_search_order_box()
+ self.changed_signal.emit()
+
+ def reset_layout(self, model=None):
+ model.initialize(use_defaults=True)
+ self.changed_signal.emit()
+
+ 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(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 import_layout(self, model=None):
+ 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)
+ 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 commit(self):
+ self.tb_search_order_commit()
+ self.tb_hierarchical_cats_model.commit()
\ No newline at end of file
diff --git a/src/calibre/gui2/preferences/look_feel_tabs/tb_hierarchy.ui b/src/calibre/gui2/preferences/look_feel_tabs/tb_hierarchy.ui
new file mode 100644
index 0000000000..eaec795640
--- /dev/null
+++ b/src/calibre/gui2/preferences/look_feel_tabs/tb_hierarchy.ui
@@ -0,0 +1,215 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 1035
+ 547
+
+
+
+ Form
+
+
+ -
+
+
+ <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>
+<p>The categories 'authors', 'publisher', 'news', 'formats', and 'rating'
+cannot be hierarchical.</p>
+<p>User categories are always hierarchical and so do not appear in this list.</p>
+
+
+ Select categories with &hierarchical items:
+
+
+ tb_hierarchical_cats
+
+
+
+ -
+
+
+
+ 0
+ 1
+
+
+
+
+ 0
+ 200
+
+
+
+ true
+
+
+
+ -
+
+
-
+
+
+ Click this button to reset the list to its default order.
+
+
+ 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
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 0
+ 20
+
+
+
+
+ -
+
+
+ <p>Set the order of the searches when clicking on an item in
+the Tag browser. The 'or its children' options are ignored when clicking on
+top-level categories, items that aren't in a hierarchical category, and items
+that don't have children.</p>
+
+
+ Set the &order of searches when clicking on items
+
+
+ tb_search_order
+
+
+
+ -
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+
+ -
+
+
+ Move up. Keyboard shortcut: Ctrl-Up arrow
+
+
+
+ :/images/arrow-up.png:/images/arrow-up.png
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 1
+ 1
+
+
+
+
+ -
+
+
+ Move down. Keyboard shortcut: Ctrl-Down arrow
+
+
+
+ :/images/arrow-down.png:/images/arrow-down.png
+
+
+
+ -
+
+
-
+
+
+ Click this button to reset the list to its default order.
+
+
+ Reset list
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+ ListWidgetWithMoveByKeyPress
+ QListWidget
+ calibre/gui2/preferences.h
+
+
+
\ No newline at end of file
diff --git a/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.py b/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.py
new file mode 100644
index 0000000000..86ef1e83bb
--- /dev/null
+++ b/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+
+
+__license__ = 'GPL v3'
+__copyright__ = '2010, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import copy
+from functools import partial
+import os
+
+from qt.core import QAbstractItemView, QApplication, QIcon, QMenu, Qt, QTableWidgetItem
+
+from calibre.constants import config_dir
+from calibre.db.constants import TEMPLATE_ICON_INDICATOR
+from calibre.gui2 import gprefs
+from calibre.gui2.preferences import ConfigTabWidget, ConfigWidgetBase
+from calibre.gui2.preferences.look_feel_tabs.tb_icon_rules_ui import Ui_Form
+
+CATEGORY_COLUMN = 0
+VALUE_COLUMN = 1
+ICON_COLUMN = 2
+FOR_CHILDREN_COLUMN = 3
+DELECTED_COLUMN = 4
+
+
+class CategoryTableWidgetItem(QTableWidgetItem):
+
+ def __init__(self, txt):
+ super().__init__(txt)
+ self._is_deleted = False
+
+ @property
+ def is_deleted(self):
+ return self._is_deleted
+
+ @is_deleted.setter
+ def is_deleted(self, to_what):
+ self._is_deleted = to_what
+
+
+class TbIconRulesTab(ConfigTabWidget, Ui_Form):
+
+ def genesis(self, gui):
+ self.gui = gui
+ r = self.register
+ r('tag_browser_show_category_icons', gprefs)
+ r('tag_browser_show_value_icons', gprefs)
+
+ self.rules_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
+ self.rules_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
+ self.rules_table.setColumnCount(4)
+ self.rules_table.setHorizontalHeaderLabels((_('Category'), _('Value'), _('Icon file or template'),
+ _('Use for children')))
+ self.rules_table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+ self.rules_table.customContextMenuRequested.connect(self.show_context_menu)
+
+ # Capture clicks on the horizontal header to sort the table columns
+ hh = self.rules_table.horizontalHeader()
+ hh.sectionResized.connect(self.table_column_resized)
+ hh.setSectionsClickable(True)
+ hh.sectionClicked.connect(self.do_sort)
+ hh.setSortIndicatorShown(True)
+
+ v = gprefs['tags_browser_value_icons']
+ row = 0
+ for category,vdict in v.items():
+ for value in vdict:
+ self.rules_table.setRowCount(row + 1)
+ d = v[category][value]
+ self.rules_table.setItem(row, 0, CategoryTableWidgetItem(category))
+ self.rules_table.setItem(row, 1, QTableWidgetItem(value))
+ self.rules_table.setItem(row, 2, QTableWidgetItem(d[0]))
+ if value == TEMPLATE_ICON_INDICATOR:
+ txt = ''
+ else:
+ txt = _('Yes') if d[1] else _('No')
+ item = QTableWidgetItem(txt)
+ item.setTextAlignment(Qt.AlignmentFlag.AlignCenter|Qt.AlignmentFlag.AlignVCenter)
+ self.rules_table.setItem(row, 3, item)
+ row += 1
+
+ self.category_order = 1
+ self.value_order = 1
+ self.icon_order = 0
+ self.for_children_order = 0
+ self.do_sort(VALUE_COLUMN)
+ self.do_sort(CATEGORY_COLUMN)
+
+ try:
+ self.table_column_widths = gprefs.get('tag_browser_rules_dialog_table_widths', None)
+ except Exception:
+ pass
+
+ def show_context_menu(self, point):
+ clicked_item = self.rules_table.itemAt(point)
+ item = self.rules_table.item(clicked_item.row(), CATEGORY_COLUMN)
+ m = QMenu(self)
+ ac = m.addAction(_('Delete this rule'), partial(self.context_menu_handler, 'delete', item))
+ ac.setEnabled(not item.is_deleted)
+ ac = m.addAction(_('Undo delete'), partial(self.context_menu_handler, 'undelete', item))
+ ac.setEnabled(item.is_deleted)
+ m.addSeparator()
+ m.addAction(_('Copy'), partial(self.context_menu_handler, 'copy', clicked_item))
+ m.exec(self.rules_table.viewport().mapToGlobal(point))
+
+ def context_menu_handler(self, action, item):
+ if action == 'copy':
+ QApplication.clipboard().setText(item.text())
+ return
+ item.setIcon(QIcon.ic('trash.png') if action == 'delete' else QIcon())
+ item.is_deleted = action == 'delete'
+ self.changed_signal.emit()
+
+ def table_column_resized(self, col, old, new):
+ self.table_column_widths = []
+ for c in range(0, self.rules_table.columnCount()):
+ self.table_column_widths.append(self.rules_table.columnWidth(c))
+ gprefs['tag_browser_rules_dialog_table_widths'] = self.table_column_widths
+
+ def resizeEvent(self, *args):
+ super().resizeEvent(*args)
+ if self.table_column_widths is not None:
+ for c,w in enumerate(self.table_column_widths):
+ self.rules_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.tb_icon_rules_groupbox.width() - 25 - self.rules_table.verticalHeader().width()
+ w //= self.rules_table.columnCount()
+ for c in range(0, self.rules_table.columnCount()):
+ self.rules_table.setColumnWidth(c, w)
+ self.table_column_widths.append(self.rules_table.columnWidth(c))
+ gprefs['tag_browser_rules_dialog_table_widths'] = self.table_column_widths
+
+ def do_sort(self, section):
+ if section == CATEGORY_COLUMN:
+ self.category_order = 1 - self.category_order
+ self.rules_table.sortByColumn(CATEGORY_COLUMN, Qt.SortOrder(self.category_order))
+ elif section == VALUE_COLUMN:
+ self.value_order = 1 - self.value_order
+ self.rules_table.sortByColumn(VALUE_COLUMN, Qt.SortOrder(self.value_order))
+ elif section == ICON_COLUMN:
+ self.icon_order = 1 - self.icon_order
+ self.rules_table.sortByColumn(ICON_COLUMN, Qt.SortOrder(self.icon_order))
+ elif section == FOR_CHILDREN_COLUMN:
+ self.for_children_order = 1 - self.for_children_order
+ self.rules_table.sortByColumn(FOR_CHILDREN_COLUMN, Qt.SortOrder(self.for_children_order))
+
+ def commit(self):
+ rr = ConfigWidgetBase.commit(self)
+ v = copy.deepcopy(gprefs['tags_browser_value_icons'])
+ for r in range(0, self.rules_table.rowCount()):
+ cat_item = self.rules_table.item(r, CATEGORY_COLUMN)
+ if cat_item.is_deleted:
+ val = self.rules_table.item(r, VALUE_COLUMN).text()
+ if val != TEMPLATE_ICON_INDICATOR:
+ icon_file = self.rules_table.item(r, ICON_COLUMN).text()
+ path = os.path.join(config_dir, 'tb_icons', icon_file)
+ try:
+ os.remove(path)
+ except:
+ pass
+ v[cat_item.text()].pop(val, None)
+ # Remove categories with no rules
+ for category in list(v.keys()):
+ if len(v[category]) == 0:
+ v.pop(category, None)
+ gprefs['tags_browser_value_icons'] = v
diff --git a/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.ui b/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.ui
new file mode 100644
index 0000000000..1bc9e6210d
--- /dev/null
+++ b/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.ui
@@ -0,0 +1,61 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 1035
+ 547
+
+
+
+ Form
+
+
+ -
+
+
-
+
+
+ Show icons on &categories in the Tag browser
+
+
+
+ -
+
+
+ Show icons on &values in the Tag browser
+
+
+
+
+
+ -
+
+
+ Icon value rules
+
+
+
-
+
+
+ <p>View all the defined value icon rules, including template rules.
+Rules are defined and edited in the Tag browser context menus. Rules can be deleted in
+this dialog using the context menu.</p>
+
+
+ true
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py
index 51a5913c46..e75e48350a 100644
--- a/src/calibre/gui2/preferences/main.py
+++ b/src/calibre/gui2/preferences/main.py
@@ -329,8 +329,10 @@ class Preferences(QDialog):
def show_plugin(self, plugin):
self.showing_widget = plugin.create_widget(self.scroll_area)
self.showing_widget.genesis(self.gui)
+ self.showing_widget.do_on_child_tabs('genesis', self.gui)
try:
self.showing_widget.initialize()
+ self.showing_widget.do_on_child_tabs('initialize')
except AbortInitialize:
return
self.set_tooltips_for_labels()
@@ -357,6 +359,7 @@ class Preferences(QDialog):
(_('Restoring to defaults not supported for') + ' ' + plugin.gui_name))
self.restore_defaults_button.setText(_('Restore &defaults'))
self.showing_widget.changed_signal.connect(self.changed_signal)
+ self.showing_widget.do_on_child_tabs('set_changed_signal', self.changed_signal)
def changed_signal(self):
b = self.bb.button(QDialogButtonBox.StandardButton.Apply)
@@ -394,7 +397,8 @@ class Preferences(QDialog):
self.accept()
def commit(self, *args):
- must_restart = self.showing_widget.commit()
+ # Commit the child widgets first in case the main widget uses the information
+ must_restart = bool(self.showing_widget.do_on_child_tabs('commit')) | self.showing_widget.commit()
rc = self.showing_widget.restart_critical
self.committed = True
do_restart = False
@@ -407,6 +411,8 @@ class Preferences(QDialog):
' Please restart calibre as soon as possible.')
do_restart = show_restart_warning(msg, parent=self)
+ # Same with refresh -- do the child widgets first so the main widget has the info
+ self.showing_widget.do_on_child_tabs('refresh_gui', self.gui)
self.showing_widget.refresh_gui(self.gui)
if do_restart:
self.do_restart = True
diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py
index 6919800ca4..fe16253e01 100644
--- a/src/calibre/gui2/tag_browser/model.py
+++ b/src/calibre/gui2/tag_browser/model.py
@@ -115,7 +115,8 @@ class TagTreeItem: # {{{
def ensure_icon(self):
if self.icon_state_map[0] is not None:
return
- if self.type == self.TAG:
+ cc = None
+ if self.type == self.TAG and gprefs['tag_browser_show_value_icons']:
if self.tag.category == 'formats':
fmt = self.tag.original_name.replace('ORIGINAL_', '')
cc = self.file_icon_provider(fmt)
@@ -159,7 +160,7 @@ class TagTreeItem: # {{{
cc = self.category_custom_icons.get(self.tag.category, None)
else:
cc = self.icon
- elif self.type == self.CATEGORY:
+ elif self.type == self.CATEGORY and gprefs['tag_browser_show_category_icons']:
cc = self.category_custom_icons.get(self.category_key, None)
self.icon_state_map[0] = cc or QIcon()
@@ -521,6 +522,7 @@ class TagsModel(QAbstractItemModel): # {{{
def reset_tag_browser(self):
self.beginResetModel()
+ self.value_icons = self.prefs['tags_browser_value_icons']
hidden_cats = self.db.new_api.pref('tag_browser_hidden_categories', {})
self.hidden_categories = set()
# strip out any non-existent field keys