diff --git a/build.gradle b/build.gradle index ed3ae0a..07e69c5 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,7 @@ repositories { } } maven { url 'https://jitpack.io' } - // mavenCentral() + mavenCentral() } dependencies { @@ -69,11 +69,12 @@ dependencies { implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.5.36' // 必须的核心依赖 - implementation 'com.fifesoft:rsyntaxtextarea:3.3.1' - implementation 'com.fifesoft:autocomplete:3.3.1' + implementation 'com.fifesoft:rsyntaxtextarea:3.5.4' // 可选UI增强 implementation 'com.fifesoft:rstaui:3.3.1' + implementation 'com.fifesoft:languagesupport:3.3.0' + implementation 'com.fifesoft:autocomplete:3.3.2' implementation 'org.apache.commons:commons-compress:1.23.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' diff --git a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java index 389afec..a75bd18 100644 --- a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java +++ b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java @@ -252,6 +252,12 @@ public class AxisInnovatorsBox { */ private void setTopic() { try { + main.registrationTopic.addTopic(new com.formdev.flatlaf.FlatDarculaLaf(), + "Darcula主题", + "Darcula主题", + LoadIcon.loadIcon(MainWindow.class, "logo.png", 64), + "system:darcula_theme"); + main.registrationTopic.addTopic(UIManager.getSystemLookAndFeelClassName(), LanguageManager.getLoadedLanguages().getText("default_theme.system.topicName"), LanguageManager.getLoadedLanguages().getText("default_theme.default.tip"), @@ -277,13 +283,7 @@ public class AxisInnovatorsBox { LoadIcon.loadIcon(MainWindow.class, "logo.png", 64), "system:flatLightLaf_theme"); - main.registrationTopic.addTopic(new com.formdev.flatlaf.FlatDarculaLaf(), - "Darcula主题", - "Darcula主题", - LoadIcon.loadIcon(MainWindow.class, "logo.png", 64), - "system:darcula_theme"); - - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarculaLaf()); } catch (Exception e) { logger.warn("Failed to load the system facade class", e); } diff --git a/src/main/java/com/axis/innovators/box/gui/FridaSyntax.java b/src/main/java/com/axis/innovators/box/gui/FridaSyntax.java index 54db963..6760304 100644 --- a/src/main/java/com/axis/innovators/box/gui/FridaSyntax.java +++ b/src/main/java/com/axis/innovators/box/gui/FridaSyntax.java @@ -229,7 +229,7 @@ public class FridaSyntax { public static void addBasicCompletions(DefaultCompletionProvider provider) { String[] jsKeywords = {"function", "var", "let", "const", "if", "else", - "for", "while(/* logic */)", "try", "catch", "true", "false","()", "for (int i = 0; i < ; i++) {\n" + + "for", "while(/* logic */)", "try", "catch", "true", "false","()", "for (var i = 0; i < ; i++) {\n" + " \n" + "}"}; diff --git a/src/main/java/com/axis/innovators/box/gui/FridaWindow.java b/src/main/java/com/axis/innovators/box/gui/FridaWindow.java index c3c3a52..d998423 100644 --- a/src/main/java/com/axis/innovators/box/gui/FridaWindow.java +++ b/src/main/java/com/axis/innovators/box/gui/FridaWindow.java @@ -3,7 +3,12 @@ package com.axis.innovators.box.gui; import com.axis.innovators.box.gui.renderer.DarculaCompletionCellRenderer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.fife.rsta.ac.LanguageSupport; +import org.fife.rsta.ac.LanguageSupportFactory; +import org.fife.rsta.ac.perl.PerlLanguageSupport; import org.fife.ui.autocomplete.*; +import org.fife.ui.rsyntaxtextarea.folding.CurlyFoldParser; +import org.fife.ui.rsyntaxtextarea.folding.FoldParserManager; import org.tzd.frida.windows.Frida; import org.fife.ui.rsyntaxtextarea.*; @@ -11,14 +16,12 @@ import org.fife.ui.rtextarea.RTextScrollPane; import javax.swing.*; import javax.swing.border.EmptyBorder; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; +import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.awt.event.*; import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; @@ -32,12 +35,11 @@ public class FridaWindow extends WindowsJDialog { private static final Logger logger = LogManager.getLogger(FridaWindow.class); private RSyntaxTextArea scriptArea; private JTextArea logArea; - private JTextField pidField; private boolean isRepetition = false; - + private JComboBox processCombo; + private JTextField pidField; // IDEA风格配色 private static final Color BACKGROUND = new Color(0x2B2B2B); - private static final Color FOREGROUND = new Color(0xBBBBBB); private static final Color SELECTION_COLOR = new Color(0x214283); public FridaWindow(Window owner) { @@ -45,6 +47,14 @@ public class FridaWindow extends WindowsJDialog { initUI(); } + static { + Font chineseFont = new Font("Microsoft YaHei", Font.PLAIN, 13); + UIManager.put("Menu.font", chineseFont); + UIManager.put("MenuItem.font", chineseFont); + UIManager.put("Button.font", chineseFont); + UIManager.put("ComboBox.font", chineseFont); + } + @Override public void initUI() { super.initUI(); @@ -56,6 +66,10 @@ public class FridaWindow extends WindowsJDialog { mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); mainPanel.setBackground(BACKGROUND); + pidField = new JTextField(); + styleTextField(pidField, "输入PID或进程名"); + mainPanel.add(pidField, BorderLayout.CENTER); + // 输入面板 JPanel inputPanel = createInputPanel(); mainPanel.add(inputPanel, BorderLayout.NORTH); @@ -74,7 +88,7 @@ public class FridaWindow extends WindowsJDialog { centerSplit.setBottomComponent(logPanel); mainPanel.add(centerSplit, BorderLayout.CENTER); - + createMenuBar(); setContentPane(mainPanel); addWindowListener(new WindowAdapter() { @@ -85,41 +99,186 @@ public class FridaWindow extends WindowsJDialog { }); } + private void styleTextField(JTextField field, String placeholder) { + field.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12)); + field.setBackground(new Color(0x3C3F41)); + field.setForeground(Color.WHITE); + field.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(0x515658)), + BorderFactory.createEmptyBorder(5, 8, 5, 8) + )); + field.putClientProperty("JTextField.placeholderText", placeholder); + } + private JPanel createInputPanel() { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); + JPanel panel = new JPanel(new BorderLayout(10, 0)); + panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); panel.setOpaque(false); - pidField = new JTextField(20); - styleTextField(pidField, "输入进程ID或选择进程..."); + // 进程组合框 + processCombo = new JComboBox<>(); + styleComboBox(processCombo); - JButton browseButton = new JButton("选择进程"); - styleButton(browseButton, new Color(65, 131, 215)); - browseButton.addActionListener(this::openProcessSelectionWindow); + // 操作工具栏 + JPanel controlPanel = new JPanel(new GridLayout(1, 2, 8, 0)); + controlPanel.setOpaque(false); - JButton injectButton = new JButton("注入脚本"); - styleButton(injectButton, new Color(215, 65, 65)); - injectButton.addActionListener(this::handleInject); + JButton injectBtn = createModernButton(language.getText("fridaWindow.button.inject"), 0x6897BB); + JButton stopBtn = createModernButton(language.getText("fridaWindow.button.inject.pid"), 0xD25252); - panel.add(new JLabel("目标进程:")); - panel.add(pidField); - panel.add(browseButton); - panel.add(injectButton); + injectBtn.addActionListener(this::handleInject); + stopBtn.addActionListener(this::openProcessSelectionWindow); + + controlPanel.add(injectBtn); + controlPanel.add(stopBtn); + + panel.add(processCombo, BorderLayout.CENTER); + panel.add(controlPanel, BorderLayout.EAST); return panel; } + private JButton createModernButton(String text, int color) { + JButton btn = new JButton(text); + btn.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12)); + btn.setFocusPainted(false); + btn.setBackground(new Color(color)); + btn.setForeground(Color.WHITE); + btn.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(color).darker(), 1), + BorderFactory.createEmptyBorder(6, 15, 6, 15) + )); + + // 悬停效果 + btn.addMouseListener(new MouseAdapter() { + public void mouseEntered(MouseEvent e) { + btn.setBackground(new Color(color).brighter()); + } + public void mouseExited(MouseEvent e) { + btn.setBackground(new Color(color)); + } + }); + return btn; + } + + private void styleComboBox(JComboBox combo) { + combo.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + setBackground(isSelected ? new Color(0x214283) : new Color(0x3C3F41)); + setForeground(Color.WHITE); + return this; + } + }); + } + + private void createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + // 菜单栏深色背景 + menuBar.setBackground(new Color(0x3C3F41)); + menuBar.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0)); + + // 统一设置菜单UI属性 + UIManager.put("Menu.background", new Color(0x3C3F41)); + UIManager.put("Menu.foreground", new Color(0xBBBBBB)); + UIManager.put("MenuItem.background", new Color(0x3C3F41)); + UIManager.put("MenuItem.foreground", new Color(0xBBBBBB)); + UIManager.put("MenuItem.selectionBackground", new Color(0x214283)); + + // 文件菜单 + JMenu fileMenu = new JMenu(language.getText("fridaWindow.menu")); + styleMenu(fileMenu); + + // 退出菜单项 + JMenuItem exitItem = new JMenuItem(language.getText("fridaWindow.menu.exit")); + styleMenuItem(exitItem); + exitItem.addActionListener(e -> dispose()); + + // 帮助菜单 + JMenu helpMenu = new JMenu(language.getText("fridaWindow.menu.help")); + styleMenu(helpMenu); + + // 关于菜单项 + JMenuItem aboutItem = new JMenuItem(language.getText("fridaWindow.menu.help.about")); + styleMenuItem(aboutItem); + aboutItem.addActionListener(e -> showAboutDialog()); + + menuBar.add(fileMenu); + menuBar.add(helpMenu); + setJMenuBar(menuBar); + } + + private void styleMenu(JMenu menu) { + menu.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12)); + menu.setForeground(new Color(0xBBBBBB)); + menu.setBorder(BorderFactory.createEmptyBorder(5, 8, 5, 8)); + menu.setOpaque(true); + menu.setBackground(new Color(0x3C3F41)); + } + + private void styleMenuItem(JMenuItem item) { + item.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12)); + item.setForeground(new Color(0xBBBBBB)); + item.setBackground(new Color(0x3C3F41)); + item.setBorder(BorderFactory.createEmptyBorder(5, 20, 5, 10)); // 增加左侧间距 + item.setOpaque(true); + + // 鼠标悬停效果 + item.addMouseListener(new MouseAdapter() { + public void mouseEntered(MouseEvent e) { + item.setBackground(new Color(0x515658)); + } + public void mouseExited(MouseEvent e) { + item.setBackground(new Color(0x3C3F41)); + } + }); + } + + private void showAboutDialog() { + JDialog aboutDialog = new JDialog(this, "关于", true); + aboutDialog.setSize(300, 200); + aboutDialog.setLocationRelativeTo(this); + + JPanel content = new JPanel(new BorderLayout()); + content.setBorder(new EmptyBorder(20, 20, 20, 20)); + + JLabel title = new JLabel("Frida Injector v2.0"); + title.setFont(new Font("Segoe UI", Font.BOLD, 16)); + + JTextArea info = new JTextArea("基于Frida的现代注入工具\n\nCopyright ? 2024"); + info.setEditable(false); + + content.add(title, BorderLayout.NORTH); + content.add(info, BorderLayout.CENTER); + + aboutDialog.setContentPane(content); + aboutDialog.setVisible(true); + } + private JPanel createScriptPanel() { JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createTitledBorder("Frida脚本编辑器")); scriptArea = new RSyntaxTextArea(); - scriptArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + LanguageSupportFactory lsf = LanguageSupportFactory.get(); + LanguageSupport support = lsf. + getSupportFor(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + support.install(scriptArea); + scriptArea.setCodeFoldingEnabled(true); + FoldParserManager.get().addFoldParserMapping("text/javascript", new CurlyFoldParser()); + + scriptArea.setFractionalFontMetricsEnabled(true); + scriptArea.setAutoIndentEnabled(true); + + scriptArea.setCodeFoldingEnabled(true); + scriptArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); scriptArea.setAntiAliasingEnabled(true); scriptArea.setTabSize(4); scriptArea.setCaretPosition(0); scriptArea.setSelectionColor(SELECTION_COLOR); - scriptArea.setCurrentLineHighlightColor(new Color(0x323232)); scriptArea.setMarkOccurrences(true); scriptArea.setBracketMatchingEnabled(true); setupAutoCompletion(); @@ -131,7 +290,6 @@ public class FridaWindow extends WindowsJDialog { scriptArea.setFont(ideaFont); scriptArea.setBackground(new Color(0x1E1F22)); - //codeArea.setForeground(new Color(0xBBBBBB)); scriptArea.setSelectionColor(new Color(0x214283)); scriptArea.setCurrentLineHighlightColor(new Color(0x323232)); @@ -158,6 +316,11 @@ public class FridaWindow extends WindowsJDialog { defaultStyle.foreground = foreground; defaultStyle.background = background; + // ======== JavaScript 标准库配置 ======== + // console 相关 API + setTokenStyle(scheme, Token.RESERVED_WORD_2, 0xCC7832); // 橙色 + setTokenStyle(scheme, Token.FUNCTION, 0xFFC66D); // 黄色 + // ======== Frida 关键字和 API 配置 ======== // Frida 关键字(如 Java.perform, Interceptor.attach 等) setTokenStyle(scheme, Token.RESERVED_WORD, 0xCC7832); // 橙色 @@ -188,16 +351,6 @@ public class FridaWindow extends WindowsJDialog { // 正则表达式 setTokenStyle(scheme, Token.REGEX, 0xD25252); // 红色 - // ======== Frida 特有 API 配置 ======== - // Java 相关 API - setTokenStyle(scheme, Token.RESERVED_WORD_2, 0xCC7832); // 橙色 - - // 内存操作 API - setTokenStyle(scheme, Token.DATA_TYPE, 0xE8BF6A); // 浅黄色 - - // 拦截器相关 API - setTokenStyle(scheme, Token.ANNOTATION, 0xBBB529); // 黄色 - // ======== 跨语言通用配置 ======== // XML/HTML 标签(如果有需要) setTokenStyle(scheme, Token.MARKUP_TAG_NAME, 0xE8BF6A); // 浅黄色 @@ -211,6 +364,7 @@ public class FridaWindow extends WindowsJDialog { return scheme; } + private void setTokenStyle(SyntaxScheme scheme, int tokenType, int rgb) { Style style = scheme.getStyle(tokenType); style.foreground = new Color(rgb); @@ -238,7 +392,6 @@ public class FridaWindow extends WindowsJDialog { CompletionProvider provider = createCompletionProvider(); AutoCompletion ac = new AutoCompletion(provider); - // 关键配置 ac.setAutoActivationEnabled(true); ac.setParameterAssistanceEnabled(true); ac.setShowDescWindow(true); @@ -246,7 +399,6 @@ public class FridaWindow extends WindowsJDialog { ac.install(scriptArea); ac.setDescriptionWindowColor(new Color(0x323232)); - //ac.setChoicesWindowSize(386,200); scriptArea.addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent e) { @@ -268,33 +420,10 @@ public class FridaWindow extends WindowsJDialog { return provider; } - private void styleTextField(JTextField field, String placeholder) { - field.setFont(new Font("JetBrains Mono", Font.PLAIN, 14)); - field.setBackground(new Color(0x3C3F41)); - field.setForeground(Color.WHITE); - field.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(new Color(0x515658)), - BorderFactory.createEmptyBorder(5, 8, 5, 8) - )); - field.putClientProperty("JTextField.placeholderText", placeholder); - } - - private void styleButton(JButton button, Color bgColor) { - button.setFont(new Font("Segoe UI", Font.PLAIN, 13)); - button.setFocusPainted(false); - button.setBackground(bgColor); - button.setForeground(Color.WHITE); - button.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(bgColor.darker()), - BorderFactory.createEmptyBorder(5, 15, 5, 15) - )); - } - private void handleInject(ActionEvent e) { try { long pid = Long.parseLong(pidField.getText()); String script = scriptArea.getText(); - Frida frida = new Frida(script, pid); if (!isRepetition) { try { @@ -341,67 +470,93 @@ public class FridaWindow extends WindowsJDialog { } private void initializeUI() { - JPanel panel = new JPanel(); - panel.setLayout(new BorderLayout()); + JPanel panel = new JPanel(new BorderLayout(5, 5)); + panel.setBorder(new EmptyBorder(10, 10, 10, 10)); + panel.setBackground(new Color(0x2B2B2B)); - // 获取所有进程信息 - processList = getProcesses(); + JTextField searchField = new JTextField(20); + styleSearchField(searchField); - // 创建搜索框 - JPanel searchPanel = new JPanel(); - searchPanel.setLayout(new BorderLayout()); - JTextField searchField = new JTextField(); - searchField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - filterProcesses(searchField.getText()); - } + table = new JTable(); + table.setModel(createTableModel()); + styleTable(table); - @Override - public void removeUpdate(DocumentEvent e) { - filterProcesses(searchField.getText()); - } - - @Override - public void changedUpdate(DocumentEvent e) { - filterProcesses(searchField.getText()); - } - }); - searchPanel.add(new JLabel(language.getText("fridaWindow.processSelectionWindow.search")), BorderLayout.WEST); - searchPanel.add(searchField, BorderLayout.CENTER); - panel.add(searchPanel, BorderLayout.NORTH); - - String[] columns = {language.getText("fridaWindow.processSelectionWindow.columns.1"), - language.getText("fridaWindow.processSelectionWindow.columns.2")}; - Object[][] data = getTableData(processList); - table = new JTable(data, columns); JScrollPane scrollPane = new JScrollPane(table); - panel.add(scrollPane, BorderLayout.CENTER); + scrollPane.setBorder(BorderFactory.createLineBorder(new Color(0x3C3F41))); - JButton selectButton = new JButton(language.getText("fridaWindow.processSelectionWindow.selectButton")); - selectButton.addActionListener(e -> { - int selectedRow = table.getSelectedRow(); - if (selectedRow >= 0) { - long selectedPid = (Long) table.getValueAt(selectedRow, 1); - ((FridaWindow) getOwner()).pidField.setText(String.valueOf(selectedPid)); - dispose(); + JPopupMenu contextMenu = createContextMenu(); + table.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + contextMenu.show(table, e.getX(), e.getY()); + } + else if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { + injectSelectedProcess(); + } } }); - panel.add(selectButton, BorderLayout.SOUTH); + panel.add(searchField, BorderLayout.NORTH); + panel.add(scrollPane, BorderLayout.CENTER); setContentPane(panel); } - private void filterProcesses(String query) { - List filteredList = new ArrayList<>(); - for (ProcessInfo process : processList) { - if (process.name().toLowerCase().contains(query.toLowerCase())) { - filteredList.add(process); + private DefaultTableModel createTableModel() { + processList = getProcesses(); + String[] columns = { "进程名称", "进程ID" }; + Object[][] data = getTableData(processList); + return new DefaultTableModel(data, columns) { + @Override + public boolean isCellEditable(int row, int column) { + return false; } - } + }; + } - Object[][] filteredData = getTableData(filteredList); - table.setModel(new DefaultTableModel(filteredData, new String[]{"进程名称", "进程ID"})); + private void injectSelectedProcess() { + int selectedRow = table.getSelectedRow(); + if (selectedRow != -1) { + long pid = (Long) table.getModel().getValueAt(selectedRow, 1); + ((FridaWindow) getOwner()).pidField.setText(String.valueOf(pid)); + dispose(); + } + } + + private void styleSearchField(JTextField field) { + field.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12)); + field.putClientProperty("JTextField.placeholderText", "输入进程名搜索..."); + field.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(0x515658)), + BorderFactory.createEmptyBorder(5, 8, 5, 8) + )); + } + + private void styleTable(JTable table) { + table.setRowHeight(28); + table.setIntercellSpacing(new Dimension(0, 0)); + table.setShowGrid(false); + table.setSelectionBackground(new Color(0x214283)); + + // 自定义渲染器 + table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + Component c = super.getTableCellRendererComponent(table, value, + isSelected, hasFocus, row, column); + c.setBackground(row % 2 == 0 ? new Color(0x323232) : new Color(0x2B2B2B)); + c.setForeground(Color.WHITE); + return c; + } + }); + } + + private JPopupMenu createContextMenu() { + JPopupMenu menu = new JPopupMenu(); + JMenuItem injectItem = new JMenuItem("注入选中进程"); + injectItem.addActionListener(e -> injectSelectedProcess()); + menu.add(injectItem); + return menu; } private Object[][] getTableData(List processes) { @@ -441,4 +596,8 @@ public class FridaWindow extends WindowsJDialog { } private record ProcessInfo(long pid, String name) {} + + public static void main(String[] args) { + + } } diff --git a/src/main/java/com/axis/innovators/box/gui/MainWindow.java b/src/main/java/com/axis/innovators/box/gui/MainWindow.java index 757de91..7223eb5 100644 --- a/src/main/java/com/axis/innovators/box/gui/MainWindow.java +++ b/src/main/java/com/axis/innovators/box/gui/MainWindow.java @@ -626,7 +626,7 @@ public class MainWindow extends JFrame { private final ToolItem tool; private Timer pressTimer; private Timer releaseTimer; - + private static int delay = 0; public CardMouseAdapter(JPanel card, ToolItem tool) { this.card = card; this.tool = tool; @@ -639,9 +639,14 @@ public class MainWindow extends JFrame { @Override public void mouseReleased(MouseEvent e) { - startReleaseAnimation(() -> tool.getAction().actionPerformed( - new ActionEvent(card, ActionEvent.ACTION_PERFORMED, "") - )); + if (delay == 0) { + delay++; + startReleaseAnimation(() -> tool.getAction().actionPerformed( + new ActionEvent(card, ActionEvent.ACTION_PERFORMED, "") + )); + } else { + delay--; + } } @Override