refactor(browser): 重构消息路由和界面组件以支持双路由和主题适配

- 实现双CefMessageRouter配置,支持javaQuery和cefQuery两种查询方式
- 移除handler判空注释,优化system请求处理逻辑
- 修复java-response解析中的数组越界问题
- 添加运行时计时器管理,避免计时器冲突
- 优化背景图绘制和主题色动态适配
- 实现卡片组件的动态布局和悬停效果
- 改进搜索框的动画和主题适配
- 修复JAR文件处理中的异常捕获和错误响应
- 优化工具卡片的鼠标事件处理和线程安全
- 实现响应式面板布局和组件尺寸同步
This commit is contained in:
2026-01-03 10:41:24 +08:00
parent 7a20c3988f
commit d2e40744cf
5 changed files with 284 additions and 249 deletions

View File

@@ -493,19 +493,10 @@ public class BrowserCore {
}
private void setupMessageHandlers(WindowOperationHandler handler) {
// 1. 配置 (保持上一轮修复的代码)
CefMessageRouter.CefMessageRouterConfig routerConfig = new CefMessageRouter.CefMessageRouterConfig();
routerConfig.jsQueryFunction = "javaQuery";
routerConfig.jsCancelFunction = "javaQueryCancel";
// 2. 创建 (保持上一轮修复的代码)
msgRouter = CefMessageRouter.create(routerConfig);
// 3. 添加处理器
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
// 1. 定义共享的处理器逻辑 (为了避免代码重复,先提取出来)
CefMessageRouterHandlerAdapter commonHandler = new CefMessageRouterHandlerAdapter() {
@Override
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) {
// A. 处理 JS Bridge 请求 (Controller)
if (jsController != null && request.startsWith("jsbridge:")) {
return jsController.handleQuery(browser, frame, queryId, request, persistent, callback);
@@ -513,7 +504,6 @@ public class BrowserCore {
// B. 处理 System 请求 (OperationHandler)
if (request.startsWith("system:")) {
// ★★★ [增加判空] 防止 handler 为空导致空指针异常 ★★★
if (handler != null) {
String[] parts = request.split(":");
String operation = parts.length >= 2 ? parts[1] : null;
@@ -521,7 +511,7 @@ public class BrowserCore {
handler.handleOperation(new WindowOperation(operation, targetWindow, callback));
return true;
} else {
BrowserLog.warn("收到 system 请求但 handler 为空: {}" , request);
BrowserLog.warn("收到 system 请求但 handler 为空: {}", request);
callback.failure(404, "No handler for system operation");
return true;
}
@@ -530,7 +520,7 @@ public class BrowserCore {
// C. 处理 Java Response
if (request.startsWith("java-response:")) {
String[] parts = request.split(":");
String requestId = parts[1];
String requestId = parts.length > 1 ? parts[1] : "";
String responseData = parts.length > 2 ? parts[2] : "";
Consumer<String> cb = WindowRegistry.getInstance().getCallback(requestId);
if (cb != null) {
@@ -543,9 +533,26 @@ public class BrowserCore {
}
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() {

View File

@@ -67,9 +67,7 @@ public class ModernJarViewer {
switch (type) {
case "openJar":
// 支持从前端传路径(拖拽进入)或弹窗选择
String path = json.optString("path", null);
handleOpenJar(parent, path, callback);
handleOpenJar(parent, json.optString("path", null), callback);
return true;
case "getFile":
handleGetFile(json.optString("path"), callback);
@@ -137,16 +135,21 @@ public class ModernJarViewer {
chooser.setFileFilter(new FileNameExtensionFilter("JAR Files", "jar", "zip", "war"));
chooser.setDialogTitle("Select JAR");
if (chooser.showOpenDialog(parent) != JFileChooser.APPROVE_OPTION) {
if (callback != null) callback.failure(404, "Cancelled");
callback.failure(404, "User cancelled");
return;
}
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) {
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();
if (callback != null) callback.success(root.toString());
} catch (IOException e) {
if (callback != null) callback.failure(500, "Failed to open JAR: " + e.getMessage());
} catch (Exception e) { // 关键修改:从 IOException 改为 Exception
e.printStackTrace();
if (callback != null) callback.failure(500, "Load Error: " + e.getMessage());
}
}

View File

@@ -70,6 +70,7 @@ public class MainWindow extends JFrame {
// settings dialog
private WindowsJDialog dialog;
private final Map<JComponent, Timer> runningTimers = new ConcurrentHashMap<>();
public MainWindow() {
// 字体配置:优先使用无衬线现代字体
@@ -219,8 +220,7 @@ public class MainWindow extends JFrame {
if (backgroundImage != null) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, backgroundOpacity));
Dimension size = getSize();
BufferedImage bg = createBlurredBackground(size);
BufferedImage bg = createBlurredBackground(getSize());
if (bg != null) g2d.drawImage(bg, 0, 0, null);
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) {
mainPanel.setOpaque(false);
mainPanel.setBackground(new Color(0,0,0,0));
} else {
mainPanel.setOpaque(true);
// 获取主题中最深的背景色(通常是 Window 的背景,比 Panel 深)
Color deepBg = UIManager.getColor("Window.background");
// 如果获取不到,就用一个极深的颜色兜底
if (deepBg == null) deepBg = new Color(24, 24, 24);
mainPanel.setBackground(deepBg);
// --- 修复点:动态获取窗口背景色,不要硬编码 24,24,24 ---
Color bg = UIManager.getColor("Window.background");
if (bg == null) bg = UIManager.getColor("Panel.background");
// 如果还是 null根据主题手动设置安全的浅色/深色
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));
@@ -254,11 +252,19 @@ public class MainWindow extends JFrame {
sideBar = createSideBar();
center.add(sideBar, BorderLayout.WEST);
// 内容区
cardsLayout = new CardLayout();
cardsPanel = new JPanel(cardsLayout);
cardsPanel.setOpaque(false);
cardsPanel.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
syncLayeredBounds();
}
});
for (ToolCategory category : categories) {
JPanel toolsPanel = createToolsPanel(category);
toolsPanel.setOpaque(false);
@@ -285,7 +291,12 @@ public class MainWindow extends JFrame {
contentPanel.setOpaque(false);
contentPanel.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 12)); // 内容区左右边距
contentPanel.add(layeredPane, BorderLayout.CENTER);
contentPanel.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
syncLayeredBounds();
}
});
center.add(contentPanel, BorderLayout.CENTER);
mainPanel.add(center, BorderLayout.CENTER);
mainPanel.add(createFooter(), BorderLayout.SOUTH);
@@ -313,7 +324,7 @@ public class MainWindow extends JFrame {
SwingUtilities.updateComponentTreeUI(this);
if (searchField != null) {
searchField.defaultBorderColor = UIManager.getColor("TextField.borderColor");
//searchField.defaultBorderColor = UIManager.getColor("TextField.borderColor");
searchField.repaint();
}
@@ -341,14 +352,19 @@ public class MainWindow extends JFrame {
private void syncLayeredBounds() {
if (layeredPane == null || contentPanel == null) return;
Dimension d = contentPanel.getSize();
// Fallback size check
if (d.width <= 0) d = new Dimension(800, 600);
if (d.width <= 0) return;
layeredPane.setBounds(0, 0, d.width, d.height);
// Important: Update cardsPanel size explicitly
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();
layeredPane.repaint();
}
// ---------- Header ----------
@@ -359,28 +375,21 @@ public class MainWindow extends JFrame {
JLabel title = new JLabel(LanguageManager.getLoadedLanguages().getText("mainWindow.title.2"));
title.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei").getName(), Font.BOLD, 20));
title.setForeground(UIManager.getColor("Label.foreground"));
JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
left.setOpaque(false);
left.add(title);
title.setForeground(UIManager.getColor("Label.foreground")); // 动态颜色
searchField = new RoundedSearchField(320, 36);
// --- 修复开始 ---
// 1. 使用深色半透明背景 (黑色,透明度 80/255),保证在亮色背景图上能看清
searchField.setBackground(new Color(0, 0, 0, 80));
// 2. 文字强制设为白色
searchField.setForeground(Color.WHITE);
// 3. 添加一个淡淡的白色边框,增加轮廓感
searchField.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(255, 255, 255, 50), 1, true),
BorderFactory.createEmptyBorder(4, 10, 4, 10)
));
// --- 修复结束 ---
// --- 适配浅色模式逻辑 ---
if (backgroundImage != null) {
searchField.setBackground(isDarkTheme() ? new Color(0, 0, 0, 80) : new Color(255, 255, 255, 120));
searchField.setForeground(isDarkTheme() ? Color.WHITE : Color.BLACK);
} else {
// --- 修复点:直接使用 UI 库的文本框颜色 ---
searchField.setBackground(UIManager.getColor("TextField.background"));
searchField.setForeground(UIManager.getColor("TextField.foreground"));
}
// 如果您的组件支持 setPlaceholder
searchField.putClientProperty("JTextField.placeholderText", "Search tools...");
searchField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, "Search tools...");
searchField.addDocumentListener(new DocumentListener() {
@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()); }
});
JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
left.setOpaque(false);
left.add(title);
JPanel center = new JPanel(new GridBagLayout());
center.setOpaque(false);
center.add(searchField);
JButton settings = new JButton(LoadIcon.loadIcon("settings.png", 22));
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());
header.add(left, BorderLayout.WEST);
header.add(center, BorderLayout.CENTER);
header.add(settings, BorderLayout.EAST);
return header;
}
@@ -411,20 +420,22 @@ public class MainWindow extends JFrame {
JPanel sidebar = new JPanel(new BorderLayout()) {
@Override
protected void paintComponent(Graphics g) {
// 如果有背景图片,绘制半透明遮罩
Graphics2D g2 = (Graphics2D) g.create();
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.dispose();
} else {
// --- 修复点:没有背景图时,侧边栏完全透明,只画右侧的一条淡淡的线 ---
Graphics2D g2 = (Graphics2D) g.create();
// 线条颜色:白色,透明度 10% (非常淡,若隐若现)
g2.setColor(new Color(255, 255, 255, 25));
// --- 修复点:没有背景图时,绘制侧边栏背景 ---
// 浅色模式下,侧边栏通常比背景稍微深一点点(或者有边框)
if (isDarkTheme()) {
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.dispose();
}
g2.dispose();
}
};
@@ -477,20 +488,17 @@ public class MainWindow extends JFrame {
boolean isSelected = Objects.equals(currentCategoryId, category.getId().toString());
boolean isHover = getMousePosition() != null;
// 1. 先绘制背景
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);
} 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.dispose();
// 2. 再调用 super 绘制文字和图标
super.paintComponent(g);
}
};
@@ -529,6 +537,15 @@ public class MainWindow extends JFrame {
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() {
public void mouseEntered(MouseEvent e) { button.repaint(); }
public void mouseExited(MouseEvent e) { button.repaint(); }
@@ -574,101 +591,119 @@ public class MainWindow extends JFrame {
int w = getWidth();
int h = getHeight();
int arc = 18; // 圆角稍微大一点,更现代
int arc = 18;
// --- 修复重点:使用叠加色 ---
if (backgroundImage != null) {
// 背景图模式:较重的磨砂黑
g2.setColor(new Color(30, 30, 30, 180));
g2.setColor(isDarkTheme() ? new Color(30, 30, 30, 180) : new Color(255, 255, 255, 180));
} else {
// 纯色模式:不要用实色,用半透明的白色覆盖在深色背景上
// 这种技术叫 Surface Overlay能保证色调绝对统一
// 12/255 ≈ 5% 的白色,形成自然的层级感
g2.setColor(new Color(255, 255, 255, 12));
// --- 修复点:浅色模式下的卡片应为白色且带有微弱边框 ---
if (isDarkTheme()) {
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.setColor(new Color(255, 255, 255, 30)); // 12% 透明度的白边
g2.drawRoundRect(0, 0, w-1, h-1, arc, arc);
// 如果鼠标悬停(根据 cardElevations 判断),加深一点高亮
if (cardElevations.getOrDefault(this, 2) > 2) {
g2.setColor(new Color(255, 255, 255, 10)); // 叠加一层高亮
// 悬停动画效果
int elevation = cardElevations.getOrDefault(this, 2);
if (elevation > 2) {
// 悬停时加深一点点阴影或光晕
g2.setColor(isDarkTheme() ? new Color(255, 255, 255, 10) : new Color(0, 0, 0, 5));
g2.fillRoundRect(0, 0, w, h, arc, arc);
}
super.paintComponent(g);
g2.dispose();
}
@Override public boolean isOpaque() { return false; }
};
// --- 内容布局保持不变 ---
card.setLayout(new BorderLayout(12, 10));
card.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
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());
titleLabel.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei").getName(), Font.BOLD, 15));
// 强制标题稍亮
titleLabel.setForeground(new Color(240, 240, 240));
titleLabel.setFont(new Font(UIManager.getFont("Label.font").getName(), Font.BOLD, 15));
// --- 关键修复:动态前景色 ---
titleLabel.setForeground(UIManager.getColor("Label.foreground"));
JTextArea descArea = new JTextArea(tool.getDescription());
descArea.setFont(new Font(selectFont("Segoe UI").getName(), Font.PLAIN, 12));
// 描述文字稍暗,形成对比
descArea.setForeground(new Color(170, 170, 170));
descArea.setFont(new Font(UIManager.getFont("Label.font").getName(), Font.PLAIN, 12));
// --- 关键修复:动态辅助色 ---
descArea.setForeground(UIManager.getColor("Label.disabledForeground"));
descArea.setLineWrap(true);
descArea.setWrapStyleWord(true);
descArea.setEditable(false);
descArea.setOpaque(false);
descArea.setBorder(null);
descArea.setRows(2);
descArea.setEnabled(false);
JPanel textPanel = new JPanel(new BorderLayout(0, 4));
textPanel.setOpaque(false);
textPanel.add(titleLabel, BorderLayout.NORTH);
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(textPanel, BorderLayout.CENTER);
card.setToolTipText(tool.getName());
cardScales.put(card, 1.0f);
cardElevations.put(card, 2);
CardMouseAdapter adapter = new CardMouseAdapter(card, tool);
card.addMouseListener(adapter);
card.addMouseListener(new MouseAdapter() {
MouseAdapter cardListener = new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) { animateCardElevation(card, 8); }
@Override
public void mouseExited(MouseEvent e) { animateCardElevation(card, 2); }
});
// 保留事件总线逻辑
card.setUI(new PanelUI() {
@Override public void installUI(JComponent c) {
GlobalEventBus.EVENT_BUS.post(new TABUIEvents(card, c));
super.installUI(c);
@Override
public void mousePressed(MouseEvent e) {
// 可以添加一个微弱的按下效果
}
});
@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;
}
@@ -707,19 +742,22 @@ public class MainWindow extends JFrame {
}
private void animateCardElevation(JComponent card, int targetElevation) {
new Timer(15, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
int current = cardElevations.getOrDefault(card, 2);
if (current == targetElevation) {
((Timer)e.getSource()).stop();
return;
}
int next = current < targetElevation ? current + 1 : current - 1;
cardElevations.put(card, next);
card.repaint();
Timer existing = runningTimers.get(card);
if (existing != null) existing.stop();
Timer newTimer = new Timer(15, e -> {
int current = cardElevations.getOrDefault(card, 2);
if (current == targetElevation) {
((Timer)e.getSource()).stop();
runningTimers.remove(card);
return;
}
}).start();
int next = current < targetElevation ? current + 1 : current - 1;
cardElevations.put(card, next);
card.repaint();
});
runningTimers.put(card, newTimer);
newTimer.start();
}
// ---------- Settings (Redesigned) ----------
@@ -848,28 +886,25 @@ public class MainWindow extends JFrame {
private void filterCurrentCategory(String query) {
if (currentCategoryId == null) return;
String q = query == null ? "" : query.trim().toLowerCase();
String q = query.toLowerCase().trim();
ToolCategory category = categories.stream()
.filter(tc -> Objects.equals(tc.getId().toString(), currentCategoryId))
.filter(tc -> tc.getId().toString().equals(currentCategoryId))
.findFirst().orElse(null);
if (category == null) return;
SwingUtilities.invokeLater(() -> {
JPanel newPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 20, 20));
newPanel.setOpaque(false);
ResponsivePanel newPanel = new ResponsivePanel();
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));
}
}
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.setOpaque(false);
wrapper.add(newPanel, BorderLayout.NORTH);
JScrollPane scrollPane = categoryScrollPanes.get(currentCategoryId);
if (scrollPane != null) {
scrollPane.setViewportView(wrapper);
scrollPane.setViewportView(newPanel);
newPanel.revalidate();
scrollPane.repaint();
}
});
}

