diff --git a/jadx-gui/build.gradle.kts b/jadx-gui/build.gradle.kts index e6b916294cc..984e27453c6 100644 --- a/jadx-gui/build.gradle.kts +++ b/jadx-gui/build.gradle.kts @@ -49,6 +49,14 @@ dependencies { implementation("com.android.tools.build:apksig:8.9.2") implementation("io.github.skylot:jdwp:2.0.0") + // Library for hex viewing data + val bined = "0.2.2" + implementation("org.exbin.bined:bined-swing:$bined") + implementation("org.exbin.bined:bined-highlight-swing:$bined") + implementation("org.exbin.bined:bined-swing-section:$bined") + implementation("org.exbin.auxiliary:binary_data:$bined") + implementation("org.exbin.auxiliary:binary_data-array:$bined") + testImplementation(project.project(":jadx-core").sourceSets.getByName("test").output) } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java b/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java index ed6de813775..dbbe0c9252a 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java @@ -9,8 +9,8 @@ import jadx.api.metadata.ICodeNodeRef; import jadx.gui.treemodel.JNode; +import jadx.gui.ui.action.JNodeAction; import jadx.gui.ui.codearea.CodeArea; -import jadx.gui.ui.codearea.JNodeAction; public class CodePopupAction { private final String name; diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java index e29aebdf83f..dca568bb7f9 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java @@ -1,6 +1,7 @@ package jadx.gui.plugins.script; import java.awt.BorderLayout; +import java.awt.Component; import java.awt.Dimension; import java.awt.event.KeyEvent; import java.util.Collections; @@ -237,6 +238,11 @@ public AbstractCodeArea getCodeArea() { return scriptArea; } + @Override + public Component getChildrenComponent() { + return getCodeArea(); + } + @Override public void loadSettings() { applySettings(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index ce7ea3c8020..9bcf4d83f82 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -21,6 +21,7 @@ import java.awt.event.WindowEvent; import java.awt.geom.AffineTransform; import java.io.File; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -64,6 +65,7 @@ import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; +import org.exbin.bined.swing.section.SectCodeArea; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -132,12 +134,16 @@ import jadx.gui.ui.codearea.theme.EditorThemeManager; import jadx.gui.ui.dialog.ADBDialog; import jadx.gui.ui.dialog.AboutDialog; +import jadx.gui.ui.dialog.CharsetDialog; import jadx.gui.ui.dialog.ExceptionDialog; +import jadx.gui.ui.dialog.GotoAddressDialog; import jadx.gui.ui.dialog.LogViewerDialog; import jadx.gui.ui.dialog.SearchDialog; import jadx.gui.ui.export.ExportProjectDialog; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; +import jadx.gui.ui.hexviewer.HexInspectorPanel; +import jadx.gui.ui.hexviewer.HexPreviewPanel; import jadx.gui.ui.menu.HiddenMenuItem; import jadx.gui.ui.menu.JadxMenu; import jadx.gui.ui.menu.JadxMenuBar; @@ -237,6 +243,7 @@ public class MainWindow extends JFrame { private final ShortcutsController shortcutsController; private JadxMenuBar menuBar; private JMenu pluginsMenu; + public JMenu hexViewerMenu; private final transient RenameMappingsGui renameMappings; @@ -970,6 +977,46 @@ public void textSearch() { SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.TEXT); } + private void sendActionsToHexViewer(ActionModel action) { + HexPreviewPanel hexPreviewPanel = getCurrentHexViewTab(); + if (hexPreviewPanel != null) { + HexInspectorPanel inspector = hexPreviewPanel.getInspector(); + SectCodeArea hexEditor = hexPreviewPanel.getEditor(); + switch (action) { + case HEX_VIEWER_SHOW_INSPECTOR: + hexPreviewPanel.getInspector().setVisible(!inspector.isVisible()); + break; + case HEX_VIEWER_CHANGE_ENCODING: + String result = CharsetDialog.chooseCharset(this, hexEditor.getCharset().name()); + if (!StringUtils.isEmpty(result)) { + hexEditor.setCharset(Charset.forName(result)); + } + break; + case HEX_VIEWER_GO_TO_ADDRESS: + new GotoAddressDialog().showSetSelectionDialog(hexEditor); + break; + case HEX_VIEWER_FIND: + hexPreviewPanel.showSearchBar(); + break; + } + } + } + + public HexPreviewPanel getCurrentHexViewTab() { + ContentPanel panel = tabbedPane.getSelectedContentPanel(); + if (panel instanceof AbstractCodeContentPanel) { + Component childrenComponent = ((AbstractCodeContentPanel) panel).getChildrenComponent(); + if (childrenComponent instanceof HexPreviewPanel) { + return (HexPreviewPanel) childrenComponent; + } + } + return null; + } + + public void toggleHexViewMenu() { + hexViewerMenu.setEnabled(getCurrentHexViewTab() != null); + } + public void goToMainActivity() { AndroidManifestParser parser = new AndroidManifestParser( AndroidManifestParser.getAndroidManifest(getWrapper().getResources()), @@ -1066,6 +1113,9 @@ private void initMenuAndToolbar() { JMenu recentProjects = new JadxMenu(NLS.str("menu.recent_projects"), shortcutsController); recentProjects.addMenuListener(new RecentProjectsMenuListener(this, recentProjects)); + hexViewerMenu = new JadxMenu(NLS.str("menu.hex_viewer"), shortcutsController); + initHexViewMenu(); + JadxGuiAction prefsAction = new JadxGuiAction(ActionModel.PREFS, this::openSettings); JadxGuiAction exitAction = new JadxGuiAction(ActionModel.EXIT, this::closeWindow); @@ -1167,6 +1217,7 @@ private void initMenuAndToolbar() { JMenu view = new JadxMenu(NLS.str("menu.view"), shortcutsController); view.setMnemonic(KeyEvent.VK_V); view.add(quickTabsAction.makeCheckBoxMenuItem()); + view.add(hexViewerMenu); view.add(flatPkgMenuItem); view.addSeparator(); view.add(enablePreviewTabAction.makeCheckBoxMenuItem()); @@ -1743,4 +1794,26 @@ public EditorThemeManager getEditorThemeManager() { public JadxGuiEventsImpl events() { return events; } + + private void initHexViewMenu() { + hexViewerMenu.setEnabled(false); + + JadxGuiAction showInspectorAction = new JadxGuiAction(ActionModel.HEX_VIEWER_SHOW_INSPECTOR, + () -> sendActionsToHexViewer(ActionModel.HEX_VIEWER_SHOW_INSPECTOR)); + JCheckBoxMenuItem showInspectorMenuItem = new JCheckBoxMenuItem(showInspectorAction); + + JadxGuiAction changeEncoding = new JadxGuiAction(ActionModel.HEX_VIEWER_CHANGE_ENCODING, + () -> sendActionsToHexViewer(ActionModel.HEX_VIEWER_CHANGE_ENCODING)); + JadxGuiAction goToAddress = new JadxGuiAction(ActionModel.HEX_VIEWER_GO_TO_ADDRESS, + () -> sendActionsToHexViewer(ActionModel.HEX_VIEWER_GO_TO_ADDRESS)); + + JadxGuiAction findAction = new JadxGuiAction(ActionModel.HEX_VIEWER_FIND, + () -> sendActionsToHexViewer(ActionModel.HEX_VIEWER_FIND)); + + hexViewerMenu.add(showInspectorMenuItem); + hexViewerMenu.add(changeEncoding); + hexViewerMenu.add(goToAddress); + hexViewerMenu.addSeparator(); + hexViewerMenu.add(findAction); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java index c72b3256281..0b4fd723752 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java @@ -5,7 +5,8 @@ public enum ActionCategory { MENU_TOOLBAR("action_category.menu_toolbar"), CODE_AREA("action_category.code_area"), - PLUGIN_SCRIPT("action_category.plugin_script"); + PLUGIN_SCRIPT("action_category.plugin_script"), + HEX_VIEWER_MENU("action_category.hex_viewer"); private final String nameRes; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java index 94abe303824..0ba7b0ccadd 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java @@ -103,7 +103,16 @@ public enum ActionModel { SCRIPT_SAVE(PLUGIN_SCRIPT, "script.save", "script.save", "ui/menu-saveall", Shortcut.keyboard(KeyEvent.VK_S, UiUtils.ctrlButton())), SCRIPT_AUTO_COMPLETE(PLUGIN_SCRIPT, "script.auto_complete", "script.auto_complete", null, - Shortcut.keyboard(KeyEvent.VK_SPACE, UiUtils.ctrlButton())); + Shortcut.keyboard(KeyEvent.VK_SPACE, UiUtils.ctrlButton())), + + HEX_VIEWER_SHOW_INSPECTOR(HEX_VIEWER_MENU, "hex_viewer.show_inspector", "hex_viewer.show_inspector", + null, Shortcut.none()), + HEX_VIEWER_CHANGE_ENCODING(HEX_VIEWER_MENU, "hex_viewer.change_encoding", "hex_viewer.change_encoding", + null, Shortcut.none()), + HEX_VIEWER_GO_TO_ADDRESS(HEX_VIEWER_MENU, "hex_viewer.goto_address", "hex_viewer.goto_address", + null, Shortcut.keyboard(KeyEvent.VK_J, UiUtils.ctrlButton())), + HEX_VIEWER_FIND(HEX_VIEWER_MENU, "hex_viewer.find", "hex_viewer.find", + null, Shortcut.keyboard(KeyEvent.VK_F, UiUtils.ctrlButton())); private final ActionCategory category; private final String nameRes; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeAreaAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/CodeAreaAction.java similarity index 79% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeAreaAction.java rename to jadx-gui/src/main/java/jadx/gui/ui/action/CodeAreaAction.java index 5bda980273d..6a40339a2ea 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeAreaAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/CodeAreaAction.java @@ -1,7 +1,6 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.action; -import jadx.gui.ui.action.ActionModel; -import jadx.gui.ui.action.JadxGuiAction; +import jadx.gui.ui.codearea.CodeArea; public class CodeAreaAction extends JadxGuiAction { protected transient CodeArea codeArea; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/CommentSearchAction.java similarity index 88% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java rename to jadx-gui/src/main/java/jadx/gui/ui/action/CommentSearchAction.java index 5df1a39f352..a79fce6f860 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/CommentSearchAction.java @@ -1,8 +1,8 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.action; import java.awt.event.ActionEvent; -import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.SearchDialog; public class CommentSearchAction extends CodeAreaAction { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FindUsageAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/FindUsageAction.java similarity index 85% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/FindUsageAction.java rename to jadx-gui/src/main/java/jadx/gui/ui/action/FindUsageAction.java index 2f413654c5a..e61d9f04219 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FindUsageAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/FindUsageAction.java @@ -1,7 +1,7 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.action; import jadx.gui.treemodel.JNode; -import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.UsageDialog; public final class FindUsageAction extends JNodeAction { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/FridaAction.java similarity index 98% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java rename to jadx-gui/src/main/java/jadx/gui/ui/action/FridaAction.java index 28e592a0519..bab39ed2917 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/FridaAction.java @@ -1,4 +1,4 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.action; import java.util.List; import java.util.Objects; @@ -23,7 +23,7 @@ import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; -import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.codearea.CodeArea; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/GoToDeclarationAction.java similarity index 85% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java rename to jadx-gui/src/main/java/jadx/gui/ui/action/GoToDeclarationAction.java index 4b1b4531c45..6db829deb76 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/GoToDeclarationAction.java @@ -1,7 +1,7 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.action; import jadx.gui.treemodel.JNode; -import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.codearea.CodeArea; public final class GoToDeclarationAction extends JNodeAction { private static final long serialVersionUID = -1186470538894941301L; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/JNodeAction.java similarity index 92% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeAction.java rename to jadx-gui/src/main/java/jadx/gui/ui/action/JNodeAction.java index 11b84b5eda0..1f7e9afd371 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/JNodeAction.java @@ -1,4 +1,4 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.action; import java.awt.event.ActionEvent; import java.beans.PropertyChangeListener; @@ -6,8 +6,7 @@ import org.jetbrains.annotations.Nullable; import jadx.gui.treemodel.JNode; -import jadx.gui.ui.action.ActionModel; -import jadx.gui.ui.action.JadxGuiAction; +import jadx.gui.ui.codearea.CodeArea; /** * Add menu and key binding actions for JNode in code area diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JsonPrettifyAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/JsonPrettifyAction.java similarity index 91% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/JsonPrettifyAction.java rename to jadx-gui/src/main/java/jadx/gui/ui/action/JsonPrettifyAction.java index c1c04fdff16..0e1ca036d8b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JsonPrettifyAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/JsonPrettifyAction.java @@ -1,4 +1,4 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.action; import com.google.gson.Gson; import com.google.gson.JsonElement; @@ -6,7 +6,7 @@ import jadx.core.utils.GsonUtils; import jadx.gui.treemodel.JNode; -import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.codearea.CodeArea; public class JsonPrettifyAction extends JNodeAction { private static final long serialVersionUID = -2682529369671695550L; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/RenameAction.java similarity index 90% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java rename to jadx-gui/src/main/java/jadx/gui/ui/action/RenameAction.java index bf7fe65e7f0..2124c05d8f9 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/RenameAction.java @@ -1,8 +1,8 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.action; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JRenameNode; -import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.RenameDialog; public final class RenameAction extends JNodeAction { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/XposedAction.kt b/jadx-gui/src/main/java/jadx/gui/ui/action/XposedAction.kt similarity index 98% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/XposedAction.kt rename to jadx-gui/src/main/java/jadx/gui/ui/action/XposedAction.kt index c359c06cb93..300545a78eb 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/XposedAction.kt +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/XposedAction.kt @@ -1,4 +1,4 @@ -package jadx.gui.ui.codearea +package jadx.gui.ui.action import jadx.core.dex.instructions.args.ArgType import jadx.core.dex.instructions.args.PrimitiveType @@ -8,7 +8,7 @@ import jadx.gui.treemodel.JClass import jadx.gui.treemodel.JField import jadx.gui.treemodel.JMethod import jadx.gui.treemodel.JNode -import jadx.gui.ui.action.ActionModel +import jadx.gui.ui.codearea.CodeArea import jadx.gui.utils.NLS import jadx.gui.utils.UiUtils import org.slf4j.Logger diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index 360f70fd4c3..131204ac73a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -50,6 +50,7 @@ import jadx.gui.treemodel.JEditableNode; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; +import jadx.gui.ui.action.JNodeAction; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.JumpPosition; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java index 07107e7bf7e..06fb3f73389 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java @@ -1,5 +1,7 @@ package jadx.gui.ui.codearea; +import java.awt.Component; + import jadx.gui.treemodel.JNode; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; @@ -15,4 +17,6 @@ protected AbstractCodeContentPanel(TabbedPane panel, JNode jnode) { } public abstract AbstractCodeArea getCodeArea(); + + public abstract Component getChildrenComponent(); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java index 07f7c656436..3bf69255a16 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java @@ -2,20 +2,29 @@ import java.awt.BorderLayout; import java.awt.Component; +import java.nio.charset.StandardCharsets; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.ResourcesLoader; +import jadx.core.utils.exceptions.JadxException; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.LineNumbersMode; import jadx.gui.treemodel.JNode; +import jadx.gui.treemodel.JResource; +import jadx.gui.ui.hexviewer.HexPreviewPanel; import jadx.gui.ui.tab.TabbedPane; public class BinaryContentPanel extends AbstractCodeContentPanel { + private static final Logger LOG = LoggerFactory.getLogger(BinaryContentPanel.class); private final transient CodePanel textCodePanel; - private final transient CodePanel hexCodePanel; - private final transient HexConfigurationPanel hexConfigurationPanel; + private final transient HexPreviewPanel hexPreviewPanel; private final transient JTabbedPane areaTabbedPane; public BinaryContentPanel(TabbedPane panel, JNode jnode) { @@ -31,48 +40,75 @@ public BinaryContentPanel(TabbedPane panel, JNode jnode, boolean supportsText) { } else { textCodePanel = null; } - HexArea hexArea = new HexArea(this, jnode); - hexConfigurationPanel = new HexConfigurationPanel(hexArea.getConfiguration()); - hexArea.setConfigurationPanel(hexConfigurationPanel); - hexCodePanel = new CodePanel(hexArea); + hexPreviewPanel = new HexPreviewPanel(getSettings()); + hexPreviewPanel.getInspector().setVisible(false); + areaTabbedPane = buildTabbedPane(); add(areaTabbedPane); - getSelectedPanel().load(); + SwingUtilities.invokeLater(this::loadCodePanel); } - private JTabbedPane buildTabbedPane() { - JSplitPane hexSplitPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, hexCodePanel, hexConfigurationPanel); - hexSplitPanel.setResizeWeight(0.8); + private void loadToHexView(JNode binaryNode) { + byte[] bytes = null; + if (binaryNode instanceof JResource) { + JResource jResource = (JResource) binaryNode; + try { + bytes = ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes()); + } catch (JadxException e) { + LOG.error("Failed to directly load resource binary data {}: {}", jResource.getName(), e.getMessage()); + } + } + if (bytes == null) { + bytes = binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.UTF_8); + } + hexPreviewPanel.setData(bytes); + } + private JTabbedPane buildTabbedPane() { JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); tabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0)); tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); if (textCodePanel != null) { tabbedPane.add(textCodePanel, "Text"); } - tabbedPane.add(hexSplitPanel, "Hex"); + tabbedPane.add(hexPreviewPanel, "Hex"); tabbedPane.addChangeListener(e -> { - getSelectedPanel().load(); + getMainWindow().toggleHexViewMenu(); }); return tabbedPane; } + private void loadCodePanel() { + Component codePanel = getSelectedPanel(); + if (codePanel instanceof CodeArea) { + CodeArea codeArea = (CodeArea) codePanel; + codeArea.load(); + loadToHexView(getNode()); + } else { + loadToHexView(getNode()); + } + } + @Override public AbstractCodeArea getCodeArea() { if (textCodePanel != null) { return textCodePanel.getCodeArea(); } else { - return hexCodePanel.getCodeArea(); + return null; } } + @Override + public Component getChildrenComponent() { + return getSelectedPanel(); + } + @Override public void loadSettings() { if (textCodePanel != null) { textCodePanel.loadSettings(); } - hexCodePanel.loadSettings(); updateUI(); } @@ -83,13 +119,15 @@ public JadxSettings getSettings() { return settings; } - private CodePanel getSelectedPanel() { + private Component getSelectedPanel() { Component selectedComponent = areaTabbedPane.getSelectedComponent(); - CodePanel selectedPanel; + Component selectedPanel; if (selectedComponent instanceof CodePanel) { - selectedPanel = (CodePanel) selectedComponent; + selectedPanel = ((CodePanel) selectedComponent).getCodeArea(); } else if (selectedComponent instanceof JSplitPane) { - selectedPanel = (CodePanel) ((JSplitPane) selectedComponent).getLeftComponent(); + selectedPanel = ((JSplitPane) selectedComponent).getLeftComponent(); + } else if (selectedComponent instanceof HexPreviewPanel) { + selectedPanel = selectedComponent; } else { throw new RuntimeException("tabbedPane.getSelectedComponent returned a Component " + "of unexpected type " + selectedComponent); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java index c8bbcadb272..2dc9e5608e5 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java @@ -125,6 +125,11 @@ public AbstractCodeArea getCodeArea() { return javaCodePanel.getCodeArea(); } + @Override + public Component getChildrenComponent() { + return getCodeArea(); + } + public CodePanel getJavaCodePanel() { return javaCodePanel; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index e8b9b7a83fc..2f63a6f7381 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -26,6 +26,7 @@ import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; import jadx.gui.ui.MainWindow; +import jadx.gui.ui.action.*; import jadx.gui.ui.codearea.mode.JCodeMode; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.CaretPositionFix; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java index f9708354247..0ddab125afb 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java @@ -1,6 +1,7 @@ package jadx.gui.ui.codearea; import java.awt.BorderLayout; +import java.awt.Component; import java.awt.Point; import org.slf4j.Logger; @@ -40,6 +41,11 @@ public AbstractCodeArea getCodeArea() { return codePanel.getCodeArea(); } + @Override + public Component getChildrenComponent() { + return getCodeArea(); + } + @Override public String getTabTooltip() { String s = node.getName(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java index e782e5db3a8..d182803e0bd 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java @@ -29,6 +29,7 @@ import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.ui.action.ActionModel; +import jadx.gui.ui.action.CodeAreaAction; import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.ui.dialog.CommentDialog; import jadx.gui.utils.DefaultPopupMenuListener; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexArea.java deleted file mode 100644 index 41aebb4f0d3..00000000000 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexArea.java +++ /dev/null @@ -1,213 +0,0 @@ -package jadx.gui.ui.codearea; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.nio.charset.StandardCharsets; - -import javax.swing.border.EmptyBorder; -import javax.swing.event.CaretEvent; -import javax.swing.event.CaretListener; - -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.api.ICodeInfo; -import jadx.api.ResourcesLoader; -import jadx.core.utils.exceptions.JadxException; -import jadx.gui.treemodel.JNode; -import jadx.gui.treemodel.JResource; -import jadx.gui.ui.MainWindow; -import jadx.gui.ui.panel.ContentPanel; -import jadx.gui.utils.UiUtils; - -public class HexArea extends AbstractCodeArea { - private static final Logger LOG = LoggerFactory.getLogger(HexArea.class); - - private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); - - private final HexAreaConfiguration config; - private final JNode binaryNode; - private final HexPreviewPanel hexPreviewPanel; - private HexConfigurationPanel hexConfigurationPanel; - - private byte[] bytes; - - public HexArea(ContentPanel contentPanel, JNode node) { - super(contentPanel, node); - binaryNode = node; - config = new HexAreaConfiguration(); - hexPreviewPanel = new HexPreviewPanel(config); - - initView(); - applyTheme(); - } - - @Override - public @NotNull ICodeInfo getCodeInfo() { - return ICodeInfo.EMPTY; - } - - @Override - public void load() { - byte[] bytes = null; - if (binaryNode instanceof JResource) { - JResource jResource = (JResource) binaryNode; - try { - bytes = ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes()); - } catch (JadxException e) { - LOG.error("Failed to directly load resource binary data {}: {}", jResource.getName(), e.getMessage()); - } - } - if (bytes == null) { - bytes = binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.UTF_8); - } - setBytes(bytes); - if (getBytes().length > 0) { - // We set the caret after the first byte to prevent it from being highlighted - setCaretPosition(2); - } else { - setCaretPosition(0); - } - setLoaded(); - } - - @Override - public void refresh() { - - } - - @Override - public void loadSettings() { - super.loadSettings(); - applyTheme(); - } - - private void applyTheme() { - MainWindow mainWindow = getContentPanel().getMainWindow(); - mainWindow.getEditorThemeManager().apply(this); - Font font = mainWindow.getSettings().getSmaliFont(); - setFont(font); - if (hexPreviewPanel != null) { - hexPreviewPanel.applyTheme(this, font); - } - } - - private void initView() { - addCaretListener(new HexCaretListener()); - setLayout(new BorderLayout()); - setBorder(new EmptyBorder(0, 0, 0, 0)); - hexPreviewPanel.setFont(getFont()); - add(hexPreviewPanel, BorderLayout.EAST); - } - - private void setBytes(byte[] bytes) { - this.bytes = bytes; - - String text; - if (bytes.length > 0) { - byte[] hexChars = new byte[bytes.length * 4 - 2]; - - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 4] = HEX_ARRAY[v >>> 4]; - hexChars[j * 4 + 1] = HEX_ARRAY[v & 0x0F]; - if (j != bytes.length - 1) { - hexChars[j * 4 + 2] = ' '; - hexChars[j * 4 + 3] = (byte) ((j % config.bytesPerLine == config.bytesPerLine - 1) ? '\n' : ' '); - } - } - text = new String(hexChars, StandardCharsets.UTF_8); - } else { - text = ""; - } - setText(text); - hexPreviewPanel.setBytes(bytes); - hexConfigurationPanel.setBytes(bytes); - } - - public byte[] getBytes() { - return bytes; - } - - public void setConfigurationPanel(HexConfigurationPanel hexConfigurationPanel) { - this.hexConfigurationPanel = hexConfigurationPanel; - } - - @Override - public void copyAsStyledText() { - String text = getSelectedText(); - if (text != null && !StringUtils.isEmpty(text)) { - text = text - .replace(" ", "") - .replace("\n", ""); - UiUtils.copyToClipboard(text); - } - } - - public HexAreaConfiguration getConfiguration() { - return config; - } - - private class HexCaretListener implements CaretListener { - private boolean isListening = true; - private int previousCaretDot = -1; - - @Override - public void caretUpdate(CaretEvent caretEvent) { - int dot = caretEvent.getDot(); - int mark = caretEvent.getMark(); - - if (!isListening) { - return; - } - - if (dot % 2 == 1) { - if (previousCaretDot > dot) { - if (mark == dot) { - mark--; - } - dot--; - } else { - if (mark == dot) { - mark++; - } - dot++; - } - - isListening = false; - HexArea.this.setCaretPosition(mark); - HexArea.this.moveCaretPosition(dot); - isListening = true; - } - - if (previousCaretDot != dot) { - onTextCursorMoved(dot, mark); - } - - previousCaretDot = dot; - } - - private void onTextCursorMoved(int dot, int mark) { - hexConfigurationPanel.setOffset(dot / 4); - - int startIndex = Math.min(dot, mark); - int endIndex = Math.max(dot, mark); - int startOffset = startIndex / 4; - int endOffset = endIndex / 4; - if (startIndex % 4 == 2 && endIndex == startIndex + 2) { - // Highlighted an empty space - hexPreviewPanel.clearHighlights(); - return; - } - if (startOffset < endOffset && startIndex % 4 == 2) { - startOffset++; - } - if (endOffset > startOffset && endIndex % 4 == 0) { - endOffset--; - } - hexPreviewPanel.highlightBytes(startOffset, endOffset); - } - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexAreaConfiguration.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexAreaConfiguration.java deleted file mode 100644 index 8ae0330250a..00000000000 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexAreaConfiguration.java +++ /dev/null @@ -1,7 +0,0 @@ -package jadx.gui.ui.codearea; - -public class HexAreaConfiguration { - public int bytesPerLine = 16; - - public boolean littleEndian = false; -} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexPreviewPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexPreviewPanel.java deleted file mode 100644 index 564f686028c..00000000000 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexPreviewPanel.java +++ /dev/null @@ -1,93 +0,0 @@ -package jadx.gui.ui.codearea; - -import java.awt.Color; -import java.awt.Font; - -import javax.swing.JTextArea; -import javax.swing.border.MatteBorder; -import javax.swing.text.BadLocationException; -import javax.swing.text.DefaultHighlighter; -import javax.swing.text.Highlighter; - -import org.fife.ui.rsyntaxtextarea.SyntaxScheme; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class HexPreviewPanel extends JTextArea { - private static final Logger LOG = LoggerFactory.getLogger(HexPreviewPanel.class); - - private final HexAreaConfiguration config; - - private byte[] bytes = new byte[0]; - - private Color highlightColor = Color.YELLOW; - private boolean hasHighlight = false; - - public HexPreviewPanel(HexAreaConfiguration configuration) { - super(0, configuration.bytesPerLine); - - this.config = configuration; - initView(); - } - - public void setBytes(byte[] bytes) { - this.bytes = bytes; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < bytes.length; i++) { - char c = (char) bytes[i]; - if (c <= 0x1f || (c & (1 << 7)) != 0) { - sb.append('.'); - } else { - sb.append(c); - } - if (i != bytes.length - 1 && i % config.bytesPerLine == config.bytesPerLine - 1) { - sb.append('\n'); - } - } - setText(sb.toString()); - } - - public void clearHighlights() { - hasHighlight = false; - getHighlighter().removeAllHighlights(); - } - - public void highlightBytes(int startOffset, int endOffset) { - if (hasHighlight) { - getHighlighter().removeAllHighlights(); - } - - // Include line breaks in the index - startOffset += startOffset / config.bytesPerLine; - endOffset += endOffset / config.bytesPerLine; - - Highlighter.HighlightPainter painter = new DefaultHighlighter.DefaultHighlightPainter(highlightColor); - try { - getHighlighter().addHighlight(startOffset, endOffset + 1, painter); - } catch (BadLocationException e) { - LOG.error("Unable to highlight bytes " + startOffset + ":" + endOffset, e); - } - hasHighlight = true; - } - - public void setHighlightColor(Color highlightColor) { - this.highlightColor = highlightColor; - } - - public void setBorderColor(Color borderColor) { - setBorder(new MatteBorder(0, 2, 0, 0, borderColor)); - } - - public void applyTheme(HexArea hexArea, Font font) { - setBackground(hexArea.getBackground()); - setHighlightColor(hexArea.getSelectionColor()); - setBorderColor(hexArea.getMatchedBracketBorderColor()); - setDisabledTextColor(hexArea.getSyntaxScheme().getStyle(SyntaxScheme.IDENTIFIER).foreground); - setFont(font); - } - - private void initView() { - setEnabled(false); - setEditable(false); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java index 8a0f1f748c2..780acdf9809 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java @@ -3,6 +3,7 @@ import javax.swing.JPopupMenu; import javax.swing.event.PopupMenuListener; +import jadx.gui.ui.action.JNodeAction; import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.utils.shortcut.ShortcutsController; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupListener.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupListener.java index 94537e546d7..7a9da3858bc 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupListener.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupListener.java @@ -7,6 +7,7 @@ import javax.swing.event.PopupMenuListener; import jadx.gui.treemodel.JNode; +import jadx.gui.ui.action.JNodeAction; public final class JNodePopupListener implements PopupMenuListener { private final CodeArea codeArea; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/DynamicCodeAreaTheme.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/DynamicCodeAreaTheme.java index d21d6f73def..23419696721 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/DynamicCodeAreaTheme.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/DynamicCodeAreaTheme.java @@ -11,6 +11,7 @@ import org.fife.ui.rtextarea.Gutter; import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; /** * Mix current UI theme colors and apply to code area theme. @@ -37,12 +38,12 @@ public void apply(RSyntaxTextArea textArea) { SyntaxScheme scheme = textArea.getSyntaxScheme(); - boolean isDarkTheme = isDarkTheme(themeBackground); + boolean isDarkTheme = UiUtils.isDarkTheme(themeBackground); // Background colors based on the theme Color editorBackground = isDarkTheme ? themeBackground : Color.WHITE; // Use white for light theme Color lineHighlight = isDarkTheme - ? adjustBrightness(themeBackground, 1.2f) + ? UiUtils.adjustBrightness(themeBackground, 1.2f) : Color.decode("#EBECF0"); // Light gray for light theme Color lineNumberForeground = UIManager.getColor("Label.foreground"); @@ -52,8 +53,8 @@ public void apply(RSyntaxTextArea textArea) { : new Color(51, 153, 255, 50); // Lighter blue for light theme Color markAllHighlightColor = isDarkTheme ? Color.decode("#32593D") : Color.decode("#ffc800"); - Color matchedBracketBackground = isDarkTheme ? adjustBrightness(Color.decode("#3B514D"), 1.2f) : Color.decode("#93D9D9"); - Color markOccurrencesColor = adjustBrightness(editorSelectionBackground, isDarkTheme ? 0.6f : 1.4f); + Color matchedBracketBackground = isDarkTheme ? UiUtils.adjustBrightness(Color.decode("#3B514D"), 1.2f) : Color.decode("#93D9D9"); + Color markOccurrencesColor = UiUtils.adjustBrightness(editorSelectionBackground, isDarkTheme ? 0.6f : 1.4f); // Set the syntax colors for the theme if (isDarkTheme) { @@ -133,17 +134,4 @@ public void apply(RSyntaxTextArea textArea) { } } - private static boolean isDarkTheme(Color background) { - double brightness = (background.getRed() * 0.299 - + background.getGreen() * 0.587 - + background.getBlue() * 0.114) / 255; - return brightness < 0.5; - } - - private static Color adjustBrightness(Color color, float factor) { - float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); - hsb[2] = Math.min(1.0f, hsb[2] * factor); // Adjust brightness - return Color.getHSBColor(hsb[0], hsb[1], hsb[2]); - } - } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CharsetDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CharsetDialog.java new file mode 100644 index 00000000000..4433cf9f9d0 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CharsetDialog.java @@ -0,0 +1,63 @@ +package jadx.gui.ui.dialog; + +import java.awt.Component; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import javax.swing.JOptionPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.utils.NLS; + +public class CharsetDialog { + private static final Logger LOG = LoggerFactory.getLogger(CharsetDialog.class); + + private static final Comparator CHARSET_COMPARATOR = Comparator.comparing( + Charset::displayName, + String::compareToIgnoreCase); + + public static String chooseCharset(Component parent, String currentCharsetName) { + Collection availableCharsets = Charset.availableCharsets().values(); + + List sortedCharsets = availableCharsets.stream() + .sorted(CHARSET_COMPARATOR) + .collect(Collectors.toList()); + + Charset initialSelection = null; + try { + if (currentCharsetName != null && Charset.isSupported(currentCharsetName)) { + initialSelection = Charset.forName(currentCharsetName); + if (!sortedCharsets.contains(initialSelection)) { + initialSelection = null; + } + } + } catch (Exception e) { + LOG.warn("Failed to find initial charset '{}'", currentCharsetName, e); + } + if (initialSelection == null && !sortedCharsets.isEmpty()) { + initialSelection = sortedCharsets.get(0); + } + Charset[] charsetArray = sortedCharsets.toArray(new Charset[0]); + + Object selectedValue = JOptionPane.showInputDialog( + parent, + NLS.str("encoding_dialog.message"), + NLS.str("encoding_dialog.title"), + JOptionPane.INFORMATION_MESSAGE, + null, + charsetArray, + initialSelection); + + if (selectedValue instanceof Charset) { + return ((Charset) selectedValue).name(); + } else { + return null; + } + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/GotoAddressDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/GotoAddressDialog.java new file mode 100644 index 00000000000..f8373b879c6 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/GotoAddressDialog.java @@ -0,0 +1,22 @@ +package jadx.gui.ui.dialog; + +import javax.swing.JOptionPane; + +import org.exbin.bined.swing.section.SectCodeArea; + +import jadx.gui.utils.NLS; + +public class GotoAddressDialog { + + public void showSetSelectionDialog(SectCodeArea codeArea) { + Object o = JOptionPane.showInputDialog( + codeArea, NLS.str("hex_viewer.enter_address"), NLS.str("hex_viewer.goto_address"), + JOptionPane.QUESTION_MESSAGE, null, null, + Long.toHexString(codeArea.getDataPosition())); + if (o != null) { + codeArea.setActiveCaretPosition(Long.parseLong(o.toString(), 16)); + codeArea.validateCaret(); + codeArea.revealCursor(); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/BinEdCodeAreaAssessor.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/BinEdCodeAreaAssessor.java new file mode 100644 index 00000000000..e94723c8c06 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/BinEdCodeAreaAssessor.java @@ -0,0 +1,166 @@ +package jadx.gui.ui.hexviewer; + +/* + * Copyright (C) ExBin Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.exbin.bined.CodeAreaSection; +import org.exbin.bined.highlight.swing.NonAsciiCodeAreaColorAssessor; +import org.exbin.bined.highlight.swing.NonprintablesCodeAreaAssessor; +import org.exbin.bined.highlight.swing.SearchCodeAreaColorAssessor; +import org.exbin.bined.swing.CodeAreaCharAssessor; +import org.exbin.bined.swing.CodeAreaColorAssessor; +import org.exbin.bined.swing.CodeAreaPaintState; + +/** + * Color assessor for binary editor with registrable modifiers. + * + * @author ExBin Project (https://exbin.org) + */ +public class BinEdCodeAreaAssessor implements CodeAreaColorAssessor, CodeAreaCharAssessor { + + private final List priorityColorModifiers = new ArrayList<>(); + private final List colorModifiers = new ArrayList<>(); + + private final CodeAreaColorAssessor parentColorAssessor; + private final CodeAreaCharAssessor parentCharAssessor; + + public BinEdCodeAreaAssessor(CodeAreaColorAssessor parentColorAssessor, CodeAreaCharAssessor parentCharAssessor) { + NonAsciiCodeAreaColorAssessor nonAsciiCodeAreaColorAssessor = new NonAsciiCodeAreaColorAssessor(parentColorAssessor); + NonprintablesCodeAreaAssessor nonprintablesCodeAreaAssessor = + new NonprintablesCodeAreaAssessor(nonAsciiCodeAreaColorAssessor, parentCharAssessor); + SearchCodeAreaColorAssessor searchCodeAreaColorAssessor = new SearchCodeAreaColorAssessor(nonprintablesCodeAreaAssessor); + this.parentColorAssessor = searchCodeAreaColorAssessor; + this.parentCharAssessor = nonprintablesCodeAreaAssessor; + } + + public void addColorModifier(PositionColorModifier colorModifier) { + colorModifiers.add(colorModifier); + } + + public void removeColorModifier(PositionColorModifier colorModifier) { + colorModifiers.remove(colorModifier); + } + + public void addPriorityColorModifier(PositionColorModifier colorModifier) { + priorityColorModifiers.add(colorModifier); + } + + public void removePriorityColorModifier(PositionColorModifier colorModifier) { + priorityColorModifiers.remove(colorModifier); + } + + @Override + public void startPaint(CodeAreaPaintState codeAreaPaintState) { + for (PositionColorModifier colorModifier : priorityColorModifiers) { + colorModifier.resetColors(); + } + + for (PositionColorModifier colorModifier : colorModifiers) { + colorModifier.resetColors(); + } + + if (parentColorAssessor != null) { + parentColorAssessor.startPaint(codeAreaPaintState); + } + } + + @Override + public Color getPositionBackgroundColor(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section, + boolean inSelection) { + for (PositionColorModifier colorModifier : priorityColorModifiers) { + Color positionBackgroundColor = + colorModifier.getPositionBackgroundColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); + if (positionBackgroundColor != null) { + return positionBackgroundColor; + } + } + + if (!inSelection) { + for (PositionColorModifier colorModifier : colorModifiers) { + Color positionBackgroundColor = + colorModifier.getPositionBackgroundColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); + if (positionBackgroundColor != null) { + return positionBackgroundColor; + } + } + } + + if (parentColorAssessor != null) { + return parentColorAssessor.getPositionBackgroundColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); + } + + return null; + } + + @Override + public Color getPositionTextColor(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section, boolean inSelection) { + for (PositionColorModifier colorModifier : priorityColorModifiers) { + Color positionTextColor = colorModifier.getPositionTextColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); + if (positionTextColor != null) { + return positionTextColor; + } + } + + if (!inSelection) { + for (PositionColorModifier colorModifier : colorModifiers) { + Color positionTextColor = colorModifier.getPositionTextColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); + if (positionTextColor != null) { + return positionTextColor; + } + } + } + + if (parentColorAssessor != null) { + return parentColorAssessor.getPositionTextColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); + } + + return null; + } + + @Override + public char getPreviewCharacter(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section) { + return parentCharAssessor != null ? parentCharAssessor.getPreviewCharacter(rowDataPosition, byteOnRow, charOnRow, section) : ' '; + } + + @Override + public char getPreviewCursorCharacter(long rowDataPosition, int byteOnRow, int charOnRow, byte[] cursorData, int cursorDataLength, + CodeAreaSection section) { + return parentCharAssessor != null + ? parentCharAssessor.getPreviewCursorCharacter(rowDataPosition, byteOnRow, charOnRow, cursorData, cursorDataLength, section) + : ' '; + } + + @Override + public Optional getParentCharAssessor() { + return Optional.ofNullable(parentCharAssessor); + } + + @Override + public Optional getParentColorAssessor() { + return Optional.ofNullable(parentColorAssessor); + } + + public interface PositionColorModifier { + + Color getPositionBackgroundColor(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section, boolean inSelection); + + Color getPositionTextColor(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section, boolean inSelection); + + void resetColors(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexEditorHeader.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexEditorHeader.java new file mode 100644 index 00000000000..b2221f316c6 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexEditorHeader.java @@ -0,0 +1,252 @@ +package jadx.gui.ui.hexviewer; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.RenderingHints; + +import javax.swing.JComponent; +import javax.swing.UIManager; + +import org.exbin.bined.CodeAreaCaretListener; +import org.exbin.bined.CodeAreaSection; +import org.exbin.bined.DataChangedListener; +import org.exbin.bined.SelectionChangedListener; +import org.exbin.bined.SelectionRange; +import org.exbin.bined.basic.BasicCodeAreaSection; +import org.exbin.bined.swing.section.SectCodeArea; + +public class HexEditorHeader extends JComponent { + private static final long serialVersionUID = 1L; + private static final String HEX_ALPHABET = "0123456789ABCDEF"; + private final SectCodeArea parent; + private final DataChangedListener dataChangedListener = this::repaint; + private final CodeAreaCaretListener caretMovedListener = caretPosition -> repaint(); + private final SelectionChangedListener selectionChangedListener = this::repaint; + + private Dimension minimumSize = null; + private Dimension preferredSize = null; + + public HexEditorHeader(SectCodeArea parent) { + this.parent = parent; + if (this.parent != null) { + this.parent.addCaretMovedListener(caretMovedListener); + this.parent.addSelectionChangedListener(selectionChangedListener); + this.parent.addDataChangedListener(dataChangedListener); + } + } + + @Override + public void addNotify() { + super.addNotify(); + if (this.parent != null) { + this.parent.addCaretMovedListener(caretMovedListener); + this.parent.addSelectionChangedListener(selectionChangedListener); + this.parent.addDataChangedListener(dataChangedListener); + } + } + + @Override + public void removeNotify() { + if (this.parent != null) { + this.parent.removeCaretMovedListener(caretMovedListener); + this.parent.removeSelectionChangedListener(selectionChangedListener); + this.parent.removeDataChangedListener(dataChangedListener); + } + super.removeNotify(); + } + + @Override + public Dimension getMinimumSize() { + if (minimumSize != null) { + return minimumSize; + } + Insets i = getInsets(); + FontMetrics fm = getFontMetrics(parent.getFont()); + if (fm == null) { + return new Dimension(100, 20); // Fallback + } + int ch = fm.getHeight() + 2; // Row height + int cw = fm.stringWidth(HEX_ALPHABET) / 16; // Char width estimate + + String sampleText = "Sel: 00000000:00000000 Len: 00000000/00000000 TXT UTF-8"; + int minTextWidth = fm.stringWidth(sampleText); + int minimumWidth = minTextWidth + cw * 5 + i.left + i.right; + + int minimumHeight = ch + 5 + i.top + i.bottom; + + return new Dimension(minimumWidth, minimumHeight); + } + + @Override + public void setMinimumSize(Dimension minimumSize) { + this.minimumSize = minimumSize; + revalidate(); + } + + @Override + public Dimension getPreferredSize() { + if (preferredSize != null) { + return preferredSize; + } + Insets i = getInsets(); + FontMetrics fm = getFontMetrics(parent.getFont()); + if (fm == null) { + return getMinimumSize(); // Fallback + } + int ch = fm.getHeight() + 2; + int cw = fm.stringWidth(HEX_ALPHABET) / 16; + + String sampleText = "Sel: 00000000:00000000 Len: 00000000/00000000 TXT UTF-8"; + int preferredTextWidth = fm.stringWidth(sampleText); + int preferredWidth = preferredTextWidth + cw * 10 + i.left + i.right; + int preferredHeight = ch + 5 + i.top + i.bottom; + + return new Dimension(preferredWidth, preferredHeight); + } + + @Override + public void setPreferredSize(Dimension preferredSize) { + this.preferredSize = preferredSize; + revalidate(); + } + + @Override + protected void paintComponent(Graphics g) { + // Standard Graphics2D setup + if (g instanceof Graphics2D) { + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint( + RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + } + + // Get insets, width, height + Insets i = getInsets(); + int fw = getWidth(); + int fh = getHeight(); + int w = fw - i.left - i.right; + int h = fh - i.top - i.bottom; + + // Get font metrics + g.setFont(parent.getFont()); + FontMetrics fm = g.getFontMetrics(); + if (fm == null) { + return; // Cannot paint without font metrics + } + int ca = fm.getAscent() + 1; // Character ascent for baseline + int ch = fm.getHeight() + 2; // Row height including padding + int cw = fm.stringWidth(HEX_ALPHABET) / 16; // Character width estimate + if (cw <= 0) { + cw = 1; // Avoid division by zero or incorrect calculations + } + + // Get colors and status from parent editor + Color separatorForeground = UIManager.getColor("Separator.foreground"); + Color themeBackground = UIManager.getColor("Panel.background"); + Color themeForeground = UIManager.getColor("Panel.foreground"); + + SelectionRange selectionRange = parent.getSelection(); + long ss = selectionRange.getStart(); + long se = selectionRange.getEnd(); + long sl = selectionRange.getLength(); + long length = parent.getDataSize(); + + // Vertical position for the text baseline (centered vertically) + int ty = i.top + ((h - ch) / 2) + ca; // Calculate middle Y for the single line of text + + // Draw Background + g.setColor(themeBackground); + g.fillRect(i.left, i.top, w, h); + + // Draw Text Elements (Sel, Len, Status) + g.setColor(themeForeground); + + // Start drawing position (adjust for left inset) + int currentX = i.left + cw / 2; // Start with a small left padding + + // Selection Range (Sel: start:end + String selLabel = "Sel:"; + g.drawString(selLabel, currentX, ty); + currentX += fm.stringWidth(selLabel) + cw; // "Sel:" + space + + String sss = addressString(ss); + g.drawString(sss, currentX, ty); + currentX += fm.stringWidth(sss); // start + + String separator1 = ":"; + g.drawString(separator1, currentX, ty); + currentX += fm.stringWidth(separator1); // : + + String ses = addressString(se); + g.drawString(ses, currentX, ty); + currentX += fm.stringWidth(ses) + cw; // end + larger space + + // Draw Divider After Sel Range + int dividerTopY = i.top; + int dividerHeight = h; + g.setColor(separatorForeground); + g.fillRect(currentX, dividerTopY, 1, dividerHeight); + currentX += cw; // Add larger space after the divider + + // Length Information (Len: selected/total- + g.setColor(themeForeground); + String lenLabel = "Len:"; + g.drawString(lenLabel, currentX, ty); + currentX += fm.stringWidth(lenLabel) + cw; // "Len:" + space + + String sls = addressString(sl); + g.drawString(sls, currentX, ty); + currentX += fm.stringWidth(sls); // selected length + + String separator2 = "/"; + g.drawString(separator2, currentX, ty); + currentX += fm.stringWidth(separator2); + + String ls = addressString(length); + g.drawString(ls, currentX, ty); + currentX += fm.stringWidth(ls) + cw; // total length + larger space + + // Draw Divider After Len Info + g.setColor(separatorForeground); + g.fillRect(currentX, dividerTopY, 1, dividerHeight); + currentX += cw; // Add larger space after the divider + + // Status Indicators (TXT/HEX, RO/RW, OVR/INS, LE/BE, Charset) + g.setColor(themeForeground); // Ensure color is set for text + + // Draw TXT/HEX status + String statusTxtHex = "HEX"; + CodeAreaSection section = parent.getActiveSection(); + if (section == BasicCodeAreaSection.TEXT_PREVIEW) { + statusTxtHex = "TXT"; + } + + g.drawString(statusTxtHex, currentX, ty); + currentX += fm.stringWidth(statusTxtHex) + cw; // TXT/HEX + + // Draw Divider After TXT/HEX + g.setColor(separatorForeground); + g.fillRect(currentX, dividerTopY, 1, dividerHeight); + currentX += cw; // Add larger space after the divider + + g.setColor(themeForeground); // Restore text color + + // Draw Charset + String statusCharset = parent.getCharset().name(); + g.drawString(statusCharset, currentX, ty); + // No divider or space needed after the last element + + // Draw Bottom Border + g.setColor(separatorForeground); // Use headerDivider color for the border + g.fillRect(i.left, i.top + h - 1, w, 1); + + } + + public String addressString(long address) { + return String.format("%08X", address); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexConfigurationPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexInspectorPanel.java similarity index 92% rename from jadx-gui/src/main/java/jadx/gui/ui/codearea/HexConfigurationPanel.java rename to jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexInspectorPanel.java index b64b561c0dd..936d6745807 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexConfigurationPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexInspectorPanel.java @@ -1,4 +1,4 @@ -package jadx.gui.ui.codearea; +package jadx.gui.ui.hexviewer; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -16,19 +16,15 @@ import org.apache.commons.lang3.ArrayUtils; -public class HexConfigurationPanel extends JPanel { +public class HexInspectorPanel extends JPanel { private final List formatters = new ArrayList<>(); - private final HexAreaConfiguration config; - private byte[] bytes = null; private Integer offset = null; - + private boolean isLittleEndian; private int row = 0; - public HexConfigurationPanel(HexAreaConfiguration configuration) { - this.config = configuration; - + public HexInspectorPanel() { setLayout(new GridBagLayout()); addValueFormat("Signed 8 bit", 1, b -> Integer.toString(b.get())); addValueFormat("Unsigned 8 bit", 1, b -> Integer.toString(b.get() & 0xFF)); @@ -49,7 +45,7 @@ public HexConfigurationPanel(HexAreaConfiguration configuration) { constraints.gridwidth = 2; JCheckBox littleEndianCheckBox = new JCheckBox("Little endian", false); littleEndianCheckBox.addItemListener(ev -> { - config.littleEndian = ev.getStateChange() == ItemEvent.SELECTED; + isLittleEndian = ev.getStateChange() == ItemEvent.SELECTED; reloadOffset(); }); add(littleEndianCheckBox, constraints); @@ -115,7 +111,7 @@ private boolean canDisplay(int offset, int size) { private ByteBuffer decodeByteArray(int offset, int size) { byte[] chunk = sliceBytes(offset, size); - if (config.littleEndian) { + if (isLittleEndian) { ArrayUtils.reverse(chunk); } return ByteBuffer.wrap(chunk); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexPreviewPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexPreviewPanel.java new file mode 100644 index 00000000000..50075b9c470 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexPreviewPanel.java @@ -0,0 +1,307 @@ +package jadx.gui.ui.hexviewer; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import org.exbin.auxiliary.binary_data.BinaryData; +import org.exbin.auxiliary.binary_data.array.ByteArrayEditableData; +import org.exbin.bined.CodeAreaCaretListener; +import org.exbin.bined.CodeAreaCaretPosition; +import org.exbin.bined.CodeAreaUtils; +import org.exbin.bined.CodeCharactersCase; +import org.exbin.bined.CodeType; +import org.exbin.bined.EditMode; +import org.exbin.bined.SelectionRange; +import org.exbin.bined.basic.BasicCodeAreaZone; +import org.exbin.bined.color.CodeAreaBasicColors; +import org.exbin.bined.highlight.swing.color.CodeAreaMatchColorType; +import org.exbin.bined.swing.CodeAreaPainter; +import org.exbin.bined.swing.basic.DefaultCodeAreaCommandHandler; +import org.exbin.bined.swing.capability.CharAssessorPainterCapable; +import org.exbin.bined.swing.capability.ColorAssessorPainterCapable; +import org.exbin.bined.swing.section.SectCodeArea; +import org.exbin.bined.swing.section.color.SectionCodeAreaColorProfile; + +import jadx.gui.settings.JadxSettings; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; + +public class HexPreviewPanel extends JPanel { + private static final long serialVersionUID = 1L; + private static final int CACHE_SIZE = 250; + private final byte[] valuesCache = new byte[CACHE_SIZE]; + private SectCodeArea hexCodeArea; + private SectionCodeAreaColorProfile defaultColors; + private HexEditorHeader header; + private HexSearchBar searchBar; + private HexInspectorPanel inspector; + + private JPopupMenu popupMenu; + private JMenuItem cutAction; + private JMenuItem copyAction; + private JMenuItem copyHexAction; + private JMenuItem copyStringAction; + private JMenuItem pasteAction; + private JMenuItem deleteAction; + private JMenuItem selectAllAction; + private JMenuItem copyOffsetItem; + private BasicCodeAreaZone popupMenuPositionZone = BasicCodeAreaZone.UNKNOWN; + + public HexPreviewPanel(JadxSettings settings) { + this.hexCodeArea = new SectCodeArea(); + this.hexCodeArea.setCodeFont(settings.getFont()); + this.hexCodeArea.setEditMode(EditMode.READ_ONLY); + this.hexCodeArea.setCharset(StandardCharsets.UTF_8); + this.hexCodeArea.setComponentPopupMenu(new JPopupMenu() { + @Override + public void show(Component invoker, int x, int y) { + popupMenuPositionZone = hexCodeArea.getPainter().getPositionZone(x, y); + createPopupMenu(); + if (popupMenu != null && popupMenuPositionZone != BasicCodeAreaZone.HEADER + && popupMenuPositionZone != BasicCodeAreaZone.ROW_POSITIONS) { + updatePopupActionStates(); + popupMenu.show(invoker, x, y); + } + } + }); + + this.inspector = new HexInspectorPanel(); + this.searchBar = new HexSearchBar(hexCodeArea); + this.header = new HexEditorHeader(hexCodeArea); + this.header.setFont(settings.getFont()); + + CodeAreaPainter painter = hexCodeArea.getPainter(); + defaultColors = (SectionCodeAreaColorProfile) hexCodeArea.getColorsProfile(); + + hexCodeArea.setColorsProfile(getColorsProfile()); + + BinEdCodeAreaAssessor codeAreaAssessor = new BinEdCodeAreaAssessor(((ColorAssessorPainterCapable) painter).getColorAssessor(), + ((CharAssessorPainterCapable) painter).getCharAssessor()); + ((ColorAssessorPainterCapable) painter).setColorAssessor(codeAreaAssessor); + ((CharAssessorPainterCapable) painter).setCharAssessor(codeAreaAssessor); + + setLayout(new BorderLayout()); + add(searchBar, BorderLayout.PAGE_START); + add(hexCodeArea, BorderLayout.CENTER); + add(header, BorderLayout.PAGE_END); + add(inspector, BorderLayout.EAST); + + setFocusable(true); + addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + hexCodeArea.requestFocusInWindow(); + } + + @Override + public void focusLost(FocusEvent e) { + + } + }); + createActions(); + enableUpdate(); + } + + public SectionCodeAreaColorProfile getColorsProfile() { + boolean isDarkTheme = UiUtils.isDarkTheme(Objects.requireNonNull(defaultColors.getColor(CodeAreaBasicColors.TEXT_BACKGROUND))); + Color markAllHighlightColor = isDarkTheme ? Color.decode("#32593D") : Color.decode("#ffc800"); + Color editorSelectionBackground = defaultColors.getColor(CodeAreaBasicColors.SELECTION_BACKGROUND); + Color currentMatchColor = UiUtils.adjustBrightness(editorSelectionBackground, isDarkTheme ? 0.6f : 1.4f); + defaultColors.setColor(CodeAreaMatchColorType.MATCH_BACKGROUND, markAllHighlightColor); + defaultColors.setColor(CodeAreaMatchColorType.CURRENT_MATCH_BACKGROUND, currentMatchColor); + return defaultColors; + } + + public void setData(byte[] data) { + if (hexCodeArea != null && data != null) { + hexCodeArea.setContentData(new ByteArrayEditableData(data)); + inspector.setBytes(data); + } + } + + public void enableUpdate() { + CodeAreaCaretListener caretMovedListener = (CodeAreaCaretPosition caretPosition) -> updateValues(); + hexCodeArea.addCaretMovedListener(caretMovedListener); + } + + private void updateValues() { + CodeAreaCaretPosition caretPosition = hexCodeArea.getActiveCaretPosition(); + long dataPosition = caretPosition.getDataPosition(); + long dataSize = hexCodeArea.getDataSize(); + + if (dataPosition < dataSize) { + int availableData = dataSize - dataPosition >= CACHE_SIZE ? CACHE_SIZE : (int) (dataSize - dataPosition); + BinaryData contentData = hexCodeArea.getContentData(); + contentData.copyToArray(dataPosition, valuesCache, 0, availableData); + if (availableData < CACHE_SIZE) { + Arrays.fill(valuesCache, availableData, CACHE_SIZE, (byte) 0); + } + } + + inspector.setOffset((int) dataPosition); + } + + private void createActions() { + cutAction = new JMenuItem(NLS.str("popup.cut")); + cutAction.addActionListener(e -> performCut()); + + copyAction = new JMenuItem(NLS.str("popup.copy")); + copyAction.addActionListener(e -> performCopy()); + + copyHexAction = new JMenuItem(NLS.str("popup.copy_as_hex")); + copyHexAction.addActionListener(e -> performCopyAsCode()); + + copyStringAction = new JMenuItem(NLS.str("popup.copy_as_string")); + copyStringAction.addActionListener(e -> performCopy()); + + pasteAction = new JMenuItem(NLS.str("popup.paste")); + pasteAction.addActionListener(e -> performPaste()); + + deleteAction = new JMenuItem(NLS.str("popup.delete")); + deleteAction.addActionListener(e -> { + if (!isEditable()) { + performDelete(); + } + }); + + selectAllAction = new JMenuItem(NLS.str("popup.select_all")); + selectAllAction.addActionListener(e -> performSelectAll()); + + copyOffsetItem = new JMenuItem(NLS.str("popup.copy_offset")); + copyOffsetItem.addActionListener(e -> copyOffset()); + } + + private void createPopupMenu() { + boolean isEditable = isEditable(); + popupMenu = new JPopupMenu(); + popupMenu.add(copyAction); + + if (isEditable) { + popupMenu.add(cutAction); + popupMenu.add(pasteAction); + popupMenu.add(deleteAction); + popupMenu.addSeparator(); + } + + JMenu copyMenu = new JMenu(NLS.str("popup.copy_as")); + copyMenu.add(copyHexAction); + copyMenu.add(copyStringAction); + popupMenu.add(copyMenu); + popupMenu.add(copyOffsetItem); + popupMenu.add(selectAllAction); + } + + private void updatePopupActionStates() { + + boolean selectionExists = isSelection(); + boolean isEditable = !isEditable(); + + cutAction.setEnabled(isEditable && selectionExists); + copyAction.setEnabled(selectionExists); + copyHexAction.setEnabled(selectionExists); + copyStringAction.setEnabled(selectionExists); + deleteAction.setEnabled(isEditable && selectionExists); + + selectAllAction.setEnabled(hexCodeArea.getDataSize() > 0); + } + + public SectCodeArea getEditor() { + return this.hexCodeArea; + } + + public HexEditorHeader getHeader() { + return this.header; + } + + public HexInspectorPanel getInspector() { + return this.inspector; + } + + public HexSearchBar getSearchBar() { + return this.searchBar; + } + + public void showSearchBar() { + searchBar.showAndFocus(); + } + + public void performCut() { + hexCodeArea.cut(); + } + + public void performCopy() { + hexCodeArea.copy(); + } + + public void performCopyAsCode() { + ((DefaultCodeAreaCommandHandler) hexCodeArea.getCommandHandler()).copyAsCode(); + } + + public void performPaste() { + hexCodeArea.paste(); + } + + public void performDelete() { + hexCodeArea.delete(); + } + + public void performSelectAll() { + hexCodeArea.selectAll(); + } + + public boolean isSelection() { + return hexCodeArea.hasSelection(); + } + + public boolean isEditable() { + return hexCodeArea.isEditable(); + } + + public boolean canPaste() { + return hexCodeArea.canPaste(); + } + + public static String getSelectionData(SectCodeArea core) { + SelectionRange selection = core.getSelection(); + if (!selection.isEmpty()) { + long first = selection.getFirst(); + long last = selection.getLast(); + + BinaryData copy = core.getContentData().copy(first, last - first + 1); + + CodeType codeType = core.getCodeType(); + CodeCharactersCase charactersCase = core.getCodeCharactersCase(); + + int charsPerByte = codeType.getMaxDigitsForByte() + 1; + int textLength = (int) (copy.getDataSize() * charsPerByte); + if (textLength > 0) { + textLength--; + } + + char[] targetData = new char[textLength]; + Arrays.fill(targetData, ' '); + for (int i = 0; i < (int) copy.getDataSize(); i++) { + CodeAreaUtils.byteToCharsCode(copy.getByte(i), codeType, targetData, i * charsPerByte, charactersCase); + } + return new String(targetData); + } + return null; + } + + public void copyOffset() { + String str = header.addressString(hexCodeArea.getSelection().getStart()); + UiUtils.copyToClipboard(str); + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexSearchBar.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexSearchBar.java new file mode 100644 index 00000000000..2320b1e49cb --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexSearchBar.java @@ -0,0 +1,339 @@ +package jadx.gui.ui.hexviewer; + +import java.awt.Color; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.border.EmptyBorder; + +import org.exbin.auxiliary.binary_data.array.ByteArrayEditableData; +import org.exbin.bined.CodeAreaUtils; +import org.exbin.bined.swing.section.SectCodeArea; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.utils.StringUtils; +import jadx.gui.ui.hexviewer.search.BinarySearch; +import jadx.gui.ui.hexviewer.search.SearchCondition; +import jadx.gui.ui.hexviewer.search.SearchParameters; +import jadx.gui.ui.hexviewer.search.service.BinarySearchServiceImpl; +import jadx.gui.utils.NLS; +import jadx.gui.utils.TextStandardActions; +import jadx.gui.utils.UiUtils; + +public class HexSearchBar extends JToolBar { + private static final long serialVersionUID = 1836871286618633003L; + + private static final Logger LOG = LoggerFactory.getLogger(HexSearchBar.class); + private static final Icon ICON_MARK = UiUtils.openSvgIcon("search/mark"); + private static final Icon ICON_MARK_SELECTED = UiUtils.openSvgIcon("search/previewSelected"); + private static final Icon ICON_FIND_TYPE_TXT = UiUtils.openSvgIcon("search/text"); + private static final Icon ICON_FIND_TYPE_HEX = UiUtils.openSvgIcon("search/hexSerial"); + private static final Icon ICON_MATCH = UiUtils.openSvgIcon("search/matchCaseHovered"); + private static final Icon ICON_MATCH_SELECTED = UiUtils.openSvgIcon("search/matchCaseSelected"); + private static final Icon ICON_UP = UiUtils.openSvgIcon("ui/top"); + private static final Icon ICON_DOWN = UiUtils.openSvgIcon("ui/bottom"); + private static final Icon ICON_CLOSE = UiUtils.openSvgIcon("ui/close"); + + private final SectCodeArea hexCodeArea; + + private final JTextField searchField; + private final JLabel resultCountLabel; + private final JToggleButton markAllCB; + private final JToggleButton findTypeCB; + private final JToggleButton matchCaseCB; + private final JButton nextMatchButton; + private final JButton prevMatchButton; + + private Control control = null; + + public HexSearchBar(SectCodeArea textArea) { + hexCodeArea = textArea; + + JLabel findLabel = new JLabel(NLS.str("search.find") + ':'); + add(findLabel); + + searchField = new JTextField(30); + searchField.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_ENTER: + // skip + break; + case KeyEvent.VK_ESCAPE: + toggle(); + break; + default: + control.performFind(); + break; + } + } + }); + searchField.addActionListener(e -> control.notifySearchChanging()); + new TextStandardActions(searchField); + add(searchField); + + ActionListener searchSettingListener = e -> control.notifySearchChanged(); + + resultCountLabel = new JLabel(); + resultCountLabel.setBorder(new EmptyBorder(0, 10, 0, 10)); + resultCountLabel.setForeground(Color.GRAY); + add(resultCountLabel); + + matchCaseCB = new JToggleButton(); + matchCaseCB.setIcon(ICON_MATCH); + matchCaseCB.setSelectedIcon(ICON_MATCH_SELECTED); + matchCaseCB.setToolTipText(NLS.str("search.match_case")); + matchCaseCB.addActionListener(searchSettingListener); + add(matchCaseCB); + + findTypeCB = new JToggleButton(); + findTypeCB.setIcon(ICON_FIND_TYPE_TXT); + findTypeCB.setSelectedIcon(ICON_FIND_TYPE_HEX); + if (findTypeCB.isSelected()) { + findTypeCB.setToolTipText(NLS.str("search.find_type_hex")); + } else { + findTypeCB.setToolTipText(NLS.str("search.find_type_text")); + } + findTypeCB.addActionListener(e -> { + searchField.setText(""); + updateFindStatus(); + control.notifySearchChanged(); + }); + add(findTypeCB); + + prevMatchButton = new JButton(); + prevMatchButton.setIcon(ICON_UP); + prevMatchButton.setToolTipText(NLS.str("search.previous")); + prevMatchButton.addActionListener(e -> control.prevMatch()); + prevMatchButton.setBorderPainted(false); + add(prevMatchButton); + + nextMatchButton = new JButton(); + nextMatchButton.setIcon(ICON_DOWN); + nextMatchButton.setToolTipText(NLS.str("search.next")); + nextMatchButton.addActionListener(e -> control.nextMatch()); + nextMatchButton.setBorderPainted(false); + add(nextMatchButton); + + markAllCB = new JToggleButton(); + markAllCB.setIcon(ICON_MARK); + markAllCB.setSelectedIcon(ICON_MARK_SELECTED); + markAllCB.setToolTipText(NLS.str("search.mark_all")); + markAllCB.setSelected(true); + markAllCB.addActionListener(searchSettingListener); + add(markAllCB); + + JButton closeButton = new JButton(); + closeButton.setIcon(ICON_CLOSE); + closeButton.addActionListener(e -> toggle()); + closeButton.setBorderPainted(false); + add(closeButton); + + BinarySearch binarySearch = new BinarySearch(this); + binarySearch.setBinarySearchService(new BinarySearchServiceImpl(hexCodeArea)); + setFloatable(false); + setVisible(false); + + } + + /* + * Replicates IntelliJ's search bar behavior + * 1.1. If the user has selected text, use that as the search text + * 1.2. Otherwise, use the previous search text (or empty if none) + * 2. Select all text in the search bar and give it focus + */ + public void showAndFocus() { + setVisible(true); + + if (hexCodeArea.hasSelection()) { + searchField.setText(hexCodeArea.getActiveSection().toString()); + } + String selectedText = HexPreviewPanel.getSelectionData(hexCodeArea); + if (!StringUtils.isEmpty(selectedText)) { + searchField.setText(selectedText); + makeFindByHexButton(); + } + + searchField.selectAll(); + searchField.requestFocus(); + } + + public void toggle() { + boolean visible = !isVisible(); + setVisible(visible); + + if (visible) { + String preferText = HexPreviewPanel.getSelectionData(hexCodeArea); + if (!StringUtils.isEmpty(preferText)) { + searchField.setText(preferText); + makeFindByHexButton(); + } + searchField.selectAll(); + searchField.requestFocus(); + } else { + control.performEscape(); + hexCodeArea.requestFocus(); + } + } + + public void setInfoLabel(String text) { + resultCountLabel.setText(text); + } + + public void updateMatchCount(boolean hasMatches, boolean prevMatchAvailable, boolean nextMatchAvailable) { + prevMatchButton.setEnabled(prevMatchAvailable); + nextMatchButton.setEnabled(nextMatchAvailable); + } + + public void setControl(Control control) { + this.control = control; + } + + public void clearSearch() { + setInfoLabel(""); + searchField.setText(""); + } + + public SearchParameters getSearchParameters() { + SearchParameters searchParameters = new SearchParameters(); + searchParameters.setMatchCase(matchCaseCB.isSelected()); + searchParameters.setMatchMode(SearchParameters.MatchMode.fromBoolean(markAllCB.isSelected())); + SearchParameters.SearchDirection searchDirection = control.getSearchDirection(); + searchParameters.setSearchDirection(searchDirection); + + long startPosition; + if (searchParameters.isSearchFromCursor()) { + startPosition = hexCodeArea.getActiveCaretPosition().getDataPosition(); + } else { + switch (searchDirection) { + case FORWARD: { + startPosition = 0; + break; + } + case BACKWARD: { + startPosition = hexCodeArea.getDataSize() - 1; + break; + } + default: + throw CodeAreaUtils.getInvalidTypeException(searchDirection); + } + } + searchParameters.setStartPosition(startPosition); + + searchParameters.setCondition(new SearchCondition(makeSearchCondition())); + return searchParameters; + } + + private SearchCondition makeSearchCondition() { + SearchCondition condition = new SearchCondition(); + if (findTypeCB.isSelected()) { + condition.setSearchMode(SearchCondition.SearchMode.BINARY); + } else { + condition.setSearchMode(SearchCondition.SearchMode.TEXT); + } + if (!StringUtils.isEmpty(searchField.getText())) { + if (condition.getSearchMode() == SearchCondition.SearchMode.TEXT) { + condition.setSearchText(searchField.getText()); + } else { + String hexBytes = searchField.getText(); + if (isValidHexString(hexBytes)) { + condition.setBinaryData(new ByteArrayEditableData(hexStringToByteArray(hexBytes))); + } + } + } + return condition; + } + + public void updateFindStatus() { + SearchCondition condition = makeSearchCondition(); + if (condition.getSearchMode() == SearchCondition.SearchMode.TEXT) { + findTypeCB.setSelected(false); + findTypeCB.setToolTipText(NLS.str("search.find_type_text")); + matchCaseCB.setEnabled(true); + } else { + makeFindByHexButton(); + matchCaseCB.setEnabled(false); + } + } + + private void makeFindByHexButton() { + findTypeCB.setSelected(true); + findTypeCB.setToolTipText(NLS.str("search.find_type_hex")); + } + + private boolean isValidHexString(String hexString) { + String cleanS = hexString.replace(" ", ""); + int len = cleanS.length(); + try { + boolean isPair = len % 2 == 0; + if (isPair) { + Long.parseLong(cleanS, 16); + return true; + } + } catch (NumberFormatException ex) { + // ignore error + return false; + } + return false; + } + + public byte[] hexStringToByteArray(String hexString) { + if (hexString == null || hexString.isEmpty()) { + return new byte[0]; + } + String cleanS = hexString.replace(" ", ""); + int len = cleanS.length(); + if (!isValidHexString(hexString)) { + throw new IllegalArgumentException("Hex string must have even length. Input length: " + len); + } + + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + String byteString = cleanS.substring(i, i + 2); + try { + int intValue = Integer.parseInt(byteString, 16); + data[i / 2] = (byte) intValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Input string contains non-hex characters at index " + i + ": " + byteString, e); + } + } + return data; + } + + public interface Control { + + void prevMatch(); + + void nextMatch(); + + void performEscape(); + + void performFind(); + + /** + * Parameters of search have changed. + */ + void notifySearchChanged(); + + /** + * Parameters of search are changing which might not lead to immediate + * search change. + *

+ * Typically, text typing. + */ + void notifySearchChanging(); + + SearchParameters.SearchDirection getSearchDirection(); + + void close(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/BinarySearch.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/BinarySearch.java new file mode 100644 index 00000000000..27d76b4515f --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/BinarySearch.java @@ -0,0 +1,286 @@ +package jadx.gui.ui.hexviewer.search; + +/* + * Copyright (C) ExBin Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.exbin.auxiliary.binary_data.EditableBinaryData; +import org.exbin.auxiliary.binary_data.array.ByteArrayEditableData; + +import jadx.gui.ui.hexviewer.HexSearchBar; +import jadx.gui.ui.hexviewer.search.service.BinarySearchService; +import jadx.gui.utils.NLS; + +/** + * Binary search. + * + * @author ExBin Project (https://exbin.org) + */ +public class BinarySearch { + + private static final int DEFAULT_DELAY = 500; + + private InvokeSearchThread invokeSearchThread; + private SearchThread searchThread; + + private SearchOperation currentSearchOperation = SearchOperation.FIND; + private SearchParameters.SearchDirection currentSearchDirection = SearchParameters.SearchDirection.FORWARD; + private final SearchParameters currentSearchParameters = new SearchParameters(); + private BinarySearchService.FoundMatches foundMatches = new BinarySearchService.FoundMatches(); + + private BinarySearchService binarySearchService; + private final BinarySearchService.SearchStatusListener searchStatusListener; + private HexSearchBar binarySearchPanel; + + public BinarySearch(HexSearchBar binarySearchPanel) { + this.binarySearchPanel = binarySearchPanel; + + searchStatusListener = new BinarySearchService.SearchStatusListener() { + @Override + public void setStatus(BinarySearchService.FoundMatches foundMatches, SearchParameters.MatchMode matchMode) { + BinarySearch.this.foundMatches = foundMatches; + switch (foundMatches.getMatchesCount()) { + case 0: + binarySearchPanel.setInfoLabel(NLS.str("search.match_not_found")); + break; + case 1: + binarySearchPanel.setInfoLabel( + matchMode == SearchParameters.MatchMode.MULTIPLE + ? NLS.str("search.single_match") + : NLS.str("search.match_found")); + break; + default: + binarySearchPanel.setInfoLabel(String.format(NLS.str("search.match_of"), + foundMatches.getMatchPosition() + 1, foundMatches.getMatchesCount())); + break; + } + updateMatchStatus(); + } + + @Override + public void clearStatus() { + binarySearchPanel.setInfoLabel(""); + BinarySearch.this.foundMatches = new BinarySearchService.FoundMatches(); + updateMatchStatus(); + } + + private void updateMatchStatus() { + int matchesCount = foundMatches.getMatchesCount(); + int matchPosition = foundMatches.getMatchPosition(); + binarySearchPanel.updateMatchCount(matchesCount > 0, + matchesCount > 1 && matchPosition > 0, + matchPosition < matchesCount - 1); + } + }; + binarySearchPanel.setControl(new HexSearchBar.Control() { + @Override + public void prevMatch() { + foundMatches.prev(); + binarySearchService.setMatchPosition(foundMatches.getMatchPosition()); + searchStatusListener.setStatus(foundMatches, binarySearchService.getLastSearchParameters().getMatchMode()); + } + + @Override + public void nextMatch() { + foundMatches.next(); + binarySearchService.setMatchPosition(foundMatches.getMatchPosition()); + searchStatusListener.setStatus(foundMatches, binarySearchService.getLastSearchParameters().getMatchMode()); + } + + @Override + public void performEscape() { + cancelSearch(); + close(); + clearSearch(); + } + + @Override + public void performFind() { + invokeSearch(SearchOperation.FIND); + } + + @Override + public void notifySearchChanged() { + if (currentSearchOperation == SearchOperation.FIND) { + invokeSearch(SearchOperation.FIND); + } + } + + @Override + public void notifySearchChanging() { + if (currentSearchOperation != SearchOperation.FIND) { + return; + } + + SearchCondition condition = currentSearchParameters.getCondition(); + SearchCondition updatedSearchCondition = binarySearchPanel.getSearchParameters().getCondition(); + + switch (updatedSearchCondition.getSearchMode()) { + case TEXT: { + String searchText = updatedSearchCondition.getSearchText(); + if (searchText.isEmpty()) { + condition.setSearchText(searchText); + clearSearch(); + return; + } + + if (searchText.equals(condition.getSearchText())) { + return; + } + + condition.setSearchText(searchText); + break; + } + case BINARY: { + EditableBinaryData searchData = (EditableBinaryData) updatedSearchCondition.getBinaryData(); + if (searchData == null || searchData.isEmpty()) { + condition.setBinaryData(null); + clearSearch(); + return; + } + + if (searchData.equals(condition.getBinaryData())) { + return; + } + + ByteArrayEditableData data = new ByteArrayEditableData(); + data.insert(0, searchData); + condition.setBinaryData(data); + break; + } + } + BinarySearch.this.invokeSearch(SearchOperation.FIND, DEFAULT_DELAY); + } + + @Override + public SearchParameters.SearchDirection getSearchDirection() { + return currentSearchDirection; + } + + @Override + public void close() { + cancelSearch(); + clearSearch(); + } + }); + } + + public void setBinarySearchService(BinarySearchService binarySearchService) { + this.binarySearchService = binarySearchService; + } + + public void setTargetComponent(HexSearchBar targetComponent) { + binarySearchPanel = targetComponent; + } + + public BinarySearchService.SearchStatusListener getSearchStatusListener() { + return searchStatusListener; + } + + private void invokeSearch(SearchOperation searchOperation) { + invokeSearch(searchOperation, binarySearchPanel.getSearchParameters(), 0); + } + + private void invokeSearch(SearchOperation searchOperation, final int delay) { + invokeSearch(searchOperation, binarySearchPanel.getSearchParameters(), delay); + } + + private void invokeSearch(SearchOperation searchOperation, SearchParameters searchParameters) { + invokeSearch(searchOperation, searchParameters, 0); + } + + private void invokeSearch(SearchOperation searchOperation, SearchParameters searchParameters, final int delay) { + if (invokeSearchThread != null) { + invokeSearchThread.interrupt(); + } + invokeSearchThread = new InvokeSearchThread(); + invokeSearchThread.delay = delay; + currentSearchOperation = searchOperation; + currentSearchParameters.setFromParameters(searchParameters); + invokeSearchThread.start(); + } + + public void cancelSearch() { + if (invokeSearchThread != null) { + invokeSearchThread.interrupt(); + } + if (searchThread != null) { + searchThread.interrupt(); + } + } + + public void clearSearch() { + SearchCondition condition = currentSearchParameters.getCondition(); + condition.clear(); + binarySearchPanel.clearSearch(); + binarySearchService.clearMatches(); + searchStatusListener.clearStatus(); + } + + public HexSearchBar getPanel() { + return binarySearchPanel; + } + + public void dataChanged() { + binarySearchService.clearMatches(); + invokeSearch(currentSearchOperation, DEFAULT_DELAY); + } + + private class InvokeSearchThread extends Thread { + + private int delay = DEFAULT_DELAY; + + public InvokeSearchThread() { + super("InvokeSearchThread"); + } + + @Override + public void run() { + try { + Thread.sleep(delay); + if (searchThread != null) { + searchThread.interrupt(); + } + searchThread = new SearchThread(); + searchThread.start(); + } catch (InterruptedException ex) { + // don't search + } + } + } + + private class SearchThread extends Thread { + + public SearchThread() { + super("SearchThread"); + } + + @Override + public void run() { + switch (currentSearchOperation) { + case FIND: + binarySearchService.performFind(currentSearchParameters, searchStatusListener); + break; + case FIND_AGAIN: + binarySearchService.performFindAgain(searchStatusListener); + break; + default: + throw new UnsupportedOperationException("Not supported yet."); + } + } + } + + private enum SearchOperation { + FIND, + FIND_AGAIN + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/SearchCondition.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/SearchCondition.java new file mode 100644 index 00000000000..a523077c24c --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/SearchCondition.java @@ -0,0 +1,114 @@ +package jadx.gui.ui.hexviewer.search; + +import java.util.Objects; + +import org.exbin.auxiliary.binary_data.BinaryData; +import org.exbin.auxiliary.binary_data.EditableBinaryData; +import org.exbin.auxiliary.binary_data.array.ByteArrayEditableData; +import org.exbin.bined.CodeAreaUtils; + +/** + * Parameters for action to search for occurrences of text or data. + * + * @author ExBin Project (https://exbin.org) + */ + +public class SearchCondition { + + private SearchMode searchMode = SearchMode.TEXT; + private String searchText = ""; + private EditableBinaryData binaryData; + + public SearchCondition() { + } + + /** + * This is copy constructor. + * + * @param source source condition + */ + public SearchCondition(SearchCondition source) { + searchMode = source.getSearchMode(); + searchText = source.getSearchText(); + binaryData = new ByteArrayEditableData(); + if (source.getBinaryData() != null) { + binaryData.insert(0, source.getBinaryData()); + } + } + + public SearchMode getSearchMode() { + return searchMode; + } + + public void setSearchMode(SearchMode searchMode) { + this.searchMode = searchMode; + } + + public String getSearchText() { + return searchText; + } + + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public BinaryData getBinaryData() { + return binaryData; + } + + public void setBinaryData(EditableBinaryData binaryData) { + this.binaryData = binaryData; + } + + public boolean isEmpty() { + switch (searchMode) { + case TEXT: { + return searchText == null || searchText.isEmpty(); + } + case BINARY: { + return binaryData == null || binaryData.isEmpty(); + } + default: + throw CodeAreaUtils.getInvalidTypeException(searchMode); + } + } + + @Override + public int hashCode() { + int hash = 3; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SearchCondition other = (SearchCondition) obj; + if (this.searchMode != other.searchMode) { + return false; + } + if (searchMode == SearchMode.TEXT) { + return Objects.equals(this.searchText, other.searchText); + } else { + return Objects.equals(this.binaryData, other.binaryData); + } + } + + public void clear() { + searchText = ""; + if (binaryData != null) { + binaryData.clear(); + } + } + + public enum SearchMode { + TEXT, BINARY + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/SearchParameters.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/SearchParameters.java new file mode 100644 index 00000000000..66a72bc9cec --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/SearchParameters.java @@ -0,0 +1,102 @@ +package jadx.gui.ui.hexviewer.search; + +/* + * Copyright (C) ExBin Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Parameters for action to search for occurrences of text or data. + * + * @author ExBin Project (https://exbin.org) + */ + +public class SearchParameters { + + private SearchCondition condition = new SearchCondition(); + private long startPosition; + private boolean searchFromCursor; + private boolean matchCase = true; + private MatchMode matchMode = MatchMode.MULTIPLE; + private SearchDirection searchDirection = SearchDirection.FORWARD; + + public SearchParameters() { + } + + public SearchCondition getCondition() { + return condition; + } + + public void setCondition(SearchCondition condition) { + this.condition = condition; + } + + public long getStartPosition() { + return startPosition; + } + + public void setStartPosition(long startPosition) { + this.startPosition = startPosition; + } + + public boolean isSearchFromCursor() { + return searchFromCursor; + } + + public void setSearchFromCursor(boolean searchFromCursor) { + this.searchFromCursor = searchFromCursor; + } + + public boolean isMatchCase() { + return matchCase; + } + + public void setMatchCase(boolean matchCase) { + this.matchCase = matchCase; + } + + public MatchMode getMatchMode() { + return matchMode; + } + + public void setMatchMode(MatchMode matchMode) { + this.matchMode = matchMode; + } + + public SearchDirection getSearchDirection() { + return searchDirection; + } + + public void setSearchDirection(SearchDirection searchDirection) { + this.searchDirection = searchDirection; + } + + public void setFromParameters(SearchParameters searchParameters) { + condition = searchParameters.getCondition(); + startPosition = searchParameters.getStartPosition(); + searchFromCursor = searchParameters.isSearchFromCursor(); + matchCase = searchParameters.isMatchCase(); + matchMode = searchParameters.getMatchMode(); + searchDirection = searchParameters.getSearchDirection(); + } + + public enum SearchDirection { + FORWARD, BACKWARD + } + + public enum MatchMode { + SINGLE, MULTIPLE; + + public static MatchMode fromBoolean(boolean multipleMatches) { + return multipleMatches ? MULTIPLE : SINGLE; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/service/BinarySearchService.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/service/BinarySearchService.java new file mode 100644 index 00000000000..50e08c0c4dc --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/service/BinarySearchService.java @@ -0,0 +1,75 @@ +package jadx.gui.ui.hexviewer.search.service; + +import jadx.gui.ui.hexviewer.search.SearchParameters; + +public interface BinarySearchService { + + void performFind(SearchParameters dialogSearchParameters, SearchStatusListener searchStatusListener); + + void setMatchPosition(int matchPosition); + + void performFindAgain(SearchStatusListener searchStatusListener); + + SearchParameters getLastSearchParameters(); + + void clearMatches(); + + public interface SearchStatusListener { + + void setStatus(FoundMatches foundMatches, SearchParameters.MatchMode matchMode); + + void clearStatus(); + } + + public static class FoundMatches { + + private int matchesCount; + private int matchPosition; + + public FoundMatches() { + matchesCount = 0; + matchPosition = -1; + } + + public FoundMatches(int matchesCount, int matchPosition) { + if (matchPosition >= matchesCount) { + throw new IllegalStateException("Match position is out of range"); + } + + this.matchesCount = matchesCount; + this.matchPosition = matchPosition; + } + + public int getMatchesCount() { + return matchesCount; + } + + public int getMatchPosition() { + return matchPosition; + } + + public void setMatchesCount(int matchesCount) { + this.matchesCount = matchesCount; + } + + public void setMatchPosition(int matchPosition) { + this.matchPosition = matchPosition; + } + + public void next() { + if (matchPosition == matchesCount - 1) { + throw new IllegalStateException("Cannot find next on last match"); + } + + matchPosition++; + } + + public void prev() { + if (matchPosition == 0) { + throw new IllegalStateException("Cannot find previous on first match"); + } + + matchPosition--; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/service/BinarySearchServiceImpl.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/service/BinarySearchServiceImpl.java new file mode 100644 index 00000000000..a65c85caab1 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/service/BinarySearchServiceImpl.java @@ -0,0 +1,349 @@ +package jadx.gui.ui.hexviewer.search.service; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.exbin.auxiliary.binary_data.BinaryData; +import org.exbin.bined.CharsetStreamTranslator; +import org.exbin.bined.CodeAreaUtils; +import org.exbin.bined.highlight.swing.SearchCodeAreaColorAssessor; +import org.exbin.bined.highlight.swing.SearchMatch; +import org.exbin.bined.swing.CodeAreaSwingUtils; +import org.exbin.bined.swing.capability.ColorAssessorPainterCapable; +import org.exbin.bined.swing.section.SectCodeArea; + +import jadx.gui.ui.hexviewer.search.SearchCondition; +import jadx.gui.ui.hexviewer.search.SearchParameters; + +/** + * Binary search service. + * + * @author ExBin Project (https://exbin.org) + */ + +public class BinarySearchServiceImpl implements BinarySearchService { + + private static final int MAX_MATCHES_COUNT = 100; + private final SectCodeArea codeArea; + private final SearchParameters lastSearchParameters = new SearchParameters(); + + public BinarySearchServiceImpl(SectCodeArea codeArea) { + this.codeArea = codeArea; + } + + @Override + public void performFind(SearchParameters searchParameters, SearchStatusListener searchStatusListener) { + SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils + .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); + SearchCondition condition = searchParameters.getCondition(); + searchStatusListener.clearStatus(); + if (condition.isEmpty()) { + searchAssessor.clearMatches(); + codeArea.repaint(); + return; + } + + long position; + switch (searchParameters.getSearchDirection()) { + case FORWARD: { + if (searchParameters.isSearchFromCursor()) { + position = codeArea.getActiveCaretPosition().getDataPosition(); + } else { + position = 0; + } + break; + } + case BACKWARD: { + if (searchParameters.isSearchFromCursor()) { + position = codeArea.getActiveCaretPosition().getDataPosition() - 1; + } else { + long searchDataSize; + switch (condition.getSearchMode()) { + case TEXT: { + searchDataSize = condition.getSearchText().length(); + break; + } + case BINARY: { + searchDataSize = condition.getBinaryData().getDataSize(); + break; + } + default: + throw CodeAreaUtils.getInvalidTypeException(condition.getSearchMode()); + } + position = codeArea.getDataSize() - searchDataSize; + } + break; + } + default: + throw CodeAreaUtils.getInvalidTypeException(searchParameters.getSearchDirection()); + } + searchParameters.setStartPosition(position); + + switch (condition.getSearchMode()) { + case TEXT: { + searchForText(searchParameters, searchStatusListener); + break; + } + case BINARY: { + searchForBinaryData(searchParameters, searchStatusListener); + break; + } + default: + throw CodeAreaUtils.getInvalidTypeException(condition.getSearchMode()); + } + } + + /** + * Performs search by binary data. + */ + private void searchForBinaryData(SearchParameters searchParameters, SearchStatusListener searchStatusListener) { + SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils + .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); + SearchCondition condition = searchParameters.getCondition(); + long position = searchParameters.getStartPosition(); + + BinaryData searchData = condition.getBinaryData(); + long searchDataSize = searchData.getDataSize(); + BinaryData data = codeArea.getContentData(); + + List foundMatches = new ArrayList<>(); + + long dataSize = data.getDataSize(); + while (position >= 0 && position <= dataSize - searchDataSize) { + long matchLength = 0; + while (matchLength < searchDataSize) { + if (data.getByte(position + matchLength) != searchData.getByte(matchLength)) { + break; + } + matchLength++; + } + + if (matchLength == searchDataSize) { + SearchMatch match = new SearchMatch(); + match.setPosition(position); + match.setLength(searchDataSize); + if (searchParameters.getSearchDirection() == SearchParameters.SearchDirection.BACKWARD) { + foundMatches.add(0, match); + } else { + foundMatches.add(match); + } + + if (foundMatches.size() == MAX_MATCHES_COUNT || searchParameters.getMatchMode() == SearchParameters.MatchMode.SINGLE) { + break; + } + } + + position++; + } + + searchAssessor.setMatches(foundMatches); + if (!foundMatches.isEmpty()) { + if (searchParameters.getSearchDirection() == SearchParameters.SearchDirection.BACKWARD) { + searchAssessor.setCurrentMatchIndex(foundMatches.size() - 1); + } else { + searchAssessor.setCurrentMatchIndex(0); + } + SearchMatch firstMatch = Objects.requireNonNull(searchAssessor.getCurrentMatch()); + codeArea.revealPosition(firstMatch.getPosition(), 0, codeArea.getActiveSection()); + } + lastSearchParameters.setFromParameters(searchParameters); + searchStatusListener.setStatus( + new FoundMatches(foundMatches.size(), foundMatches.isEmpty() ? -1 : searchAssessor.getCurrentMatchIndex()), + searchParameters.getMatchMode()); + codeArea.repaint(); + } + + /** + * Performs search by text/characters. + */ + private void searchForText(SearchParameters searchParameters, SearchStatusListener searchStatusListener) { + SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils + .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); + SearchCondition condition = searchParameters.getCondition(); + + long position = searchParameters.getStartPosition(); + String findText; + if (searchParameters.isMatchCase()) { + findText = condition.getSearchText(); + } else { + findText = condition.getSearchText().toLowerCase(); + } + long searchDataSize = findText.length(); + BinaryData data = codeArea.getContentData(); + + List foundMatches = new ArrayList<>(); + + Charset charset = codeArea.getCharset(); + int maxBytesPerChar; + try { + CharsetEncoder encoder = charset.newEncoder(); + maxBytesPerChar = (int) encoder.maxBytesPerChar(); + } catch (UnsupportedOperationException ex) { + maxBytesPerChar = CharsetStreamTranslator.DEFAULT_MAX_BYTES_PER_CHAR; + } + byte[] charData = new byte[maxBytesPerChar]; + long dataSize = data.getDataSize(); + long lastPosition = position; + while (position >= 0 && position <= dataSize - searchDataSize) { + int matchCharLength = 0; + int matchLength = 0; + while (matchCharLength < (int) searchDataSize) { + if (Thread.interrupted()) { + return; + } + + long searchPosition = position + (long) matchLength; + int bytesToUse = maxBytesPerChar; + if (searchPosition + bytesToUse > dataSize) { + bytesToUse = (int) (dataSize - searchPosition); + } + + if (searchPosition == lastPosition + 1) { + System.arraycopy(charData, 1, charData, 0, maxBytesPerChar - 1); + charData[bytesToUse - 1] = data.getByte(searchPosition + bytesToUse - 1); + } else if (searchPosition == lastPosition - 1) { + System.arraycopy(charData, 0, charData, 1, maxBytesPerChar - 1); + charData[0] = data.getByte(searchPosition); + } else { + data.copyToArray(searchPosition, charData, 0, bytesToUse); + } + if (bytesToUse < maxBytesPerChar) { + Arrays.fill(charData, bytesToUse, maxBytesPerChar, (byte) 0); + } + lastPosition = searchPosition; + char singleChar = new String(charData, charset).charAt(0); + + if (searchParameters.isMatchCase()) { + if (singleChar != findText.charAt(matchCharLength)) { + break; + } + } else if (Character.toLowerCase(singleChar) != findText.charAt(matchCharLength)) { + break; + } + int characterLength = String.valueOf(singleChar).getBytes(charset).length; + matchCharLength++; + matchLength += characterLength; + } + + if (matchCharLength == findText.length()) { + SearchMatch match = new SearchMatch(); + match.setPosition(position); + match.setLength(matchLength); + if (searchParameters.getSearchDirection() == SearchParameters.SearchDirection.BACKWARD) { + foundMatches.add(0, match); + } else { + foundMatches.add(match); + } + + if (foundMatches.size() == MAX_MATCHES_COUNT || searchParameters.getMatchMode() == SearchParameters.MatchMode.SINGLE) { + break; + } + } + + switch (searchParameters.getSearchDirection()) { + case FORWARD: { + position++; + break; + } + case BACKWARD: { + position--; + break; + } + default: + throw CodeAreaUtils.getInvalidTypeException(searchParameters.getSearchDirection()); + } + } + + if (Thread.interrupted()) { + return; + } + + searchAssessor.setMatches(foundMatches); + if (!foundMatches.isEmpty()) { + if (searchParameters.getSearchDirection() == SearchParameters.SearchDirection.BACKWARD) { + searchAssessor.setCurrentMatchIndex(foundMatches.size() - 1); + } else { + searchAssessor.setCurrentMatchIndex(0); + } + SearchMatch firstMatch = searchAssessor.getCurrentMatch(); + codeArea.revealPosition(firstMatch.getPosition(), 0, codeArea.getActiveSection()); + } + lastSearchParameters.setFromParameters(searchParameters); + searchStatusListener.setStatus( + new FoundMatches(foundMatches.size(), foundMatches.isEmpty() ? -1 : searchAssessor.getCurrentMatchIndex()), + searchParameters.getMatchMode()); + codeArea.repaint(); + } + + @Override + public void setMatchPosition(int matchPosition) { + SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils + .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); + searchAssessor.setCurrentMatchIndex(matchPosition); + SearchMatch currentMatch = searchAssessor.getCurrentMatch(); + codeArea.revealPosition(currentMatch.getPosition(), 0, codeArea.getActiveSection()); + codeArea.repaint(); + } + + @Override + public void performFindAgain(SearchStatusListener searchStatusListener) { + SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils + .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); + List foundMatches = searchAssessor.getMatches(); + int matchesCount = foundMatches.size(); + if (matchesCount > 0) { + switch (lastSearchParameters.getMatchMode()) { + case MULTIPLE: + if (matchesCount > 1) { + int currentMatchIndex = searchAssessor.getCurrentMatchIndex(); + setMatchPosition(currentMatchIndex < matchesCount - 1 ? currentMatchIndex + 1 : 0); + searchStatusListener.setStatus(new FoundMatches(foundMatches.size(), searchAssessor.getCurrentMatchIndex()), + lastSearchParameters.getMatchMode()); + } + + break; + case SINGLE: + switch (lastSearchParameters.getSearchDirection()) { + case FORWARD: + lastSearchParameters.setStartPosition(foundMatches.get(0).getPosition() + 1); + break; + case BACKWARD: + SearchMatch match = foundMatches.get(0); + lastSearchParameters.setStartPosition(match.getPosition() - 1); + break; + } + + SearchCondition condition = lastSearchParameters.getCondition(); + switch (condition.getSearchMode()) { + case TEXT: { + searchForText(lastSearchParameters, searchStatusListener); + break; + } + case BINARY: { + searchForBinaryData(lastSearchParameters, searchStatusListener); + break; + } + default: + throw CodeAreaUtils.getInvalidTypeException(condition.getSearchMode()); + } + break; + } + } + } + + @Override + public SearchParameters getLastSearchParameters() { + return lastSearchParameters; + } + + @Override + public void clearMatches() { + SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils + .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); + searchAssessor.clearMatches(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/EditorSyncManager.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/EditorSyncManager.java index 1485a8e8d16..ddbc4b8aa60 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/EditorSyncManager.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/EditorSyncManager.java @@ -22,6 +22,7 @@ public void sync() { @Override public void onTabSelect(TabBlueprint blueprint) { + mainWindow.toggleHexViewMenu(); if (mainWindow.getSettings().isAlwaysSelectOpened()) { // verify that tab opened for this blueprint (some nodes don't open tab with content) ContentPanel selectedContentPanel = tabbedPane.getSelectedContentPanel(); @@ -30,4 +31,10 @@ public void onTabSelect(TabBlueprint blueprint) { } } } + + @Override + public void onTabClose(TabBlueprint blueprint) { + ITabStatesListener.super.onTabClose(blueprint); + mainWindow.toggleHexViewMenu(); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java index 5e94022194d..de177b540d1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java @@ -566,7 +566,7 @@ static void listen(ContentPanel pane) { return; } if (pane instanceof AbstractCodeContentPanel) { - ((AbstractCodeContentPanel) pane).getCodeArea().addFocusListener(INSTANCE); + ((AbstractCodeContentPanel) pane).getChildrenComponent().addFocusListener(INSTANCE); return; } if (pane instanceof HtmlPanel) { @@ -586,7 +586,7 @@ static void focusOnCodePanel(ContentPanel pane) { return; } if (pane instanceof AbstractCodeContentPanel) { - SwingUtilities.invokeLater(() -> ((AbstractCodeContentPanel) pane).getCodeArea().requestFocus()); + SwingUtilities.invokeLater(() -> ((AbstractCodeContentPanel) pane).getChildrenComponent().requestFocus()); return; } if (pane instanceof HtmlPanel) { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index fba5c0fcb13..f389059475b 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -1,5 +1,6 @@ package jadx.gui.utils; +import java.awt.Color; import java.awt.Component; import java.awt.Image; import java.awt.MouseInfo; @@ -454,4 +455,17 @@ public void run() { public static void printStackTrace(String label) { LOG.debug("StackTrace: {}", label, new Exception(label)); } + + public static boolean isDarkTheme(Color background) { + double brightness = (background.getRed() * 0.299 + + background.getGreen() * 0.587 + + background.getBlue() * 0.114) / 255; + return brightness < 0.5; + } + + public static Color adjustBrightness(Color color, float factor) { + float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); + hsb[2] = Math.min(1.0f, hsb[2] * factor); // Adjust brightness + return Color.getHSBColor(hsb[0], hsb[1], hsb[2]); + } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index ec1bb22829c..5c07389e3b6 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -30,6 +30,7 @@ menu.help=Hilfe menu.about=Über menu.quark=Quark Engine menu.update_label=Neue Version %s verfügbar! +#menu.hex_viewer=Hex Viewer file.open_action=Datei öffnen… file.add_files_action=Dateien hinzufügen… @@ -82,6 +83,12 @@ search.match_case=Groß/Kleinschreibung beachten search.whole_word=Ganzes Wort search.find=Finden search.results=%s%d Ergebnisse +#search.match_of=Match %d of %d +#search.match_found=Match found +#search.match_not_found=No Matches found +#search.single_match=Single match found +#search.find_type_text=Find by text +#search.find_type_hex=Find by hex tabs.copy_class_name=Klassennamen kopieren tabs.close=Schließen @@ -346,6 +353,10 @@ popup.add_scripts=Skripte hinzufügen popup.new_script=Neues Skript popup.json_prettify=JSON-Verschönerung popup.export=Exportieren +#popup.copy_as=Copy as... +#popup.copy_as_hex=Copy as HEX +#popup.copy_as_string=Copy as String +#popup.copy_offset=Copy offset script.run=Ausführen script.save=Speichern @@ -354,6 +365,9 @@ script.check=Überprüfen script.format=Neu formatieren script.log=Protokoll anzeigen +#encoding_dialog.title=Encoding +#encoding_dialog.message=Select encoding: + exclude_dialog.title=Paketauswahl exclude_dialog.ok=OK exclude_dialog.select_all=Alles auswählen @@ -469,5 +483,12 @@ adb_dialog.starting_debugger=Debugger starten… action.variant=%s (Variante) action_category.menu_toolbar=Menü / Symbolleiste +#action_category.hex_viewer=View / Hex Viewer action_category.code_area=Code-Bereich action_category.plugin_script=Plugin-Skript + +#hex_viewer.show_inspector=Show Inspector +#hex_viewer.change_encoding=Change Encoding +#hex_viewer.goto_address=Go To Address +#hex_viewer.enter_address=Enter address range: +#hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index dd9b2722050..90b12db292a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -30,6 +30,7 @@ menu.help=Help menu.about=About menu.quark=Quark Engine menu.update_label=New version %s available! +menu.hex_viewer=Hex Viewer file.open_action=Open files ... file.add_files_action=Add files @@ -82,6 +83,12 @@ search.match_case=Match Case search.whole_word=Whole word search.find=Find search.results=%s%d results +search.match_of=Match %d of %d +search.match_found=Match found +search.match_not_found=No Matches found +search.single_match=Single match found +search.find_type_text=Find by text +search.find_type_hex=Find by hex tabs.copy_class_name=Copy Name tabs.close=Close @@ -346,6 +353,10 @@ popup.add_scripts=Add scripts popup.new_script=New script popup.json_prettify=JSON Prettify popup.export=Export +popup.copy_as=Copy as... +popup.copy_as_hex=Copy as HEX +popup.copy_as_string=Copy as String +popup.copy_offset=Copy offset script.run=Run script.save=Save @@ -354,6 +365,9 @@ script.check=Check script.format=Reformat script.log=Show log +encoding_dialog.title=Encoding +encoding_dialog.message=Select encoding: + exclude_dialog.title=Package Selector exclude_dialog.ok=OK exclude_dialog.select_all=Select all @@ -469,5 +483,12 @@ adb_dialog.starting_debugger=Starting debugger... action.variant=%s (variant) action_category.menu_toolbar=Menu / Toolbar +action_category.hex_viewer=View / Hex Viewer action_category.code_area=Code Area action_category.plugin_script=Plugin Script + +hex_viewer.show_inspector=Show Inspector +hex_viewer.change_encoding=Change Encoding +hex_viewer.goto_address=Go To Address +hex_viewer.enter_address=Enter address range: +hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 25ba17ea91d..61867a8ec6c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -30,6 +30,7 @@ menu.help=Ayuda menu.about=Acerca de... #menu.quark=Quark Engine menu.update_label=¡Nueva versión %s disponible! +#menu.hex_viewer=Hex Viewer file.open_action=Abrir archivo... #file.add_files_action=Add files ... @@ -82,6 +83,12 @@ search.match_case=Sensible a minúsculas/mayúsculas search.whole_word=Palabra entera search.find=Buscar #search.results=%s%d results +#search.match_of=Match %d of %d +#search.match_found=Match found +#search.match_not_found=No Matches found +#search.single_match=Single match found +#search.find_type_text=Find by text +#search.find_type_hex=Find by hex tabs.copy_class_name=Copy Name tabs.close=Cerrar @@ -346,6 +353,10 @@ popup.rename=Renombrar #popup.new_script=New script #popup.json_prettify=JSON Prettify #popup.export=Export +#popup.copy_as=Copy as... +#popup.copy_as_hex=Copy as HEX +#popup.copy_as_string=Copy as String +#popup.copy_offset=Copy offset #script.run=Run #script.save=Save @@ -354,6 +365,9 @@ popup.rename=Renombrar #script.format=Reformat #script.log=Show log +#encoding_dialog.title=Encoding +#encoding_dialog.message=Select encoding: + #exclude_dialog.title=Package Selector #exclude_dialog.ok=OK #exclude_dialog.select_all=Select all @@ -469,5 +483,12 @@ certificate.serialPubKeyY=Y #action.variant=%s (variant) #action_category.menu_toolbar=Menu / Toolbar +#action_category.hex_viewer=View / Hex Viewer #action_category.code_area=Code Area #action_category.plugin_script=Plugin Script + +#hex_viewer.show_inspector=Show Inspector +#hex_viewer.change_encoding=Change Encoding +#hex_viewer.goto_address=Go To Address +#hex_viewer.enter_address=Enter address range: +#hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index 458cd5fe529..9e3b3a3e053 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -30,6 +30,7 @@ menu.help=Bantuan menu.about=Tentang menu.quark=Mesin Quark menu.update_label=Versi baru %s tersedia! +#menu.hex_viewer=Hex Viewer file.open_action=Buka berkas ... file.add_files_action=Tambahkan berkas @@ -82,6 +83,12 @@ search.match_case=Sama huruf search.whole_word=Seluruh kata search.find=Cari search.results=%s%d hasil +#search.match_of=Match %d of %d +#search.match_found=Match found +#search.match_not_found=No Matches found +#search.single_match=Single match found +#search.find_type_text=Find by text +#search.find_type_hex=Find by hex tabs.copy_class_name=Salin Nama tabs.close=Tutup @@ -346,6 +353,10 @@ popup.add_scripts=Tambahkan skrip popup.new_script=Skrip baru popup.json_prettify=JSON Prettify #popup.export=Export +#popup.copy_as=Copy as... +#popup.copy_as_hex=Copy as HEX +#popup.copy_as_string=Copy as String +#popup.copy_offset=Copy offset script.run=Jalankan script.save=Simpan @@ -354,6 +365,9 @@ script.check=Periksa script.format=Format Ulang script.log=Tampilkan log +#encoding_dialog.title=Encoding +#encoding_dialog.message=Select encoding: + exclude_dialog.title=Selektor Paket exclude_dialog.ok=OK exclude_dialog.select_all=Pilih semua @@ -469,5 +483,12 @@ adb_dialog.starting_debugger=Memulai debugger... action.variant=%s (variant) action_category.menu_toolbar=Menu / Toolbar +#action_category.hex_viewer=View / Hex Viewer action_category.code_area=Area Kode action_category.plugin_script=Plugin Script + +#hex_viewer.show_inspector=Show Inspector +#hex_viewer.change_encoding=Change Encoding +#hex_viewer.goto_address=Go To Address +#hex_viewer.enter_address=Enter address range: +#hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index b15a4fcac69..1b4adbe1236 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -30,6 +30,7 @@ menu.help=도움말 menu.about=정보 #menu.quark=Quark Engine menu.update_label=새 버전 %s 이(가) 존재합니다! +#menu.hex_viewer=Hex Viewer file.open_action=파일 열기 ... file.add_files_action=파일 추가 @@ -82,6 +83,12 @@ search.match_case=매치 케이스 search.whole_word=전체 단어 search.find=찾기 #search.results=%s%d results +#search.match_of=Match %d of %d +#search.match_found=Match found +#search.match_not_found=No Matches found +#search.single_match=Single match found +#search.find_type_text=Find by text +#search.find_type_hex=Find by hex tabs.copy_class_name=이름 복사 tabs.close=닫기 @@ -346,6 +353,10 @@ popup.search_global="%s" 전역 검색 #popup.new_script=New script #popup.json_prettify=JSON Prettify #popup.export=Export +#popup.copy_as=Copy as... +#popup.copy_as_hex=Copy as HEX +#popup.copy_as_string=Copy as String +#popup.copy_offset=Copy offset #script.run=Run #script.save=Save @@ -354,6 +365,9 @@ popup.search_global="%s" 전역 검색 #script.format=Reformat #script.log=Show log +#encoding_dialog.title=Encoding +#encoding_dialog.message=Select encoding: + exclude_dialog.title=패키지 선택기 exclude_dialog.ok=확인 exclude_dialog.select_all=모두 선택 @@ -469,5 +483,12 @@ adb_dialog.starting_debugger=디버거 시작 중 ... #action.variant=%s (variant) #action_category.menu_toolbar=Menu / Toolbar +#action_category.hex_viewer=View / Hex Viewer #action_category.code_area=Code Area #action_category.plugin_script=Plugin Script + +#hex_viewer.show_inspector=Show Inspector +#hex_viewer.change_encoding=Change Encoding +#hex_viewer.goto_address=Go To Address +#hex_viewer.enter_address=Enter address range: +#hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 9241719975d..c3495d189f5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -30,6 +30,7 @@ menu.help=Ajuda menu.about=Sobre #menu.quark=Quark Engine menu.update_label=Nova versão %s disponível! +#menu.hex_viewer=Hex Viewer file.open_action=Abrir arquivos... file.add_files_action=Adicionar arquivos @@ -82,6 +83,12 @@ search.match_case=Match Case search.whole_word=Palavra inteira search.find=Encontrar #search.results=%s%d results +#search.match_of=Match %d of %d +#search.match_found=Match found +#search.match_not_found=No Matches found +#search.single_match=Single match found +#search.find_type_text=Find by text +#search.find_type_hex=Find by hex tabs.copy_class_name=Copiar nome tabs.close=Fechar @@ -346,6 +353,10 @@ popup.search_global=Busca global "%s" #popup.new_script=New script #popup.json_prettify=JSON Prettify #popup.export=Export +#popup.copy_as=Copy as... +#popup.copy_as_hex=Copy as HEX +#popup.copy_as_string=Copy as String +#popup.copy_offset=Copy offset #script.run=Run #script.save=Save @@ -354,6 +365,9 @@ popup.search_global=Busca global "%s" #script.format=Reformat #script.log=Show log +#encoding_dialog.title=Encoding +#encoding_dialog.message=Select encoding: + exclude_dialog.title=Selecionar pacote exclude_dialog.ok=OK exclude_dialog.select_all=Selecionar tudo @@ -469,5 +483,12 @@ adb_dialog.starting_debugger=Iniciando depurador... #action.variant=%s (variant) #action_category.menu_toolbar=Menu / Toolbar +#action_category.hex_viewer=View / Hex Viewer #action_category.code_area=Code Area #action_category.plugin_script=Plugin Script + +#hex_viewer.show_inspector=Show Inspector +#hex_viewer.change_encoding=Change Encoding +#hex_viewer.goto_address=Go To Address +#hex_viewer.enter_address=Enter address range: +#hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 34f5882be84..19e15ba18e1 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -30,6 +30,7 @@ menu.help=Помощь menu.about=О программе #menu.quark=Quark Engine menu.update_label=Версия %s уже доступна! +#menu.hex_viewer=Hex Viewer file.open_action=Открыть файлы... file.add_files_action=Добавить файл @@ -82,6 +83,12 @@ search.match_case=Учитывать регистр search.whole_word=Поиск по словам search.find=Найти search.results=%s%d результатов +#search.match_of=Match %d of %d +#search.match_found=Match found +#search.match_not_found=No Matches found +#search.single_match=Single match found +#search.find_type_text=Find by text +#search.find_type_hex=Find by hex tabs.copy_class_name=Копировать имя tabs.close=Закрыть @@ -346,6 +353,10 @@ popup.add_scripts=Добавить скрипты popup.new_script=Новый скрипт popup.json_prettify=Форматировать JSON #popup.export=Export +#popup.copy_as=Copy as... +#popup.copy_as_hex=Copy as HEX +#popup.copy_as_string=Copy as String +#popup.copy_offset=Copy offset script.run=Запустить script.save=Сохранить @@ -354,6 +365,9 @@ script.check=Проверить script.format=Форматировать script.log=Показать лог +#encoding_dialog.title=Encoding +#encoding_dialog.message=Select encoding: + exclude_dialog.title=Выбор пакетов exclude_dialog.ok=OK exclude_dialog.select_all=Выбрать все @@ -469,5 +483,12 @@ adb_dialog.starting_debugger=Запуск отладки... action.variant=%s (вариант) action_category.menu_toolbar=Панель инструментов +#action_category.hex_viewer=View / Hex Viewer action_category.code_area=Редактор кода action_category.plugin_script=Скрипты и плагины + +#hex_viewer.show_inspector=Show Inspector +#hex_viewer.change_encoding=Change Encoding +#hex_viewer.goto_address=Go To Address +#hex_viewer.enter_address=Enter address range: +#hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 19ade735b3c..497fe6ca17e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -30,6 +30,7 @@ menu.help=帮助 menu.about=关于 menu.quark=Quark 引擎 menu.update_label=发现新版本 %s! +#menu.hex_viewer=Hex Viewer file.open_action=打开文件… file.add_files_action=添加文件 @@ -82,6 +83,12 @@ search.match_case=区分大小写 search.whole_word=全词匹配 search.find=查找 search.results=%s %d 个结果 +#search.match_of=Match %d of %d +#search.match_found=Match found +#search.match_not_found=No Matches found +#search.single_match=Single match found +#search.find_type_text=Find by text +#search.find_type_hex=Find by hex tabs.copy_class_name=复制名称 tabs.close=关闭 @@ -346,6 +353,10 @@ popup.add_scripts=添加脚本 popup.new_script=新建脚本 popup.json_prettify=JSON 格式化 popup.export=导出 +#popup.copy_as=Copy as... +#popup.copy_as_hex=Copy as HEX +#popup.copy_as_string=Copy as String +#popup.copy_offset=Copy offset script.run=运行 script.save=保存 @@ -354,6 +365,9 @@ script.check=检查 script.format=重新格式化 script.log=显示日志 +#encoding_dialog.title=Encoding +#encoding_dialog.message=Select encoding: + exclude_dialog.title=选择要排除的包 exclude_dialog.ok=确定 exclude_dialog.select_all=全选 @@ -469,5 +483,12 @@ adb_dialog.starting_debugger=正在启动调试器… action.variant=%s(变体) action_category.menu_toolbar=菜单/工具栏 +#action_category.hex_viewer=View / Hex Viewer action_category.code_area=代码区 action_category.plugin_script=插件脚本 + +#hex_viewer.show_inspector=Show Inspector +#hex_viewer.change_encoding=Change Encoding +#hex_viewer.goto_address=Go To Address +#hex_viewer.enter_address=Enter address range: +#hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index e10bd03c848..2b447eefb1c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -30,6 +30,7 @@ menu.help=幫助 menu.about=關於 menu.quark= Quark 引擎 menu.update_label=新版本 %s 可供下載! +#menu.hex_viewer=Hex Viewer file.open_action=開啟檔案... file.add_files_action=新增檔案 @@ -82,6 +83,12 @@ search.match_case=大小寫須相符 search.whole_word=全字拼寫須相符 search.find=尋找 search.results=%s%d 個結果 +#search.match_of=Match %d of %d +#search.match_found=Match found +#search.match_not_found=No Matches found +#search.single_match=Single match found +#search.find_type_text=Find by text +#search.find_type_hex=Find by hex tabs.copy_class_name=複製名稱 tabs.close=關閉 @@ -346,6 +353,10 @@ popup.add_scripts=加入腳本 popup.new_script=新增腳本 popup.json_prettify=JSON 格式化 popup.export=匯出 +#popup.copy_as=Copy as... +#popup.copy_as_hex=Copy as HEX +#popup.copy_as_string=Copy as String +#popup.copy_offset=Copy offset script.run=執行 script.save=儲存 @@ -354,6 +365,9 @@ script.check=檢查 script.format=重新格式化 script.log=顯示記錄檔 +#encoding_dialog.title=Encoding +#encoding_dialog.message=Select encoding: + exclude_dialog.title=套件選擇 exclude_dialog.ok=OK exclude_dialog.select_all=全選 @@ -469,5 +483,12 @@ adb_dialog.starting_debugger=正在啟動偵錯工具... action.variant=%s (variant) action_category.menu_toolbar=選單 / 工具列 +#action_category.hex_viewer=View / Hex Viewer action_category.code_area=程式碼區域 action_category.plugin_script=外掛程式腳本 + +#hex_viewer.show_inspector=Show Inspector +#hex_viewer.change_encoding=Change Encoding +#hex_viewer.goto_address=Go To Address +#hex_viewer.enter_address=Enter address range: +#hex_viewer.find=Find diff --git a/jadx-gui/src/main/resources/icons/search/hexSerial.svg b/jadx-gui/src/main/resources/icons/search/hexSerial.svg new file mode 100644 index 00000000000..c4edf04bbd9 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/search/hexSerial.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/jadx-gui/src/main/resources/icons/search/text.svg b/jadx-gui/src/main/resources/icons/search/text.svg new file mode 100644 index 00000000000..28826e2abd1 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/search/text.svg @@ -0,0 +1,7 @@ + + + + + + +