feat(gui): notify user if code has non-displayable character with current font (PR #2490)

* feat(gui): notify user, if code has non-displayable character in current font (fix #621)

* fix(gui): improve check showing undisplayed chars on current font

* fix code style

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
Yaroslav
2025-05-23 01:05:05 +03:00
committed by GitHub
parent 00f0f5547b
commit a0a9f7fd41
15 changed files with 231 additions and 1 deletions
@@ -3,6 +3,7 @@ package jadx.gui.settings.ui.font;
import java.lang.reflect.Field;
import javax.swing.JCheckBox;
import javax.swing.JPanel;
import org.drjekyll.fontchooser.FontChooser;
import org.drjekyll.fontchooser.panes.FamilyPane;
@@ -23,6 +24,15 @@ public class FontChooserHack {
}
}
public static void hidePreview(FontChooser fontChooser) {
try {
JPanel previewPanel = (JPanel) getPrivateField(fontChooser, "previewPanel");
previewPanel.setVisible(false);
} catch (Throwable e) {
LOG.debug("Failed to hide preview panel", e);
}
}
private static Object getPrivateField(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
@@ -85,6 +85,9 @@ import jadx.api.plugins.events.types.ReloadProject;
import jadx.api.plugins.events.types.ReloadSettingsWindow;
import jadx.api.plugins.utils.CommonFileUtils;
import jadx.core.Jadx;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.export.TemplateFile;
import jadx.core.utils.ListUtils;
import jadx.core.utils.StringUtils;
@@ -160,6 +163,7 @@ import jadx.gui.ui.tab.TabsController;
import jadx.gui.ui.tab.dnd.TabDndController;
import jadx.gui.ui.treenodes.StartPageNode;
import jadx.gui.ui.treenodes.SummaryNode;
import jadx.gui.ui.treenodes.UndisplayedStringsNode;
import jadx.gui.update.JadxUpdate;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.DesktopEntryUtils;
@@ -240,6 +244,7 @@ public class MainWindow extends JFrame {
private final List<Consumer<JRoot>> treeUpdateListener = new ArrayList<>();
private boolean loaded;
private boolean settingsOpen = false;
private boolean showUndisplayedCharsDialog;
private final ShortcutsController shortcutsController;
private JadxMenuBar menuBar;
@@ -506,6 +511,7 @@ public class MainWindow extends JFrame {
// start new project
project = new JadxProject(this);
project.setFilePaths(paths);
showUndisplayedCharsDialog = false;
loadFiles(onFinish);
}
@@ -654,6 +660,7 @@ public class MainWindow extends JFrame {
runInitialBackgroundJobs();
notifyLoadListeners(true);
update();
checkIfCodeHasNonPrintableChars();
});
}
@@ -1794,6 +1801,60 @@ public class MainWindow extends JFrame {
}
}
private void checkIfCodeHasNonPrintableChars() {
if (getSettings().isRenamePrintable() || getSettings().isDeobfuscationOn()) {
return;
}
if (showUndisplayedCharsDialog) {
return;
}
StringBuilder nonDisplayString = new StringBuilder();
List<ClassNode> classes = wrapper.getRootNode().getClasses(true);
Font font = getSettings().getFont();
boolean hasNonDisplayable = false;
for (ClassNode cls : classes) {
String className = cls.getRawName();
if (!FontUtils.canStringBeDisplayed(className, font)) {
hasNonDisplayable = true;
nonDisplayString.append(className);
nonDisplayString.append("\n");
}
for (MethodNode methodNode : cls.getMethods()) {
String methodName = methodNode.getName();
if (!FontUtils.canStringBeDisplayed(methodName, font)) {
hasNonDisplayable = true;
nonDisplayString.append(methodName);
nonDisplayString.append("\n");
}
}
for (FieldNode fieldNode : cls.getFields()) {
String fieldName = fieldNode.getName();
if (!FontUtils.canStringBeDisplayed(fieldName, font)) {
hasNonDisplayable = true;
nonDisplayString.append(fieldName);
nonDisplayString.append("\n");
}
}
}
if (hasNonDisplayable) {
showUndisplayedCharsDialog = true;
int dialogResult = JOptionPane.showConfirmDialog(this,
NLS.str("msg.non_displayable_chars", font.getFontName()),
NLS.str("msg.warning_title"),
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (dialogResult == JOptionPane.YES_OPTION) {
tabsController.selectTab(new UndisplayedStringsNode(nonDisplayString.toString()));
}
}
}
public RenameMappingsGui getRenameMappings() {
return renameMappings;
}
@@ -0,0 +1,73 @@
package jadx.gui.ui.panel;
import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.BorderFactory;
import javax.swing.SwingUtilities;
import org.drjekyll.fontchooser.FontChooser;
import org.drjekyll.fontchooser.model.FontSelectionModel;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rtextarea.RTextScrollPane;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.settings.ui.font.FontChooserHack;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.treenodes.UndisplayedStringsNode;
public class UndisplayedStringsPanel extends ContentPanel {
private static final long serialVersionUID = 695370628262996993L;
private final RSyntaxTextArea textPane;
private final RTextScrollPane codeScrollPane;
public UndisplayedStringsPanel(TabbedPane panel, UndisplayedStringsNode node) {
super(panel, node);
setLayout(new BorderLayout());
textPane = AbstractCodeArea.getDefaultArea(panel.getMainWindow());
JadxSettings settings = getSettings();
Font selectedFont = settings.getFont();
FontChooser fontChooser = new FontChooser();
fontChooser.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
fontChooser.setSelectedFont(selectedFont);
FontChooserHack.hidePreview(fontChooser);
fontChooser.addChangeListener(event -> {
FontSelectionModel model = (FontSelectionModel) event.getSource();
settings.setFont(model.getSelectedFont());
SwingUtilities.invokeLater(() -> {
getMainWindow().loadSettings();
});
});
codeScrollPane = new RTextScrollPane(textPane);
add(codeScrollPane, BorderLayout.CENTER);
add(fontChooser, BorderLayout.EAST);
applySettings();
showData(node.makeDescString());
}
private void applySettings() {
codeScrollPane.setLineNumbersEnabled(getSettings().getLineNumbersMode() != LineNumbersMode.DISABLE);
codeScrollPane.getGutter().setLineNumberFont(getSettings().getFont());
textPane.setFont(getSettings().getFont());
}
private void showData(String data) {
textPane.setText(data);
textPane.setCaretPosition(0);
}
@Override
public void loadSettings() {
applySettings();
}
}
@@ -0,0 +1,51 @@
package jadx.gui.ui.treenodes;
import javax.swing.*;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.UndisplayedStringsPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
public class UndisplayedStringsNode extends JNode {
private static final long serialVersionUID = 2005158949697898302L;
private final String undisplayedStings;
public UndisplayedStringsNode(String undisplayedStings) {
this.undisplayedStings = undisplayedStings;
}
@Override
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
return new UndisplayedStringsPanel(tabbedPane, this);
}
@Override
public String makeString() {
return NLS.str("msg.non_displayable_chars.title");
}
@Override
public Icon getIcon() {
return Icons.FONT;
}
@Override
public JClass getJParent() {
return null;
}
@Override
public String makeDescString() {
return undisplayedStings;
}
@Override
public boolean supportsQuickTabs() {
return false;
}
}
@@ -87,6 +87,21 @@ public class FontUtils {
}
}
public static boolean canStringBeDisplayed(String str, Font font) {
if (str == null || str.isEmpty()) {
return true;
}
int offset = 0;
while (offset < str.length()) {
int codePoint = str.codePointAt(offset);
if (!font.canDisplay(codePoint)) {
return false;
}
offset += Character.charCount(codePoint);
}
return true;
}
private FontUtils() {
}
}
@@ -46,4 +46,6 @@ public class Icons {
public static final ImageIcon CHECK = UiUtils.openSvgIcon("ui/checkConstraint");
public static final ImageIcon FORMAT = UiUtils.openSvgIcon("ui/toolWindowMessages");
public static final ImageIcon RESET = UiUtils.openSvgIcon("ui/reset");
public static final ImageIcon FONT = UiUtils.openSvgIcon("nodes/fontFile");
}
@@ -184,7 +184,7 @@ search_dialog.copy=alles kopieren
usage_dialog.title=Verwendungssuche
usage_dialog.label=Verwendungen von:
#usage_dialog_plus.title=Verwendungssuche
#usage_dialog_plus.title=Usage tree search
usage_dialog_plus.jump_to=Zur aktuellen Position springen
usage_dialog_plus.copy_path=Verwendungspfad kopieren
usage_dialog_plus.search_complete=Suche abgeschlossen
@@ -343,6 +343,8 @@ msg.project_error_title=Fehler
msg.project_error=Projekt konnte nicht geladen werden
msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert nicht.
msg.cant_add_comment=Kann hier keinen Kommentar hinzufügen
#msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
#msg.non_displayable_chars.title=Undisplayed Strings
#methods_dialog.title=Select methods
@@ -343,6 +343,8 @@ msg.project_error_title=Error
msg.project_error=Project could not be loaded
msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist.
msg.cant_add_comment=Can't add comment here
msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
msg.non_displayable_chars.title=Undisplayed Strings
methods_dialog.title=Select methods
@@ -343,6 +343,8 @@ msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicac
#msg.project_error=Project could not be loaded
#msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist.
#msg.cant_add_comment=Can't add comment here
#msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
#msg.non_displayable_chars.title=Undisplayed Strings
#methods_dialog.title=Select methods
@@ -343,6 +343,8 @@ msg.project_error_title=Kesalahan
msg.project_error=Proyek tidak dapat dimuat
msg.cmd_select_class_error=Gagal memilih kelas\n%s\nKelas tidak ada.
msg.cant_add_comment=Tidak dapat menambahkan komentar di sini
#msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
#msg.non_displayable_chars.title=Undisplayed Strings
#methods_dialog.title=Select methods
@@ -343,6 +343,8 @@ msg.project_error_title=오류
msg.project_error=프로젝트를 로드 할 수 없습니다.
msg.cmd_select_class_error=클래스를 선택하지 못했습니다.\n%s\n클래스가 없습니다.
msg.cant_add_comment=여기에 주석을 추가할수 없음
#msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
#msg.non_displayable_chars.title=Undisplayed Strings
#methods_dialog.title=Select methods
@@ -343,6 +343,8 @@ msg.project_error_title=Erro
msg.project_error=Projeto não pôde ser carregado
msg.cmd_select_class_error=Falha ao selecionar classe\n%s\nA classe não existe.
msg.cant_add_comment=Não é possível adicionar comentários aqui
#msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
#msg.non_displayable_chars.title=Undisplayed Strings
#methods_dialog.title=Select methods
@@ -343,6 +343,8 @@ msg.project_error_title=Ошибка
msg.project_error=Проект не может быть загружен
msg.cmd_select_class_error=Ошибка выбора класса\n%s\nЭтот класс не существует.
msg.cant_add_comment=Невозможно добавить комментарий сюда
#msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
#msg.non_displayable_chars.title=Undisplayed Strings
#methods_dialog.title=Select methods
@@ -343,6 +343,8 @@ msg.project_error_title=错误
msg.project_error=项目无法加载
msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。
msg.cant_add_comment=无法在此添加注释
#msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
#msg.non_displayable_chars.title=Undisplayed Strings
#methods_dialog.title=Select methods
@@ -343,6 +343,8 @@ msg.project_error_title=錯誤
msg.project_error=無法載入專案
msg.cmd_select_class_error=無法選擇類別\n%s\n類別不存在。
msg.cant_add_comment=無法在此新增註解
#msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars?
#msg.non_displayable_chars.title=Undisplayed Strings
#methods_dialog.title=Select methods