feat(gui): use list component for recent projects on start page (PR #2513)

* feat(gui): use list component witch recent projects on start page

* fix: spotless check

* move classes into one package and some minor changes

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
Yaroslav
2025-05-25 22:35:35 +03:00
committed by GitHub
parent fc0f1f9a1c
commit bcd0c949dc
15 changed files with 463 additions and 130 deletions
@@ -155,13 +155,13 @@ 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.startpage.StartPageNode;
import jadx.gui.ui.tab.EditorSyncManager;
import jadx.gui.ui.tab.NavigationController;
import jadx.gui.ui.tab.QuickTabsTree;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.tab.TabsController;
import jadx.gui.ui.tab.dnd.TabDndController;
import jadx.gui.ui.treenodes.StartPageNode;
import jadx.gui.ui.treenodes.SummaryNode;
import jadx.gui.ui.treenodes.UndisplayedStringsNode;
import jadx.gui.update.JadxUpdate;
@@ -1,127 +0,0 @@
package jadx.gui.ui.panel;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.nio.file.Path;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.treenodes.StartPageNode;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
public class StartPagePanel extends ContentPanel {
public StartPagePanel(TabbedPane tabbedPane, StartPageNode node) {
super(tabbedPane, node);
MainWindow mainWindow = tabbedPane.getMainWindow();
Font baseFont = mainWindow.getSettings().getFont();
JButton openFile = new JButton(NLS.str("file.open_title"), Icons.OPEN);
openFile.addActionListener(ev -> mainWindow.openFileDialog());
JButton openProject = new JButton(NLS.str("file.open_project"), Icons.OPEN_PROJECT);
openProject.addActionListener(ev -> mainWindow.openProjectDialog());
JPanel start = new JPanel();
start.setBorder(sectionFrame(NLS.str("start_page.start"), baseFont));
start.setLayout(new BoxLayout(start, BoxLayout.LINE_AXIS));
start.add(openFile);
start.add(Box.createRigidArea(new Dimension(10, 0)));
start.add(openProject);
start.add(Box.createHorizontalGlue());
JPanel recentPanel = new JPanel();
JScrollPane scrollPane = new JScrollPane(recentPanel);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setPreferredSize(new Dimension(400, 200));
scrollPane.setBorder(BorderFactory.createEmptyBorder());
fillRecentPanel(recentPanel, scrollPane, mainWindow);
JPanel recent = new JPanel();
recent.setBorder(sectionFrame(NLS.str("start_page.recent"), baseFont));
recent.setLayout(new BoxLayout(recent, BoxLayout.PAGE_AXIS));
recent.add(scrollPane);
JPanel center = new JPanel();
center.setLayout(new BorderLayout(10, 10));
center.add(start, BorderLayout.PAGE_START);
center.add(recent, BorderLayout.CENTER);
center.setMaximumSize(new Dimension(700, 600));
center.setAlignmentX(CENTER_ALIGNMENT);
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
setBorder(BorderFactory.createEmptyBorder(50, 50, 50, 50));
add(Box.createVerticalGlue());
add(center);
add(Box.createVerticalGlue());
}
private void fillRecentPanel(JPanel panel, JScrollPane scrollPane, MainWindow mainWindow) {
JadxSettings settings = mainWindow.getSettings();
List<Path> recentProjects = settings.getRecentProjects();
panel.removeAll();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
Font baseFont = settings.getFont();
Font font = baseFont.deriveFont(baseFont.getSize() - 1f);
for (Path path : recentProjects) {
JButton openBtn = new JButton(path.getFileName().toString());
openBtn.setToolTipText(path.toAbsolutePath().toString());
openBtn.setFont(font);
openBtn.setBorderPainted(false);
openBtn.addActionListener(ev -> mainWindow.open(path));
JButton removeBtn = new JButton();
removeBtn.setIcon(Icons.CLOSE_INACTIVE);
removeBtn.setRolloverIcon(Icons.CLOSE);
removeBtn.setRolloverEnabled(true);
removeBtn.setFocusable(false);
removeBtn.setBorder(null);
removeBtn.setBorderPainted(false);
removeBtn.setContentAreaFilled(false);
removeBtn.setOpaque(true);
removeBtn.addActionListener(e -> {
mainWindow.getSettings().removeRecentProject(path);
fillRecentPanel(panel, scrollPane, mainWindow);
panel.revalidate();
scrollPane.repaint();
});
JPanel linePanel = new JPanel();
linePanel.setLayout(new BoxLayout(linePanel, BoxLayout.LINE_AXIS));
linePanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
linePanel.add(openBtn);
linePanel.add(Box.createHorizontalGlue());
linePanel.add(removeBtn);
panel.add(linePanel);
}
panel.add(Box.createVerticalGlue());
}
private static Border sectionFrame(String title, Font font) {
TitledBorder titledBorder = BorderFactory.createTitledBorder(title);
titledBorder.setTitleFont(font.deriveFont(Font.BOLD, font.getSize() + 1));
Border spacing = BorderFactory.createEmptyBorder(10, 10, 10, 10);
return BorderFactory.createCompoundBorder(titledBorder, spacing);
}
@Override
public void loadSettings() {
}
}
@@ -0,0 +1,51 @@
package jadx.gui.ui.startpage;
import java.nio.file.Path;
import java.util.Objects;
import jadx.api.plugins.utils.CommonFileUtils;
/**
* Represents an item in the recent projects list.
*/
public class RecentProjectItem {
private final Path path;
public RecentProjectItem(Path path) {
this.path = Objects.requireNonNull(path);
}
public Path getPath() {
return path;
}
public String getProjectName() {
return CommonFileUtils.removeFileExtension(path.getFileName().toString());
}
public String getAbsolutePath() {
return path.toAbsolutePath().toString();
}
@Override
public String toString() {
return getProjectName();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RecentProjectItem that = (RecentProjectItem) o;
return Objects.equals(path, that.path);
}
@Override
public int hashCode() {
return Objects.hash(path);
}
}
@@ -0,0 +1,125 @@
package jadx.gui.ui.startpage;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Rectangle;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.ListCellRenderer;
import javax.swing.UIManager;
import javax.swing.plaf.basic.BasicButtonUI;
import jadx.gui.utils.Icons;
public class RecentProjectListCellRenderer extends JPanel implements ListCellRenderer<RecentProjectItem> {
private static final long serialVersionUID = 5550591869239586857L;
private final JLabel fileNameLabel;
private final JLabel pathLabel;
private final JButton removeProjectBtn;
private final Color defaultBackground;
private final Color defaultForeground;
private final Color selectedBackground;
private final Color selectedForeground;
private Rectangle removeIconBounds;
public RecentProjectListCellRenderer(Font baseFont) {
super(new BorderLayout(5, 0));
setOpaque(true);
setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 5));
this.fileNameLabel = new JLabel();
fileNameLabel.setFont(baseFont.deriveFont(Font.BOLD, baseFont.getSize()));
this.pathLabel = new JLabel();
pathLabel.setFont(baseFont.deriveFont(baseFont.getSize() - 2f));
pathLabel.setForeground(UIManager.getColor("Label.disabledForeground"));
JPanel textPanel = new JPanel(new BorderLayout());
textPanel.setOpaque(false);
textPanel.add(fileNameLabel, BorderLayout.NORTH);
textPanel.add(pathLabel, BorderLayout.SOUTH);
removeProjectBtn = new JButton();
removeProjectBtn.setIcon(Icons.CLOSE_INACTIVE);
removeProjectBtn.setOpaque(false);
removeProjectBtn.setUI(new BasicButtonUI());
removeProjectBtn.setContentAreaFilled(false);
removeProjectBtn.setFocusable(false);
removeProjectBtn.setBorder(null);
removeProjectBtn.setBorderPainted(false);
add(textPanel, BorderLayout.CENTER);
add(removeProjectBtn, BorderLayout.EAST);
defaultBackground = UIManager.getColor("List.background");
defaultForeground = UIManager.getColor("List.foreground");
selectedBackground = UIManager.getColor("List.selectionBackground");
selectedForeground = UIManager.getColor("List.selectionForeground");
}
@Override
public Component getListCellRendererComponent(JList<? extends RecentProjectItem> list,
RecentProjectItem value, int index, boolean isSelected, boolean cellHasFocus) {
fileNameLabel.setText(value.getProjectName());
pathLabel.setText(value.getAbsolutePath());
boolean isThisRemoveButtonHovered = (index == StartPagePanel.hoveredRemoveBtnIndex);
removeProjectBtn.setIcon(isThisRemoveButtonHovered ? Icons.CLOSE : Icons.CLOSE_INACTIVE);
removeProjectBtn.setRolloverEnabled(isThisRemoveButtonHovered);
if (isSelected) {
setBackground(selectedBackground);
fileNameLabel.setForeground(selectedForeground);
pathLabel.setForeground(selectedForeground.darker());
removeProjectBtn.setForeground(selectedForeground);
} else {
setBackground(defaultBackground);
fileNameLabel.setForeground(defaultForeground);
pathLabel.setForeground(UIManager.getColor("Label.disabledForeground"));
removeProjectBtn.setForeground(defaultForeground);
}
setToolTipText(value.getAbsolutePath());
return this;
}
/**
* Overriding paint to calculate the bounds of the remove button.
* This is crucial for the MouseListener on the JList to determine if a click/hover was on the
* button.
*/
@Override
public void paint(Graphics g) {
super.paint(g);
// Ensure the button's layout is valid before getting bounds
removeProjectBtn.doLayout();
// Calculate bounds of the remove button relative to this renderer panel
int x = getWidth() - removeProjectBtn.getWidth() - getBorder().getBorderInsets(this).right;
int y = (getHeight() - removeProjectBtn.getHeight()) / 2;
removeIconBounds = new Rectangle(x, y, removeProjectBtn.getWidth(), removeProjectBtn.getHeight());
}
/**
* Returns the bounds of the remove button within the renderer component's coordinate system.
* This is crucial for the MouseListener on the JList to determine if a click was on the icon.
*
* @return Rectangle representing the bounds of the remove icon.
*/
public Rectangle getRemoveIconBounds() {
return removeIconBounds;
}
}
@@ -1,11 +1,10 @@
package jadx.gui.ui.treenodes;
package jadx.gui.ui.startpage;
import javax.swing.Icon;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
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;
@@ -0,0 +1,267 @@
package jadx.gui.ui.startpage;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.nio.file.Path;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JList;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
public class StartPagePanel extends ContentPanel {
private static final long serialVersionUID = 2457805175218770732L;
private final RecentProjectsJList recentList;
private final DefaultListModel<RecentProjectItem> recentListModel;
private final MainWindow mainWindow;
private final JadxSettings settings;
public static int hoveredRemoveBtnIndex = -1;
public StartPagePanel(TabbedPane tabbedPane, StartPageNode node) {
super(tabbedPane, node);
this.mainWindow = tabbedPane.getMainWindow();
this.settings = mainWindow.getSettings();
Font baseFont = settings.getFont();
JButton openFile = new JButton(NLS.str("file.open_title"), Icons.OPEN);
openFile.addActionListener(ev -> mainWindow.openFileDialog());
JButton openProject = new JButton(NLS.str("file.open_project"), Icons.OPEN_PROJECT);
openProject.addActionListener(ev -> mainWindow.openProjectDialog());
JPanel start = new JPanel();
start.setBorder(sectionFrame(NLS.str("start_page.start"), baseFont));
start.setLayout(new BoxLayout(start, BoxLayout.LINE_AXIS));
start.add(openFile);
start.add(Box.createRigidArea(new Dimension(10, 0)));
start.add(openProject);
start.add(Box.createHorizontalGlue());
this.recentListModel = new DefaultListModel<>();
this.recentList = new RecentProjectsJList(recentListModel);
this.recentList.setCellRenderer(new RecentProjectListCellRenderer(baseFont));
this.recentList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
JScrollPane scrollPane = new JScrollPane(recentList);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setPreferredSize(new Dimension(400, 250));
scrollPane.setBorder(BorderFactory.createEmptyBorder());
recentList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
int index = recentList.locationToIndex(e.getPoint());
if (index == -1) {
return;
}
RecentProjectItem item = recentListModel.getElementAt(index);
if (item == null) {
return;
}
RecentProjectListCellRenderer renderer = (RecentProjectListCellRenderer) recentList.getCellRenderer()
.getListCellRendererComponent(recentList, item, index, false, false);
Rectangle cellBounds = recentList.getCellBounds(index, index);
if (cellBounds != null) {
int xInCell = e.getX() - cellBounds.x;
int yInCell = e.getY() - cellBounds.y;
Rectangle removeIconBounds = renderer.getRemoveIconBounds();
if (removeIconBounds != null && removeIconBounds.contains(xInCell, yInCell)) {
removeRecentProject(item.getPath());
return;
}
}
if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) {
openRecentProject(item.getPath());
} else if (SwingUtilities.isRightMouseButton(e)) {
recentList.setSelectedIndex(index);
showRecentProjectContextMenu(e);
}
}
});
recentList.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
int oldHoveredRemoveBtnIndex = hoveredRemoveBtnIndex;
hoveredRemoveBtnIndex = -1;
int currentCellIndex = recentList.locationToIndex(e.getPoint());
if (currentCellIndex != -1) {
RecentProjectItem item = recentListModel.getElementAt(currentCellIndex);
RecentProjectListCellRenderer renderer = (RecentProjectListCellRenderer) recentList.getCellRenderer()
.getListCellRendererComponent(recentList, item, currentCellIndex, recentList.isSelectedIndex(currentCellIndex),
false);
Rectangle cellBounds = recentList.getCellBounds(currentCellIndex, currentCellIndex);
if (cellBounds != null) {
int xInCell = e.getX() - cellBounds.x;
int yInCell = e.getY() - cellBounds.y;
Rectangle removeIconBounds = renderer.getRemoveIconBounds();
if (removeIconBounds != null && removeIconBounds.contains(xInCell, yInCell)) {
hoveredRemoveBtnIndex = currentCellIndex;
}
}
}
if (oldHoveredRemoveBtnIndex != hoveredRemoveBtnIndex) {
if (oldHoveredRemoveBtnIndex != -1) {
Rectangle bounds = recentList.getCellBounds(oldHoveredRemoveBtnIndex, oldHoveredRemoveBtnIndex);
if (bounds != null) {
recentList.repaint(bounds);
}
}
if (hoveredRemoveBtnIndex != -1) {
Rectangle bounds = recentList.getCellBounds(hoveredRemoveBtnIndex, hoveredRemoveBtnIndex);
if (bounds != null) {
recentList.repaint(bounds);
}
}
}
}
});
JPanel recent = new JPanel();
recent.setBorder(sectionFrame(NLS.str("start_page.recent"), baseFont));
recent.setLayout(new BoxLayout(recent, BoxLayout.PAGE_AXIS));
recent.add(scrollPane);
JPanel center = new JPanel();
center.setLayout(new BorderLayout(10, 10));
center.add(start, BorderLayout.PAGE_START);
center.add(recent, BorderLayout.CENTER);
center.setMaximumSize(new Dimension(700, 600));
center.setAlignmentX(CENTER_ALIGNMENT);
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
setBorder(BorderFactory.createEmptyBorder(50, 50, 50, 50));
add(Box.createVerticalGlue());
add(center);
add(Box.createVerticalGlue());
fillRecentProjectsList();
}
private void fillRecentProjectsList() {
recentListModel.clear();
List<Path> recentPaths = settings.getRecentProjects();
for (Path path : recentPaths) {
recentListModel.addElement(new RecentProjectItem(path));
}
recentList.revalidate();
recentList.repaint();
}
private void openRecentProject(Path path) {
mainWindow.open(path);
}
private void removeRecentProject(Path path) {
settings.removeRecentProject(path);
fillRecentProjectsList();
if (hoveredRemoveBtnIndex != -1 && hoveredRemoveBtnIndex >= recentListModel.size()) {
hoveredRemoveBtnIndex = -1;
}
}
private void showRecentProjectContextMenu(MouseEvent e) {
JPopupMenu popupMenu = new JPopupMenu();
RecentProjectItem selectedItem = recentList.getSelectedValue();
if (selectedItem != null) {
JMenuItem openItem = new JMenuItem(NLS.str("file.open_project"));
openItem.addActionListener(actionEvent -> openRecentProject(selectedItem.getPath()));
popupMenu.add(openItem);
JMenuItem removeItem = new JMenuItem(NLS.str("start_page.list.delete_recent_project"));
removeItem.addActionListener(actionEvent -> removeRecentProject(selectedItem.getPath()));
popupMenu.add(removeItem);
}
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
private static Border sectionFrame(String title, Font font) {
TitledBorder titledBorder = BorderFactory.createTitledBorder(title);
titledBorder.setTitleFont(font.deriveFont(Font.BOLD, font.getSize() + 1));
Border spacing = BorderFactory.createEmptyBorder(10, 10, 10, 10);
return BorderFactory.createCompoundBorder(titledBorder, spacing);
}
@Override
public void loadSettings() {
}
/**
* Inner class: Custom JList to override getToolTipText method.
* This allows displaying specific tooltips based on mouse position within a cell.
*/
private static class RecentProjectsJList extends JList<RecentProjectItem> {
private static final long serialVersionUID = 1L;
public RecentProjectsJList(DefaultListModel<RecentProjectItem> model) {
super(model);
}
@Override
public String getToolTipText(MouseEvent event) {
int index = locationToIndex(event.getPoint());
if (index == -1) {
return null;
}
RecentProjectItem item = getModel().getElementAt(index);
if (item == null) {
return null;
}
RecentProjectListCellRenderer renderer = (RecentProjectListCellRenderer) getCellRenderer()
.getListCellRendererComponent(this, item, index, isSelectedIndex(index), false);
Rectangle cellBounds = getCellBounds(index, index);
if (cellBounds != null) {
int xInCell = event.getX() - cellBounds.x;
int yInCell = event.getY() - cellBounds.y;
Rectangle removeIconBounds = renderer.getRemoveIconBounds();
if (removeIconBounds != null && removeIconBounds.contains(xInCell, yInCell)) {
return NLS.str("start_page.list.delete_recent_project.tooltip");
}
}
return item.getAbsolutePath();
}
}
}
@@ -56,6 +56,8 @@ file.export_node=Datei exportieren
start_page.title=Startseite
start_page.start=Start
start_page.recent=Aktuelle Projekte
#start_page.list.delete_recent_project=Delete project
#start_page.list.delete_recent_project.tooltip=Delete project from recent list
tree.inputs_title=Eingaben
tree.input_files=Dateien
@@ -56,6 +56,8 @@ file.export_node=Export file
start_page.title=Start page
start_page.start=Start
start_page.recent=Recent projects
start_page.list.delete_recent_project=Delete project
start_page.list.delete_recent_project.tooltip=Delete project from recent list
tree.inputs_title=Inputs
tree.input_files=Files
@@ -56,6 +56,8 @@ file.exit=Salir
#start_page.title=Start page
#start_page.start=Start
#start_page.recent=Recent projects
#start_page.list.delete_recent_project=Delete project
#start_page.list.delete_recent_project.tooltip=Delete project from recent list
#tree.inputs_title=Inputs
#tree.input_files=Files
@@ -56,6 +56,8 @@ file.exit=Keluar
start_page.title=Halaman Awal
start_page.start=Mulai
start_page.recent=Proyek Terbaru
#start_page.list.delete_recent_project=Delete project
#start_page.list.delete_recent_project.tooltip=Delete project from recent list
tree.inputs_title=Input
tree.input_files=Berkas
@@ -56,6 +56,8 @@ file.exit=나가기
start_page.title=페이지 시작
start_page.start=시작
start_page.recent=최근 프로젝트
#start_page.list.delete_recent_project=Delete project
#start_page.list.delete_recent_project.tooltip=Delete project from recent list
#tree.inputs_title=Inputs
#tree.input_files=Files
@@ -56,6 +56,8 @@ file.exit=Sair
start_page.title=Página inicial
start_page.start=Começar
start_page.recent=Projetos recentes
#start_page.list.delete_recent_project=Delete project
#start_page.list.delete_recent_project.tooltip=Delete project from recent list
#tree.inputs_title=Inputs
#tree.input_files=Files
@@ -56,6 +56,8 @@ file.exit=Выход
start_page.title=Начальная страница
start_page.start=Начать
start_page.recent=Недавние проекты
#start_page.list.delete_recent_project=Delete project
#start_page.list.delete_recent_project.tooltip=Delete project from recent list
tree.inputs_title=Входные файлы
tree.input_files=Файлы
@@ -56,6 +56,8 @@ file.export_node=导出文件
start_page.title=开始页面
start_page.start=开始
start_page.recent=最近项目
#start_page.list.delete_recent_project=Delete project
#start_page.list.delete_recent_project.tooltip=Delete project from recent list
tree.inputs_title=输入
tree.input_files=文件
@@ -56,6 +56,8 @@ file.export_node=匯出檔案
start_page.title=開始頁面
start_page.start=開始
start_page.recent=近期專案
#start_page.list.delete_recent_project=Delete project
#start_page.list.delete_recent_project.tooltip=Delete project from recent list
tree.inputs_title=輸入
tree.input_files=檔案