refactor(browser): 重构消息路由和界面组件以支持双路由和主题适配
- 实现双CefMessageRouter配置,支持javaQuery和cefQuery两种查询方式 - 移除handler判空注释,优化system请求处理逻辑 - 修复java-response解析中的数组越界问题 - 添加运行时计时器管理,避免计时器冲突 - 优化背景图绘制和主题色动态适配 - 实现卡片组件的动态布局和悬停效果 - 改进搜索框的动画和主题适配 - 修复JAR文件处理中的异常捕获和错误响应 - 优化工具卡片的鼠标事件处理和线程安全 - 实现响应式面板布局和组件尺寸同步
This commit is contained in:
@@ -493,19 +493,10 @@ public class BrowserCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupMessageHandlers(WindowOperationHandler handler) {
|
private void setupMessageHandlers(WindowOperationHandler handler) {
|
||||||
// 1. 配置 (保持上一轮修复的代码)
|
// 1. 定义共享的处理器逻辑 (为了避免代码重复,先提取出来)
|
||||||
CefMessageRouter.CefMessageRouterConfig routerConfig = new CefMessageRouter.CefMessageRouterConfig();
|
CefMessageRouterHandlerAdapter commonHandler = new CefMessageRouterHandlerAdapter() {
|
||||||
routerConfig.jsQueryFunction = "javaQuery";
|
|
||||||
routerConfig.jsCancelFunction = "javaQueryCancel";
|
|
||||||
|
|
||||||
// 2. 创建 (保持上一轮修复的代码)
|
|
||||||
msgRouter = CefMessageRouter.create(routerConfig);
|
|
||||||
|
|
||||||
// 3. 添加处理器
|
|
||||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) {
|
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) {
|
||||||
|
|
||||||
// A. 处理 JS Bridge 请求 (Controller)
|
// A. 处理 JS Bridge 请求 (Controller)
|
||||||
if (jsController != null && request.startsWith("jsbridge:")) {
|
if (jsController != null && request.startsWith("jsbridge:")) {
|
||||||
return jsController.handleQuery(browser, frame, queryId, request, persistent, callback);
|
return jsController.handleQuery(browser, frame, queryId, request, persistent, callback);
|
||||||
@@ -513,7 +504,6 @@ public class BrowserCore {
|
|||||||
|
|
||||||
// B. 处理 System 请求 (OperationHandler)
|
// B. 处理 System 请求 (OperationHandler)
|
||||||
if (request.startsWith("system:")) {
|
if (request.startsWith("system:")) {
|
||||||
// ★★★ [增加判空] 防止 handler 为空导致空指针异常 ★★★
|
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
String[] parts = request.split(":");
|
String[] parts = request.split(":");
|
||||||
String operation = parts.length >= 2 ? parts[1] : null;
|
String operation = parts.length >= 2 ? parts[1] : null;
|
||||||
@@ -521,7 +511,7 @@ public class BrowserCore {
|
|||||||
handler.handleOperation(new WindowOperation(operation, targetWindow, callback));
|
handler.handleOperation(new WindowOperation(operation, targetWindow, callback));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
BrowserLog.warn("收到 system 请求但 handler 为空: {}" , request);
|
BrowserLog.warn("收到 system 请求但 handler 为空: {}", request);
|
||||||
callback.failure(404, "No handler for system operation");
|
callback.failure(404, "No handler for system operation");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -530,7 +520,7 @@ public class BrowserCore {
|
|||||||
// C. 处理 Java Response
|
// C. 处理 Java Response
|
||||||
if (request.startsWith("java-response:")) {
|
if (request.startsWith("java-response:")) {
|
||||||
String[] parts = request.split(":");
|
String[] parts = request.split(":");
|
||||||
String requestId = parts[1];
|
String requestId = parts.length > 1 ? parts[1] : "";
|
||||||
String responseData = parts.length > 2 ? parts[2] : "";
|
String responseData = parts.length > 2 ? parts[2] : "";
|
||||||
Consumer<String> cb = WindowRegistry.getInstance().getCallback(requestId);
|
Consumer<String> cb = WindowRegistry.getInstance().getCallback(requestId);
|
||||||
if (cb != null) {
|
if (cb != null) {
|
||||||
@@ -543,9 +533,26 @@ public class BrowserCore {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, true);
|
};
|
||||||
|
|
||||||
client.addMessageRouter(msgRouter);
|
// 2. 配置并创建第一个 Router (使用 javaQuery)
|
||||||
|
CefMessageRouter.CefMessageRouterConfig config1 = new CefMessageRouter.CefMessageRouterConfig();
|
||||||
|
config1.jsQueryFunction = "javaQuery";
|
||||||
|
config1.jsCancelFunction = "javaQueryCancel";
|
||||||
|
CefMessageRouter msgRouter1 = CefMessageRouter.create(config1);
|
||||||
|
msgRouter1.addHandler(commonHandler, true);
|
||||||
|
client.addMessageRouter(msgRouter1);
|
||||||
|
|
||||||
|
// 3. 配置并创建第二个 Router (使用 cefQuery - 满足“再注册”的需求)
|
||||||
|
CefMessageRouter.CefMessageRouterConfig config2 = new CefMessageRouter.CefMessageRouterConfig();
|
||||||
|
config2.jsQueryFunction = "cefQuery";
|
||||||
|
config2.jsCancelFunction = "cefQueryCancel";
|
||||||
|
CefMessageRouter msgRouter2 = CefMessageRouter.create(config2);
|
||||||
|
msgRouter2.addHandler(commonHandler, true);
|
||||||
|
client.addMessageRouter(msgRouter2);
|
||||||
|
|
||||||
|
// 如果类中有 msgRouter 成员变量,可以指向其中任意一个或维护一个列表
|
||||||
|
this.msgRouter = msgRouter2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void injectJsBridge() {
|
private void injectJsBridge() {
|
||||||
|
|||||||
@@ -67,9 +67,7 @@ public class ModernJarViewer {
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "openJar":
|
case "openJar":
|
||||||
// 支持从前端传路径(拖拽进入)或弹窗选择
|
handleOpenJar(parent, json.optString("path", null), callback);
|
||||||
String path = json.optString("path", null);
|
|
||||||
handleOpenJar(parent, path, callback);
|
|
||||||
return true;
|
return true;
|
||||||
case "getFile":
|
case "getFile":
|
||||||
handleGetFile(json.optString("path"), callback);
|
handleGetFile(json.optString("path"), callback);
|
||||||
@@ -137,16 +135,21 @@ public class ModernJarViewer {
|
|||||||
chooser.setFileFilter(new FileNameExtensionFilter("JAR Files", "jar", "zip", "war"));
|
chooser.setFileFilter(new FileNameExtensionFilter("JAR Files", "jar", "zip", "war"));
|
||||||
chooser.setDialogTitle("Select JAR");
|
chooser.setDialogTitle("Select JAR");
|
||||||
if (chooser.showOpenDialog(parent) != JFileChooser.APPROVE_OPTION) {
|
if (chooser.showOpenDialog(parent) != JFileChooser.APPROVE_OPTION) {
|
||||||
if (callback != null) callback.failure(404, "Cancelled");
|
callback.failure(404, "User cancelled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file = chooser.getSelectedFile();
|
file = chooser.getSelectedFile();
|
||||||
|
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
loadJarAndRespond(file, callback);
|
||||||
|
} else {
|
||||||
|
callback.failure(404, "File not found or invalid");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadJarAndRespond(file, callback);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (callback != null) callback.failure(500, e.getMessage());
|
e.printStackTrace();
|
||||||
|
callback.failure(500, "Swing Error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -189,8 +192,9 @@ public class ModernJarViewer {
|
|||||||
new Thread(() -> JarAnalyzer.analyze(currentJarFile, classEntries, globalIndex)).start();
|
new Thread(() -> JarAnalyzer.analyze(currentJarFile, classEntries, globalIndex)).start();
|
||||||
|
|
||||||
if (callback != null) callback.success(root.toString());
|
if (callback != null) callback.success(root.toString());
|
||||||
} catch (IOException e) {
|
} catch (Exception e) { // 关键修改:从 IOException 改为 Exception
|
||||||
if (callback != null) callback.failure(500, "Failed to open JAR: " + e.getMessage());
|
e.printStackTrace();
|
||||||
|
if (callback != null) callback.failure(500, "Load Error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ public class MainWindow extends JFrame {
|
|||||||
|
|
||||||
// settings dialog
|
// settings dialog
|
||||||
private WindowsJDialog dialog;
|
private WindowsJDialog dialog;
|
||||||
|
private final Map<JComponent, Timer> runningTimers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public MainWindow() {
|
public MainWindow() {
|
||||||
// 字体配置:优先使用无衬线现代字体
|
// 字体配置:优先使用无衬线现代字体
|
||||||
@@ -219,8 +220,7 @@ public class MainWindow extends JFrame {
|
|||||||
if (backgroundImage != null) {
|
if (backgroundImage != null) {
|
||||||
Graphics2D g2d = (Graphics2D) g.create();
|
Graphics2D g2d = (Graphics2D) g.create();
|
||||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, backgroundOpacity));
|
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, backgroundOpacity));
|
||||||
Dimension size = getSize();
|
BufferedImage bg = createBlurredBackground(getSize());
|
||||||
BufferedImage bg = createBlurredBackground(size);
|
|
||||||
if (bg != null) g2d.drawImage(bg, 0, 0, null);
|
if (bg != null) g2d.drawImage(bg, 0, 0, null);
|
||||||
g2d.dispose();
|
g2d.dispose();
|
||||||
}
|
}
|
||||||
@@ -228,18 +228,16 @@ public class MainWindow extends JFrame {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 背景色逻辑优化
|
// 背景色逻辑优化
|
||||||
Color baseBg = UIManager.getColor("Panel.background");
|
|
||||||
if (baseBg == null) baseBg = new Color(245, 246, 248);
|
|
||||||
if (backgroundImage != null) {
|
if (backgroundImage != null) {
|
||||||
mainPanel.setOpaque(false);
|
mainPanel.setOpaque(false);
|
||||||
mainPanel.setBackground(new Color(0,0,0,0));
|
|
||||||
} else {
|
} else {
|
||||||
mainPanel.setOpaque(true);
|
mainPanel.setOpaque(true);
|
||||||
// 获取主题中最深的背景色(通常是 Window 的背景,比 Panel 深)
|
// --- 修复点:动态获取窗口背景色,不要硬编码 24,24,24 ---
|
||||||
Color deepBg = UIManager.getColor("Window.background");
|
Color bg = UIManager.getColor("Window.background");
|
||||||
// 如果获取不到,就用一个极深的颜色兜底
|
if (bg == null) bg = UIManager.getColor("Panel.background");
|
||||||
if (deepBg == null) deepBg = new Color(24, 24, 24);
|
// 如果还是 null,根据主题手动设置安全的浅色/深色
|
||||||
mainPanel.setBackground(deepBg);
|
if (bg == null) bg = isDarkTheme() ? new Color(30, 30, 30) : new Color(242, 242, 242);
|
||||||
|
mainPanel.setBackground(bg);
|
||||||
}
|
}
|
||||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
|
mainPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
|
||||||
|
|
||||||
@@ -254,11 +252,19 @@ public class MainWindow extends JFrame {
|
|||||||
sideBar = createSideBar();
|
sideBar = createSideBar();
|
||||||
center.add(sideBar, BorderLayout.WEST);
|
center.add(sideBar, BorderLayout.WEST);
|
||||||
|
|
||||||
|
|
||||||
// 内容区
|
// 内容区
|
||||||
cardsLayout = new CardLayout();
|
cardsLayout = new CardLayout();
|
||||||
cardsPanel = new JPanel(cardsLayout);
|
cardsPanel = new JPanel(cardsLayout);
|
||||||
cardsPanel.setOpaque(false);
|
cardsPanel.setOpaque(false);
|
||||||
|
|
||||||
|
cardsPanel.addComponentListener(new ComponentAdapter() {
|
||||||
|
@Override
|
||||||
|
public void componentResized(ComponentEvent e) {
|
||||||
|
syncLayeredBounds();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for (ToolCategory category : categories) {
|
for (ToolCategory category : categories) {
|
||||||
JPanel toolsPanel = createToolsPanel(category);
|
JPanel toolsPanel = createToolsPanel(category);
|
||||||
toolsPanel.setOpaque(false);
|
toolsPanel.setOpaque(false);
|
||||||
@@ -285,7 +291,12 @@ public class MainWindow extends JFrame {
|
|||||||
contentPanel.setOpaque(false);
|
contentPanel.setOpaque(false);
|
||||||
contentPanel.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 12)); // 内容区左右边距
|
contentPanel.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 12)); // 内容区左右边距
|
||||||
contentPanel.add(layeredPane, BorderLayout.CENTER);
|
contentPanel.add(layeredPane, BorderLayout.CENTER);
|
||||||
|
contentPanel.addComponentListener(new ComponentAdapter() {
|
||||||
|
@Override
|
||||||
|
public void componentResized(ComponentEvent e) {
|
||||||
|
syncLayeredBounds();
|
||||||
|
}
|
||||||
|
});
|
||||||
center.add(contentPanel, BorderLayout.CENTER);
|
center.add(contentPanel, BorderLayout.CENTER);
|
||||||
mainPanel.add(center, BorderLayout.CENTER);
|
mainPanel.add(center, BorderLayout.CENTER);
|
||||||
mainPanel.add(createFooter(), BorderLayout.SOUTH);
|
mainPanel.add(createFooter(), BorderLayout.SOUTH);
|
||||||
@@ -313,7 +324,7 @@ public class MainWindow extends JFrame {
|
|||||||
SwingUtilities.updateComponentTreeUI(this);
|
SwingUtilities.updateComponentTreeUI(this);
|
||||||
|
|
||||||
if (searchField != null) {
|
if (searchField != null) {
|
||||||
searchField.defaultBorderColor = UIManager.getColor("TextField.borderColor");
|
//searchField.defaultBorderColor = UIManager.getColor("TextField.borderColor");
|
||||||
searchField.repaint();
|
searchField.repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,14 +352,19 @@ public class MainWindow extends JFrame {
|
|||||||
private void syncLayeredBounds() {
|
private void syncLayeredBounds() {
|
||||||
if (layeredPane == null || contentPanel == null) return;
|
if (layeredPane == null || contentPanel == null) return;
|
||||||
Dimension d = contentPanel.getSize();
|
Dimension d = contentPanel.getSize();
|
||||||
// Fallback size check
|
if (d.width <= 0) return;
|
||||||
if (d.width <= 0) d = new Dimension(800, 600);
|
|
||||||
|
|
||||||
layeredPane.setBounds(0, 0, d.width, d.height);
|
layeredPane.setBounds(0, 0, d.width, d.height);
|
||||||
// Important: Update cardsPanel size explicitly
|
|
||||||
cardsPanel.setBounds(0, 0, d.width, d.height);
|
cardsPanel.setBounds(0, 0, d.width, d.height);
|
||||||
|
|
||||||
|
// 强制当前显示的滚动面板及其内部组件重新布局
|
||||||
|
for (Component comp : cardsPanel.getComponents()) {
|
||||||
|
if (comp.isVisible() && comp instanceof JScrollPane) {
|
||||||
|
JScrollPane sp = (JScrollPane) comp;
|
||||||
|
sp.getViewport().getView().revalidate(); // 触发 WrapLayout 计算
|
||||||
|
}
|
||||||
|
}
|
||||||
cardsPanel.revalidate();
|
cardsPanel.revalidate();
|
||||||
layeredPane.repaint();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Header ----------
|
// ---------- Header ----------
|
||||||
@@ -359,28 +375,21 @@ public class MainWindow extends JFrame {
|
|||||||
|
|
||||||
JLabel title = new JLabel(LanguageManager.getLoadedLanguages().getText("mainWindow.title.2"));
|
JLabel title = new JLabel(LanguageManager.getLoadedLanguages().getText("mainWindow.title.2"));
|
||||||
title.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei").getName(), Font.BOLD, 20));
|
title.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei").getName(), Font.BOLD, 20));
|
||||||
title.setForeground(UIManager.getColor("Label.foreground"));
|
title.setForeground(UIManager.getColor("Label.foreground")); // 动态颜色
|
||||||
|
|
||||||
JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
|
||||||
left.setOpaque(false);
|
|
||||||
left.add(title);
|
|
||||||
|
|
||||||
searchField = new RoundedSearchField(320, 36);
|
searchField = new RoundedSearchField(320, 36);
|
||||||
|
|
||||||
// --- 修复开始 ---
|
// --- 适配浅色模式逻辑 ---
|
||||||
// 1. 使用深色半透明背景 (黑色,透明度 80/255),保证在亮色背景图上能看清
|
if (backgroundImage != null) {
|
||||||
searchField.setBackground(new Color(0, 0, 0, 80));
|
searchField.setBackground(isDarkTheme() ? new Color(0, 0, 0, 80) : new Color(255, 255, 255, 120));
|
||||||
// 2. 文字强制设为白色
|
searchField.setForeground(isDarkTheme() ? Color.WHITE : Color.BLACK);
|
||||||
searchField.setForeground(Color.WHITE);
|
} else {
|
||||||
// 3. 添加一个淡淡的白色边框,增加轮廓感
|
// --- 修复点:直接使用 UI 库的文本框颜色 ---
|
||||||
searchField.setBorder(BorderFactory.createCompoundBorder(
|
searchField.setBackground(UIManager.getColor("TextField.background"));
|
||||||
BorderFactory.createLineBorder(new Color(255, 255, 255, 50), 1, true),
|
searchField.setForeground(UIManager.getColor("TextField.foreground"));
|
||||||
BorderFactory.createEmptyBorder(4, 10, 4, 10)
|
}
|
||||||
));
|
|
||||||
// --- 修复结束 ---
|
|
||||||
|
|
||||||
// 如果您的组件支持 setPlaceholder
|
searchField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, "Search tools...");
|
||||||
searchField.putClientProperty("JTextField.placeholderText", "Search tools...");
|
|
||||||
|
|
||||||
searchField.addDocumentListener(new DocumentListener() {
|
searchField.addDocumentListener(new DocumentListener() {
|
||||||
@Override public void insertUpdate(DocumentEvent e) { filterCurrentCategory(searchField.getText()); }
|
@Override public void insertUpdate(DocumentEvent e) { filterCurrentCategory(searchField.getText()); }
|
||||||
@@ -388,21 +397,21 @@ public class MainWindow extends JFrame {
|
|||||||
@Override public void changedUpdate(DocumentEvent e) { filterCurrentCategory(searchField.getText()); }
|
@Override public void changedUpdate(DocumentEvent e) { filterCurrentCategory(searchField.getText()); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||||
|
left.setOpaque(false);
|
||||||
|
left.add(title);
|
||||||
|
|
||||||
JPanel center = new JPanel(new GridBagLayout());
|
JPanel center = new JPanel(new GridBagLayout());
|
||||||
center.setOpaque(false);
|
center.setOpaque(false);
|
||||||
center.add(searchField);
|
center.add(searchField);
|
||||||
|
|
||||||
JButton settings = new JButton(LoadIcon.loadIcon("settings.png", 22));
|
JButton settings = new JButton(LoadIcon.loadIcon("settings.png", 22));
|
||||||
settings.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_BORDERLESS);
|
settings.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_BORDERLESS);
|
||||||
settings.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
|
||||||
settings.setContentAreaFilled(false);
|
|
||||||
settings.setFocusPainted(false);
|
|
||||||
settings.addActionListener(e -> showSettings());
|
settings.addActionListener(e -> showSettings());
|
||||||
|
|
||||||
header.add(left, BorderLayout.WEST);
|
header.add(left, BorderLayout.WEST);
|
||||||
header.add(center, BorderLayout.CENTER);
|
header.add(center, BorderLayout.CENTER);
|
||||||
header.add(settings, BorderLayout.EAST);
|
header.add(settings, BorderLayout.EAST);
|
||||||
|
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,20 +420,22 @@ public class MainWindow extends JFrame {
|
|||||||
JPanel sidebar = new JPanel(new BorderLayout()) {
|
JPanel sidebar = new JPanel(new BorderLayout()) {
|
||||||
@Override
|
@Override
|
||||||
protected void paintComponent(Graphics g) {
|
protected void paintComponent(Graphics g) {
|
||||||
// 如果有背景图片,绘制半透明遮罩
|
Graphics2D g2 = (Graphics2D) g.create();
|
||||||
if (backgroundImage != null) {
|
if (backgroundImage != null) {
|
||||||
Graphics2D g2 = (Graphics2D) g.create();
|
// 有背景图时,叠加半透明遮罩
|
||||||
g2.setColor(new Color(20, 20, 20, 150)); // 加深遮罩,防止背景图干扰文字
|
g2.setColor(isDarkTheme() ? new Color(20, 20, 20, 150) : new Color(255, 255, 255, 120));
|
||||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||||
g2.dispose();
|
|
||||||
} else {
|
} else {
|
||||||
// --- 修复重点:没有背景图时,侧边栏完全透明,只画右侧的一条淡淡的线 ---
|
// --- 修复点:没有背景图时,绘制侧边栏背景 ---
|
||||||
Graphics2D g2 = (Graphics2D) g.create();
|
// 浅色模式下,侧边栏通常比背景稍微深一点点(或者有边框)
|
||||||
// 线条颜色:白色,透明度 10% (非常淡,若隐若现)
|
if (isDarkTheme()) {
|
||||||
g2.setColor(new Color(255, 255, 255, 25));
|
g2.setColor(new Color(255, 255, 255, 15)); // 深色模式淡淡的线
|
||||||
|
} else {
|
||||||
|
g2.setColor(new Color(0, 0, 0, 20)); // 浅色模式淡淡的线
|
||||||
|
}
|
||||||
g2.drawLine(getWidth() - 1, 0, getWidth() - 1, getHeight());
|
g2.drawLine(getWidth() - 1, 0, getWidth() - 1, getHeight());
|
||||||
g2.dispose();
|
|
||||||
}
|
}
|
||||||
|
g2.dispose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -477,20 +488,17 @@ public class MainWindow extends JFrame {
|
|||||||
boolean isSelected = Objects.equals(currentCategoryId, category.getId().toString());
|
boolean isSelected = Objects.equals(currentCategoryId, category.getId().toString());
|
||||||
boolean isHover = getMousePosition() != null;
|
boolean isHover = getMousePosition() != null;
|
||||||
|
|
||||||
// 1. 先绘制背景
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
// 选中状态:使用 FlatLaf 的强调色或默认蓝色,带一点透明度让背景图透出来一点点
|
// 选中颜色使用主题强调色
|
||||||
g2.setColor(new Color(0, 120, 215, 200));
|
g2.setColor(UIManager.getColor("Component.accentColor") != null ?
|
||||||
|
UIManager.getColor("Component.accentColor") : new Color(0, 120, 215));
|
||||||
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
|
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
|
||||||
} else if (isHover) {
|
} else if (isHover) {
|
||||||
// 悬停状态:淡淡的白色/灰色
|
// 悬停背景适配
|
||||||
g2.setColor(new Color(255, 255, 255, 30));
|
g2.setColor(isDarkTheme() ? new Color(255, 255, 255, 30) : new Color(0, 0, 0, 20));
|
||||||
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
|
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
g2.dispose();
|
g2.dispose();
|
||||||
|
|
||||||
// 2. 再调用 super 绘制文字和图标
|
|
||||||
super.paintComponent(g);
|
super.paintComponent(g);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -529,6 +537,15 @@ public class MainWindow extends JFrame {
|
|||||||
switchCategory(category.getId().toString(), false);
|
switchCategory(category.getId().toString(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
button.addChangeListener(e -> {
|
||||||
|
boolean isSelected = Objects.equals(currentCategoryId, category.getId().toString());
|
||||||
|
if (isSelected) {
|
||||||
|
button.setForeground(Color.WHITE); // 选中时固定白色
|
||||||
|
} else {
|
||||||
|
button.setForeground(UIManager.getColor("Label.foreground")); // 未选中跟随主题
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
button.addMouseListener(new MouseAdapter() {
|
button.addMouseListener(new MouseAdapter() {
|
||||||
public void mouseEntered(MouseEvent e) { button.repaint(); }
|
public void mouseEntered(MouseEvent e) { button.repaint(); }
|
||||||
public void mouseExited(MouseEvent e) { button.repaint(); }
|
public void mouseExited(MouseEvent e) { button.repaint(); }
|
||||||
@@ -574,101 +591,119 @@ public class MainWindow extends JFrame {
|
|||||||
|
|
||||||
int w = getWidth();
|
int w = getWidth();
|
||||||
int h = getHeight();
|
int h = getHeight();
|
||||||
int arc = 18; // 圆角稍微大一点,更现代
|
int arc = 18;
|
||||||
|
|
||||||
// --- 修复重点:使用叠加色 ---
|
|
||||||
if (backgroundImage != null) {
|
if (backgroundImage != null) {
|
||||||
// 背景图模式:较重的磨砂黑
|
g2.setColor(isDarkTheme() ? new Color(30, 30, 30, 180) : new Color(255, 255, 255, 180));
|
||||||
g2.setColor(new Color(30, 30, 30, 180));
|
|
||||||
} else {
|
} else {
|
||||||
// 纯色模式:不要用实色,用半透明的白色覆盖在深色背景上
|
// --- 修复点:浅色模式下的卡片应为白色且带有微弱边框 ---
|
||||||
// 这种技术叫 Surface Overlay,能保证色调绝对统一
|
if (isDarkTheme()) {
|
||||||
// 12/255 ≈ 5% 的白色,形成自然的层级感
|
g2.setColor(new Color(255, 255, 255, 12));
|
||||||
g2.setColor(new Color(255, 255, 255, 12));
|
} else {
|
||||||
|
g2.setColor(Color.WHITE); // 浅色模式卡片用纯白
|
||||||
|
// 绘制一个非常淡的灰色边框,增加层次感
|
||||||
|
g2.setPaint(new Color(0, 0, 0, 30));
|
||||||
|
g2.drawRoundRect(0, 0, w - 1, h - 1, arc, arc);
|
||||||
|
g2.setColor(Color.WHITE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 填充背景
|
|
||||||
g2.fillRoundRect(0, 0, w, h, arc, arc);
|
g2.fillRoundRect(0, 0, w, h, arc, arc);
|
||||||
|
|
||||||
// 绘制极淡的边框 (增加精致感)
|
// 悬停动画效果
|
||||||
g2.setColor(new Color(255, 255, 255, 30)); // 12% 透明度的白边
|
int elevation = cardElevations.getOrDefault(this, 2);
|
||||||
g2.drawRoundRect(0, 0, w-1, h-1, arc, arc);
|
if (elevation > 2) {
|
||||||
|
// 悬停时加深一点点阴影或光晕
|
||||||
// 如果鼠标悬停(根据 cardElevations 判断),加深一点高亮
|
g2.setColor(isDarkTheme() ? new Color(255, 255, 255, 10) : new Color(0, 0, 0, 5));
|
||||||
if (cardElevations.getOrDefault(this, 2) > 2) {
|
|
||||||
g2.setColor(new Color(255, 255, 255, 10)); // 叠加一层高亮
|
|
||||||
g2.fillRoundRect(0, 0, w, h, arc, arc);
|
g2.fillRoundRect(0, 0, w, h, arc, arc);
|
||||||
}
|
}
|
||||||
|
|
||||||
super.paintComponent(g);
|
|
||||||
g2.dispose();
|
g2.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public boolean isOpaque() { return false; }
|
@Override public boolean isOpaque() { return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 内容布局保持不变 ---
|
|
||||||
card.setLayout(new BorderLayout(12, 10));
|
card.setLayout(new BorderLayout(12, 10));
|
||||||
card.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
card.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||||
card.setPreferredSize(new Dimension(260, 110));
|
card.setPreferredSize(new Dimension(260, 110));
|
||||||
card.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
|
||||||
|
|
||||||
JLabel iconLabel;
|
|
||||||
if (tool.getIcon() == null) {
|
|
||||||
iconLabel = new JLabel(tool.getImageIcon());
|
|
||||||
} else {
|
|
||||||
iconLabel = new JLabel(LoadIcon.loadIcon(tool.getIcon(), 40));
|
|
||||||
}
|
|
||||||
iconLabel.setVerticalAlignment(SwingConstants.TOP);
|
|
||||||
|
|
||||||
JPanel textPanel = new JPanel(new BorderLayout(0, 4));
|
|
||||||
textPanel.setOpaque(false);
|
|
||||||
|
|
||||||
JLabel titleLabel = new JLabel(tool.getTitle());
|
JLabel titleLabel = new JLabel(tool.getTitle());
|
||||||
titleLabel.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei").getName(), Font.BOLD, 15));
|
titleLabel.setFont(new Font(UIManager.getFont("Label.font").getName(), Font.BOLD, 15));
|
||||||
// 强制标题稍亮
|
// --- 关键修复:动态前景色 ---
|
||||||
titleLabel.setForeground(new Color(240, 240, 240));
|
titleLabel.setForeground(UIManager.getColor("Label.foreground"));
|
||||||
|
|
||||||
JTextArea descArea = new JTextArea(tool.getDescription());
|
JTextArea descArea = new JTextArea(tool.getDescription());
|
||||||
descArea.setFont(new Font(selectFont("Segoe UI").getName(), Font.PLAIN, 12));
|
descArea.setFont(new Font(UIManager.getFont("Label.font").getName(), Font.PLAIN, 12));
|
||||||
// 描述文字稍暗,形成对比
|
// --- 关键修复:动态辅助色 ---
|
||||||
descArea.setForeground(new Color(170, 170, 170));
|
descArea.setForeground(UIManager.getColor("Label.disabledForeground"));
|
||||||
descArea.setLineWrap(true);
|
descArea.setLineWrap(true);
|
||||||
descArea.setWrapStyleWord(true);
|
descArea.setWrapStyleWord(true);
|
||||||
descArea.setEditable(false);
|
descArea.setEditable(false);
|
||||||
descArea.setOpaque(false);
|
descArea.setOpaque(false);
|
||||||
descArea.setBorder(null);
|
|
||||||
descArea.setRows(2);
|
descArea.setRows(2);
|
||||||
|
descArea.setEnabled(false);
|
||||||
|
JPanel textPanel = new JPanel(new BorderLayout(0, 4));
|
||||||
|
textPanel.setOpaque(false);
|
||||||
textPanel.add(titleLabel, BorderLayout.NORTH);
|
textPanel.add(titleLabel, BorderLayout.NORTH);
|
||||||
textPanel.add(descArea, BorderLayout.CENTER);
|
textPanel.add(descArea, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JLabel iconLabel = new JLabel(tool.getIcon() == null ? tool.getImageIcon() : LoadIcon.loadIcon(tool.getIcon(), 40));
|
||||||
|
iconLabel.setVerticalAlignment(SwingConstants.TOP);
|
||||||
|
|
||||||
card.add(iconLabel, BorderLayout.WEST);
|
card.add(iconLabel, BorderLayout.WEST);
|
||||||
card.add(textPanel, BorderLayout.CENTER);
|
card.add(textPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
card.setToolTipText(tool.getName());
|
MouseAdapter cardListener = new MouseAdapter() {
|
||||||
|
|
||||||
cardScales.put(card, 1.0f);
|
|
||||||
cardElevations.put(card, 2);
|
|
||||||
|
|
||||||
CardMouseAdapter adapter = new CardMouseAdapter(card, tool);
|
|
||||||
card.addMouseListener(adapter);
|
|
||||||
|
|
||||||
card.addMouseListener(new MouseAdapter() {
|
|
||||||
@Override
|
@Override
|
||||||
public void mouseEntered(MouseEvent e) { animateCardElevation(card, 8); }
|
public void mouseEntered(MouseEvent e) { animateCardElevation(card, 8); }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mouseExited(MouseEvent e) { animateCardElevation(card, 2); }
|
public void mouseExited(MouseEvent e) { animateCardElevation(card, 2); }
|
||||||
});
|
|
||||||
|
|
||||||
// 保留事件总线逻辑
|
@Override
|
||||||
card.setUI(new PanelUI() {
|
public void mousePressed(MouseEvent e) {
|
||||||
@Override public void installUI(JComponent c) {
|
// 可以添加一个微弱的按下效果
|
||||||
GlobalEventBus.EVENT_BUS.post(new TABUIEvents(card, c));
|
|
||||||
super.installUI(c);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
// 修复点 1: 使用组件自身的尺寸判断,而不是 getBounds()
|
||||||
|
if (new Rectangle(0, 0, card.getWidth(), card.getHeight()).contains(e.getPoint())) {
|
||||||
|
if (tool.getAction() != null) {
|
||||||
|
// 修复点 2: 确保在 EDT 线程执行,并捕获异常防止界面锁死
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
try {
|
||||||
|
tool.getAction().actionPerformed(new ActionEvent(card, ActionEvent.ACTION_PERFORMED, ""));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Tool action execution failed", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
String tooltip = tool.getDescription();
|
||||||
|
card.setToolTipText(tooltip);
|
||||||
|
titleLabel.setToolTipText(tooltip);
|
||||||
|
iconLabel.setToolTipText(tooltip);
|
||||||
|
descArea.setToolTipText(tooltip);
|
||||||
|
|
||||||
|
MouseAdapter forwarder = new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseEntered(MouseEvent e) { dispatch(e); }
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent e) { dispatch(e); }
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) { dispatch(e); }
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) { dispatch(e); }
|
||||||
|
|
||||||
|
private void dispatch(MouseEvent e) {
|
||||||
|
card.dispatchEvent(SwingUtilities.convertMouseEvent(descArea, e, card));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
card.addMouseListener(cardListener);
|
||||||
|
iconLabel.addMouseListener(cardListener);
|
||||||
|
descArea.addMouseListener(forwarder);
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,19 +742,22 @@ public class MainWindow extends JFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void animateCardElevation(JComponent card, int targetElevation) {
|
private void animateCardElevation(JComponent card, int targetElevation) {
|
||||||
new Timer(15, new ActionListener() {
|
Timer existing = runningTimers.get(card);
|
||||||
@Override
|
if (existing != null) existing.stop();
|
||||||
public void actionPerformed(ActionEvent e) {
|
|
||||||
int current = cardElevations.getOrDefault(card, 2);
|
Timer newTimer = new Timer(15, e -> {
|
||||||
if (current == targetElevation) {
|
int current = cardElevations.getOrDefault(card, 2);
|
||||||
((Timer)e.getSource()).stop();
|
if (current == targetElevation) {
|
||||||
return;
|
((Timer)e.getSource()).stop();
|
||||||
}
|
runningTimers.remove(card);
|
||||||
int next = current < targetElevation ? current + 1 : current - 1;
|
return;
|
||||||
cardElevations.put(card, next);
|
|
||||||
card.repaint();
|
|
||||||
}
|
}
|
||||||
}).start();
|
int next = current < targetElevation ? current + 1 : current - 1;
|
||||||
|
cardElevations.put(card, next);
|
||||||
|
card.repaint();
|
||||||
|
});
|
||||||
|
runningTimers.put(card, newTimer);
|
||||||
|
newTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Settings (Redesigned) ----------
|
// ---------- Settings (Redesigned) ----------
|
||||||
@@ -848,28 +886,25 @@ public class MainWindow extends JFrame {
|
|||||||
|
|
||||||
private void filterCurrentCategory(String query) {
|
private void filterCurrentCategory(String query) {
|
||||||
if (currentCategoryId == null) return;
|
if (currentCategoryId == null) return;
|
||||||
String q = query == null ? "" : query.trim().toLowerCase();
|
String q = query.toLowerCase().trim();
|
||||||
ToolCategory category = categories.stream()
|
ToolCategory category = categories.stream()
|
||||||
.filter(tc -> Objects.equals(tc.getId().toString(), currentCategoryId))
|
.filter(tc -> tc.getId().toString().equals(currentCategoryId))
|
||||||
.findFirst().orElse(null);
|
.findFirst().orElse(null);
|
||||||
|
|
||||||
if (category == null) return;
|
if (category == null) return;
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
JPanel newPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 20, 20));
|
ResponsivePanel newPanel = new ResponsivePanel();
|
||||||
newPanel.setOpaque(false);
|
|
||||||
for (ToolItem tool : category.getTools()) {
|
for (ToolItem tool : category.getTools()) {
|
||||||
if (q.isEmpty() || tool.getTitle().toLowerCase().contains(q) || tool.getName().toLowerCase().contains(q)) {
|
if (q.isEmpty() || tool.getTitle().toLowerCase().contains(q)) {
|
||||||
newPanel.add(createToolCard(tool));
|
newPanel.add(createToolCard(tool));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JPanel wrapper = new JPanel(new BorderLayout());
|
|
||||||
wrapper.setOpaque(false);
|
|
||||||
wrapper.add(newPanel, BorderLayout.NORTH);
|
|
||||||
|
|
||||||
JScrollPane scrollPane = categoryScrollPanes.get(currentCategoryId);
|
JScrollPane scrollPane = categoryScrollPanes.get(currentCategoryId);
|
||||||
if (scrollPane != null) {
|
if (scrollPane != null) {
|
||||||
scrollPane.setViewportView(wrapper);
|
scrollPane.setViewportView(newPanel);
|
||||||
|
newPanel.revalidate();
|
||||||
|
scrollPane.repaint();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,154 +8,143 @@ import java.awt.geom.Point2D;
|
|||||||
|
|
||||||
public class RoundedSearchField extends JPanel {
|
public class RoundedSearchField extends JPanel {
|
||||||
private final JTextField textField;
|
private final JTextField textField;
|
||||||
private int targetWidth;
|
|
||||||
private float animProgress = 0f;
|
|
||||||
private Timer animTimer;
|
|
||||||
private final int baseWidth;
|
private final int baseWidth;
|
||||||
private final int heightPx;
|
private final int heightPx;
|
||||||
|
|
||||||
// 动画相关变量
|
// 动画核心变量
|
||||||
|
private float targetWidth;
|
||||||
|
private float currentWidth; // 记录当前动画中的宽度,避免依赖 getWidth()
|
||||||
|
private Timer animTimer;
|
||||||
|
|
||||||
private float glowPosition = 0f;
|
private float glowPosition = 0f;
|
||||||
private final Timer glowTimer;
|
private final Timer glowTimer;
|
||||||
public Color defaultBorderColor;
|
|
||||||
private boolean focused = false;
|
private boolean focused = false;
|
||||||
|
|
||||||
|
private static final float[] GRAD_FRACTIONS = {0f, 0.25f, 0.5f, 0.75f, 1f};
|
||||||
|
private static final Color[] GRAD_COLORS = {
|
||||||
|
new Color(255, 50, 50),
|
||||||
|
new Color(255, 180, 0),
|
||||||
|
new Color(0, 200, 0),
|
||||||
|
new Color(0, 150, 255),
|
||||||
|
new Color(180, 0, 255)
|
||||||
|
};
|
||||||
|
|
||||||
public RoundedSearchField(int baseWidth, int heightPx) {
|
public RoundedSearchField(int baseWidth, int heightPx) {
|
||||||
this.baseWidth = baseWidth;
|
this.baseWidth = baseWidth;
|
||||||
this.heightPx = heightPx;
|
this.heightPx = heightPx;
|
||||||
this.targetWidth = baseWidth;
|
this.targetWidth = baseWidth;
|
||||||
|
this.currentWidth = baseWidth; // 初始宽度
|
||||||
|
|
||||||
setOpaque(false);
|
setOpaque(false);
|
||||||
setLayout(new BorderLayout());
|
setLayout(new BorderLayout());
|
||||||
|
|
||||||
// 获取系统默认边框色
|
|
||||||
defaultBorderColor = UIManager.getColor("TextField.borderColor");
|
|
||||||
if (defaultBorderColor == null) {
|
|
||||||
defaultBorderColor = new Color(180, 180, 180); // 备用默认色
|
|
||||||
}
|
|
||||||
|
|
||||||
textField = new JTextField();
|
textField = new JTextField();
|
||||||
textField.setBorder(BorderFactory.createEmptyBorder(6, 10, 6, 10));
|
textField.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 15));
|
||||||
textField.setOpaque(false);
|
textField.setOpaque(false);
|
||||||
textField.setFont(UIManager.getFont("TextField.font"));
|
textField.setBackground(new Color(0,0,0,0));
|
||||||
|
|
||||||
|
// 初始尺寸
|
||||||
setPreferredSize(new Dimension(baseWidth, heightPx));
|
setPreferredSize(new Dimension(baseWidth, heightPx));
|
||||||
add(textField, BorderLayout.CENTER);
|
add(textField, BorderLayout.CENTER);
|
||||||
|
|
||||||
// 焦点监听器
|
|
||||||
textField.addFocusListener(new FocusAdapter() {
|
textField.addFocusListener(new FocusAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void focusGained(FocusEvent e) {
|
public void focusGained(FocusEvent e) {
|
||||||
focused = true;
|
focused = true;
|
||||||
animateTo(baseWidth + 80);
|
startAnimation(baseWidth + 80);
|
||||||
glowTimer.start();
|
glowTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void focusLost(FocusEvent e) {
|
public void focusLost(FocusEvent e) {
|
||||||
focused = false;
|
focused = false;
|
||||||
animateTo(baseWidth);
|
startAnimation(baseWidth);
|
||||||
glowTimer.stop();
|
glowTimer.stop();
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 点击面板聚焦文本框
|
// 尺寸动画:使用差值平滑移动
|
||||||
addMouseListener(new MouseAdapter() {
|
animTimer = new Timer(10, ae -> {
|
||||||
@Override public void mouseClicked(MouseEvent e) {
|
float diff = targetWidth - currentWidth;
|
||||||
textField.requestFocusInWindow();
|
if (Math.abs(diff) < 0.5f) { // 足够接近目标值
|
||||||
}
|
currentWidth = targetWidth;
|
||||||
});
|
|
||||||
|
|
||||||
// 尺寸动画定时器
|
|
||||||
animTimer = new Timer(16, ae -> {
|
|
||||||
int curW = getWidth();
|
|
||||||
int diff = targetWidth - curW;
|
|
||||||
if (Math.abs(diff) <= 1) {
|
|
||||||
setPreferredSize(new Dimension(targetWidth, heightPx));
|
|
||||||
revalidate();
|
|
||||||
repaint();
|
|
||||||
animTimer.stop();
|
animTimer.stop();
|
||||||
} else {
|
} else {
|
||||||
int step = Math.max(1, Math.abs(diff) / 6);
|
// 缓动公式:每次移动剩余距离的 20%,产生平滑减速效果
|
||||||
int newW = curW + (diff > 0 ? step : -step);
|
currentWidth += diff * 0.2f;
|
||||||
setPreferredSize(new Dimension(newW, heightPx));
|
|
||||||
revalidate();
|
|
||||||
repaint();
|
|
||||||
}
|
}
|
||||||
|
// 关键:强制更新布局
|
||||||
|
revalidate();
|
||||||
|
repaint();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 发光动画定时器
|
|
||||||
glowTimer = new Timer(30, e -> {
|
glowTimer = new Timer(30, e -> {
|
||||||
glowPosition = (glowPosition + 0.03f) % 1f;
|
glowPosition = (glowPosition + 0.02f) % 1f;
|
||||||
repaint();
|
repaint();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateThemeColors() {
|
// 重写此方法,让布局管理器(如 FlowLayout)实时跟随动画宽度
|
||||||
defaultBorderColor = UIManager.getColor("TextField.borderColor");
|
@Override
|
||||||
if (defaultBorderColor == null) {
|
public Dimension getPreferredSize() {
|
||||||
defaultBorderColor = new Color(180, 180, 180);
|
return new Dimension((int) currentWidth, heightPx);
|
||||||
}
|
}
|
||||||
repaint();
|
|
||||||
|
private void startAnimation(int w) {
|
||||||
|
this.targetWidth = w;
|
||||||
|
if (!animTimer.isRunning()) animTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void paintComponent(Graphics g) {
|
protected void paintComponent(Graphics g) {
|
||||||
super.paintComponent(g);
|
|
||||||
Graphics2D g2d = (Graphics2D) g.create();
|
Graphics2D g2d = (Graphics2D) g.create();
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||||
|
|
||||||
int arc = heightPx / 2; // 圆角半径
|
int w = getWidth();
|
||||||
int borderThickness = focused ? 2 : 1;
|
int h = getHeight();
|
||||||
|
int arc = h;
|
||||||
|
|
||||||
// 绘制背景
|
// 背景颜色处理
|
||||||
g2d.setColor(getBackground());
|
Color bgBase = UIManager.getColor("TextField.background");
|
||||||
g2d.fillRoundRect(0, 0, getWidth(), getHeight(), arc, arc);
|
if (!focused && bgBase != null && bgBase.getRed() > 240) {
|
||||||
|
g2d.setColor(new Color(245, 245, 247)); // 浅色模式稍微加深一点区分
|
||||||
// 绘制边框
|
|
||||||
if (focused) {
|
|
||||||
// 流动彩虹渐变
|
|
||||||
float[] fractions = {0f, 0.25f, 0.5f, 0.75f, 1f};
|
|
||||||
Color[] colors = {
|
|
||||||
new Color(255, 0, 0, 200), // 红
|
|
||||||
new Color(255, 165, 0, 200), // 橙
|
|
||||||
new Color(0, 255, 0, 200), // 绿
|
|
||||||
new Color(0, 191, 255, 200), // 蓝
|
|
||||||
new Color(148, 0, 211, 200) // 紫
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建循环渐变
|
|
||||||
Point2D start = new Point2D.Float(getWidth() * glowPosition, 0);
|
|
||||||
Point2D end = new Point2D.Float(getWidth() * glowPosition + getWidth(), 0);
|
|
||||||
LinearGradientPaint gradient = new LinearGradientPaint(
|
|
||||||
start, end, fractions, colors
|
|
||||||
);
|
|
||||||
|
|
||||||
g2d.setPaint(gradient);
|
|
||||||
g2d.setStroke(new BasicStroke(2.5f));
|
|
||||||
} else {
|
} else {
|
||||||
g2d.setColor(defaultBorderColor);
|
g2d.setColor(bgBase != null ? bgBase : Color.WHITE);
|
||||||
g2d.setStroke(new BasicStroke(1f));
|
|
||||||
}
|
}
|
||||||
|
g2d.fillRoundRect(0, 0, w, h, arc, arc);
|
||||||
|
|
||||||
g2d.drawRoundRect(borderThickness/2, borderThickness/2,
|
|
||||||
getWidth() - borderThickness, getHeight() - borderThickness,
|
|
||||||
arc, arc);
|
|
||||||
|
|
||||||
// 添加发光效果
|
|
||||||
if (focused) {
|
if (focused) {
|
||||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f));
|
// 彩虹边框
|
||||||
g2d.setStroke(new BasicStroke(4f));
|
float startX = w * glowPosition;
|
||||||
g2d.drawRoundRect(0, 0, getWidth(), getHeight(), arc, arc);
|
LinearGradientPaint grad = new LinearGradientPaint(
|
||||||
|
new Point2D.Float(startX - w, 0),
|
||||||
|
new Point2D.Float(startX, 0),
|
||||||
|
GRAD_FRACTIONS, GRAD_COLORS,
|
||||||
|
MultipleGradientPaint.CycleMethod.REPEAT
|
||||||
|
);
|
||||||
|
g2d.setPaint(grad);
|
||||||
|
g2d.setStroke(new BasicStroke(2.0f));
|
||||||
|
g2d.drawRoundRect(1, 1, w - 2, h - 2, arc, arc);
|
||||||
|
|
||||||
|
// 外发光
|
||||||
|
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.15f));
|
||||||
|
g2d.setStroke(new BasicStroke(3.0f));
|
||||||
|
g2d.drawRoundRect(0, 0, w, h, arc, arc);
|
||||||
|
} else {
|
||||||
|
// 默认边框
|
||||||
|
Color borderCol = UIManager.getColor("Component.borderColor");
|
||||||
|
if (borderCol == null || borderCol.getRed() > 220) {
|
||||||
|
borderCol = new Color(210, 210, 215);
|
||||||
|
}
|
||||||
|
g2d.setColor(borderCol);
|
||||||
|
g2d.setStroke(new BasicStroke(1.0f));
|
||||||
|
g2d.drawRoundRect(1, 1, w - 2, h - 2, arc, arc);
|
||||||
}
|
}
|
||||||
|
|
||||||
g2d.dispose();
|
g2d.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void animateTo(int w) {
|
|
||||||
this.targetWidth = w;
|
|
||||||
if (!animTimer.isRunning()) animTimer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getText() { return textField.getText(); }
|
public String getText() { return textField.getText(); }
|
||||||
public void setText(String t) { textField.setText(t); }
|
public void setText(String t) { textField.setText(t); }
|
||||||
public void addActionListener(ActionListener l) { textField.addActionListener(l); }
|
public void addActionListener(ActionListener l) { textField.addActionListener(l); }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Auto-generated build information
|
# Auto-generated build information
|
||||||
version=0.0.1
|
version=0.0.1
|
||||||
buildTimestamp=2026-01-03T09:20:12.6386031
|
buildTimestamp=2026-01-03T10:40:36.7967778
|
||||||
buildSystem=WINDOWS
|
buildSystem=WINDOWS
|
||||||
|
|||||||
Reference in New Issue
Block a user