From e443002d379175322b4a184d57cf048d2e72a78f Mon Sep 17 00:00:00 2001 From: vee1e Date: Mon, 6 Apr 2026 21:16:13 +0530 Subject: [PATCH 01/11] fix: add adjustable font menu --- capa/ida/plugin/form.py | 41 ++++++++++++++++++++++++++++-- capa/ida/plugin/model.py | 14 ++++++++-- capa/ida/plugin/view.py | 55 ++++++++++++++++++++++++++++++++++------ 3 files changed, 98 insertions(+), 12 deletions(-) diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 800453bbfa..74fc8cc7db 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -60,6 +60,7 @@ CAPA_SETTINGS_RULEGEN_AUTHOR = "rulegen_author" CAPA_SETTINGS_RULEGEN_SCOPE = "rulegen_scope" CAPA_SETTINGS_ANALYZE = "analyze" +CAPA_SETTINGS_FONT = "font" CAPA_OFFICIAL_RULESET_URL = f"https://github.com/mandiant/capa-rules/releases/tag/v{capa.version.__version__}" @@ -304,6 +305,39 @@ def load_interface(self): # load parent view self.load_view_parent() + self.load_font() + + def load_font(self): + """load the user-configured font or fall back to the system fixed font""" + font_str = settings.user.get(CAPA_SETTINGS_FONT, "") + font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + if font_str: + font.fromString(font_str) + self.update_fonts(font) + + def slot_font(self): + """launch the font dialog and apply the chosen font""" + font_str = settings.user.get(CAPA_SETTINGS_FONT, "") + current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + if font_str: + current_font.fromString(font_str) + font, ok = QtWidgets.QFontDialog.getFont(current_font, self.parent, "Select Plugin Font") + if ok: + settings.user[CAPA_SETTINGS_FONT] = font.toString() + self.update_fonts(font) + + def update_fonts(self, font: QtGui.QFont): + """propagate the selected font throughout the plugin UI""" + if hasattr(self, "model_data") and self.model_data: + self.model_data.update_font(font) + if hasattr(self, "view_tree") and self.view_tree: + self.view_tree.update_font(font) + if hasattr(self, "view_rulegen_preview") and self.view_rulegen_preview: + self.view_rulegen_preview.update_font(font) + if hasattr(self, "view_rulegen_editor") and self.view_rulegen_editor: + self.view_rulegen_editor.update_font(font) + if hasattr(self, "view_rulegen_features") and self.view_rulegen_features: + self.view_rulegen_features.update_font(font) def load_view_tabs(self): """load tabs""" @@ -345,22 +379,26 @@ def load_view_buttons(self): reset_button = QtWidgets.QPushButton("Reset Selections") save_button = QtWidgets.QPushButton("Save") settings_button = QtWidgets.QPushButton("Settings") + font_button = QtWidgets.QPushButton("Font...") analyze_button.clicked.connect(self.slot_analyze) reset_button.clicked.connect(self.slot_reset) save_button.clicked.connect(self.slot_save) settings_button.clicked.connect(self.slot_settings) + font_button.clicked.connect(self.slot_font) layout = QtWidgets.QHBoxLayout() layout.addWidget(analyze_button) layout.addWidget(reset_button) layout.addWidget(settings_button) + layout.addWidget(font_button) layout.addStretch(3) layout.addWidget(save_button, alignment=QtCore.Qt.AlignRight) self.view_analyze_button = analyze_button self.view_reset_button = reset_button self.view_settings_button = settings_button + self.view_font_button = font_button self.view_save_button = save_button self.view_buttons = layout @@ -416,9 +454,8 @@ def load_view_rulegen_tab(self): left = QtWidgets.QWidget() left.setLayout(layout2) - font = QtGui.QFont() + font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) font.setBold(True) - font.setPointSize(11) label1 = QtWidgets.QLabel() label1.setAlignment(QtCore.Qt.AlignLeft) diff --git a/capa/ida/plugin/model.py b/capa/ida/plugin/model.py index c5e510ba8a..92b76eac6a 100644 --- a/capa/ida/plugin/model.py +++ b/capa/ida/plugin/model.py @@ -61,6 +61,12 @@ def __init__(self, parent=None): super().__init__(parent) # root node does not have parent, contains header columns self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"]) + self.current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + + def update_font(self, font: QtGui.QFont): + """update the font used to render items""" + self.current_font = font + self.layoutChanged.emit() def reset(self): """reset UI elements (e.g. checkboxes, IDA color highlights) @@ -134,7 +140,8 @@ def data(self, model_index, role): CapaExplorerDataModel.COLUMN_INDEX_DETAILS, ): # set font for virtual address and details columns - font = QtGui.QFont("Courier", weight=QtGui.QFont.Medium) + font = QtGui.QFont(self.current_font) + font.setWeight(QtGui.QFont.Medium) if column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS: font.setBold(True) return font @@ -156,10 +163,13 @@ def data(self, model_index, role): and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION ): # set bold font for important items - font = QtGui.QFont() + font = QtGui.QFont(self.current_font) font.setBold(True) return font + if role == QtCore.Qt.FontRole: + return QtGui.QFont(self.current_font) + if role == QtCore.Qt.ForegroundRole and column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS: # set color for virtual address column return QtGui.QColor(37, 147, 215) diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index a442f4d1e9..31a40b7d70 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -182,11 +182,20 @@ def __init__(self, parent=None): """ """ super().__init__(parent) - self.setFont(QtGui.QFont("Courier", weight=QtGui.QFont.Bold)) + self.current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + font = QtGui.QFont(self.current_font) + font.setBold(True) + self.setFont(font) self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.setAcceptRichText(False) + def update_font(self, font: QtGui.QFont): + self.current_font = font + preview_font = QtGui.QFont(self.current_font) + preview_font.setBold(True) + self.setFont(preview_font) + def reset_view(self): """ """ self.clear() @@ -347,6 +356,7 @@ def __init__(self, preview, parent=None): self.reset_view() self.is_editing = False + self.current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) @staticmethod def get_column_feature_index(): @@ -498,6 +508,20 @@ def slot_item_double_clicked(self, o, column): o.setFlags(o.flags() & ~QtCore.Qt.ItemIsEditable) self.is_editing = True + def update_font(self, font: QtGui.QFont): + """apply a new font to the editor and restyle existing nodes""" + self.current_font = font + self.setFont(font) + self.header().setFont(font) + for node in iterate_tree(self): + if getattr(node, "capa_type", None) == CapaExplorerRulegenEditor.get_node_type_expression(): + self.style_expression_node(node) + elif getattr(node, "capa_type", None) == CapaExplorerRulegenEditor.get_node_type_feature(): + self.style_feature_node(node) + elif getattr(node, "capa_type", None) == CapaExplorerRulegenEditor.get_node_type_comment(): + self.style_comment_node(node) + self.slot_resize_columns_to_content() + def update_preview(self): """ """ rule_text = self.preview.toPlainText() @@ -577,17 +601,16 @@ def load_custom_context_menu_expression(self, pos): def style_expression_node(self, o): """ """ - font = QtGui.QFont() + font = QtGui.QFont(self.current_font) font.setBold(True) o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font) def style_feature_node(self, o): """ """ - font = QtGui.QFont() + font = QtGui.QFont(self.current_font) brush = QtGui.QBrush() - font.setFamily("Courier") font.setWeight(QtGui.QFont.Medium) brush.setColor(QtGui.QColor(*COLOR_GREEN_RGB)) @@ -596,9 +619,8 @@ def style_feature_node(self, o): def style_comment_node(self, o): """ """ - font = QtGui.QFont() + font = QtGui.QFont(self.current_font) font.setBold(True) - font.setFamily("Courier") o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font) @@ -814,6 +836,7 @@ def __init__(self, editor, parent=None): self.parent_items = {} self.editor = editor + self.current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) self.setHeaderLabels(["Feature", "Address"]) self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}") @@ -972,14 +995,15 @@ def show_item_and_parents(_o): def style_parent_node(self, o): """ """ - font = QtGui.QFont() + font = QtGui.QFont(self.current_font) font.setBold(True) o.setFont(CapaExplorerRulegenFeatures.get_column_feature_index(), font) def style_leaf_node(self, o): """ """ - font = QtGui.QFont("Courier", weight=QtGui.QFont.Bold) + font = QtGui.QFont(self.current_font) + font.setBold(True) brush = QtGui.QBrush() o.setFont(CapaExplorerRulegenFeatures.get_column_feature_index(), font) @@ -990,6 +1014,17 @@ def style_leaf_node(self, o): brush.setColor(QtGui.QColor(*COLOR_BLUE_RGB)) o.setForeground(CapaExplorerRulegenFeatures.get_column_address_index(), brush) + def update_font(self, font: QtGui.QFont): + """apply a new font to the feature tree and restyle nodes""" + self.current_font = font + self.setFont(font) + self.header().setFont(font) + for node in iterate_tree(self): + if getattr(node, "capa_type", None) == CapaExplorerRulegenFeatures.get_node_type_parent(): + self.style_parent_node(node) + elif getattr(node, "capa_type", None) == CapaExplorerRulegenFeatures.get_node_type_leaf(): + self.style_leaf_node(node) + self.slot_resize_columns_to_content() def set_parent_node(self, o): """ """ @@ -1133,6 +1168,10 @@ def __init__(self, model, parent=None): self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}") + def update_font(self, font: QtGui.QFont): + self.setFont(font) + self.header().setFont(font) + def reset_ui(self, should_sort=True): """reset user interface changes From d363239178bccdc714fe7aac346d9bf4aebe310d Mon Sep 17 00:00:00 2001 From: vee1e Date: Mon, 6 Apr 2026 21:54:45 +0530 Subject: [PATCH 02/11] fix: SIGSEGV in mapToSource and update changelog --- CHANGELOG.md | 3 ++- capa/ida/plugin/model.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1cc6d2365..0985c655d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## master (unreleased) ### New Features +- ida plugin: add a "Font…" action so users can adjust the explorer font on demand, with desktop DPI awareness baked in #2570 ### Breaking Changes @@ -161,7 +162,7 @@ Additionally a Binary Ninja bug has been fixed. Released binaries now include AR ### Bug Fixes -- binja: fix a crash during feature extraction when the MLIL is unavailable @xusheng6 #2714 +- binja: fix a crash during feature extraction when the MLIL is unavailable @xusheng6 #2714 ### capa Explorer Web diff --git a/capa/ida/plugin/model.py b/capa/ida/plugin/model.py index 92b76eac6a..b371bc3239 100644 --- a/capa/ida/plugin/model.py +++ b/capa/ida/plugin/model.py @@ -65,8 +65,9 @@ def __init__(self, parent=None): def update_font(self, font: QtGui.QFont): """update the font used to render items""" + self.beginResetModel() self.current_font = font - self.layoutChanged.emit() + self.endResetModel() def reset(self): """reset UI elements (e.g. checkboxes, IDA color highlights) From e473b4caa80403107cfef51312a3542273416a4f Mon Sep 17 00:00:00 2001 From: vee1e Date: Mon, 6 Apr 2026 22:53:04 +0530 Subject: [PATCH 03/11] ci: fix black failure --- capa/ida/plugin/view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index 31a40b7d70..4796eb95a0 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -1014,6 +1014,7 @@ def style_leaf_node(self, o): brush.setColor(QtGui.QColor(*COLOR_BLUE_RGB)) o.setForeground(CapaExplorerRulegenFeatures.get_column_address_index(), brush) + def update_font(self, font: QtGui.QFont): """apply a new font to the feature tree and restyle nodes""" self.current_font = font From 6f68b9ff1231f2c9b2600d0bac92af6b10d778cd Mon Sep 17 00:00:00 2001 From: vee1e Date: Wed, 8 Apr 2026 10:36:56 +0530 Subject: [PATCH 04/11] ida: fix submenu reset, add live updates and move to settings --- capa/ida/plugin/form.py | 60 ++++++++++++++++++++++++++++------------ capa/ida/plugin/model.py | 9 ++++++ capa/ida/plugin/view.py | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 17 deletions(-) diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 74fc8cc7db..bfbaed6e30 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -118,10 +118,12 @@ def mouseReleaseEvent(self, e): class CapaSettingsInputDialog(QtWidgets.QDialog): - def __init__(self, title, parent=None): + def __init__(self, title, parent=None, on_font_changed=None): """ """ super().__init__(parent) + self.on_font_changed = on_font_changed + self.setWindowTitle(title) self.setMinimumWidth(500) self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) @@ -131,9 +133,14 @@ def __init__(self, title, parent=None): self.edit_rule_scope = QtWidgets.QComboBox() self.edit_rules_link = QtWidgets.QLabel() self.edit_analyze = QtWidgets.QComboBox() + self.btn_font = QtWidgets.QPushButton("Font") self.btn_delete_results = QtWidgets.QPushButton( self.style().standardIcon(QtWidgets.QStyle.SP_BrowserStop), "Delete cached capa results" ) + self.font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + font_str = settings.user.get(CAPA_SETTINGS_FONT, "") + if font_str: + self.font.fromString(font_str) self.edit_rules_link.setText( f'Download and extract official capa rules' @@ -155,6 +162,7 @@ def __init__(self, title, parent=None): layout.addRow("", self.edit_rules_link) layout.addRow("Plugin start option", self.edit_analyze) + layout.addRow("Explorer font", self.btn_font) if capa.ida.helpers.idb_contains_cached_results(): self.btn_delete_results.clicked.connect(capa.ida.helpers.delete_cached_results) self.btn_delete_results.clicked.connect(lambda state: self.btn_delete_results.setEnabled(False)) @@ -168,9 +176,25 @@ def __init__(self, title, parent=None): layout.addWidget(buttons) + self.btn_font.clicked.connect(self.select_font) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) + def select_font(self): + """launch the font dialog""" + original_font = QtGui.QFont(self.font) + dialog = QtWidgets.QFontDialog(self.font, self) + dialog.setWindowTitle("Select Plugin Font") + if self.on_font_changed: + dialog.currentFontChanged.connect(self.on_font_changed) + + if dialog.exec_(): + self.font = dialog.currentFont() + if self.on_font_changed: + self.on_font_changed(self.font) + elif self.on_font_changed: + self.on_font_changed(original_font) + def get_values(self): """ """ return ( @@ -178,6 +202,7 @@ def get_values(self): self.edit_rule_author.text(), self.edit_rule_scope.currentText(), self.edit_analyze.currentIndex(), + self.font.toString(), ) @@ -315,23 +340,17 @@ def load_font(self): font.fromString(font_str) self.update_fonts(font) - def slot_font(self): - """launch the font dialog and apply the chosen font""" - font_str = settings.user.get(CAPA_SETTINGS_FONT, "") - current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - if font_str: - current_font.fromString(font_str) - font, ok = QtWidgets.QFontDialog.getFont(current_font, self.parent, "Select Plugin Font") - if ok: - settings.user[CAPA_SETTINGS_FONT] = font.toString() - self.update_fonts(font) - def update_fonts(self, font: QtGui.QFont): """propagate the selected font throughout the plugin UI""" + expanded_items = [] + if hasattr(self, "view_tree") and self.view_tree: + expanded_items = self.view_tree.get_expanded_source_items() + if hasattr(self, "model_data") and self.model_data: self.model_data.update_font(font) if hasattr(self, "view_tree") and self.view_tree: self.view_tree.update_font(font) + self.view_tree.restore_expanded_source_items(expanded_items) if hasattr(self, "view_rulegen_preview") and self.view_rulegen_preview: self.view_rulegen_preview.update_font(font) if hasattr(self, "view_rulegen_editor") and self.view_rulegen_editor: @@ -379,26 +398,22 @@ def load_view_buttons(self): reset_button = QtWidgets.QPushButton("Reset Selections") save_button = QtWidgets.QPushButton("Save") settings_button = QtWidgets.QPushButton("Settings") - font_button = QtWidgets.QPushButton("Font...") analyze_button.clicked.connect(self.slot_analyze) reset_button.clicked.connect(self.slot_reset) save_button.clicked.connect(self.slot_save) settings_button.clicked.connect(self.slot_settings) - font_button.clicked.connect(self.slot_font) layout = QtWidgets.QHBoxLayout() layout.addWidget(analyze_button) layout.addWidget(reset_button) layout.addWidget(settings_button) - layout.addWidget(font_button) layout.addStretch(3) layout.addWidget(save_button, alignment=QtCore.Qt.AlignRight) self.view_analyze_button = analyze_button self.view_reset_button = reset_button self.view_settings_button = settings_button - self.view_font_button = font_button self.view_save_button = save_button self.view_buttons = layout @@ -1338,14 +1353,25 @@ def slot_save(self): def slot_settings(self): """ """ - dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent) + original_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + font_str = settings.user.get(CAPA_SETTINGS_FONT, "") + if font_str: + original_font.fromString(font_str) + + dialog = CapaSettingsInputDialog( + "capa explorer settings", parent=self.parent, on_font_changed=self.update_fonts + ) if dialog.exec_(): ( settings.user[CAPA_SETTINGS_RULE_PATH], settings.user[CAPA_SETTINGS_RULEGEN_AUTHOR], settings.user[CAPA_SETTINGS_RULEGEN_SCOPE], settings.user[CAPA_SETTINGS_ANALYZE], + settings.user[CAPA_SETTINGS_FONT], ) = dialog.get_values() + self.load_font() + else: + self.update_fonts(original_font) def save_program_analysis(self): """ """ diff --git a/capa/ida/plugin/model.py b/capa/ida/plugin/model.py index b371bc3239..6e23f1df74 100644 --- a/capa/ida/plugin/model.py +++ b/capa/ida/plugin/model.py @@ -255,6 +255,15 @@ def parent(self, model_index): return self.createIndex(parent.row(), 0, parent) + def index_from_item(self, item, column=0): + """return the model index for the given item""" + if item is None or item == self.root_node: + return QtCore.QModelIndex() + + parent = item.parent() + parent_index = self.index_from_item(parent, 0) + return self.index(item.row(), column, parent_index) + def iterateChildrenIndexFromRootIndex(self, model_index, ignore_root=True): """depth-first traversal of child nodes diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index 4796eb95a0..53d47738f4 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -1173,6 +1173,54 @@ def update_font(self, font: QtGui.QFont): self.setFont(font) self.header().setFont(font) + def iter_model_indexes(self, parent=QtCore.QModelIndex()): + """yield all indexes in the current model""" + for row in range(self.model.rowCount(parent)): + model_index = self.model.index(row, 0, parent) + if not model_index.isValid(): + continue + yield model_index + yield from self.iter_model_indexes(model_index) + + def get_expanded_source_items(self): + """capture expanded items before a model reset""" + expanded = [] + for model_index in self.iter_model_indexes(): + if self.isExpanded(model_index): + expanded.append(self.map_index_to_source_item(model_index)) + return expanded + + def map_source_index_to_proxy(self, source_index): + """map a source-model index through the proxy chain""" + if not source_index.isValid(): + return QtCore.QModelIndex() + + models = [] + model = self.model + while not isinstance(model, CapaExplorerDataModel): + models.append(model) + model = model.sourceModel() + + proxy_index = source_index + for proxy_model in reversed(models): + proxy_index = proxy_model.mapFromSource(proxy_index) + if not proxy_index.isValid(): + break + + return proxy_index + + def restore_expanded_source_items(self, items): + """restore expanded items after a model reset""" + model = self.model + while not isinstance(model, CapaExplorerDataModel): + model = model.sourceModel() + + for item in items: + source_index = model.index_from_item(item) + proxy_index = self.map_source_index_to_proxy(source_index) + if proxy_index.isValid(): + self.expand(proxy_index) + def reset_ui(self, should_sort=True): """reset user interface changes From 1a3ef0339ed26e2686d30a27a8a9660b621e2518 Mon Sep 17 00:00:00 2001 From: yuno <51952975+vee1e@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:50:06 +0530 Subject: [PATCH 05/11] ida(ai): add null check to model Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- capa/ida/plugin/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index 7ac0c35cdc..794a768136 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -1202,7 +1202,7 @@ def map_source_index_to_proxy(self, source_index): models = [] model = self.model - while not isinstance(model, CapaExplorerDataModel): + while model is not None and not isinstance(model, CapaExplorerDataModel): models.append(model) model = model.sourceModel() From 62b64d04cc0dfaf07b5c92a9d679aaec29a1d53c Mon Sep 17 00:00:00 2001 From: vee1e Date: Wed, 8 Apr 2026 11:00:13 +0530 Subject: [PATCH 06/11] ida(ai): add ai suggestions --- capa/ida/plugin/form.py | 88 +++++++++++++++++++++++++---------------- capa/ida/plugin/view.py | 1 + 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 07d13cd13a..052755bf51 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -64,6 +64,15 @@ CAPA_OFFICIAL_RULESET_URL = f"https://github.com/mandiant/capa-rules/releases/tag/v{capa.version.__version__}" + + +def get_configured_font() -> QtGui.QFont: + """return the saved font or fall back to the system fixed font""" + font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + font_str = settings.user.get(CAPA_SETTINGS_FONT, "") + if font_str: + font.fromString(font_str) + return font CAPA_RULESET_DOC_URL = "https://github.com/mandiant/capa/blob/master/doc/rules.md" @@ -137,10 +146,7 @@ def __init__(self, title, parent=None, on_font_changed=None): self.btn_delete_results = QtWidgets.QPushButton( self.style().standardIcon(QtWidgets.QStyle.SP_BrowserStop), "Delete cached capa results" ) - self.font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - font_str = settings.user.get(CAPA_SETTINGS_FONT, "") - if font_str: - self.font.fromString(font_str) + self.font = get_configured_font() self.edit_rules_link.setText( f'Download and extract official capa rules' @@ -254,6 +260,8 @@ def __init__(self, name: str, option=Options.NO_ANALYSIS): self.view_rulegen_preview: CapaExplorerRulegenPreview self.view_rulegen_features: CapaExplorerRulegenFeatures self.view_rulegen_editor: CapaExplorerRulegenEditor + self.view_rulegen_preview_label: QtWidgets.QLabel + self.view_rulegen_editor_label: QtWidgets.QLabel self.view_rulegen_header_label: QtWidgets.QLabel self.view_rulegen_search: QtWidgets.QLineEdit self.view_rulegen_limit_features_by_ea: QtWidgets.QCheckBox @@ -334,11 +342,7 @@ def load_interface(self): def load_font(self): """load the user-configured font or fall back to the system fixed font""" - font_str = settings.user.get(CAPA_SETTINGS_FONT, "") - font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - if font_str: - font.fromString(font_str) - self.update_fonts(font) + self.update_fonts(get_configured_font()) def update_fonts(self, font: QtGui.QFont): """propagate the selected font throughout the plugin UI""" @@ -346,17 +350,38 @@ def update_fonts(self, font: QtGui.QFont): if hasattr(self, "view_tree") and self.view_tree: expanded_items = self.view_tree.get_expanded_source_items() - if hasattr(self, "model_data") and self.model_data: - self.model_data.update_font(font) + for widget_name in ( + "view_search_bar", + "view_rulegen_search", + "view_status_label", + "view_rulegen_status_label", + ): + widget = getattr(self, widget_name, None) + if widget: + widget.setFont(font) + + for component_name in ( + "model_data", + "view_tree", + "view_rulegen_preview", + "view_rulegen_editor", + "view_rulegen_features", + ): + component = getattr(self, component_name, None) + if component: + component.update_font(font) + if hasattr(self, "view_tree") and self.view_tree: - self.view_tree.update_font(font) self.view_tree.restore_expanded_source_items(expanded_items) - if hasattr(self, "view_rulegen_preview") and self.view_rulegen_preview: - self.view_rulegen_preview.update_font(font) - if hasattr(self, "view_rulegen_editor") and self.view_rulegen_editor: - self.view_rulegen_editor.update_font(font) - if hasattr(self, "view_rulegen_features") and self.view_rulegen_features: - self.view_rulegen_features.update_font(font) + + bold_font = QtGui.QFont(font) + bold_font.setBold(True) + if hasattr(self, "view_rulegen_preview_label") and self.view_rulegen_preview_label: + self.view_rulegen_preview_label.setFont(bold_font) + if hasattr(self, "view_rulegen_editor_label") and self.view_rulegen_editor_label: + self.view_rulegen_editor_label.setFont(bold_font) + if hasattr(self, "view_rulegen_header_label") and self.view_rulegen_header_label: + self.view_rulegen_header_label.setFont(bold_font) def load_view_tabs(self): """load tabs""" @@ -469,18 +494,18 @@ def load_view_rulegen_tab(self): left = QtWidgets.QWidget() left.setLayout(layout2) - font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + font = get_configured_font() font.setBold(True) - label1 = QtWidgets.QLabel() - label1.setAlignment(QtCore.Qt.AlignLeft) - label1.setText("Preview") - label1.setFont(font) + self.view_rulegen_preview_label = QtWidgets.QLabel() + self.view_rulegen_preview_label.setAlignment(QtCore.Qt.AlignLeft) + self.view_rulegen_preview_label.setText("Preview") + self.view_rulegen_preview_label.setFont(font) - label2 = QtWidgets.QLabel() - label2.setAlignment(QtCore.Qt.AlignLeft) - label2.setText("Editor") - label2.setFont(font) + self.view_rulegen_editor_label = QtWidgets.QLabel() + self.view_rulegen_editor_label.setAlignment(QtCore.Qt.AlignLeft) + self.view_rulegen_editor_label.setText("Editor") + self.view_rulegen_editor_label.setFont(font) self.view_rulegen_limit_features_by_ea = QtWidgets.QCheckBox("Limit features to current disassembly address") self.view_rulegen_limit_features_by_ea.setChecked(False) @@ -509,10 +534,10 @@ def load_view_rulegen_tab(self): self.set_rulegen_preview_border_neutral() - layout1.addWidget(label1) + layout1.addWidget(self.view_rulegen_preview_label) layout1.addWidget(self.view_rulegen_preview, 45) layout1.addWidget(self.view_rulegen_status_label) - layout3.addWidget(label2) + layout3.addWidget(self.view_rulegen_editor_label) layout3.addWidget(self.view_rulegen_editor, 65) layout2.addWidget(self.view_rulegen_header_label) @@ -1353,10 +1378,7 @@ def slot_save(self): def slot_settings(self): """ """ - original_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - font_str = settings.user.get(CAPA_SETTINGS_FONT, "") - if font_str: - original_font.fromString(font_str) + original_font = get_configured_font() dialog = CapaSettingsInputDialog( "capa explorer settings", parent=self.parent, on_font_changed=self.update_fonts diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index 794a768136..b87e4a054a 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -1175,6 +1175,7 @@ def __init__(self, model, parent=None): self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}") def update_font(self, font: QtGui.QFont): + """apply a new font to the tree view and its header""" self.setFont(font) self.header().setFont(font) From 71292871c8c9f886e497b778a8904ef57c22d511 Mon Sep 17 00:00:00 2001 From: vee1e Date: Wed, 8 Apr 2026 11:01:54 +0530 Subject: [PATCH 07/11] update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec754c5562..12d8eb3839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ ## master (unreleased) ### New Features -- ida plugin: add a "Font…" action so users can adjust the explorer font on demand, with desktop DPI awareness baked in #2570 ### Breaking Changes @@ -16,6 +15,7 @@ ### capa Explorer Web ### capa Explorer IDA Pro plugin +- ida plugin: add a font explorer in Settings with live preview #2570 ### Development - ci: use explicit and per job permissions @mike-hunhoff #3002 From 67691fd055253b148e7af9c68b68b54866177825 Mon Sep 17 00:00:00 2001 From: vee1e Date: Wed, 8 Apr 2026 11:16:15 +0530 Subject: [PATCH 08/11] ci: fix ruff --- capa/ida/plugin/form.py | 2 ++ capa/ida/plugin/view.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 052755bf51..cc93756f1a 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -73,6 +73,8 @@ def get_configured_font() -> QtGui.QFont: if font_str: font.fromString(font_str) return font + + CAPA_RULESET_DOC_URL = "https://github.com/mandiant/capa/blob/master/doc/rules.md" diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index b87e4a054a..0efdafe27a 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -1179,8 +1179,11 @@ def update_font(self, font: QtGui.QFont): self.setFont(font) self.header().setFont(font) - def iter_model_indexes(self, parent=QtCore.QModelIndex()): + def iter_model_indexes(self, parent=None): """yield all indexes in the current model""" + if parent is None: + parent = QtCore.QModelIndex() + for row in range(self.model.rowCount(parent)): model_index = self.model.index(row, 0, parent) if not model_index.isValid(): From 9eef4e7e802dd1c77dd4dd7e719d953f1d65950b Mon Sep 17 00:00:00 2001 From: vee1e Date: Wed, 8 Apr 2026 21:30:31 +0530 Subject: [PATCH 09/11] fix: only change font/size of code elements --- capa/ida/plugin/form.py | 24 ++++++++++++------------ capa/ida/plugin/model.py | 9 ++++----- capa/ida/plugin/view.py | 40 ++++++++++++++++++++++++++-------------- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index cc93756f1a..506bd23ba9 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -75,6 +75,14 @@ def get_configured_font() -> QtGui.QFont: return font +def get_scaled_ui_font(font: QtGui.QFont) -> QtGui.QFont: + """return the default UI font scaled to the configured point size""" + ui_font = QtGui.QFont() + if font.pointSize() > 0: + ui_font.setPointSize(font.pointSize()) + return ui_font + + CAPA_RULESET_DOC_URL = "https://github.com/mandiant/capa/blob/master/doc/rules.md" @@ -349,6 +357,7 @@ def load_font(self): def update_fonts(self, font: QtGui.QFont): """propagate the selected font throughout the plugin UI""" expanded_items = [] + ui_font = get_scaled_ui_font(font) if hasattr(self, "view_tree") and self.view_tree: expanded_items = self.view_tree.get_expanded_source_items() @@ -360,7 +369,7 @@ def update_fonts(self, font: QtGui.QFont): ): widget = getattr(self, widget_name, None) if widget: - widget.setFont(font) + widget.setFont(ui_font) for component_name in ( "model_data", @@ -371,20 +380,11 @@ def update_fonts(self, font: QtGui.QFont): ): component = getattr(self, component_name, None) if component: - component.update_font(font) + component.update_font(font, ui_font) if hasattr(self, "view_tree") and self.view_tree: self.view_tree.restore_expanded_source_items(expanded_items) - bold_font = QtGui.QFont(font) - bold_font.setBold(True) - if hasattr(self, "view_rulegen_preview_label") and self.view_rulegen_preview_label: - self.view_rulegen_preview_label.setFont(bold_font) - if hasattr(self, "view_rulegen_editor_label") and self.view_rulegen_editor_label: - self.view_rulegen_editor_label.setFont(bold_font) - if hasattr(self, "view_rulegen_header_label") and self.view_rulegen_header_label: - self.view_rulegen_header_label.setFont(bold_font) - def load_view_tabs(self): """load tabs""" tabs = QtWidgets.QTabWidget() @@ -496,7 +496,7 @@ def load_view_rulegen_tab(self): left = QtWidgets.QWidget() left.setLayout(layout2) - font = get_configured_font() + font = QtGui.QFont() font.setBold(True) self.view_rulegen_preview_label = QtWidgets.QLabel() diff --git a/capa/ida/plugin/model.py b/capa/ida/plugin/model.py index 6e23f1df74..be103b347f 100644 --- a/capa/ida/plugin/model.py +++ b/capa/ida/plugin/model.py @@ -62,11 +62,13 @@ def __init__(self, parent=None): # root node does not have parent, contains header columns self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"]) self.current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + self.current_ui_font = QtGui.QFont() - def update_font(self, font: QtGui.QFont): + def update_font(self, font: QtGui.QFont, ui_font: Optional[QtGui.QFont] = None): """update the font used to render items""" self.beginResetModel() self.current_font = font + self.current_ui_font = QtGui.QFont(self.current_ui_font if ui_font is None else ui_font) self.endResetModel() def reset(self): @@ -164,13 +166,10 @@ def data(self, model_index, role): and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION ): # set bold font for important items - font = QtGui.QFont(self.current_font) + font = QtGui.QFont(self.current_ui_font) font.setBold(True) return font - if role == QtCore.Qt.FontRole: - return QtGui.QFont(self.current_font) - if role == QtCore.Qt.ForegroundRole and column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS: # set color for virtual address column return QtGui.QColor(37, 147, 215) diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index 0efdafe27a..3e081e25b0 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -175,6 +175,18 @@ def resize_columns_to_content(header): header.resizeSection(0, MAX_SECTION_SIZE) +def clone_ui_font(ui_font: Optional[QtGui.QFont]) -> QtGui.QFont: + """return a copy of the given UI font or the default UI font""" + return QtGui.QFont() if ui_font is None else QtGui.QFont(ui_font) + + +def get_bold_widget_font(widget: QtWidgets.QWidget) -> QtGui.QFont: + """return the widget font with bold enabled""" + font = QtGui.QFont(widget.font()) + font.setBold(True) + return font + + class CapaExplorerRulegenPreview(QtWidgets.QTextEdit): INDENT = " " * 2 @@ -190,7 +202,7 @@ def __init__(self, parent=None): self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.setAcceptRichText(False) - def update_font(self, font: QtGui.QFont): + def update_font(self, font: QtGui.QFont, ui_font: Optional[QtGui.QFont] = None): self.current_font = font preview_font = QtGui.QFont(self.current_font) preview_font.setBold(True) @@ -508,11 +520,12 @@ def slot_item_double_clicked(self, o, column): o.setFlags(o.flags() & ~QtCore.Qt.ItemIsEditable) self.is_editing = True - def update_font(self, font: QtGui.QFont): + def update_font(self, font: QtGui.QFont, ui_font: Optional[QtGui.QFont] = None): """apply a new font to the editor and restyle existing nodes""" self.current_font = font - self.setFont(font) - self.header().setFont(font) + ui_font = clone_ui_font(ui_font) + self.setFont(ui_font) + self.header().setFont(ui_font) for node in iterate_tree(self): if getattr(node, "capa_type", None) == CapaExplorerRulegenEditor.get_node_type_expression(): self.style_expression_node(node) @@ -601,8 +614,7 @@ def load_custom_context_menu_expression(self, pos): def style_expression_node(self, o): """ """ - font = QtGui.QFont(self.current_font) - font.setBold(True) + font = get_bold_widget_font(self) o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font) @@ -1000,8 +1012,7 @@ def show_item_and_parents(_o): def style_parent_node(self, o): """ """ - font = QtGui.QFont(self.current_font) - font.setBold(True) + font = get_bold_widget_font(self) o.setFont(CapaExplorerRulegenFeatures.get_column_feature_index(), font) @@ -1020,11 +1031,12 @@ def style_leaf_node(self, o): brush.setColor(QtGui.QColor(*COLOR_BLUE_RGB)) o.setForeground(CapaExplorerRulegenFeatures.get_column_address_index(), brush) - def update_font(self, font: QtGui.QFont): + def update_font(self, font: QtGui.QFont, ui_font: Optional[QtGui.QFont] = None): """apply a new font to the feature tree and restyle nodes""" self.current_font = font - self.setFont(font) - self.header().setFont(font) + ui_font = clone_ui_font(ui_font) + self.setFont(ui_font) + self.header().setFont(ui_font) for node in iterate_tree(self): if getattr(node, "capa_type", None) == CapaExplorerRulegenFeatures.get_node_type_parent(): self.style_parent_node(node) @@ -1174,10 +1186,10 @@ def __init__(self, model, parent=None): self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}") - def update_font(self, font: QtGui.QFont): + def update_font(self, font: QtGui.QFont, ui_font: Optional[QtGui.QFont] = None): """apply a new font to the tree view and its header""" - self.setFont(font) - self.header().setFont(font) + ui_font = clone_ui_font(ui_font) + self.setFont(ui_font) def iter_model_indexes(self, parent=None): """yield all indexes in the current model""" From 8dc7affa05616236d73940c0117211dd1f183648 Mon Sep 17 00:00:00 2001 From: vee1e Date: Wed, 8 Apr 2026 21:46:54 +0530 Subject: [PATCH 10/11] fix: rulegen preview bug and remove more elements from sizing --- capa/ida/plugin/form.py | 19 +++++++++---------- capa/ida/plugin/view.py | 8 ++------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 506bd23ba9..20f019c389 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -83,6 +83,11 @@ def get_scaled_ui_font(font: QtGui.QFont) -> QtGui.QFont: return ui_font +def get_default_ui_font() -> QtGui.QFont: + """return the default UI font without any user scaling""" + return QtGui.QFont() + + CAPA_RULESET_DOC_URL = "https://github.com/mandiant/capa/blob/master/doc/rules.md" @@ -361,16 +366,6 @@ def update_fonts(self, font: QtGui.QFont): if hasattr(self, "view_tree") and self.view_tree: expanded_items = self.view_tree.get_expanded_source_items() - for widget_name in ( - "view_search_bar", - "view_rulegen_search", - "view_status_label", - "view_rulegen_status_label", - ): - widget = getattr(self, widget_name, None) - if widget: - widget.setFont(ui_font) - for component_name in ( "model_data", "view_tree", @@ -413,6 +408,7 @@ def load_view_status_label(self): label = QtWidgets.QLabel() label.setAlignment(QtCore.Qt.AlignLeft) label.setText(status) + label.setFont(get_default_ui_font()) self.view_status_label_rulegen_cache = status self.view_status_label_analysis_cache = status @@ -448,6 +444,7 @@ def load_view_search_bar(self): """load the search bar control""" line = QtWidgets.QLineEdit() line.setPlaceholderText("search...") + line.setFont(get_default_ui_font()) line.textChanged.connect(self.slot_limit_results_to_search) self.view_search_bar = line @@ -516,10 +513,12 @@ def load_view_rulegen_tab(self): self.view_rulegen_status_label = QtWidgets.QLabel() self.view_rulegen_status_label.setAlignment(QtCore.Qt.AlignLeft) self.view_rulegen_status_label.setText("") + self.view_rulegen_status_label.setFont(get_default_ui_font()) self.view_rulegen_search = QtWidgets.QLineEdit() self.view_rulegen_search.setPlaceholderText("search...") self.view_rulegen_search.setClearButtonEnabled(True) + self.view_rulegen_search.setFont(get_default_ui_font()) self.view_rulegen_search.textChanged.connect(self.slot_limit_rulegen_features_to_search) self.view_rulegen_header_label = QtWidgets.QLabel() diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index 3e081e25b0..6c2f666e8b 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -195,18 +195,14 @@ def __init__(self, parent=None): super().__init__(parent) self.current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - font = QtGui.QFont(self.current_font) - font.setBold(True) - self.setFont(font) + self.setFont(QtGui.QFont(self.current_font)) self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.setAcceptRichText(False) def update_font(self, font: QtGui.QFont, ui_font: Optional[QtGui.QFont] = None): self.current_font = font - preview_font = QtGui.QFont(self.current_font) - preview_font.setBold(True) - self.setFont(preview_font) + self.setFont(QtGui.QFont(self.current_font)) def reset_view(self): """ """ From 9f7ad3cc2eac0bfe10021f0b369f2f5d116938a4 Mon Sep 17 00:00:00 2001 From: vee1e Date: Sun, 12 Apr 2026 01:17:57 +0530 Subject: [PATCH 11/11] chore: fix CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12d8eb3839..f17c33ed76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ ### capa Explorer Web ### capa Explorer IDA Pro plugin -- ida plugin: add a font explorer in Settings with live preview #2570 +- ida plugin: add a font explorer in settings @vee1e #2995 ### Development - ci: use explicit and per job permissions @mike-hunhoff #3002