Files
jadx/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java
T

636 lines
18 KiB
Java

package jadx.gui.ui.tab;
import java.awt.Component;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.jobs.SilentTask;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.ui.codearea.SmaliArea;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.FontPanel;
import jadx.gui.ui.panel.HtmlPanel;
import jadx.gui.ui.panel.IViewStateSupport;
import jadx.gui.ui.panel.ImagePanel;
import jadx.gui.ui.tab.dnd.TabDndController;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.UiUtils;
public class TabbedPane extends JTabbedPane implements ITabStatesListener {
private static final long serialVersionUID = -8833600618794570904L;
private static final Logger LOG = LoggerFactory.getLogger(TabbedPane.class);
private final transient MainWindow mainWindow;
private final transient TabsController controller;
private final transient Map<JNode, ContentPanel> tabsMap = new HashMap<>();
private transient ContentPanel curTab;
private transient ContentPanel lastTab;
private transient TabDndController dnd;
public TabbedPane(MainWindow window, TabsController controller) {
this.mainWindow = window;
this.controller = controller;
controller.addListener(this);
setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
MouseAdapter clickAdapter = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
int tabIndex = indexAtLocation(e.getX(), e.getY());
if (tabIndex == -1 || tabIndex > getTabCount()) {
return;
}
TabComponent tab = (TabComponent) getTabComponentAt(tabIndex);
tab.dispatchEvent(e);
}
};
addMouseListener(clickAdapter);
addMouseWheelListener(event -> {
if (dnd != null && dnd.isDragging()) {
return;
}
int direction = event.getWheelRotation();
if (getTabCount() == 0 || direction == 0) {
return;
}
direction = (direction < 0) ? -1 : 1; // normalize direction
int index = getSelectedIndex();
int maxIndex = getTabCount() - 1;
index += direction;
index = Math.max(0, Math.min(maxIndex, index));
try {
setSelectedIndex(index);
} catch (IndexOutOfBoundsException e) {
// ignore error
}
});
interceptTabKey();
interceptCloseKey();
enableSwitchingTabs();
}
private void interceptTabKey() {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
private static final int ctrlDown = KeyEvent.CTRL_DOWN_MASK;
private long ctrlInterval = 0;
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
long cur = System.currentTimeMillis();
if (!FocusManager.isActive()) {
return false; // don't do nothing when tab is not on focus.
}
int code = e.getKeyCode();
boolean consume = code == KeyEvent.VK_TAB; // consume Tab key event anyway
boolean isReleased = e.getID() == KeyEvent.KEY_RELEASED;
if (isReleased) {
if (code == KeyEvent.VK_CONTROL) {
ctrlInterval = cur;
} else if (code == KeyEvent.VK_TAB) {
boolean doSwitch = false;
if ((e.getModifiersEx() & ctrlDown) != 0) {
doSwitch = lastTab != null && getTabCount() > 1;
} else {
// the gap of the release of ctrl and tab is very close, nearly the same time,
// but ctrl released first.
ctrlInterval = cur - ctrlInterval;
if (ctrlInterval <= 90) {
doSwitch = lastTab != null && getTabCount() > 1;
}
}
if (doSwitch) {
selectTab(lastTab);
}
}
} else if (consume && (e.getModifiersEx() & ctrlDown) == 0) {
// switch between source and smali
if (curTab instanceof ClassCodeContentPanel) {
((ClassCodeContentPanel) curTab).switchPanel();
}
}
return consume;
}
});
}
private void interceptCloseKey() {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
private static final int closeKey = KeyEvent.VK_W;
private boolean canClose = true;
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
if (!FocusManager.isActive()) {
return false; // do nothing when tab is not on focus.
}
if (e.getKeyCode() != closeKey) {
return false; // only intercept the events of the close key
}
if (e.getID() == KeyEvent.KEY_RELEASED) {
canClose = true; // after the close key is lifted we allow to use it again
return false;
}
if (e.isControlDown() && canClose) {
// close the current tab
closeCodePanel(curTab);
canClose = false; // make sure we dont close more tabs until the close key is lifted
return true;
}
return false;
}
});
}
private void enableSwitchingTabs() {
addChangeListener(e -> {
ContentPanel tab = getSelectedContentPanel();
if (tab == null) { // all closed
curTab = null;
lastTab = null;
return;
}
FocusManager.focusOnCodePanel(tab);
if (tab == curTab) { // a tab was closed by not the current one.
if (lastTab != null && indexOfComponent(lastTab) == -1) { // lastTab was closed
setLastTabAdjacentToCurTab();
}
return;
}
if (tab == lastTab) {
if (indexOfComponent(curTab) == -1) { // curTab was closed and lastTab is the current one.
curTab = lastTab;
setLastTabAdjacentToCurTab();
return;
}
// it's switching between lastTab and curTab.
}
lastTab = curTab;
curTab = tab;
});
}
private void setLastTabAdjacentToCurTab() {
if (getTabCount() < 2) {
lastTab = null;
return;
}
int idx = indexOfComponent(curTab);
if (idx == 0) {
lastTab = (ContentPanel) getComponentAt(idx + 1);
} else {
lastTab = (ContentPanel) getComponentAt(idx - 1);
}
}
public MainWindow getMainWindow() {
return mainWindow;
}
public TabsController getTabsController() {
return controller;
}
private @Nullable ContentPanel showCode(JumpPosition jumpPos) {
JNode jumpNode = jumpPos.getNode();
ContentPanel contentPanel = getTabByNode(jumpNode);
if (contentPanel == null) {
return null;
}
selectTab(contentPanel);
int pos = jumpPos.getPos();
if (pos < 0) {
LOG.warn("Invalid jump: {}", jumpPos, new JadxRuntimeException());
pos = 0;
}
contentPanel.scrollToPos(pos);
return contentPanel;
}
public void selectTab(ContentPanel contentPanel) {
controller.selectTab(contentPanel.getNode());
}
private void smaliJump(JClass cls, int pos, boolean debugMode) {
ContentPanel panel = getTabByNode(cls);
if (panel == null) {
panel = showCode(new JumpPosition(cls, 1));
if (panel == null) {
throw new JadxRuntimeException("Failed to open panel for JClass: " + cls);
}
} else {
selectTab(panel);
}
ClassCodeContentPanel codePane = (ClassCodeContentPanel) panel;
codePane.showSmaliPane();
SmaliArea smaliArea = (SmaliArea) codePane.getSmaliCodeArea();
if (debugMode) {
smaliArea.scrollToDebugPos(pos);
}
smaliArea.scrollToPos(pos);
smaliArea.requestFocus();
}
public @Nullable JumpPosition getCurrentPosition() {
ContentPanel selectedCodePanel = getSelectedContentPanel();
if (selectedCodePanel instanceof AbstractCodeContentPanel) {
AbstractCodeArea codeArea = ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea();
if (codeArea != null) {
return codeArea.getCurrentPosition();
}
}
return null;
}
private void addContentPanel(ContentPanel contentPanel) {
tabsMap.put(contentPanel.getNode(), contentPanel);
int tabCount = getTabCount();
add(contentPanel, tabCount);
setTabComponentAt(tabCount, makeTabComponent(contentPanel));
}
public void closeCodePanel(ContentPanel contentPanel) {
closeCodePanel(contentPanel, false);
}
public void closeCodePanel(ContentPanel contentPanel, boolean considerPins) {
controller.closeTab(contentPanel.getNode(), considerPins);
}
public List<ContentPanel> getTabs() {
List<ContentPanel> list = new ArrayList<>(getTabCount());
for (int i = 0; i < getTabCount(); i++) {
list.add((ContentPanel) getComponentAt(i));
}
return list;
}
public @Nullable ContentPanel getTabByNode(JNode node) {
return tabsMap.get(node);
}
public @Nullable TabComponent getTabComponentByNode(JNode node) {
ContentPanel contentPanel = getTabByNode(node);
if (contentPanel == null) {
return null;
}
int index = indexOfComponent(contentPanel);
if (index == -1) {
return null;
}
Component component = getTabComponentAt(index);
if (!(component instanceof TabComponent)) {
return null;
}
return (TabComponent) component;
}
public void refresh(JNode node) {
ContentPanel panel = getTabByNode(node);
if (panel != null) {
setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel));
fireStateChanged();
}
}
public void reloadInactiveTabs() {
UiUtils.uiThreadGuard();
int tabCount = getTabCount();
if (tabCount == 1) {
return;
}
int current = getSelectedIndex();
for (int i = 0; i < tabCount; i++) {
if (i == current) {
continue;
}
ContentPanel oldPanel = (ContentPanel) getComponentAt(i);
TabBlueprint tab = controller.getTabByNode(oldPanel.getNode());
if (tab == null) {
continue;
}
EditorViewState viewState = controller.getEditorViewState(tab);
JNode node = oldPanel.getNode();
ContentPanel panel = node.getContentPanel(this);
FocusManager.listen(panel);
tabsMap.put(node, panel);
setComponentAt(i, panel);
setTabComponentAt(i, makeTabComponent(panel));
controller.restoreEditorViewState(viewState);
}
fireStateChanged();
}
@Nullable
public ContentPanel getSelectedContentPanel() {
return (ContentPanel) getSelectedComponent();
}
private Component makeTabComponent(final ContentPanel contentPanel) {
return new TabComponent(this, contentPanel);
}
public void closeAllTabs() {
closeAllTabs(false);
}
public void closeAllTabs(boolean considerPins) {
for (ContentPanel panel : getTabs()) {
closeCodePanel(panel, considerPins);
}
}
public void loadSettings() {
for (int i = 0; i < getTabCount(); i++) {
((ContentPanel) getComponentAt(i)).loadSettings();
((TabComponent) getTabComponentAt(i)).loadSettings();
}
}
public void reset() {
closeAllTabs();
tabsMap.clear();
curTab = null;
lastTab = null;
FocusManager.reset();
}
@Nullable
public Component getFocusedComp() {
return FocusManager.getFocusedComp();
}
public TabDndController getDnd() {
return dnd;
}
public void setDnd(TabDndController dnd) {
this.dnd = dnd;
}
@Override
public void onTabOpen(TabBlueprint blueprint) {
if (blueprint.isHidden()) {
return;
}
JNode node = blueprint.getNode();
ContentPanel newPanel = node.getContentPanel(this);
if (newPanel != null) {
if (node != newPanel.getNode()) {
throw new JadxRuntimeException("Incorrect node found in content panel");
}
FocusManager.listen(newPanel);
addContentPanel(newPanel);
blueprint.setCreated(true);
}
}
@Override
public void onTabSelect(TabBlueprint blueprint) {
ContentPanel contentPanel = getTabByNode(blueprint.getNode());
if (contentPanel != null) {
setSelectedComponent(contentPanel);
}
}
@Override
public void onTabCodeJump(TabBlueprint blueprint, @Nullable JumpPosition prevPos, JumpPosition position) {
// queue task to wait completion of loading tasks
mainWindow.getBackgroundExecutor().execute(new SilentTask(() -> showCode(position)));
}
@Override
public void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) {
JNode node = blueprint.getNode();
if (node instanceof JClass) {
smaliJump((JClass) node, pos, debugMode);
}
}
@Override
public void onTabClose(TabBlueprint blueprint) {
ContentPanel contentPanelToClose = getTabByNode(blueprint.getNode());
if (contentPanelToClose == null) {
return;
}
ContentPanel currentContentPanel = getSelectedContentPanel();
if (currentContentPanel == contentPanelToClose) {
if (lastTab != null && lastTab.getNode() != null) {
selectTab(lastTab);
} else if (getTabCount() > 1) {
int removalIdx = indexOfComponent(contentPanelToClose);
if (removalIdx > 0) { // select left tab
setSelectedIndex(removalIdx - 1);
} else if (removalIdx == 0) { // select right tab
setSelectedIndex(removalIdx + 1);
}
}
}
tabsMap.remove(contentPanelToClose.getNode());
remove(contentPanelToClose);
contentPanelToClose.dispose();
}
@Override
public void onTabPositionFirst(TabBlueprint blueprint) {
ContentPanel contentPanel = getTabByNode(blueprint.getNode());
if (contentPanel == null) {
return;
}
setTabPosition(contentPanel, 0);
}
private void setTabPosition(ContentPanel contentPanel, int position) {
TabComponent tabComponent = getTabComponentByNode(contentPanel.getNode());
if (tabComponent == null) {
return;
}
boolean restoreSelection = contentPanel == getSelectedContentPanel();
remove(contentPanel);
add(contentPanel, position);
setTabComponentAt(position, tabComponent);
if (restoreSelection) {
setSelectedIndex(position);
}
}
@Override
public void onTabPinChange(TabBlueprint blueprint) {
TabComponent tabComponent = getTabComponentByNode(blueprint.getNode());
if (tabComponent == null) {
return;
}
tabComponent.update();
}
@Override
public void onTabBookmarkChange(TabBlueprint blueprint) {
TabComponent tabComponent = getTabComponentByNode(blueprint.getNode());
if (tabComponent == null) {
return;
}
tabComponent.update();
}
@Override
public void onTabVisibilityChange(TabBlueprint blueprint) {
if (!blueprint.isHidden() && !tabsMap.containsKey(blueprint.getNode())) {
onTabOpen(blueprint);
}
if (blueprint.isHidden() && tabsMap.containsKey(blueprint.getNode())) {
onTabClose(blueprint);
}
}
@Override
public void onTabPreviewChange(TabBlueprint blueprint) {
TabComponent tabComponent = getTabComponentByNode(blueprint.getNode());
if (tabComponent == null) {
return;
}
tabComponent.update();
}
@Override
public void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) {
ContentPanel contentPanel = getTabByNode(blueprint.getNode());
if (contentPanel instanceof IViewStateSupport) {
((IViewStateSupport) contentPanel).restoreEditorViewState(viewState);
}
}
@Override
public void onTabsReorder(List<TabBlueprint> blueprints) {
List<TabBlueprint> newBlueprints = new ArrayList<>(blueprints.size());
for (ContentPanel contentPanel : getTabs()) {
TabBlueprint blueprint = controller.getTabByNode(contentPanel.getNode());
if (blueprint != null) {
newBlueprints.add(blueprint);
}
}
// Add back hidden tabs
Set<TabBlueprint> set = new LinkedHashSet<>(blueprints);
newBlueprints.forEach(set::remove);
newBlueprints.addAll(set);
blueprints.clear();
blueprints.addAll(newBlueprints);
}
@Override
public void onTabSave(TabBlueprint blueprint, EditorViewState viewState) {
ContentPanel contentPanel = getTabByNode(blueprint.getNode());
if (contentPanel instanceof IViewStateSupport) {
((IViewStateSupport) contentPanel).saveEditorViewState(viewState);
}
}
private static class FocusManager implements FocusListener {
private static final FocusManager INSTANCE = new FocusManager();
private static @Nullable Component focusedComp;
static boolean isActive() {
return focusedComp != null;
}
static void reset() {
focusedComp = null;
}
static Component getFocusedComp() {
return focusedComp;
}
@Override
public void focusGained(FocusEvent e) {
focusedComp = (Component) e.getSource();
}
@Override
public void focusLost(FocusEvent e) {
focusedComp = null;
}
static void listen(ContentPanel pane) {
if (pane instanceof ClassCodeContentPanel) {
((ClassCodeContentPanel) pane).getCodeArea().addFocusListener(INSTANCE);
((ClassCodeContentPanel) pane).getSmaliCodeArea().addFocusListener(INSTANCE);
return;
}
if (pane instanceof AbstractCodeContentPanel) {
((AbstractCodeContentPanel) pane).getChildrenComponent().addFocusListener(INSTANCE);
return;
}
if (pane instanceof HtmlPanel) {
((HtmlPanel) pane).getHtmlArea().addFocusListener(INSTANCE);
return;
}
if (pane instanceof ImagePanel) {
pane.addFocusListener(INSTANCE);
return;
}
if (pane instanceof FontPanel) {
pane.addFocusListener(INSTANCE);
return;
}
// throw new JadxRuntimeException("Add the new ContentPanel to TabbedPane.FocusManager: " + pane);
}
static void focusOnCodePanel(ContentPanel pane) {
if (pane instanceof ClassCodeContentPanel) {
SwingUtilities.invokeLater(() -> ((ClassCodeContentPanel) pane).getCurrentCodeArea().requestFocus());
return;
}
if (pane instanceof AbstractCodeContentPanel) {
SwingUtilities.invokeLater(() -> ((AbstractCodeContentPanel) pane).getChildrenComponent().requestFocus());
return;
}
if (pane instanceof HtmlPanel) {
SwingUtilities.invokeLater(() -> ((HtmlPanel) pane).getHtmlArea().requestFocusInWindow());
return;
}
if (pane instanceof ImagePanel) {
SwingUtilities.invokeLater(((ImagePanel) pane)::requestFocusInWindow);
return;
}
if (pane instanceof FontPanel) {
SwingUtilities.invokeLater(((FontPanel) pane)::requestFocusInWindow);
return;
}
// throw new JadxRuntimeException("Add the new ContentPanel to TabbedPane.FocusManager: " + pane);
}
}
}