feat(gui): tabs drag and drop reorder support (#1212) (PR #2109)

This commit is contained in:
Andrei Kudryavtsev
2024-02-26 00:36:46 +05:00
committed by GitHub
parent d51362ed50
commit d7ec35791b
32 changed files with 835 additions and 28 deletions
@@ -27,10 +27,10 @@ import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JRenameNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.NLS;
@@ -18,9 +18,9 @@ import jadx.core.utils.files.FileUtils;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JEditableNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.SimpleMenuItem;
@@ -30,10 +30,10 @@ import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JRoot;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.ActionHandler;
@@ -19,9 +19,9 @@ import jadx.api.ICodeInfo;
import jadx.api.impl.SimpleCodeInfo;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.HtmlPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.UiUtils;
public class QuarkReportNode extends JNode {
@@ -41,8 +41,8 @@ import jadx.core.utils.Utils;
import jadx.gui.JadxWrapper;
import jadx.gui.treemodel.JMethod;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.ui.NodeLabel;
@@ -28,12 +28,12 @@ import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.action.ActionModel;
import jadx.gui.ui.action.JadxGuiAction;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
import jadx.gui.ui.codearea.SearchBar;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -21,9 +21,9 @@ import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.impl.SimpleCodeInfo;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.HtmlPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.CertificateManager;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -23,10 +23,10 @@ import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ICodeNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
import jadx.gui.ui.dialog.RenameDialog;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
@@ -17,8 +17,8 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.plugins.script.ScriptContentPanel;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.SimpleMenuItem;
@@ -16,8 +16,8 @@ import jadx.api.ICodeInfo;
import jadx.api.JavaNode;
import jadx.api.metadata.ICodeNodeRef;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
public abstract class JNode extends DefaultMutableTreeNode implements Comparable<JNode> {
@@ -20,11 +20,11 @@ import jadx.api.impl.SimpleCodeInfo;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.xmlgen.ResContainer;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.BinaryContentPanel;
import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.ImagePanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -140,6 +140,8 @@ import jadx.gui.ui.panel.IssuesPanel;
import jadx.gui.ui.panel.JDebuggerPanel;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.ui.popupmenu.RecentProjectsMenuListener;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.tab.dnd.TabDndController;
import jadx.gui.ui.treenodes.StartPageNode;
import jadx.gui.ui.treenodes.SummaryNode;
import jadx.gui.update.JadxUpdate;
@@ -1304,6 +1306,7 @@ public class MainWindow extends JFrame {
tabbedPane = new TabbedPane(this);
tabbedPane.setMinimumSize(new Dimension(150, 150));
new TabDndController(tabbedPane);
rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
rightSplitPane.setTopComponent(tabbedPane);
@@ -1,8 +1,8 @@
package jadx.gui.ui.codearea;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
/**
* The abstract base class for a content panel that show text based code or a.g. a resource
@@ -10,7 +10,7 @@ import javax.swing.border.EmptyBorder;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.tab.TabbedPane;
public class BinaryContentPanel extends AbstractCodeContentPanel {
private final transient CodePanel textCodePanel;
@@ -17,9 +17,9 @@ import org.slf4j.LoggerFactory;
import jadx.api.DecompilationMode;
import jadx.gui.jobs.BackgroundExecutor;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.mode.JCodeMode;
import jadx.gui.ui.panel.IViewStateSupport;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.NLS;
import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_TRAILING_COMPONENT;
@@ -7,8 +7,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.IViewStateSupport;
import jadx.gui.ui.tab.TabbedPane;
public final class CodeContentPanel extends AbstractCodeContentPanel implements IViewStateSupport {
private static final long serialVersionUID = 5310536092010045565L;
@@ -50,9 +50,9 @@ import jadx.gui.logs.LogOptions;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResSearchNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.JumpPosition;
@@ -7,7 +7,7 @@ import org.jetbrains.annotations.Nullable;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.tab.TabbedPane;
public abstract class ContentPanel extends JPanel {
@@ -10,7 +10,7 @@ import javax.swing.JScrollPane;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.ui.ZoomActions;
public final class HtmlPanel extends ContentPanel {
@@ -17,8 +17,8 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResContainer;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.tab.TabbedPane;
public class ImagePanel extends ContentPanel {
private static final long serialVersionUID = 4071356367073142688L;
@@ -18,7 +18,7 @@ import javax.swing.border.TitledBorder;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.treenodes.StartPageNode;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
@@ -1,7 +1,12 @@
package jadx.gui.ui;
package jadx.gui.ui.tab;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Point;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;
@@ -20,6 +25,7 @@ import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JEditableNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.dnd.TabDndGestureListener;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -42,6 +48,9 @@ public class TabComponent extends JPanel {
public void loadSettings() {
label.setFont(getLabelFont());
if (tabbedPane.getDnd() != null) {
tabbedPane.getDnd().loadSettings();
}
}
private Font getLabelFont() {
@@ -57,7 +66,7 @@ public class TabComponent extends JPanel {
label.setFont(getLabelFont());
String toolTip = contentPanel.getTabTooltip();
if (toolTip != null) {
label.setToolTipText(toolTip);
setToolTipText(toolTip);
}
label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10));
label.setIcon(node.getIcon());
@@ -93,14 +102,28 @@ public class TabComponent extends JPanel {
}
};
addMouseListener(clickAdapter);
label.addMouseListener(clickAdapter);
closeBtn.addMouseListener(clickAdapter);
addListenerForDnd();
add(label);
add(closeBtn);
setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
}
private void addListenerForDnd() {
if (tabbedPane.getDnd() == null) {
return;
}
TabComponent comp = this;
DragGestureListener dgl = new TabDndGestureListener(tabbedPane.getDnd()) {
@Override
protected Point getDragOrigin(DragGestureEvent e) {
return SwingUtilities.convertPoint(comp, e.getDragOrigin(), tabbedPane);
}
};
DragSource.getDefaultDragSource()
.createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY_OR_MOVE, dgl);
}
private String buildTabTitle(JNode node) {
String tabTitle;
if (node.getRootClass() != null) {
@@ -1,4 +1,4 @@
package jadx.gui.ui;
package jadx.gui.ui.tab;
import java.awt.Component;
import java.awt.KeyEventDispatcher;
@@ -26,6 +26,7 @@ import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.core.utils.exceptions.JadxRuntimeException;
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;
@@ -35,6 +36,7 @@ import jadx.gui.ui.panel.ContentPanel;
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.JumpManager;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
@@ -52,12 +54,17 @@ public class TabbedPane extends JTabbedPane {
private transient ContentPanel curTab;
private transient ContentPanel lastTab;
TabbedPane(MainWindow window) {
private transient TabDndController dnd;
public TabbedPane(MainWindow window) {
this.mainWindow = window;
setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
addMouseWheelListener(event -> {
if (dnd != null && dnd.isDragging()) {
return;
}
int direction = event.getWheelRotation();
if (getTabCount() == 0 || direction == 0) {
return;
@@ -483,6 +490,14 @@ public class TabbedPane extends JTabbedPane {
return FocusManager.getFocusedComp();
}
public TabDndController getDnd() {
return dnd;
}
public void setDnd(TabDndController dnd) {
this.dnd = dnd;
}
private static class FocusManager implements FocusListener {
private static final FocusManager INSTANCE = new FocusManager();
private static @Nullable Component focusedComp;
@@ -0,0 +1,323 @@
/*
* The MIT License (MIT)
* Copyright (c) 2015 TERAI Atsuhiro
* Copyright (c) 2024 Skylot
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package jadx.gui.ui.tab.dnd;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragSource;
import java.awt.dnd.DropTarget;
import java.awt.image.BufferedImage;
import java.util.Objects;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.plaf.metal.MetalTabbedPaneUI;
import jadx.gui.ui.tab.TabbedPane;
public class TabDndController {
private final transient JTabbedPane pane;
private static final int DROP_TARGET_MARK_SIZE = 4;
private static final int SCROLL_AREA_SIZE = 30;
private static final int SCROLL_AREA_EXTRA = 30; // Making area with scroll buttons a bit bigger.
private static final String ACTION_SCROLL_FORWARD = "scrollTabsForwardAction";
private static final String ACTION_SCROLL_BACKWARD = "scrollTabsBackwardAction";
private final transient TabDndGhostPane tabDndGhostPane;
protected int dragTabIndex = -1;
protected boolean drawGhost = true; // Semi-transparent tab copy moving along with cursor.
protected boolean paintScrollTriggerAreas = false; // For debug purposes.
protected Rectangle rectBackward = new Rectangle();
protected Rectangle rectForward = new Rectangle();
private boolean isDragging = false;
public TabDndController(TabbedPane pane) {
pane.setDnd(this);
this.pane = pane;
tabDndGhostPane = new TabDndGhostPane(this);
new DropTarget(tabDndGhostPane, DnDConstants.ACTION_COPY_OR_MOVE, new TabDndTargetListener(this), true);
DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(pane,
DnDConstants.ACTION_COPY_OR_MOVE,
new TabDndGestureListener(this));
}
public static boolean isHorizontalTabPlacement(int tabPlacement) {
return tabPlacement == JTabbedPane.TOP || tabPlacement == JTabbedPane.BOTTOM;
}
/**
* Check if dragging near edges and scroll to according
* direction through programmatically clicking system's scroll buttons.
*
* @param glassPt Cursor position in TabbedPane coordinates.
*/
public void scrollIfNeeded(Point glassPt) {
Rectangle r = getTabAreaBounds();
boolean isHorizontal = isHorizontalTabPlacement(pane.getTabPlacement());
// Trying to avoid calculating two directions simultaneously. Forward first.
if (isHorizontal) {
rectForward.setBounds(r.x + r.width - SCROLL_AREA_SIZE - SCROLL_AREA_EXTRA,
r.y,
SCROLL_AREA_SIZE + SCROLL_AREA_EXTRA,
r.height);
} else {
rectForward.setBounds(r.x,
r.y + r.height - SCROLL_AREA_SIZE - SCROLL_AREA_EXTRA,
r.width,
SCROLL_AREA_SIZE + SCROLL_AREA_EXTRA);
}
rectForward = SwingUtilities.convertRectangle(pane.getParent(), rectForward, tabDndGhostPane);
if (rectForward.contains(glassPt)) {
clickScrollButton(ACTION_SCROLL_FORWARD);
}
// Backward.
if (isHorizontal) {
rectBackward.setBounds(r.x, r.y, SCROLL_AREA_SIZE, r.height);
} else {
rectBackward.setBounds(r.x, r.y, r.width, SCROLL_AREA_SIZE);
}
rectBackward = SwingUtilities.convertRectangle(pane.getParent(), rectBackward, tabDndGhostPane);
if (rectBackward.contains(glassPt)) {
clickScrollButton(ACTION_SCROLL_BACKWARD);
}
}
private void clickScrollButton(String actionKey) {
JButton forwardButton = null;
JButton backwardButton = null;
for (Component c : pane.getComponents()) {
if (c instanceof JButton) {
if (Objects.isNull(forwardButton)) {
forwardButton = (JButton) c;
} else {
backwardButton = (JButton) c;
break;
}
}
}
JButton scrollButton = ACTION_SCROLL_FORWARD.equals(actionKey) ? forwardButton : backwardButton;
if (scrollButton != null && scrollButton.isEnabled()) {
scrollButton.doClick();
}
}
/**
* Finds the tab index by cursor position. If tabs first half contains the point,
* then its index is returned. Second half means inserting at next index.
*
* @param glassPt Cursor position in TabbedPane coordinates.
* @return Tab index.
*/
protected int getTargetTabIndex(Point glassPt) {
Point tabPt = SwingUtilities.convertPoint(tabDndGhostPane, glassPt, pane);
boolean isHorizontal = isHorizontalTabPlacement(pane.getTabPlacement());
for (int i = 0; i < pane.getTabCount(); ++i) {
Rectangle r = pane.getBoundsAt(i);
// First half.
if (isHorizontal) {
r.width = r.width / 2 + 1;
} else {
r.height = r.height / 2 + 1;
}
if (r.contains(tabPt)) {
return i;
}
// Second half.
if (isHorizontal) {
r.x = r.x + r.width;
} else {
r.y = r.y + r.height;
}
if (r.contains(tabPt)) {
return i + 1;
}
}
int count = pane.getTabCount();
if (count == 0) {
return -1;
}
Rectangle lastRect = pane.getBoundsAt(count - 1);
Point d = isHorizontal ? new Point(1, 0) : new Point(0, 1);
lastRect.translate(lastRect.width * d.x, lastRect.height * d.y);
return lastRect.contains(tabPt) ? count : -1;
}
protected void swapTabs(int oldIdx, int newIdx) {
if (newIdx < 0 || oldIdx == newIdx) {
return;
}
final Component cmp = pane.getComponentAt(oldIdx);
final Component tab = pane.getTabComponentAt(oldIdx);
final String title = pane.getTitleAt(oldIdx);
final Icon icon = pane.getIconAt(oldIdx);
final String tip = pane.getToolTipTextAt(oldIdx);
final boolean isEnabled = pane.isEnabledAt(oldIdx);
newIdx = oldIdx > newIdx ? newIdx : (newIdx - 1);
pane.remove(oldIdx);
pane.insertTab(title, icon, cmp, tip, newIdx);
pane.setEnabledAt(newIdx, isEnabled);
if (isEnabled) {
pane.setSelectedIndex(newIdx);
}
pane.setTabComponentAt(newIdx, tab);
}
protected void updateTargetMark(int tabIdx) {
boolean isSideNeighbor = tabIdx < 0 || dragTabIndex == tabIdx || tabIdx == dragTabIndex + 1;
if (isSideNeighbor) {
tabDndGhostPane.setTargetRect(0, 0, 0, 0);
return;
}
Rectangle boundsRect = pane.getBoundsAt(Math.max(0, tabIdx - 1));
final Rectangle r = SwingUtilities.convertRectangle(pane, boundsRect, tabDndGhostPane);
int a = Math.min(tabIdx, 1);
if (isHorizontalTabPlacement(pane.getTabPlacement())) {
tabDndGhostPane.setTargetRect(r.x + r.width * a - DROP_TARGET_MARK_SIZE / 2,
r.y,
DROP_TARGET_MARK_SIZE,
r.height);
} else {
tabDndGhostPane.setTargetRect(r.x,
r.y + r.height * a - DROP_TARGET_MARK_SIZE / 2,
r.width,
DROP_TARGET_MARK_SIZE);
}
}
protected void initGlassPane(Point tabPt) {
pane.getRootPane().setGlassPane(tabDndGhostPane);
if (drawGhost) {
Component c = pane.getTabComponentAt(dragTabIndex);
if (c == null) {
return;
}
Dimension d = c.getPreferredSize();
switch (tabDndGhostPane.getGhostType()) {
case IMAGE: {
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice device = env.getDefaultScreenDevice();
GraphicsConfiguration config = device.getDefaultConfiguration();
BufferedImage image = config.createCompatibleImage(d.width, d.height, BufferedImage.TRANSLUCENT);
Graphics2D g2 = image.createGraphics();
SwingUtilities.paintComponent(g2, c, tabDndGhostPane, 0, 0, d.width, d.height);
g2.dispose();
tabDndGhostPane.setGhostImage(image);
pane.setTabComponentAt(dragTabIndex, c);
break;
}
case COLORFUL_RECT: {
tabDndGhostPane.setGhostSize(d);
break;
}
case NONE:
break;
}
}
Point glassPt = SwingUtilities.convertPoint(pane, tabPt, tabDndGhostPane);
tabDndGhostPane.setPoint(glassPt);
tabDndGhostPane.setVisible(true);
}
protected Rectangle getTabAreaBounds() {
Rectangle tabbedRect = pane.getBounds();
Rectangle compRect;
if (pane.getSelectedComponent() != null) {
compRect = pane.getSelectedComponent().getBounds();
} else {
compRect = new Rectangle();
}
int tabPlacement = pane.getTabPlacement();
if (isHorizontalTabPlacement(tabPlacement)) {
tabbedRect.height = tabbedRect.height - compRect.height;
if (tabPlacement == JTabbedPane.BOTTOM) {
tabbedRect.y += compRect.y + compRect.height;
}
} else {
tabbedRect.width = tabbedRect.width - compRect.width;
if (tabPlacement == JTabbedPane.RIGHT) {
tabbedRect.x += compRect.x + compRect.width;
}
}
tabbedRect.grow(2, 2);
return tabbedRect;
}
public void onPaintGlassPane(Graphics2D g) {
boolean isScrollLayout = pane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT;
if (isScrollLayout && paintScrollTriggerAreas) {
g.setPaint(tabDndGhostPane.getColor());
g.fill(rectBackward);
g.fill(rectForward);
}
}
public boolean onStartDrag(Point pt) {
setDragging(true);
int idx = pane.indexAtLocation(pt.x, pt.y);
int selIdx = pane.getSelectedIndex();
boolean isTabRunsRotated =
!(pane.getUI() instanceof MetalTabbedPaneUI) && pane.getTabLayoutPolicy() == JTabbedPane.WRAP_TAB_LAYOUT && idx != selIdx;
dragTabIndex = isTabRunsRotated ? selIdx : idx;
if (dragTabIndex >= 0 && pane.isEnabledAt(dragTabIndex)) {
initGlassPane(pt);
return true;
}
return false;
}
public void loadSettings() {
tabDndGhostPane.loadSettings();
}
public boolean isDragging() {
return isDragging;
}
public void setDragging(boolean dragging) {
isDragging = dragging;
}
public TabDndGhostPane getDndGhostPane() {
return tabDndGhostPane;
}
}
@@ -0,0 +1,54 @@
/*
* The MIT License (MIT)
* Copyright (c) 2015 TERAI Atsuhiro
* Copyright (c) 2024 Skylot
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package jadx.gui.ui.tab.dnd;
import java.awt.Point;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.InvalidDnDOperationException;
public class TabDndGestureListener implements DragGestureListener {
private final transient TabDndController dnd;
public TabDndGestureListener(TabDndController dnd) {
this.dnd = dnd;
}
@Override
public void dragGestureRecognized(DragGestureEvent e) {
Point tabPt = getDragOrigin(e);
if (!dnd.onStartDrag(tabPt)) {
return;
}
try {
e.startDrag(DragSource.DefaultMoveDrop, new TabDndTransferable(), new TabDndSourceListener(dnd));
} catch (InvalidDnDOperationException ex) {
throw new IllegalStateException(ex);
}
}
protected Point getDragOrigin(DragGestureEvent e) {
return e.getDragOrigin();
}
}
@@ -0,0 +1,153 @@
/*
* The MIT License (MIT)
* Copyright (c) 2015 TERAI Atsuhiro
* Copyright (c) 2024 Skylot
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package jadx.gui.ui.tab.dnd;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;
import javax.swing.UIManager;
public class TabDndGhostPane extends JComponent {
private final TabDndController dnd;
private final Rectangle lineRect = new Rectangle();
private final Point location = new Point();
private transient BufferedImage ghostImage;
private TabDndGhostType tabDndGhostType = TabDndGhostType.COLORFUL_RECT;
private Dimension ghostSize;
private Color ghostColor;
private Insets insets;
protected TabDndGhostPane(TabDndController dnd) {
super();
this.dnd = dnd;
loadSettings();
}
public void loadSettings() {
Color systemColor = UIManager.getColor("Component.focusColor");
Color fallbackColor = new Color(0, 100, 255);
ghostColor = systemColor != null ? systemColor : fallbackColor;
Insets ins = UIManager.getInsets("TabbedPane.tabInsets");
insets = ins != null ? ins : new Insets(0, 0, 0, 0);
}
public void setTargetRect(int x, int y, int width, int height) {
lineRect.setBounds(x, y, width, height);
}
public void setGhostImage(BufferedImage ghostImage) {
this.ghostImage = ghostImage;
}
public void setGhostSize(Dimension ghostSize) {
ghostSize.setSize(ghostSize.width + insets.left + insets.right, ghostSize.height + insets.top + insets.bottom);
this.ghostSize = ghostSize;
}
public void setGhostType(TabDndGhostType tabDndGhostType) {
this.tabDndGhostType = tabDndGhostType;
}
public TabDndGhostType getGhostType() {
return this.tabDndGhostType;
}
public void setColor(Color color) {
this.ghostColor = color;
}
public Color getColor() {
return this.ghostColor;
}
public void setPoint(Point pt) {
this.location.setLocation(pt);
}
@Override
public boolean isOpaque() {
return false;
}
@Override
public void setVisible(boolean v) {
super.setVisible(v);
if (!v) {
setTargetRect(0, 0, 0, 0);
setGhostImage(null);
setGhostSize(new Dimension());
}
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
dnd.onPaintGlassPane(g2);
renderMark(g2);
renderGhost(g2);
g2.dispose();
}
private void renderGhost(Graphics2D g) {
switch (tabDndGhostType) {
case IMAGE: {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
if (ghostImage == null) {
return;
}
double x = location.getX() - ghostImage.getWidth(this) / 2d;
double y = location.getY() - ghostImage.getHeight(this) / 2d;
g.drawImage(ghostImage, (int) x, (int) y, this);
break;
}
case COLORFUL_RECT: {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .2f));
if (ghostSize == null) {
return;
}
double x = location.getX() - ghostSize.getWidth() / 2d;
double y = location.getY() - ghostSize.getHeight() / 2d;
g.setPaint(ghostColor);
g.fillRect((int) x, (int) y, ghostSize.width, ghostSize.height);
break;
}
case NONE:
break;
}
}
private void renderMark(Graphics2D g) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .7f));
g.setPaint(ghostColor);
g.fill(lineRect);
}
}
@@ -0,0 +1,19 @@
package jadx.gui.ui.tab.dnd;
public enum TabDndGhostType {
/**
* Bitmap is rendered from tabs component and dragged along with cursor.
* May be impactful on performance.
*/
IMAGE,
/**
* Colored rect of tabs size is dragged along with cursor.
*/
COLORFUL_RECT,
/**
* Only insert mark is rendered.
*/
NONE,
}
@@ -0,0 +1,71 @@
/*
* The MIT License (MIT)
* Copyright (c) 2015 TERAI Atsuhiro
* Copyright (c) 2024 Skylot
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package jadx.gui.ui.tab.dnd;
import java.awt.Component;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import javax.swing.JComponent;
import javax.swing.JRootPane;
class TabDndSourceListener implements DragSourceListener {
private final transient TabDndController dnd;
public TabDndSourceListener(TabDndController dnd) {
this.dnd = dnd;
}
@Override
public void dragEnter(DragSourceDragEvent e) {
e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
}
@Override
public void dragExit(DragSourceEvent e) {
e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
}
@Override
public void dragOver(DragSourceDragEvent e) {
}
@Override
public void dragDropEnd(DragSourceDropEvent e) {
dnd.setDragging(false);
Component c = e.getDragSourceContext().getComponent();
if (c instanceof JComponent) {
JRootPane rp = ((JComponent) c).getRootPane();
if (rp.getGlassPane() != null) {
rp.getGlassPane().setVisible(false);
}
}
}
@Override
public void dropActionChanged(DragSourceDragEvent e) {
}
}
@@ -0,0 +1,102 @@
/*
* The MIT License (MIT)
* Copyright (c) 2015 TERAI Atsuhiro
* Copyright (c) 2024 Skylot
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package jadx.gui.ui.tab.dnd;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
class TabDndTargetListener implements DropTargetListener {
private static final Point HIDDEN_POINT = new Point(0, -1000);
private final transient TabDndController dnd;
public TabDndTargetListener(TabDndController dnd) {
this.dnd = dnd;
}
@Override
public void dragEnter(DropTargetDragEvent e) {
TabDndGhostPane pane = dnd.getDndGhostPane();
if (pane == null || e.getDropTargetContext().getComponent() != pane) {
return;
}
Transferable t = e.getTransferable();
DataFlavor[] f = e.getCurrentDataFlavors();
if (t.isDataFlavorSupported(f[0])) {
e.acceptDrag(e.getDropAction());
} else {
e.rejectDrag();
}
}
@Override
public void dragExit(DropTargetEvent e) {
TabDndGhostPane pane = dnd.getDndGhostPane();
if (pane == null || e.getDropTargetContext().getComponent() != pane) {
return;
}
pane.setPoint(HIDDEN_POINT);
pane.setTargetRect(0, 0, 0, 0);
pane.repaint();
}
@Override
public void dropActionChanged(DropTargetDragEvent e) {
}
@Override
public void dragOver(DropTargetDragEvent e) {
TabDndGhostPane pane = dnd.getDndGhostPane();
if (pane == null || e.getDropTargetContext().getComponent() != pane) {
return;
}
Point glassPt = e.getLocation();
dnd.updateTargetMark(dnd.getTargetTabIndex(glassPt));
dnd.scrollIfNeeded(glassPt); // backward and forward scrolling
pane.setPoint(glassPt);
pane.repaint();
}
@Override
public void drop(DropTargetDropEvent e) {
TabDndGhostPane pane = dnd.getDndGhostPane();
if (pane == null || e.getDropTargetContext().getComponent() != pane) {
return;
}
Transferable t = e.getTransferable();
DataFlavor[] f = t.getTransferDataFlavors();
int oldIdx = dnd.dragTabIndex;
int newIdx = dnd.getTargetTabIndex(e.getLocation());
if (t.isDataFlavorSupported(f[0]) && oldIdx != newIdx) {
dnd.swapTabs(oldIdx, newIdx);
e.dropComplete(true);
} else {
e.dropComplete(false);
}
pane.setVisible(false);
}
}
@@ -0,0 +1,44 @@
/*
* The MIT License (MIT)
* Copyright (c) 2015 TERAI Atsuhiro
* Copyright (c) 2024 Skylot
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package jadx.gui.ui.tab.dnd;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
class TabDndTransferable implements Transferable {
private static final String NAME = "Transferable Tab";
@Override
public Object getTransferData(DataFlavor flavor) {
return this;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] { new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType, NAME) };
}
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return NAME.equals(flavor.getHumanPresentableName());
}
}
@@ -4,9 +4,9 @@ import javax.swing.Icon;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.StartPagePanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
@@ -29,9 +29,9 @@ import jadx.gui.JadxWrapper;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.HtmlPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.UiUtils;
public class SummaryNode extends JNode {