fix(gui): improve code tabs, sync and related code

This commit is contained in:
Skylot
2026-05-30 21:10:41 +01:00
parent e648e60af9
commit 6775cc5a93
28 changed files with 358 additions and 375 deletions
@@ -899,13 +899,4 @@ public class JadxSettings {
public void setSaveOption(SaveOptionEnum saveOption) {
settingsData.setSaveOption(saveOption);
}
public boolean isSmaliAreaShowBytecode() {
return settingsData.isSmaliAreaShowBytecode();
}
public void setSmaliAreaShowBytecode(boolean smaliAreaShowBytecode) {
settingsData.setSmaliAreaShowBytecode(smaliAreaShowBytecode);
}
}
@@ -80,7 +80,6 @@ public class JadxSettingsData extends JadxGUIArgs {
private int searchResultsPerPage = 50;
private boolean useAutoSearch = true;
private boolean keepCommonDialogOpen = false;
private boolean smaliAreaShowBytecode = false;
private LineNumbersMode lineNumbersMode = LineNumbersMode.AUTO;
private int mainWindowVerticalSplitterLoc = 300;
@@ -398,14 +397,6 @@ public class JadxSettingsData extends JadxGUIArgs {
this.showHeapUsageBar = showHeapUsageBar;
}
public boolean isSmaliAreaShowBytecode() {
return smaliAreaShowBytecode;
}
public void setSmaliAreaShowBytecode(boolean smaliAreaShowBytecode) {
this.smaliAreaShowBytecode = smaliAreaShowBytecode;
}
public String getUiFontStr() {
return uiFontStr;
}
@@ -22,6 +22,7 @@ public abstract class AbstractCodeContentPanel extends ContentPanel {
public abstract Component getChildrenComponent();
@Override
public void scrollToPos(int pos) {
AbstractCodeArea codeArea = getCodeArea();
if (codeArea != null) {
@@ -12,20 +12,27 @@ import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretListener;
import javax.swing.text.JTextComponent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.DecompilationMode;
import jadx.core.utils.Utils;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.codearea.mode.JCodeMode;
import jadx.gui.ui.codearea.sync.CodePanelSyncee;
import jadx.gui.ui.codearea.sync.CodePanelSyncer;
import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.CodeAreaSyncee;
import jadx.gui.ui.codearea.sync.CodeAreaSyncer;
import jadx.gui.ui.codearea.sync.CodeAreaSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.fallback.FallbackSyncer;
import jadx.gui.ui.panel.IViewStateSupport;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.ListenersHelper;
import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_TRAILING_COMPONENT;
@@ -41,152 +48,92 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
private static final Logger LOG = LoggerFactory.getLogger(ClassCodeContentPanel.class);
private static final long serialVersionUID = -7229931102504634591L;
private final transient CodePanel javaCodePanel;
private final transient CodePanel smaliCodePanel;
private final transient JTabbedPane areaTabbedPane;
private final JClass jCls;
private final ListenersHelper<JTextComponent, CaretListener> caretListeners = ListenersHelper.buildForCaretListener();
private final AtomicBoolean syncInProgress = new AtomicBoolean(false);
private boolean splitView = false;
private final JCheckBox splitCheckboxNormal;
private final JTabbedPane leftTabbedPane;
private @Nullable JTabbedPane rightTabbedPane;
private CodePanel javaCodePanel;
private CodePanel smaliCodePanel;
public ClassCodeContentPanel(TabbedPane panel, JClass jCls) {
super(panel, jCls);
private boolean isSplitViewActivated = false;
javaCodePanel = new CodePanel(new CodeArea(this, jCls));
smaliCodePanel = new CodePanel(new SmaliArea(this, jCls, false));
areaTabbedPane = buildTabbedPane(jCls);
splitCheckboxNormal = addCustomControls(areaTabbedPane, false);
javaCodePanel.load();
initView(false);
public ClassCodeContentPanel(TabbedPane panel, JClass jClass) {
super(panel, jClass);
jCls = jClass;
leftTabbedPane = buildTabbedPane(jClass, true);
addCustomControls(leftTabbedPane);
initView();
activateCodePanel(javaCodePanel);
}
private void initView(boolean splitViewEnabled) {
splitView = splitViewEnabled;
private void initView() {
removeAll();
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 0, 0));
if (splitViewEnabled) {
setupSplitPane();
if (isSplitViewActivated) {
rightTabbedPane = buildTabbedPane(jCls, false);
rightTabbedPane.setSelectedIndex(1); // default to Smali
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftTabbedPane, rightTabbedPane);
splitPane.setResizeWeight(0.5);
add(splitPane);
revalidate();
repaint();
// set divider location after layout
SwingUtilities.invokeLater(() -> splitPane.setDividerLocation(0.5));
} else {
javaCodePanel.load();
smaliCodePanel.load();
attachSyncListeners(javaCodePanel, smaliCodePanel);
areaTabbedPane.setSelectedIndex(0); // default to Java
splitCheckboxNormal.setSelected(false);
add(areaTabbedPane);
}
disposeTabbedPane(rightTabbedPane);
rightTabbedPane = null;
add(leftTabbedPane);
revalidate();
repaint();
}
private void attachSyncListeners(CodePanel javaPanel, CodePanel smaliPanel) {
javaPanel.getCodeArea().addCaretListener(e -> {
if (syncInProgress.get()) {
return;
}
syncInProgress.set(true);
syncToMethod(javaPanel, smaliPanel);
syncInProgress.set(false);
});
smaliPanel.getCodeArea().addCaretListener(e -> {
if (syncInProgress.get()) {
return;
}
syncInProgress.set(true);
syncToMethod(smaliPanel, javaPanel);
syncInProgress.set(false);
});
}
private void setupSplitPane() {
JTabbedPane leftTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
JTabbedPane rightTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
CodePanel[] leftPanels = {
new CodePanel(new CodeArea(this, (JClass) node)), // Java
new CodePanel(new SmaliArea(this, (JClass) node, false)), // Smali
new CodePanel(new SmaliArea(this, (JClass) node, true)), // Smali with Dalvik
new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.SIMPLE))), // Simple
new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.FALLBACK))) // Fallback
};
CodePanel[] rightPanels = {
new CodePanel(new SmaliArea(this, (JClass) node, false)), // Smali
new CodePanel(new SmaliArea(this, (JClass) node, true)), // Smali with Dalvik
new CodePanel(new CodeArea(this, (JClass) node)), // Java
new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.SIMPLE))), // Simple
new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.FALLBACK))) // Fallback
};
leftTabbedPane.add(leftPanels[0], NLS.str("tabs.code"));
leftTabbedPane.add(leftPanels[1], NLS.str("tabs.smali"));
leftTabbedPane.add(leftPanels[2], NLS.str("tabs.smali_bytecode"));
leftTabbedPane.add(leftPanels[3], "Simple");
leftTabbedPane.add(leftPanels[4], "Fallback");
rightTabbedPane.add(rightPanels[0], NLS.str("tabs.smali"));
rightTabbedPane.add(rightPanels[1], NLS.str("tabs.smali_bytecode"));
rightTabbedPane.add(rightPanels[2], NLS.str("tabs.code"));
rightTabbedPane.add(rightPanels[3], "Simple");
rightTabbedPane.add(rightPanels[4], "Fallback");
for (CodePanel p : leftPanels) {
p.load();
}
for (CodePanel p : rightPanels) {
p.load();
}
leftTabbedPane.addChangeListener(e -> ((CodePanel) leftTabbedPane.getSelectedComponent()).load());
rightTabbedPane.addChangeListener(e -> ((CodePanel) rightTabbedPane.getSelectedComponent()).load());
// Attach caret sync between all combinations
for (CodePanel leftPanel : leftPanels) {
for (CodePanel rightPanel : rightPanels) {
attachSyncListeners(leftPanel, rightPanel);
}
}
// Create and configure split pane
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftTabbedPane, rightTabbedPane);
splitPane.setResizeWeight(0.5);
leftTabbedPane.setMinimumSize(new Dimension(200, 200));
rightTabbedPane.setMinimumSize(new Dimension(200, 200));
add(splitPane);
// Set divider location after layout
SwingUtilities.invokeLater(() -> splitPane.setDividerLocation(0.5));
rightTabbedPane.setSelectedIndex(0);
addCustomControls(leftTabbedPane, true);
}
private JTabbedPane buildTabbedPane(JClass jCls) {
private JTabbedPane buildTabbedPane(JClass jCls, boolean leftPanel) {
JTabbedPane areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
areaTabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0));
areaTabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code"));
areaTabbedPane.add(smaliCodePanel, NLS.str("tabs.smali"));
CodePanel javaPanel = new CodePanel(new CodeArea(this, jCls));
CodePanel smaliPanel = new CodePanel(new SmaliArea(this, jCls, false));
if (leftPanel) {
this.javaCodePanel = javaPanel;
this.smaliCodePanel = smaliPanel;
}
areaTabbedPane.add(javaPanel, NLS.str("tabs.code"));
areaTabbedPane.add(smaliPanel, NLS.str("tabs.smali"));
areaTabbedPane.add(new CodePanel(new SmaliArea(this, jCls, true)), NLS.str("tabs.smali_bytecode"));
areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.SIMPLE))), "Simple");
areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.FALLBACK))), "Fallback");
areaTabbedPane.addChangeListener(e -> {
CodePanel selectedPanel = (CodePanel) areaTabbedPane.getSelectedComponent();
// TODO: to run background load extract ui update to other method
selectedPanel.load();
// execInBackground(selectedPanel::load);
});
areaTabbedPane.setMinimumSize(new Dimension(200, 200));
areaTabbedPane.addChangeListener(e -> onCodePanelActivation((CodePanel) areaTabbedPane.getSelectedComponent()));
return areaTabbedPane;
}
private JCheckBox addCustomControls(JTabbedPane tabbedPane, boolean splitCheckboxInitialState) {
JCheckBox splitCheckBox = new JCheckBox("Split view", splitCheckboxInitialState);
private void onCodePanelActivation(CodePanel selectedPanel) {
selectedPanel.load();
updateSync();
}
private void activateCodePanel(CodePanel javaCodePanel) {
if (leftTabbedPane.getSelectedComponent() == javaCodePanel) {
// already selected, change listener will not be called, run update manually
onCodePanelActivation(javaCodePanel);
} else {
leftTabbedPane.setSelectedComponent(javaCodePanel);
}
}
private void addCustomControls(JTabbedPane tabbedPane) {
JCheckBox splitCheckBox = new JCheckBox("Split view", false);
splitCheckBox.addItemListener(e -> {
boolean newSplitView = splitCheckBox.isSelected();
if (splitView != newSplitView) {
this.initView(newSplitView);
if (isSplitViewActivated != newSplitView) {
isSplitViewActivated = newSplitView;
initView();
}
});
@@ -197,18 +144,77 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
trailing.addSeparator(new Dimension(50, 1));
trailing.add(splitCheckBox);
tabbedPane.putClientProperty(TABBED_PANE_TRAILING_COMPONENT, trailing);
return splitCheckBox;
}
private void updateSync() {
caretListeners.removeAll();
if (!isSplitViewActivated) {
return;
}
AbstractCodeArea leftArea = getCodePanel(leftTabbedPane).getCodeArea();
AbstractCodeArea rightArea = getCodePanel(rightTabbedPane).getCodeArea();
if (leftArea instanceof CodeAreaSyncee && rightArea instanceof CodeAreaSyncee) {
CodeAreaSyncer leftSyncer = buildCodeAreaSyncer(leftArea);
CodeAreaSyncer rightSyncer = buildCodeAreaSyncer(rightArea);
if (leftSyncer != null && rightSyncer != null) {
caretListeners.add(leftArea, e -> syncCodeArea(leftArea, rightArea, leftSyncer));
caretListeners.add(rightArea, e -> syncCodeArea(rightArea, leftArea, rightSyncer));
}
}
}
private void syncCodeArea(AbstractCodeArea fromArea, AbstractCodeArea toArea, CodeAreaSyncer syncer) {
if (syncInProgress.get()) {
return;
}
try {
syncInProgress.set(true);
boolean synced = ((CodeAreaSyncee) toArea).sync(syncer);
if (!synced) {
if (!FallbackSyncer.sync(fromArea, toArea)) {
LOG.warn("Code pane area sync not possible");
}
}
} catch (Exception ex) {
LOG.warn("Failed to sync method/class across views: {}", ex.getLocalizedMessage());
} finally {
syncInProgress.set(false);
}
}
private static CodePanel getCodePanel(@Nullable JTabbedPane tabbedPane) {
if (tabbedPane == null) {
throw new IllegalStateException("tabbedPane is null");
}
return (CodePanel) tabbedPane.getSelectedComponent();
}
private static @Nullable CodeAreaSyncer buildCodeAreaSyncer(AbstractCodeArea codeArea) {
if (codeArea instanceof CodeAreaSyncerAbstractFactory) {
return ((CodeAreaSyncerAbstractFactory) codeArea).createCodeAreaSyncer();
}
return null;
}
@Override
public void loadSettings() {
javaCodePanel.loadSettings();
smaliCodePanel.loadSettings();
for (Component component : leftTabbedPane.getComponents()) {
if (component instanceof CodePanel) {
((CodePanel) component).loadSettings();
}
}
if (rightTabbedPane != null) {
for (Component component : rightTabbedPane.getComponents()) {
if (component instanceof CodePanel) {
((CodePanel) component).loadSettings();
}
}
}
updateUI();
}
@Override
public AbstractCodeArea getCodeArea() {
public @NotNull AbstractCodeArea getCodeArea() {
return javaCodePanel.getCodeArea();
}
@@ -222,12 +228,12 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
}
public void switchPanel() {
boolean toSmali = areaTabbedPane.getSelectedComponent() == javaCodePanel;
areaTabbedPane.setSelectedComponent(toSmali ? smaliCodePanel : javaCodePanel);
boolean toSmali = leftTabbedPane.getSelectedComponent() == javaCodePanel;
activateCodePanel(toSmali ? smaliCodePanel : javaCodePanel);
}
public AbstractCodeArea getCurrentCodeArea() {
return ((CodePanel) areaTabbedPane.getSelectedComponent()).getCodeArea();
return ((CodePanel) leftTabbedPane.getSelectedComponent()).getCodeArea();
}
public AbstractCodeArea getSmaliCodeArea() {
@@ -235,25 +241,40 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
}
public void showSmaliPane() {
areaTabbedPane.setSelectedComponent(smaliCodePanel);
activateCodePanel(smaliCodePanel);
}
@Override
public void saveEditorViewState(EditorViewState viewState) {
CodePanel codePanel = (CodePanel) areaTabbedPane.getSelectedComponent();
CodePanel codePanel = (CodePanel) leftTabbedPane.getSelectedComponent();
int caretPos = codePanel.getCodeArea().getCaretPosition();
Point viewPoint = codePanel.getCodeScrollPane().getViewport().getViewPosition();
String subPath = codePanel == javaCodePanel ? "java" : "smali";
viewState.setSubPath(subPath);
viewState.setSubPath(String.valueOf(leftTabbedPane.getSelectedIndex()));
viewState.setCaretPos(caretPos);
viewState.setViewPoint(viewPoint);
}
@Override
public void restoreEditorViewState(EditorViewState viewState) {
boolean isJava = viewState.getSubPath().equals("java");
CodePanel activePanel = isJava ? javaCodePanel : smaliCodePanel;
areaTabbedPane.setSelectedComponent(activePanel);
UiUtils.uiThreadGuard();
String subPath = viewState.getSubPath();
CodePanel activePanel = null;
if (subPath.equals("java")) {
activePanel = javaCodePanel;
} else if (subPath.equals("smali")) {
activePanel = smaliCodePanel;
} else {
try {
int index = Utils.safeParseInt(subPath, 0);
activePanel = (CodePanel) leftTabbedPane.getComponentAt(index);
} catch (Exception e) {
LOG.debug("Failed to restore active code panel: {}", subPath, e);
}
}
if (activePanel == null) {
return;
}
activateCodePanel(activePanel);
try {
activePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
} catch (Exception e) {
@@ -273,36 +294,19 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
@Override
public void dispose() {
javaCodePanel.dispose();
smaliCodePanel.dispose();
for (Component component : areaTabbedPane.getComponents()) {
caretListeners.removeAll();
disposeTabbedPane(leftTabbedPane);
disposeTabbedPane(rightTabbedPane);
super.dispose();
}
private void disposeTabbedPane(@Nullable JTabbedPane tabbedPane) {
if (tabbedPane != null) {
for (Component component : tabbedPane.getComponents()) {
if (component instanceof CodePanel) {
((CodePanel) component).dispose();
}
}
super.dispose();
}
private void syncToMethod(CodePanel fromPanel, CodePanel toPanel) {
if (!fromPanel.isShowing() || !toPanel.isShowing()) {
return;
}
try {
AbstractCodeArea from = fromPanel.getCodeArea();
AbstractCodeArea to = toPanel.getCodeArea();
toPanel.load();
if (from instanceof CodePanelSyncerAbstractFactory && to instanceof CodePanelSyncee) {
CodePanelSyncer syncer = ((CodePanelSyncerAbstractFactory) from).createCodePanelSyncer();
if (((CodePanelSyncee) to).sync(syncer)) {
return;
}
}
if (!FallbackSyncer.sync(fromPanel, toPanel)) {
LOG.warn("Code pane area sync not possible");
}
} catch (Exception ex) {
LOG.warn("Failed to sync method/class across views: {}", ex.getLocalizedMessage());
}
}
}
@@ -4,6 +4,7 @@ import java.awt.Point;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
@@ -15,6 +16,7 @@ import javax.swing.event.PopupMenuEvent;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.Token;
import org.fife.ui.rsyntaxtextarea.TokenTypes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,9 +52,9 @@ import jadx.gui.ui.action.ViewRawControlFlowGraphAction;
import jadx.gui.ui.action.ViewRegionControlFlowGraphAction;
import jadx.gui.ui.action.XposedAction;
import jadx.gui.ui.codearea.mode.JCodeMode;
import jadx.gui.ui.codearea.sync.CodePanelSyncee;
import jadx.gui.ui.codearea.sync.CodePanelSyncer;
import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.CodeAreaSyncee;
import jadx.gui.ui.codearea.sync.CodeAreaSyncer;
import jadx.gui.ui.codearea.sync.CodeAreaSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.JavaSyncer;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.CaretPositionFix;
@@ -67,7 +69,7 @@ import jadx.gui.utils.shortcut.ShortcutsController;
* The {@link AbstractCodeArea} implementation used for displaying Java code and text based
* resources (e.g. AndroidManifest.xml)
*/
public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerAbstractFactory, CodePanelSyncee {
public final class CodeArea extends AbstractCodeArea implements CodeAreaSyncerAbstractFactory, CodeAreaSyncee {
private static final Logger LOG = LoggerFactory.getLogger(CodeArea.class);
private static final long serialVersionUID = 6312736869579635796L;
@@ -335,7 +337,7 @@ public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerA
/**
* Search referenced java node by offset in {@code jCls} code
*/
public JavaNode getJavaNodeAtOffset(int offset) {
public @Nullable JavaNode getJavaNodeAtOffset(int offset) {
if (offset == -1) {
return null;
}
@@ -347,7 +349,7 @@ public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerA
return null;
}
public JavaNode getClosestJavaNode(int offset) {
public @Nullable JavaNode getClosestJavaNode(int offset) {
if (offset == -1) {
return null;
}
@@ -359,7 +361,7 @@ public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerA
}
}
public JavaNode getEnclosingJavaNode(int offset) {
public @Nullable JavaNode getEnclosingJavaNode(int offset) {
if (offset == -1) {
return null;
}
@@ -459,20 +461,19 @@ public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerA
}
@Override
public CodePanelSyncer createCodePanelSyncer() {
public CodeAreaSyncer createCodeAreaSyncer() {
return new JavaSyncer(this);
}
@Override
public boolean sync(CodePanelSyncer codePanelSyncer) {
return codePanelSyncer.syncTo(this);
public boolean sync(CodeAreaSyncer codeAreaSyncer) {
return codeAreaSyncer.syncTo(this);
}
@Nullable
public ICodeMetadata getCodeMetadata() {
public @Nullable ICodeMetadata getCodeMetadata() {
ICodeInfo codeInfo = getCodeInfo();
if (!codeInfo.hasMetadata()) {
LOG.warn("No code info metadata for {}", codeInfo.toString());
LOG.warn("No code info metadata for {}", codeInfo);
return null;
}
return codeInfo.getCodeMetadata();
@@ -487,34 +488,44 @@ public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerA
public Map<Integer, Integer> getLineMappings() {
ICodeInfo codeInfo = getCodeInfo();
if (!codeInfo.hasMetadata()) {
LOG.debug("No code info metadata for {}", codeInfo.toString());
return Map.of();
LOG.debug("No code info metadata for {}", codeInfo);
return Collections.emptyMap();
}
Map<Integer, Integer> lineMapping = codeInfo.getCodeMetadata().getLineMapping();
if (lineMapping.isEmpty()) {
LOG.debug("Line mappings are empty for {}", codeInfo.toString());
return Map.of();
LOG.debug("Line mappings are empty for {}", codeInfo);
return Collections.emptyMap();
}
return lineMapping;
}
private Map<Integer, Integer> cachedUniqueLineMappings;
/**
* Returns the same as {@link #getLineMappings()} but only if each value (dex debug line number)
* appears only once.
* If a value appears more than once then it suggests that methods might share dex debug line
* numbers.
* If this is the case then the line mapping cannot be used for code sync correlation.
*
* @return the line mapping
*/
public Map<Integer, Integer> getFunctionUniqueLineMappings() {
final var lineMappings = getLineMappings();
final boolean isAnyRepeated =
lineMappings.values().stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).values().stream()
.filter(v -> v > 1).findAny().isPresent();
Map<Integer, Integer> mappings = cachedUniqueLineMappings;
if (mappings == null) {
mappings = calcUniqueLineMappings();
cachedUniqueLineMappings = mappings;
}
return mappings;
}
private @NotNull Map<Integer, Integer> calcUniqueLineMappings() {
Map<Integer, Integer> lineMappings = getLineMappings();
boolean isAnyRepeated = lineMappings.values().stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.values().stream()
.anyMatch(v -> v > 1);
if (isAnyRepeated) {
LOG.debug("Dex debug line mappings are not unique");
return Map.of();
return Collections.emptyMap();
}
return lineMappings;
}
@@ -12,7 +12,6 @@ import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.KeyStroke;
import javax.swing.text.BadLocationException;
import javax.swing.text.EditorKit;
@@ -39,19 +38,17 @@ import jadx.gui.device.debugger.BreakpointManager;
import jadx.gui.device.debugger.DbgUtils;
import jadx.gui.jobs.IBackgroundTask;
import jadx.gui.jobs.LoadTask;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.TextNode;
import jadx.gui.ui.codearea.sync.CodePanelSyncee;
import jadx.gui.ui.codearea.sync.CodePanelSyncer;
import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.CodeAreaSyncee;
import jadx.gui.ui.codearea.sync.CodeAreaSyncer;
import jadx.gui.ui.codearea.sync.CodeAreaSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.SmaliSyncer;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncerAbstractFactory, CodePanelSyncee {
public final class SmaliArea extends AbstractCodeArea implements CodeAreaSyncerAbstractFactory, CodeAreaSyncee {
private static final Logger LOG = LoggerFactory.getLogger(SmaliArea.class);
private static final long serialVersionUID = 1334485631870306494L;
@@ -62,49 +59,22 @@ public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncer
private static final Color DEBUG_LINE_COLOR = Color.decode("#9c1138");
private final JNode textNode;
private final JCheckBoxMenuItem cbUseSmaliV2;
private final boolean allowToggleV2 = false; // add to constructor args to change back
private final boolean initialDisplayV2;
private final SmaliModel model;
private boolean curVersion = false;
private SmaliModel model;
SmaliArea(ContentPanel contentPanel, JClass node, boolean initialDisplayV2) {
SmaliArea(ContentPanel contentPanel, JClass node, boolean showBytecode) {
super(contentPanel, node);
this.textNode = new TextNode(node.getName());
this.initialDisplayV2 = initialDisplayV2;
setCodeFoldingEnabled(true);
cbUseSmaliV2 = new JCheckBoxMenuItem(NLS.str("popup.bytecode_col"), shouldUseSmaliPrinterV2());
cbUseSmaliV2.setAction(new AbstractAction(NLS.str("popup.bytecode_col")) {
private static final long serialVersionUID = -1111111202103170737L;
@Override
public void actionPerformed(ActionEvent e) {
JadxSettings settings = getContentPanel().getMainWindow().getSettings();
settings.setSmaliAreaShowBytecode(!settings.isSmaliAreaShowBytecode());
contentPanel.getTabbedPane().getTabs().forEach(v -> {
if (v instanceof ClassCodeContentPanel) {
switchModel();
((ClassCodeContentPanel) v).getSmaliCodeArea().refresh();
}
});
settings.sync();
}
});
if (allowToggleV2) {
getPopupMenu().add(cbUseSmaliV2);
}
switchModel();
this.textNode = new TextNode(node.getName());
this.model = showBytecode ? new DebugModel() : new NormalModel(this);
setUnLoaded();
load();
}
@Override
public IBackgroundTask getLoadTask() {
return new LoadTask<>(
() -> model.loadCode(),
model::loadCode,
code -> {
curVersion = shouldUseSmaliPrinterV2();
model.loadUI(code);
setCaretPosition(0);
setLoaded();
@@ -135,24 +105,7 @@ public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncer
return (JClass) node;
}
private void switchModel() {
if (model != null) {
model.unload();
}
curVersion = shouldUseSmaliPrinterV2();
model = curVersion ? new DebugModel() : new NormalModel(this);
setUnLoaded();
load();
}
public void scrollToDebugPos(int pos) {
// don't sync when it's set programmatically.
getContentPanel().getMainWindow().getSettings().setSmaliAreaShowBytecode(true);
cbUseSmaliV2.setState(shouldUseSmaliPrinterV2());
if (!(model instanceof DebugModel)) {
switchModel();
refresh();
}
model.togglePosHighlight(pos);
}
@@ -169,10 +122,6 @@ public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncer
return getFont();
}
private boolean shouldUseSmaliPrinterV2() {
return getContentPanel().getMainWindow().getSettings().isSmaliAreaShowBytecode();
}
private abstract class SmaliModel {
abstract String loadCode();
@@ -196,7 +145,7 @@ public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncer
}
private class NormalModel extends SmaliModel {
public NormalModel(SmaliArea smaliArea) {
private NormalModel(SmaliArea smaliArea) {
getContentPanel().getMainWindow().getEditorThemeManager().apply(smaliArea);
setSyntaxEditingStyle(SYNTAX_STYLE_SMALI);
}
@@ -228,7 +177,7 @@ public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncer
}
};
public DebugModel() {
private DebugModel() {
loadV2Style();
setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_ASSEMBLER_6502);
addPropertyChangeListener(SYNTAX_SCHEME_PROPERTY, schemeListener);
@@ -470,12 +419,12 @@ public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncer
}
@Override
public CodePanelSyncer createCodePanelSyncer() {
public CodeAreaSyncer createCodeAreaSyncer() {
return new SmaliSyncer(this);
}
@Override
public boolean sync(CodePanelSyncer codePanelSyncer) {
return codePanelSyncer.syncTo(this);
public boolean sync(CodeAreaSyncer codeAreaSyncer) {
return codeAreaSyncer.syncTo(this);
}
}
@@ -0,0 +1,8 @@
package jadx.gui.ui.codearea.sync;
/**
* Accepts syncer for syncing code areas
*/
public interface CodeAreaSyncee {
boolean sync(CodeAreaSyncer syncer);
}
@@ -0,0 +1,4 @@
package jadx.gui.ui.codearea.sync;
public interface CodeAreaSyncer extends IToJavaSyncStrategy, IToSmaliSyncStrategy {
}
@@ -0,0 +1,5 @@
package jadx.gui.ui.codearea.sync;
public interface CodeAreaSyncerAbstractFactory {
CodeAreaSyncer createCodeAreaSyncer();
}
@@ -1,8 +0,0 @@
package jadx.gui.ui.codearea.sync;
/**
* Accepts a code panel syncer for syncing code areas
*/
public interface CodePanelSyncee {
boolean sync(CodePanelSyncer syncer);
}
@@ -1,4 +0,0 @@
package jadx.gui.ui.codearea.sync;
public interface CodePanelSyncer extends IToJavaSyncStrategy, IToSmaliSyncStrategy {
}
@@ -1,5 +0,0 @@
package jadx.gui.ui.codearea.sync;
public interface CodePanelSyncerAbstractFactory {
CodePanelSyncer createCodePanelSyncer();
}
@@ -25,16 +25,19 @@ public class DebugLineJavaSyncer implements IToSmaliSyncStrategy, IToJavaSyncStr
public boolean syncTo(CodeArea to) {
// This might be any combination between java/simple/fallback
// We cannot just rely on the current line.
// Instead try to correlate with line mappings.
// Instead, try to correlate with line mappings.
try {
int lineIndex = from.getCaretLineNumber();
Map<Integer, Integer> toLineMapping = to.getFunctionUniqueLineMappings();
if (toLineMapping.isEmpty()) {
return false;
}
int lineIndex = from.getCaretLineNumber();
// lineIndex is 0-indexed whereas the line mappings are based off a 1-index.
Integer sourceLine = getClosestSourceLine(lineIndex + 1);
if (sourceLine == null) {
return false;
}
// find the equivalent linenumber in the 'to' by a reverse lookup from the source line
// find the equivalent line number in the 'to' by a reverse lookup from the source line
for (Map.Entry<Integer, Integer> entry : toLineMapping.entrySet()) {
int toLine = entry.getKey();
int candidateSourceLine = entry.getValue();
@@ -85,7 +88,7 @@ public class DebugLineJavaSyncer implements IToSmaliSyncStrategy, IToJavaSyncStr
private @Nullable Integer getClosestSourceLine(int lineNum) {
// get the line mappings of the Java/Simple/Fallback code
Map<Integer, Integer> lineMapping = from.getFunctionUniqueLineMappings();
if (lineMapping == null || lineMapping.isEmpty()) {
if (lineMapping.isEmpty()) {
return null;
}
// get the source line from the decomp line
@@ -26,7 +26,7 @@ public class DebugLineSmaliSyncer implements IToJavaSyncStrategy {
@Override
public boolean syncTo(CodeArea to) {
try {
// Get the from lines and currentline index
// Get the from lines and current line index
int lineIndex = from.getCaretLineNumber();
String[] fromLines = from.getText().split("\\R");
if (lineIndex >= fromLines.length) {
@@ -62,8 +62,7 @@ public class DebugLineSmaliSyncer implements IToJavaSyncStrategy {
return false;
}
@Nullable
private Anchor findNearestAnchor(int smaliLineNumber, String[] lines) {
private @Nullable Anchor findNearestAnchor(int smaliLineNumber, String[] lines) {
for (int i = smaliLineNumber; i >= 0; i--) {
String trimmedLine = lines[i].trim();
if (trimmedLine.startsWith(".line")) {
@@ -11,6 +11,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.NodeDeclareRef;
@@ -95,27 +96,20 @@ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncSt
if (fromMthRange == null) {
return false;
}
Integer mthDefPos = fromMthRange.getStart().getKey();
Integer mthEndPos = fromMthRange.getEnd().getKey();
LOG.debug("InsnOffsetJavaSyncer caretPos = {}", caretPos);
LOG.debug("InsnOffsetJavaSyncer mthDefPos = {}", mthDefPos);
LOG.debug("InsnOffsetJavaSyncer mthEndPos = {}", mthEndPos);
LOG.debug("InsnOffsetJavaSyncer caretPos = {}, mthDefPos = {}, mthEndPos = {}", caretPos, mthDefPos, mthEndPos);
CodeMetadataRange fromInsnOffsetRange = findOffsetRange(caretPos, mthDefPos, mthEndPos);
if (fromInsnOffsetRange == null) {
return false;
}
String mthID = getMthRawFullID(mthDefPos);
// now search for this range within the target area
CodeMetadataRange toMthRange = findMethodRange(mthID, to);
if (toMthRange == null) {
return false;
}
// search for the first insn offset
int firstInsnOffset = ((InsnCodeOffset) fromInsnOffsetRange.getStart().getValue()).getOffset();
Integer highlightPosStart = to.getCodeMetadata().searchDown(toMthRange.getStart().getKey(), (offset, ann) -> {
@@ -149,7 +143,6 @@ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncSt
if (highlightPosEnd == null) {
return false;
}
to.scrollToPos(highlightPosStart);
try {
CodeSyncHighlighter.defaultHighlighter().highlightRange(to, highlightPosStart, highlightPosEnd);
@@ -162,9 +155,12 @@ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncSt
return false;
}
@Nullable
private static CodeMetadataRange findMethodRange(String mthFullRawID, CodeArea area) {
Map.Entry<Integer, ICodeAnnotation> toMthDecl = area.getCodeMetadata().searchDown(0, (offset, ann) -> {
private static @Nullable CodeMetadataRange findMethodRange(String mthFullRawID, CodeArea area) {
ICodeMetadata codeMetadata = area.getCodeMetadata();
if (codeMetadata == null) {
return null;
}
Map.Entry<Integer, ICodeAnnotation> toMthDecl = codeMetadata.searchDown(0, (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.DECLARATION) {
return null;
}
@@ -179,28 +175,27 @@ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncSt
}
return new SimpleEntry<>(offset, ann);
});
if (toMthDecl == null) {
return null;
}
Map.Entry<Integer, ICodeAnnotation> toMthEnd = area.getCodeMetadata().searchDown(toMthDecl.getKey(), (offset, ann) -> {
Map.Entry<Integer, ICodeAnnotation> toMthEnd = codeMetadata.searchDown(toMthDecl.getKey(), (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.END) {
return null;
}
return new SimpleEntry<>(offset, ann);
});
if (toMthEnd == null) {
return null;
}
return new CodeMetadataRange(toMthDecl, toMthEnd);
}
@Nullable
private CodeMetadataRange findEnclosingMethodRange(Integer startPos) {
Map.Entry<Integer, ICodeAnnotation> mthDef = from.getCodeMetadata().searchUp(startPos, (offset, ann) -> {
private @Nullable CodeMetadataRange findEnclosingMethodRange(Integer startPos) {
ICodeMetadata codeMetadata = from.getCodeMetadata();
if (codeMetadata == null) {
return null;
}
Map.Entry<Integer, ICodeAnnotation> mthDef = codeMetadata.searchUp(startPos, (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.DECLARATION) {
return null;
}
@@ -211,22 +206,18 @@ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncSt
}
return new SimpleEntry<>(offset, ann);
});
if (mthDef == null) {
return null;
}
Map.Entry<Integer, ICodeAnnotation> mthEnd = from.getCodeMetadata().searchDown(startPos, (offset, ann) -> {
Map.Entry<Integer, ICodeAnnotation> mthEnd = codeMetadata.searchDown(startPos, (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.END) {
return null;
}
return new SimpleEntry<>(offset, ann);
});
if (mthEnd == null) {
return null;
}
return new CodeMetadataRange(mthDef, mthEnd);
}
@@ -234,12 +225,11 @@ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncSt
* Gets a CodeMetadataRange for the from CodeArea where start and end
* are InsnCodeOffsets whose offsets are monotonically increasing.
*
* @param - startPos the starting position to start searching from
* @param - mthDefPos the method node decl position enclosing the range
* @param - mthEndPos the method end position enclosing the range
* @param startPos the starting position to start searching from
* @param mthDefPos the method node decl position enclosing the range
* @param mthEndPos the method end position enclosing the range
*/
@Nullable
private CodeMetadataRange findOffsetRange(Integer startPos, Integer mthDefPos, Integer mthEndPos) {
private @Nullable CodeMetadataRange findOffsetRange(Integer startPos, Integer mthDefPos, Integer mthEndPos) {
Map.Entry<Integer, ICodeAnnotation> first = findInsnOffsetBeforePos(startPos, mthDefPos);
Map.Entry<Integer, ICodeAnnotation> second = findInsnOffsetAfterPos(startPos, mthEndPos);
if (first == null || second == null) {
@@ -256,29 +246,35 @@ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncSt
return new CodeMetadataRange(first, second);
}
@Nullable
private Map.Entry<Integer, ICodeAnnotation> findInsnOffsetBeforePos(Integer startPos, Integer limit) {
return from.getCodeMetadata().searchUp(startPos, (offset, ann) -> {
private @Nullable Map.Entry<Integer, ICodeAnnotation> findInsnOffsetBeforePos(Integer startPos, Integer limit) {
ICodeMetadata codeMetadata = from.getCodeMetadata();
if (codeMetadata == null) {
return null;
}
return codeMetadata.searchUp(startPos, (offset, ann) -> {
if (offset <= limit) {
return null;
}
if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) {
return null;
}
return new SimpleEntry<Integer, ICodeAnnotation>(offset, ann);
return new SimpleEntry<>(offset, ann);
});
}
@Nullable
private Map.Entry<Integer, ICodeAnnotation> findInsnOffsetAfterPos(Integer startPos, Integer limit) {
return from.getCodeMetadata().searchDown(startPos, (offset, ann) -> {
private @Nullable Map.Entry<Integer, ICodeAnnotation> findInsnOffsetAfterPos(Integer startPos, Integer limit) {
ICodeMetadata codeMetadata = from.getCodeMetadata();
if (codeMetadata == null) {
return null;
}
return codeMetadata.searchDown(startPos, (offset, ann) -> {
if (offset >= limit) {
return null;
}
if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) {
return null;
}
return new SimpleEntry<Integer, ICodeAnnotation>(offset, ann);
return new SimpleEntry<>(offset, ann);
});
}
@@ -298,7 +294,6 @@ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncSt
*
* @param smaliMethodNode - method of interest
* @param insnCodeOffsetRange - code offset range from the caret pos
* @return
*/
private static List<Integer> getMappedSmaliLines(
SmaliMethodNode smaliMethodNode,
@@ -1,17 +1,12 @@
package jadx.gui.ui.codearea.sync;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.SmaliArea;
/**
* Syncs a Java code panel area (Java/Simple/Fallback) to another area
*/
public class JavaSyncer implements CodePanelSyncer {
private static final Logger LOG = LoggerFactory.getLogger(JavaSyncer.class);
public class JavaSyncer implements CodeAreaSyncer {
private final DebugLineJavaSyncer debugLineSyncer;
private final InsnOffsetJavaSyncer insnOffsetSyncer;
@@ -9,7 +9,7 @@ import jadx.gui.ui.codearea.SmaliArea;
/**
* Syncs a Smali code panel area to another area
*/
public class SmaliSyncer implements CodePanelSyncer {
public class SmaliSyncer implements CodeAreaSyncer {
private static final Logger LOG = LoggerFactory.getLogger(SmaliSyncer.class);
private final SmaliArea from;
@@ -10,17 +10,16 @@ import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.CodePanel;
import jadx.gui.ui.codearea.SmaliArea;
import jadx.gui.ui.codearea.sync.CodeSyncHighlighter;
/**
* Regex/String based sync strategy of toPanel when clicking in fromPanel
* Regex/String based sync strategy of toArea when clicking in fromArea
* Summary of syncing strategy:
* 1) Look for an identifying class member token under the caret position.
* 2) If found look for the enclosing method or class declaration.
* 3) If the line is a declaration line, find the equivalent line in the other code panel.
* 4) Otherwise find the nth occurence of the token in the enclosing method/class in the other code
* 4) Otherwise find the nth occurrence of the token in the enclosing method/class in the other code
* panel.
* The following are not yet supported:
* - generic classes/methods
@@ -31,15 +30,12 @@ import jadx.gui.ui.codearea.sync.CodeSyncHighlighter;
public class FallbackSyncer {
private static final Logger LOG = LoggerFactory.getLogger(FallbackSyncer.class);
public static boolean sync(CodePanel fromPanel, CodePanel toPanel) throws BadLocationException, Exception {
public static boolean sync(AbstractCodeArea fromArea, AbstractCodeArea toArea) throws Exception {
LOG.debug("FALLBACK SYNC START");
try {
AbstractCodeArea from = fromPanel.getCodeArea();
AbstractCodeArea to = toPanel.getCodeArea();
int caretPos = from.getCaretPosition();
int lineIndex = from.getLineOfOffset(caretPos);
String[] fromLines = from.getText().split("\\R");
int caretPos = fromArea.getCaretPosition();
int lineIndex = fromArea.getLineOfOffset(caretPos);
String[] fromLines = fromArea.getText().split("\\R");
if (lineIndex >= fromLines.length) {
return false;
}
@@ -48,7 +44,7 @@ public class FallbackSyncer {
LOG.debug("Caret line [{}]: {}", caretPos, caretLine);
// Extract token under caret (string literal or identifier)
AbstractCodeAreaToken areaToken = FallbackSyncer.getToken(from, caretPos);
AbstractCodeAreaToken areaToken = FallbackSyncer.getToken(fromArea, caretPos);
String token = areaToken.getStr();
LOG.debug("Token at caret: '{}'", token);
if (token == null || token.isEmpty()) {
@@ -60,14 +56,14 @@ public class FallbackSyncer {
return false;
}
return syncToIdentifyingNthOccurence(areaToken, to);
return syncToIdentifyingNthOccurence(areaToken, toArea);
} finally {
LOG.debug("FALLBACK SYNC END");
}
}
// This function just serves as a way to create the correct Token type
// FallbackSyncer should be refactored to use CodePanelSyncer
// FallbackSyncer should be refactored to use CodeAreaSyncer
private static AbstractCodeAreaToken getToken(AbstractCodeArea from, int caretPos) throws BadLocationException, FallbackSyncException {
if (from instanceof SmaliArea) {
return new SmaliAreaToken((SmaliArea) from, caretPos);
@@ -196,7 +192,7 @@ public class FallbackSyncer {
return null;
}
// Similar with the function above if refactored to use the CodePanelSyncer Abstraction we can
// Similar with the function above if refactored to use the CodeAreaSyncer Abstraction we can
// remove this.
private static AbstractCodeAreaLine getLine(AbstractCodeArea area, int lineIndex) throws BadLocationException, FallbackSyncException {
if (area instanceof SmaliArea) {
@@ -0,0 +1,57 @@
package jadx.gui.utils.ui;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import javax.swing.event.CaretListener;
import javax.swing.text.JTextComponent;
/**
* Track attached UI listeners and remove on request
*/
public class ListenersHelper<C, L> {
public static ListenersHelper<JTextComponent, CaretListener> buildForCaretListener() {
return new ListenersHelper<>(JTextComponent::addCaretListener, JTextComponent::removeCaretListener);
}
private final Map<C, List<L>> listenerMap = new IdentityHashMap<>();
private final BiConsumer<C, L> addMth;
private final BiConsumer<C, L> removeMth;
private ListenersHelper(BiConsumer<C, L> add, BiConsumer<C, L> remove) {
this.addMth = add;
this.removeMth = remove;
}
public synchronized void add(C component, L listener) {
addMth.accept(component, listener);
listenerMap.computeIfAbsent(component, c -> new ArrayList<>()).add(listener);
}
public synchronized void removeAll() {
listenerMap.forEach((comp, list) -> {
for (L l : list) {
remove(comp, l);
}
});
listenerMap.clear();
}
public synchronized void removeFor(C component) {
List<L> list = listenerMap.get(component);
if (list != null) {
list.forEach(l -> remove(component, l));
listenerMap.remove(component);
}
}
private void remove(C component, L listener) {
removeMth.accept(component, listener);
}
}
@@ -112,7 +112,7 @@ tabs.closeAllRight=Alles rechts schließen
#tabs.closeAllLeft=Close All Left
tabs.code=Code
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
#tabs.smali_bytecode=Bytecode
nav.back=Zurück
nav.forward=Vorwärts
@@ -360,7 +360,6 @@ msg.cant_add_comment=Kann hier keinen Kommentar hinzufügen
#methods_dialog.title=Select methods
popup.bytecode_col=Dalvik-Bytecode anzeigen
popup.line_wrap=Zeilenumbruch
popup.undo=Rückgängig
popup.redo=Wiederholen
@@ -112,7 +112,7 @@ tabs.closeAllRight=Close All Right
tabs.closeAllLeft=Close All Left
tabs.code=Code
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
tabs.smali_bytecode=Bytecode
nav.back=Back
nav.forward=Forward
@@ -360,7 +360,6 @@ msg.non_displayable_chars.title=Undisplayed Strings
methods_dialog.title=Select methods
popup.bytecode_col=Show Dalvik Bytecode
popup.line_wrap=Line Wrap
popup.undo=Undo
popup.redo=Redo
@@ -112,7 +112,7 @@ tabs.closeAllRight=Cierra todo a la derecha
#tabs.closeAllLeft=Close All Left
#tabs.code=Code
#tabs.smali=Smali
#tabs.smali_bytecode=Smali+Bytecode
#tabs.smali_bytecode=Bytecode
nav.back=Atrás
nav.forward=Adelante
@@ -360,7 +360,6 @@ msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicac
#methods_dialog.title=Select methods
#popup.bytecode_col=Show Dalvik Bytecode
#popup.line_wrap=Line Wrap
popup.undo=Deshacer
popup.redo=Rehacer
@@ -112,7 +112,7 @@ tabs.closeAllRight=Tutup Semua yang Kanan
#tabs.closeAllLeft=Close All Left
tabs.code=Kode
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
#tabs.smali_bytecode=Bytecode
nav.back=Kembali
nav.forward=Maju
@@ -360,7 +360,6 @@ msg.cant_add_comment=Tidak dapat menambahkan komentar di sini
#methods_dialog.title=Select methods
popup.bytecode_col=Tampilkan Bytecode Dalvik
popup.line_wrap=Baris Wrap
popup.undo=Kembalikan
popup.redo=Ulang
@@ -112,7 +112,7 @@ tabs.closeAllRight=오른쪽의 모든 것을 닫으십시오
#tabs.closeAllLeft=Close All Left
tabs.code=코드
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
#tabs.smali_bytecode=Bytecode
nav.back=뒤로
nav.forward=앞으로
@@ -360,7 +360,6 @@ msg.cant_add_comment=여기에 주석을 추가할수 없음
#methods_dialog.title=Select methods
popup.bytecode_col=Dalvik Bytecode 보이기
popup.line_wrap=줄 바꿈
popup.undo=실행 취소
popup.redo=다시 실행
@@ -112,7 +112,7 @@ tabs.closeAllRight=Feche tudo à direita
#tabs.closeAllLeft=Close All Left
tabs.code=Código
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
#tabs.smali_bytecode=Bytecode
nav.back=Voltar
nav.forward=Avançar
@@ -360,7 +360,6 @@ msg.cant_add_comment=Não é possível adicionar comentários aqui
#methods_dialog.title=Select methods
popup.bytecode_col=Mostrar Dalvik Bytecode
popup.line_wrap=Quebra de linha
popup.undo=Desfazer
popup.redo=Refazer
@@ -112,7 +112,7 @@ tabs.closeAllRight=Закройте все справа
#tabs.closeAllLeft=Close All Left
tabs.code=Код
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
#tabs.smali_bytecode=Bytecode
nav.back=Назад
nav.forward=Вперед
@@ -360,7 +360,6 @@ msg.cant_add_comment=Невозможно добавить комментари
#methods_dialog.title=Select methods
popup.bytecode_col=Показать Dalvik байткод
popup.line_wrap=Перенос строк
popup.undo=Отменить
popup.redo=Вернуть
@@ -112,7 +112,7 @@ tabs.closeAllRight=关闭右边的所有
tabs.closeAllLeft=关闭左边的所有
tabs.code=代码
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
#tabs.smali_bytecode=Bytecode
nav.back=后退
nav.forward=前进
@@ -360,7 +360,6 @@ msg.non_displayable_chars.title=未显示的字符串
methods_dialog.title=选择方法
popup.bytecode_col=显示Dalvik字节码
popup.line_wrap=自动换行
popup.undo=撤销
popup.redo=重做
@@ -112,7 +112,7 @@ tabs.closeAllRight=關閉右邊的所有
#tabs.closeAllLeft=Close All Left
tabs.code=程式碼
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
#tabs.smali_bytecode=Bytecode
nav.back=返回
nav.forward=向前
@@ -360,7 +360,6 @@ msg.cant_add_comment=無法在此新增註解
methods_dialog.title=選擇方法
popup.bytecode_col=顯示 Dalvik 位元組碼
popup.line_wrap=自動換行
popup.undo=復原
popup.redo=重做