View File

@@ -8,156 +8,145 @@ import java.awt.geom.Point2D;
public class RoundedSearchField extends JPanel {
private final JTextField textField;
private int targetWidth;
private float animProgress = 0f;
private Timer animTimer;
private final int baseWidth;
private final int heightPx;
// 动画相关变量
// 动画核心变量
private float targetWidth;
private float currentWidth; // 记录当前动画中的宽度,避免依赖 getWidth()
private Timer animTimer;
private float glowPosition = 0f;
private final Timer glowTimer;
public Color defaultBorderColor;
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) {
this.baseWidth = baseWidth;
this.heightPx = heightPx;
this.targetWidth = baseWidth;
this.currentWidth = baseWidth; // 初始宽度
setOpaque(false);
setLayout(new BorderLayout());
// 获取系统默认边框色
defaultBorderColor = UIManager.getColor("TextField.borderColor");
if (defaultBorderColor == null) {
defaultBorderColor = new Color(180, 180, 180); // 备用默认色
}
textField = new JTextField();
textField.setBorder(BorderFactory.createEmptyBorder(6, 10, 6, 10));
textField.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 15));
textField.setOpaque(false);
textField.setFont(UIManager.getFont("TextField.font"));
textField.setBackground(new Color(0,0,0,0));
// 初始尺寸
setPreferredSize(new Dimension(baseWidth, heightPx));
add(textField, BorderLayout.CENTER);
// 焦点监听器
textField.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
focused = true;
animateTo(baseWidth + 80);
startAnimation(baseWidth + 80);
glowTimer.start();
}
@Override
public void focusLost(FocusEvent e) {
focused = false;
animateTo(baseWidth);
startAnimation(baseWidth);
glowTimer.stop();
repaint();
}
});
// 点击面板聚焦文本框
addMouseListener(new MouseAdapter() {
@Override public void mouseClicked(MouseEvent e) {
textField.requestFocusInWindow();
}
});
// 尺寸动画定时器
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 = new Timer(10, ae -> {
float diff = targetWidth - currentWidth;
if (Math.abs(diff) < 0.5f) { // 足够接近目标值
currentWidth = targetWidth;
animTimer.stop();
} else {
int step = Math.max(1, Math.abs(diff) / 6);
int newW = curW + (diff > 0 ? step : -step);
setPreferredSize(new Dimension(newW, heightPx));
revalidate();
repaint();
// 缓动公式:每次移动剩余距离的 20%,产生平滑减速效果
currentWidth += diff * 0.2f;
}
// 关键:强制更新布局
revalidate();
repaint();
});
// 发光动画定时器
glowTimer = new Timer(30, e -> {
glowPosition = (glowPosition + 0.03f) % 1f;
glowPosition = (glowPosition + 0.02f) % 1f;
repaint();
});
}
public void updateThemeColors() {
defaultBorderColor = UIManager.getColor("TextField.borderColor");
if (defaultBorderColor == null) {
defaultBorderColor = new Color(180, 180, 180);
}
repaint();
// 重写此方法,让布局管理器(如 FlowLayout实时跟随动画宽度
@Override
public Dimension getPreferredSize() {
return new Dimension((int) currentWidth, heightPx);
}
private void startAnimation(int w) {
this.targetWidth = w;
if (!animTimer.isRunning()) animTimer.start();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
int arc = heightPx / 2; // 圆角半径
int borderThickness = focused ? 2 : 1;
int w = getWidth();
int h = getHeight();
int arc = h;
// 绘制背景
g2d.setColor(getBackground());
g2d.fillRoundRect(0, 0, getWidth(), getHeight(), arc, arc);
// 绘制边框
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));
// 背景颜色处理
Color bgBase = UIManager.getColor("TextField.background");
if (!focused && bgBase != null && bgBase.getRed() > 240) {
g2d.setColor(new Color(245, 245, 247)); // 浅色模式稍微加深一点区分
} else {
g2d.setColor(defaultBorderColor);
g2d.setStroke(new BasicStroke(1f));
g2d.setColor(bgBase != null ? bgBase : Color.WHITE);
}
g2d.fillRoundRect(0, 0, w, h, arc, arc);
g2d.drawRoundRect(borderThickness/2, borderThickness/2,
getWidth() - borderThickness, getHeight() - borderThickness,
arc, arc);
// 添加发光效果
if (focused) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f));
g2d.setStroke(new BasicStroke(4f));
g2d.drawRoundRect(0, 0, getWidth(), getHeight(), arc, arc);
// 彩虹边框
float startX = w * glowPosition;
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();
}
void animateTo(int w) {
this.targetWidth = w;
if (!animTimer.isRunning()) animTimer.start();
}
public String getText() { return textField.getText(); }
public void setText(String t) { textField.setText(t); }
public void addActionListener(ActionListener l) { textField.addActionListener(l); }
public void addDocumentListener(DocumentListener dl) { textField.getDocument().addDocumentListener(dl); }
}
}

View File

@@ -1,4 +1,4 @@
# Auto-generated build information
version=0.0.1
buildTimestamp=2026-01-03T09:20:12.6386031
buildTimestamp=2026-01-03T10:40:36.7967778
buildSystem=WINDOWS