feat(gui): add Preview Tab Feature (PR #2474)

* feat(gui): created simple preview mode (#756)

* feat(gui): fixed opening code preview tab, if open from available tabs

* fix(gui): rollback mouse events for tree
This commit is contained in:
Yaroslav
2025-05-04 22:17:00 +03:00
committed by GitHub
parent e31d697cd9
commit 47647bbb9a
14 changed files with 106 additions and 22 deletions
@@ -104,6 +104,7 @@ public class JadxSettings extends JadxCLIArgs {
private boolean showHeapUsageBar = false;
private boolean alwaysSelectOpened = false;
private boolean enablePreviewTab = false;
private boolean useAlternativeFileDialog = false;
private Map<String, WindowLocation> windowPos = new HashMap<>();
@@ -319,6 +320,15 @@ public class JadxSettings extends JadxCLIArgs {
partialSync(settings -> settings.alwaysSelectOpened = alwaysSelectOpened);
}
public boolean isEnablePreviewTab() {
return enablePreviewTab;
}
public void setEnablePreviewTab(boolean enablePreviewTab) {
this.enablePreviewTab = enablePreviewTab;
partialSync(settings -> settings.enablePreviewTab = enablePreviewTab);
}
public boolean isUseAlternativeFileDialog() {
return useAlternativeFileDialog;
}
@@ -878,11 +878,11 @@ public class MainWindow extends JFrame {
JResource res = (JResource) obj;
ResourceFile resFile = res.getResFile();
if (resFile != null && JResource.isSupportedForView(resFile.getType())) {
tabsController.selectTab(res);
tabsController.selectTab(res, true);
return true;
}
} else if (obj instanceof JNode) {
tabsController.codeJump((JNode) obj);
tabsController.codeJump((JNode) obj, true);
return true;
}
} catch (Exception e) {
@@ -1062,6 +1062,12 @@ public class MainWindow extends JFrame {
flatPkgMenuItem = new JCheckBoxMenuItem(NLS.str("menu.flatten"), Icons.FLAT_PKG);
flatPkgMenuItem.setState(isFlattenPackage);
JCheckBoxMenuItem enablePreviewTabMenuItem = new JCheckBoxMenuItem(NLS.str("menu.enable_preview_tab"));
enablePreviewTabMenuItem.setState(settings.isEnablePreviewTab());
enablePreviewTabMenuItem.addActionListener(event -> {
settings.setEnablePreviewTab(!settings.isEnablePreviewTab());
});
JCheckBoxMenuItem heapUsageBarMenuItem = new JCheckBoxMenuItem(NLS.str("menu.heapUsageBar"));
heapUsageBarMenuItem.setState(settings.isShowHeapUsageBar());
heapUsageBarMenuItem.addActionListener(event -> {
@@ -1153,6 +1159,7 @@ public class MainWindow extends JFrame {
view.add(quickTabsAction.makeCheckBoxMenuItem());
view.add(flatPkgMenuItem);
view.addSeparator();
view.add(enablePreviewTabMenuItem);
view.add(syncAction);
view.add(alwaysSelectOpened);
view.addSeparator();
@@ -9,6 +9,7 @@ public class TabBlueprint {
private boolean pinned;
private boolean bookmarked;
private boolean hidden;
private boolean previewTab;
public TabBlueprint(JNode node) {
this.node = Objects.requireNonNull(node);
@@ -50,6 +51,14 @@ public class TabBlueprint {
this.hidden = hidden;
}
public boolean isPreviewTab() {
return previewTab;
}
public void setPreviewTab(boolean previewTab) {
this.previewTab = previewTab;
}
@Override
public final boolean equals(Object o) {
if (this == o) {
@@ -68,10 +77,11 @@ public class TabBlueprint {
@Override
public String toString() {
return "TabBlueprint{node=" + node
+ ", bookmarked=" + bookmarked
+ ", pinned=" + pinned
+ ", hidden=" + hidden
+ '}';
return "TabBlueprint{" + "node="
+ node + ", pinned="
+ pinned + ", bookmarked="
+ bookmarked + ", hidden="
+ hidden + ", previewTab="
+ previewTab + '}';
}
}
@@ -75,7 +75,7 @@ public class TabComponent extends JPanel {
icon = new OverlayIcon(node.getIcon());
label = new NodeLabel(buildTabTitle(node), node.disableHtml());
label.setFont(getLabelFont());
makeLabelFont();
String toolTip = contentPanel.getTabTooltip();
if (toolTip != null) {
setToolTipText(toolTip);
@@ -174,6 +174,16 @@ public class TabComponent extends JPanel {
tabsController.setTabBookmarked(getNode(), bookmarked);
}
private void makeLabelFont() {
boolean previewTab = getBlueprint().isPreviewTab();
if (previewTab) {
Font newLabelFont = new Font(label.getFont().getName(), Font.ITALIC, label.getFont().getSize());
label.setFont(newLabelFont);
} else {
label.setFont(getLabelFont());
}
}
private void addListenerForDnd() {
if (tabbedPane.getDnd() == null) {
return;
@@ -32,7 +32,6 @@ public class TabsController {
private final List<ITabStatesListener> listeners = new ArrayList<>();
private boolean forceClose;
private @Nullable TabBlueprint selectedTab;
public TabsController(MainWindow mainWindow) {
@@ -57,15 +56,20 @@ public class TabsController {
}
public TabBlueprint openTab(JNode node) {
return openTab(node, false);
return openTab(node, false, false);
}
public TabBlueprint openTab(JNode node, boolean hidden) {
return openTab(node, hidden, false);
}
public TabBlueprint openTab(JNode node, boolean hidden, boolean preview) {
TabBlueprint blueprint = getTabByNode(node);
if (blueprint == null) {
TabBlueprint newBlueprint = new TabBlueprint(node);
tabsMap.put(node, newBlueprint);
newBlueprint.setHidden(hidden);
newBlueprint.setPreviewTab(preview);
tabsMap.put(node, newBlueprint);
listeners.forEach(l -> l.onTabOpen(newBlueprint));
if (hidden) {
listeners.forEach(l -> l.onTabVisibilityChange(newBlueprint));
@@ -76,20 +80,45 @@ public class TabsController {
return blueprint;
}
public TabBlueprint previewTab(JNode node) {
TabBlueprint blueprint = getPreviewTab();
if (blueprint != null) {
closeTabForce(blueprint);
}
blueprint = openTab(node, false, true);
return blueprint;
}
public void selectTab(JNode node) {
selectTab(node, false);
}
public void selectTab(JNode node, boolean fromTree) {
if (selectedTab != null && selectedTab.getNode() == node) {
// already selected
return;
}
TabBlueprint blueprint = openTab(node);
selectedTab = blueprint;
listeners.forEach(l -> l.onTabSelect(blueprint));
if (mainWindow.getSettings().isEnablePreviewTab() && fromTree) {
selectedTab = previewTab(node);
} else {
selectedTab = openTab(node);
}
listeners.forEach(l -> l.onTabSelect(selectedTab));
}
/**
* Jump to node definition
*/
public void codeJump(JNode node) {
codeJump(node, false);
}
/**
* Jump to node definition
*/
public void codeJump(JNode node, boolean fromTree) {
JClass parentCls = node.getJParent();
if (parentCls != null) {
JavaClass cls = node.getJParent().getCls();
@@ -97,23 +126,23 @@ public class TabsController {
JavaClass codeParent = cls.getTopParentClass();
if (!Objects.equals(codeParent, origTopCls)) {
JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent);
loadCodeWithUIAction(jumpCls, () -> jumpToInnerClass(node, codeParent, jumpCls));
loadCodeWithUIAction(jumpCls, () -> jumpToInnerClass(node, codeParent, jumpCls, fromTree));
return;
}
}
// Not an inline node, jump normally
if (node.getPos() > 0) {
codeJump(new JumpPosition(node));
codeJump(new JumpPosition(node), fromTree);
return;
}
if (node.getRootClass() == null) {
// not a class, select tab without position scroll
selectTab(node);
selectTab(node, fromTree);
return;
}
// node need loading
loadCodeWithUIAction(node.getRootClass(), () -> codeJump(new JumpPosition(node)));
loadCodeWithUIAction(node.getRootClass(), () -> codeJump(new JumpPosition(node), fromTree));
}
private void loadCodeWithUIAction(JClass cls, Runnable action) {
@@ -129,12 +158,12 @@ public class TabsController {
/**
* Search and jump to original node in jumpCls
*/
private void jumpToInnerClass(JNode node, JavaClass codeParent, JClass jumpCls) {
private void jumpToInnerClass(JNode node, JavaClass codeParent, JClass jumpCls, boolean fromTree) {
codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> {
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
ICodeNodeRef declNode = ((NodeDeclareRef) ann).getNode();
if (declNode.equals(node.getJavaNode().getCodeNodeRef())) {
codeJump(new JumpPosition(jumpCls, pos));
codeJump(new JumpPosition(jumpCls, pos), fromTree);
return true;
}
}
@@ -142,13 +171,17 @@ public class TabsController {
});
}
public void codeJump(JumpPosition pos) {
codeJump(pos, false);
}
/**
* Prefer {@link TabsController#codeJump(JNode)} method
*/
public void codeJump(JumpPosition pos) {
public void codeJump(JumpPosition pos, boolean fromTree) {
JumpPosition currentPosition = mainWindow.getTabbedPane().getCurrentPosition();
if (selectedTab == null || selectedTab.getNode() != pos.getNode()) {
selectTab(pos.getNode());
selectTab(pos.getNode(), fromTree);
}
listeners.forEach(l -> l.onTabCodeJump(selectedTab, currentPosition, pos));
}
@@ -298,6 +331,11 @@ public class TabsController {
.collect(Collectors.toUnmodifiableList());
}
public TabBlueprint getPreviewTab() {
return tabsMap.values().stream()
.filter(TabBlueprint::isPreviewTab).findFirst().orElse(null);
}
public void restoreEditorViewState(EditorViewState viewState) {
JNode node = viewState.getNode();
TabBlueprint blueprint = openTab(node, viewState.isHidden());
@@ -7,6 +7,7 @@ menu.no_recent_projects=Keine aktuellen Projekte
menu.preferences=Einstellungen
menu.sync=Mit Editor synchronisieren
menu.flatten=Codepaket erweitern
#menu.enable_preview_tab=Enable Preview Tab
menu.heapUsageBar=Speicherverbrauchsleiste anzeigen
menu.alwaysSelectOpened=Immer geöffnete Datei/Klasse auswählen
menu.dock_log=Dock-Protokollanzeige
@@ -7,6 +7,7 @@ menu.no_recent_projects=No recent projects
menu.preferences=Preferences
menu.sync=Select in Tree
menu.flatten=Use Flatten Packages
menu.enable_preview_tab=Enable Preview Tab
menu.heapUsageBar=Show Memory Usage Bar
menu.alwaysSelectOpened=Auto Select in Tree
menu.dock_log=Dock Log Viewer
@@ -7,6 +7,7 @@ menu.view=Vista
menu.preferences=Preferencias
menu.sync=Sincronizar con el editor
menu.flatten=Mostrar paquetes en vista plana
#menu.enable_preview_tab=Enable Preview Tab
#menu.heapUsageBar=Show memory usage bar
#menu.alwaysSelectOpened=Always Select Opened File/Class
#menu.dock_log=Dock log viewer
@@ -7,6 +7,7 @@ menu.no_recent_projects=Tidak ada proyek terbaru
menu.preferences=Preferensi
menu.sync=Sinkronisasi dengan editor
menu.flatten=Tampilkan paket yang diratakan
#menu.enable_preview_tab=Enable Preview Tab
menu.heapUsageBar=Tampilkan penggunaan memori
menu.alwaysSelectOpened=Selalu Pilih Berkas/Kelas yang Terbuka
menu.dock_log=Kaitkan pemantau log
@@ -7,6 +7,7 @@ menu.no_recent_projects=최근 프로젝트 없음
menu.preferences=설정
menu.sync=에디터와 동기화
menu.flatten=플랫 패키지 표시
#menu.enable_preview_tab=Enable Preview Tab
menu.heapUsageBar=메모리 사용량 표시
menu.alwaysSelectOpened=항상 열린 파일/클래스 선택
#menu.dock_log=Dock log viewer
@@ -7,6 +7,7 @@ menu.no_recent_projects=Nenhum projeto recente
menu.preferences=Preferências
menu.sync=Sincronizar com editor
menu.flatten=Mostrar pacotes achatados
#menu.enable_preview_tab=Enable Preview Tab
menu.heapUsageBar=Mostrar uso de memória
menu.alwaysSelectOpened=Sempre selecionar arquivo/classe aberta
#menu.dock_log=Dock log viewer
@@ -7,6 +7,7 @@ menu.no_recent_projects=Нет недавно открытых проектов
menu.preferences=Параметры
menu.sync=Синхр. файлы с редактором
menu.flatten=Плоская структура пакетов
#menu.enable_preview_tab=Enable Preview Tab
menu.heapUsageBar=Использование ОЗУ
menu.alwaysSelectOpened=Выбирать открытый файл/класс
menu.dock_log=Просмотр логов в панели
@@ -7,6 +7,7 @@ menu.no_recent_projects=没有最近的项目
menu.preferences=首选项
menu.sync=与编辑器同步
menu.flatten=展开显示代码包
#menu.enable_preview_tab=Enable Preview Tab
menu.heapUsageBar=显示内存使用栏
menu.alwaysSelectOpened=始终选中打开的文件/类
menu.dock_log=停靠日志查看器
@@ -7,6 +7,7 @@ menu.no_recent_projects=無近期專案
menu.preferences=選項
menu.sync=與編輯器同步
menu.flatten=展開顯示套件
#menu.enable_preview_tab=Enable Preview Tab
menu.heapUsageBar=顯示記憶體使用率條
menu.alwaysSelectOpened=總是選擇已開啟的檔案/類別
menu.dock_log=固定記錄檔檢視器