mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-08-30 23:00:21 -04:00
Add ability to tag browser specify icons for individual values, as is already done automatically for 'formats'.
This scheme could be used to replace the existing tag browser category icon selection. I didn't do that because the risk seemed high. The category icon dict is used in several places and in plugins. It also has meaning for search and user categories, where the new value icon stuff doesn't. If the new facilities are not used then performance risk is near zero. Performance shouldn't be an issue if used because the icon dictionaries scale well. The exception might be templates. We have no control over the complexity or performance of user-written templates. This PR fixes a few bugs I found while implementing the new feature.
This commit is contained in:
parent
851c826bd8
commit
2595645240
@ -420,6 +420,7 @@ def create_defs():
|
||||
defs['bd_show_cover'] = True
|
||||
defs['bd_overlay_cover_size'] = False
|
||||
defs['tags_browser_category_icons'] = {}
|
||||
defs['tags_browser_value_icons'] = {}
|
||||
defs['cover_browser_reflections'] = True
|
||||
defs['book_list_extra_row_spacing'] = 0
|
||||
defs['refresh_book_list_on_bulk_edit'] = True
|
||||
|
@ -488,11 +488,16 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
icon_field_key=None, icon_rule_kind=None, doing_emblem=False,
|
||||
text_is_placeholder=False, dialog_is_st_editor=False,
|
||||
global_vars=None, all_functions=None, builtin_functions=None,
|
||||
python_context_object=None, dialog_number=None):
|
||||
python_context_object=None, dialog_number=None,
|
||||
formatter=SafeFormat, icon_dir='cc_icons'):
|
||||
# If dialog_number isn't None then we want separate non-modal windows
|
||||
# that don't stay on top of the main dialog. This lets Alt-Tab work to
|
||||
# switch between them. dialog_number must be set only by the template
|
||||
# tester, not the rules dialogs etc that depend on modality.
|
||||
|
||||
# doing_emblem is also used for tag browser value icon rules in order to
|
||||
# show the icon selection widgets.
|
||||
|
||||
if dialog_number is None:
|
||||
QDialog.__init__(self, parent, flags=Qt.WindowType.Dialog)
|
||||
else:
|
||||
@ -502,6 +507,8 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
self.setupUi(self)
|
||||
self.setWindowIcon(self.windowIcon())
|
||||
|
||||
self.formatter = formatter
|
||||
self.icon_dir = icon_dir
|
||||
self.ffml = FFMLProcessor()
|
||||
self.dialog_number = dialog_number
|
||||
self.coloring = color_field is not None
|
||||
@ -544,7 +551,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
for n1, k1 in cols:
|
||||
self.icon_field.addItem(f'{n1} ({k1})', k1)
|
||||
self.icon_file_names = []
|
||||
d = os.path.join(config_dir, 'cc_icons')
|
||||
d = os.path.join(config_dir, icon_dir)
|
||||
if os.path.exists(d):
|
||||
for icon_file in os.listdir(d):
|
||||
icon_file = icu_lower(icon_file)
|
||||
@ -766,7 +773,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
for r in range(0, len(mi)):
|
||||
w = QLineEdit(tv)
|
||||
w.setReadOnly(True)
|
||||
w.setText(mi[r].title)
|
||||
w.setText(mi[r].get('title', _('No title provided')))
|
||||
tv.setCellWidget(r, 0, w)
|
||||
tb = QToolButton()
|
||||
tb.setContentsMargins(0, 0, 0, 0)
|
||||
@ -992,7 +999,7 @@ def evaluate(book, context):
|
||||
self.update_filename_box()
|
||||
try:
|
||||
p = QIcon(icon_path).pixmap(QSize(128, 128))
|
||||
d = os.path.join(config_dir, 'cc_icons')
|
||||
d = os.path.join(config_dir, self.icon_dir)
|
||||
if not os.path.exists(os.path.join(d, icon_name)):
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
@ -1012,7 +1019,7 @@ def evaluate(book, context):
|
||||
self.icon_files.addItem('')
|
||||
self.icon_files.addItems(self.icon_file_names)
|
||||
for i,filename in enumerate(self.icon_file_names):
|
||||
icon = QIcon(os.path.join(config_dir, 'cc_icons', filename))
|
||||
icon = QIcon(os.path.join(config_dir, self.icon_dir, filename))
|
||||
self.icon_files.setItemIcon(i+1, icon)
|
||||
|
||||
def color_to_clipboard(self):
|
||||
@ -1078,14 +1085,14 @@ def evaluate(book, context):
|
||||
break_on_mi = 0 if len(l) == 0 else l[0].row()
|
||||
for r,mi in enumerate(self.mi):
|
||||
w = tv.cellWidget(r, 0)
|
||||
w.setText(mi.title)
|
||||
w.setText(mi.get('title', _('No title provided')))
|
||||
w.setCursorPosition(0)
|
||||
if self.break_box.isChecked() and r == break_on_mi and self.is_python:
|
||||
sys.settrace(self.trace_calls)
|
||||
else:
|
||||
sys.settrace(None)
|
||||
try:
|
||||
v = SafeFormat().safe_format(txt, mi, _('EXCEPTION:'),
|
||||
v = self.formatter().safe_format(txt, mi, _('EXCEPTION:'),
|
||||
mi, global_vars=self.global_vars,
|
||||
template_functions=self.all_functions,
|
||||
break_reporter=self.break_reporter if r == break_on_mi else None,
|
||||
|
@ -30,6 +30,7 @@ TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_plusplus': 2,
|
||||
'mark_minus': 3, 'mark_minusminus': 4}
|
||||
DRAG_IMAGE_ROLE = Qt.ItemDataRole.UserRole + 1000
|
||||
COUNT_ROLE = DRAG_IMAGE_ROLE + 1
|
||||
TEMPLATE_ICON_INDICATOR = ' template ' # Item values cannot start or end with space
|
||||
|
||||
_bf = None
|
||||
|
||||
@ -49,7 +50,11 @@ class TagTreeItem: # {{{
|
||||
TAG = 1
|
||||
ROOT = 2
|
||||
category_custom_icons = {}
|
||||
value_icons = {}
|
||||
value_icon_cache = {}
|
||||
icon_config_dir = {}
|
||||
file_icon_provider = None
|
||||
eval_formatter = EvalFormatter()
|
||||
|
||||
def __init__(self, data=None, is_category=False, icon_map=None,
|
||||
parent=None, tooltip=None, category_key=None, temporary=False,
|
||||
@ -60,6 +65,7 @@ class TagTreeItem: # {{{
|
||||
self.children = []
|
||||
self.blank = QIcon()
|
||||
self.is_gst = is_gst
|
||||
self.icon = None
|
||||
self.boxed = False
|
||||
self.temporary = False
|
||||
self.can_be_edited = False
|
||||
@ -117,9 +123,40 @@ class TagTreeItem: # {{{
|
||||
if self.is_gst:
|
||||
cc = self.category_custom_icons.get(self.root_node().category_key, None)
|
||||
elif self.tag.category == 'search' and not self.tag.is_searchable:
|
||||
cc = self.category_custom_icons.get('search_folder:', None)
|
||||
cc = self.category_custom_icons.get('search_folder', None)
|
||||
else:
|
||||
cc = self.category_custom_icons.get(self.tag.category, None)
|
||||
if self.icon is None:
|
||||
node = self
|
||||
val_icon = None
|
||||
category = node.tag.category
|
||||
if category in self.value_icons:
|
||||
while True:
|
||||
val_icon = self.value_icons.get(category, {}).get(node.tag.original_name)
|
||||
if val_icon is not None:
|
||||
# Have an icon. Use it if value exact match or
|
||||
# it applies to children
|
||||
if node != self and not val_icon[1]:
|
||||
val_icon = None
|
||||
break
|
||||
node = node.parent
|
||||
if node.type != self.TAG or node.type == self.ROOT:
|
||||
break
|
||||
if val_icon is None and TEMPLATE_ICON_INDICATOR in self.value_icons[category]:
|
||||
t = self.eval_formatter.safe_format(self.value_icons[category][TEMPLATE_ICON_INDICATOR][0],
|
||||
{'category': category, 'value': node.tag.original_name},
|
||||
'VALUE_ICON_TEMPLATE_ERROR', None)
|
||||
if t:
|
||||
val_icon = (os.path.join('template_icons', t), False)
|
||||
if val_icon is not None:
|
||||
cc = self.value_icon_cache.get(val_icon[0])
|
||||
if cc is None:
|
||||
cc = QIcon.ic(os.path.join(self.icon_config_dir, val_icon[0]))
|
||||
self.value_icon_cache[val_icon[0]] = cc
|
||||
self.icon = cc
|
||||
else:
|
||||
cc = self.category_custom_icons.get(self.tag.category, None)
|
||||
else:
|
||||
cc = self.icon
|
||||
elif self.type == self.CATEGORY:
|
||||
cc = self.category_custom_icons.get(self.category_key, None)
|
||||
self.icon_state_map[0] = cc or QIcon()
|
||||
@ -350,8 +387,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
self.node_map = {}
|
||||
self.category_nodes = []
|
||||
self.category_custom_icons = {}
|
||||
self.value_icons = {}
|
||||
self.value_icon_cache = {}
|
||||
self.icon_config_dir = os.path.join(config_dir, 'tb_icons')
|
||||
for k, v in iteritems(self.prefs['tags_browser_category_icons']):
|
||||
icon = QIcon(os.path.join(config_dir, 'tb_icons', v))
|
||||
icon = QIcon(os.path.join(self.icon_config_dir, v))
|
||||
if len(icon.availableSizes()) > 0:
|
||||
self.category_custom_icons[k] = icon
|
||||
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
|
||||
@ -387,9 +427,44 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
new_icon = new_key + ext
|
||||
new_path = os.path.join(config_dir, 'tb_icons', new_icon)
|
||||
os.replace(old_path, new_path)
|
||||
self.set_custom_category_icon(new_key, new_path)
|
||||
self.set_custom_category_icon(new_key, new_icon)
|
||||
self.set_custom_category_icon(old_key, None)
|
||||
|
||||
def set_value_icon(self, key, value, file_name, children):
|
||||
'''
|
||||
Add a 'rule' for an icon for a value in the tag browser as a dict entry:
|
||||
value_icons[key] = {value: (file_name, children)}
|
||||
|
||||
:param key: the lookup name for the tag browser category
|
||||
:param value: the item value in the category. If the value is
|
||||
TEMPLATE_ICON_INDICATOR then the rule applies to all items
|
||||
that don't have a specific rule.
|
||||
:param file_name: the name of the icon file to use for this value. If
|
||||
this is a template rule then this is the text of the template.
|
||||
:param children: for specific (non-template) rules: if True then the rule
|
||||
is to be used for any children of the item that don't have
|
||||
a specific rule. If False then this rule is used only for
|
||||
the specified item.
|
||||
'''
|
||||
v = self.value_icons = self.prefs['tags_browser_value_icons']
|
||||
if key not in v:
|
||||
self.value_icons[key] = {value: (file_name, children)}
|
||||
else:
|
||||
self.value_icons[key].update({value: (file_name, children)})
|
||||
self.value_icon_cache.pop(file_name, None)
|
||||
self.prefs['tags_browser_value_icons'] = self.value_icons
|
||||
|
||||
def remove_value_icon(self, key, value, file_name):
|
||||
self.value_icons = self.prefs['tags_browser_value_icons']
|
||||
self.value_icons.get(key).pop(value, None)
|
||||
self.prefs['tags_browser_value_icons'] =self.value_icons
|
||||
if file_name is not None:
|
||||
path = os.path.join(config_dir, 'tb_icons', file_name)
|
||||
try:
|
||||
os.remove(path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def set_custom_category_icon(self, key, path):
|
||||
d = self.prefs['tags_browser_category_icons']
|
||||
if path:
|
||||
@ -403,8 +478,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
os.remove(path)
|
||||
except:
|
||||
pass
|
||||
del d[key]
|
||||
del self.category_custom_icons[key]
|
||||
d.pop(key, None)
|
||||
self.category_custom_icons.pop(key, None)
|
||||
self.prefs['tags_browser_category_icons'] = d
|
||||
|
||||
def reread_collapse_model(self, state_map, rebuild=True):
|
||||
@ -417,6 +492,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
|
||||
def set_database(self, db, hidden_categories=None):
|
||||
self.beginResetModel()
|
||||
self.value_icons = self.prefs['tags_browser_value_icons']
|
||||
hidden_cats = db.new_api.pref('tag_browser_hidden_categories', None)
|
||||
# migrate from config to db prefs
|
||||
if hidden_cats is None:
|
||||
@ -514,8 +590,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
data = self._get_category_nodes(config['sort_tags_by'])
|
||||
gst = self.db.new_api.pref('grouped_search_terms', {})
|
||||
|
||||
if self.category_custom_icons.get('search_folder:', None) is None:
|
||||
self.category_custom_icons['search_folder:'] = QIcon.ic('folder_saved_search')
|
||||
if self.category_custom_icons.get('search_folder', None) is None:
|
||||
self.category_custom_icons['search_folder'] = QIcon.ic('folder_saved_search')
|
||||
last_category_node = None
|
||||
category_node_map = {}
|
||||
self.user_category_node_tree = {}
|
||||
@ -1393,6 +1469,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
node = TagTreeItem(*args, **kwargs)
|
||||
self.node_map[id(node)] = node
|
||||
node.category_custom_icons = self.category_custom_icons
|
||||
node.value_icons = self.value_icons
|
||||
node.value_icon_cache = self.value_icon_cache
|
||||
node.icon_config_dir = self.icon_config_dir
|
||||
return node
|
||||
|
||||
def get_node(self, idx):
|
||||
|
@ -48,7 +48,8 @@ from calibre.ebooks.metadata import rating_to_stars
|
||||
from calibre.gui2 import FunctionDispatcher, choose_files, config, empty_index, gprefs, pixmap_to_data, question_dialog, rating_font, safe_open_url
|
||||
from calibre.gui2.complete2 import EditWithComplete
|
||||
from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog
|
||||
from calibre.gui2.tag_browser.model import COUNT_ROLE, DRAG_IMAGE_ROLE, TAG_SEARCH_STATES, TagsModel, TagTreeItem, rename_only_in_vl_question
|
||||
from calibre.gui2.tag_browser.model import COUNT_ROLE, DRAG_IMAGE_ROLE, TAG_SEARCH_STATES, TEMPLATE_ICON_INDICATOR
|
||||
from calibre.gui2.tag_browser.model import TagsModel, TagTreeItem, rename_only_in_vl_question
|
||||
from calibre.gui2.widgets import EnLineEdit
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.serialize import json_loads
|
||||
@ -634,6 +635,12 @@ class TagsView(QTreeView): # {{{
|
||||
key=None, index=None, search_state=None,
|
||||
is_first_letter=False, ignore_vl=False,
|
||||
extra=None):
|
||||
'''
|
||||
action: a string specifying the operation
|
||||
category: the human readable label for the category
|
||||
key: the lookup name for the category
|
||||
index: the index of the item, if there is one.
|
||||
'''
|
||||
if not action:
|
||||
return
|
||||
from calibre.gui2.ui import get_gui
|
||||
@ -654,28 +661,79 @@ class TagsView(QTreeView): # {{{
|
||||
self.db.prefs.set('tag_browser_dont_collapse', extra)
|
||||
self.recount()
|
||||
return
|
||||
|
||||
# category is None if the user asked to specify a template
|
||||
# index is None if the user clicked on a category (top level) node
|
||||
# extra is a tuple: (icon_file_name: string or None, children: True or False)
|
||||
def make_icon_name(key, index):
|
||||
icon_file_name = 'icon_' + sanitize_file_name(key)
|
||||
if index is not None:
|
||||
item_val = self._model.get_node(index).tag.original_name
|
||||
icon_file_name = icon_file_name + '@@' + sanitize_file_name(item_val)
|
||||
else:
|
||||
item_val = None
|
||||
icon_file_name += '.png'
|
||||
return item_val, icon_file_name
|
||||
|
||||
if action == 'set_icon':
|
||||
try:
|
||||
path = choose_files(self, 'choose_category_icon',
|
||||
_('Change icon for: %s')%key, filters=[
|
||||
('Images', ['png', 'gif', 'jpg', 'jpeg'])],
|
||||
all_files=False, select_only_single_file=True)
|
||||
if path:
|
||||
path = path[0]
|
||||
p = QIcon(path).pixmap(QSize(128, 128))
|
||||
d = os.path.join(config_dir, 'tb_icons')
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
with open(os.path.join(d, 'icon_' + sanitize_file_name(key)+'.png'), 'wb') as f:
|
||||
f.write(pixmap_to_data(p, format='PNG'))
|
||||
path = os.path.basename(f.name)
|
||||
self._model.set_custom_category_icon(key, str(path))
|
||||
if category is None:
|
||||
if index is not None:
|
||||
current_item = self._model.get_node(index).tag.original_name
|
||||
else:
|
||||
current_item = _('No value available')
|
||||
template = self._model.value_icons.get(key, {}).get(TEMPLATE_ICON_INDICATOR, ('', False))[0]
|
||||
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
||||
from calibre.utils.formatter import EvalFormatter
|
||||
d = TemplateDialog(parent=self, text=template,
|
||||
mi={'title': key, 'category': key, 'value': current_item},
|
||||
doing_emblem=True,
|
||||
# fm=None, color_field=None, icon_field_key=None,
|
||||
# icon_rule_kind=None, text_is_placeholder=False,
|
||||
# dialog_is_st_editor=False,
|
||||
# global_vars=None, all_functions=None, builtin_functions=None,
|
||||
# python_context_object=None, dialog_number=None,
|
||||
formatter=EvalFormatter, icon_dir='tb_icons/template_icons')
|
||||
if d.exec():
|
||||
self._model.set_value_icon(key, TEMPLATE_ICON_INDICATOR, d.rule[2], False)
|
||||
self.recount()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return
|
||||
(icon_file_name, for_children) = extra
|
||||
item_val, desired_file_name = make_icon_name(key, index)
|
||||
if icon_file_name is None:
|
||||
# User wants to specify a specific icon
|
||||
try:
|
||||
icon_file_name = desired_file_name
|
||||
path = choose_files(self, 'choose_category_icon',
|
||||
_('Change icon for: %s')%key, filters=[
|
||||
('Images', ['png', 'gif', 'jpg', 'jpeg'])],
|
||||
all_files=False, select_only_single_file=True)
|
||||
if path:
|
||||
path = path[0]
|
||||
p = QIcon(path).pixmap(QSize(128, 128))
|
||||
d = os.path.join(config_dir, 'tb_icons')
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
with open(os.path.join(d, icon_file_name), 'wb') as f:
|
||||
f.write(pixmap_to_data(p, format='PNG'))
|
||||
except:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
# Already have an icon. User wants to change whether it applies to children
|
||||
icon_file_name = desired_file_name
|
||||
if index is None: # category icon
|
||||
self._model.set_custom_category_icon(key, str(icon_file_name))
|
||||
self.recount()
|
||||
else: # value icon
|
||||
self._model.set_value_icon(key, item_val, icon_file_name, bool(for_children))
|
||||
self.recount()
|
||||
return
|
||||
if action == 'clear_icon':
|
||||
self._model.set_custom_category_icon(key, None)
|
||||
if index is not None:
|
||||
val, icon_name = make_icon_name(key, index)
|
||||
self._model.remove_value_icon(key, val, icon_name)
|
||||
else:
|
||||
self._model.set_custom_category_icon(key, None)
|
||||
self._model.remove_value_icon(key, TEMPLATE_ICON_INDICATOR, None)
|
||||
self.recount()
|
||||
return
|
||||
|
||||
@ -1163,9 +1221,9 @@ class TagsView(QTreeView): # {{{
|
||||
category=key)).setIcon(QIcon.ic('minus.png'))
|
||||
add_show_hidden_categories()
|
||||
|
||||
cm = self.context_menu
|
||||
cm.addSeparator()
|
||||
if tag is None:
|
||||
cm = self.context_menu
|
||||
cm.addSeparator()
|
||||
acategory = category.replace('&', '&&')
|
||||
sm = cm.addAction(_('Change {} category icon').format(acategory),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
@ -1178,13 +1236,56 @@ class TagsView(QTreeView): # {{{
|
||||
if key == 'search' and 'search' in self.db.new_api.pref('categories_using_hierarchy', ()):
|
||||
sm = cm.addAction(_('Change Saved searches folder icon'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key='search_folder:', category=_('Saved searches folder')))
|
||||
key='search_folder', category=_('Saved searches folder')))
|
||||
sm.setIcon(QIcon.ic('icon_choose.png'))
|
||||
sm = cm.addAction(_('Restore Saved searches folder default icon'),
|
||||
partial(self.context_menu_handler, action='clear_icon',
|
||||
key='search_folder:', category=_('Saved searches folder')))
|
||||
key='search_folder', category=_('Saved searches folder')))
|
||||
sm.setIcon(QIcon.ic('edit-clear.png'))
|
||||
if key not in ('search', 'formats') and not key.startswith('@'):
|
||||
im = cm.addMenu(_('Manage value icons'))
|
||||
def get_rule_data(tag, key):
|
||||
if tag is None:
|
||||
return (None, None, None)
|
||||
name = tag.original_name
|
||||
cat_rules = self._model.value_icons.get(key, {})
|
||||
icon_name, for_child = cat_rules.get(name, (None, None))
|
||||
return (name, icon_name, for_child)
|
||||
|
||||
name,icon_name,for_child = get_rule_data(tag, key)
|
||||
if name is not None:
|
||||
im.addSection(_('Current value: {}').format(name))
|
||||
else:
|
||||
im.addSection(_('No value available'))
|
||||
im.addSeparator
|
||||
ma = im.addAction(_('Choose an icon for this value but not its children'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=category, extra=(None, False)))
|
||||
ma.setEnabled(name is not None)
|
||||
ma = im.addAction(_('Choose an icon for this value and children'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=category, extra=(None, True)))
|
||||
ma.setEnabled(name is not None)
|
||||
im.addSeparator()
|
||||
ma = im.addAction(_('Use the existing icon for this value but not its children'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=category, extra=(icon_name, False)))
|
||||
ma.setEnabled(icon_name is not None and for_child)
|
||||
ma = im.addAction(_('Use the existing icon for this value and its children'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=category, extra=(icon_name, True)))
|
||||
ma.setEnabled(icon_name is not None and not for_child)
|
||||
im.addAction(_('Use the default icon for this value'),
|
||||
partial(self.context_menu_handler, action='clear_icon',
|
||||
key=key, index=index, category=category))
|
||||
im.addSection(_('Defaults'))
|
||||
im.addAction(_('Use a template to choose the default value icon'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=None, extra=(None, None)))
|
||||
im.addAction(_('Use the category icon for the default value icon'),
|
||||
partial(self.context_menu_handler, action='clear_icon',
|
||||
key=key, index=None, category=category))
|
||||
im.addSeparator()
|
||||
# Always show the User categories editor
|
||||
self.context_menu.addSeparator()
|
||||
if key.startswith('@') and \
|
||||
|
Loading…
x
Reference in New Issue
Block a user