From 08f04738141fe0f86d5cd2fbd5d06342f827f26f Mon Sep 17 00:00:00 2001 From: octopus_yan Date: Tue, 17 Sep 2024 02:37:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=89=98=E7=9B=98=E3=80=81=E9=9D=99=E9=BB=98=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cn/octopusyan/alistgui/Application.java | 8 + .../alistgui/controller/RootController.java | 13 +- .../alistgui/controller/SetupController.java | 3 + .../alistgui/manager/ConfigManager.java | 10 + .../alistgui/manager/SystemTrayManager.java | 186 ++++++++++++++++++ .../octopusyan/alistgui/model/GuiConfig.java | 1 + .../octopusyan/alistgui/util/WindowsUtil.java | 25 ++- .../octopusyan/alistgui/view/PopupMenu.java | 117 +++++++++++ .../alistgui/viewModel/SetupViewModel.java | 17 ++ src/main/java/module-info.java | 1 + .../{logo_disabled.png => logo-disabled.png} | Bin src/main/resources/fxml/setup-view.fxml | 1 + .../resources/language/language.properties | 1 + .../resources/language/language_en.properties | 1 + .../language/language_zh_CN.properties | 1 + 15 files changed, 381 insertions(+), 4 deletions(-) create mode 100644 src/main/java/cn/octopusyan/alistgui/manager/SystemTrayManager.java create mode 100644 src/main/java/cn/octopusyan/alistgui/view/PopupMenu.java rename src/main/resources/assets/{logo_disabled.png => logo-disabled.png} (100%) diff --git a/src/main/java/cn/octopusyan/alistgui/Application.java b/src/main/java/cn/octopusyan/alistgui/Application.java index 18f4245..8916f54 100644 --- a/src/main/java/cn/octopusyan/alistgui/Application.java +++ b/src/main/java/cn/octopusyan/alistgui/Application.java @@ -3,6 +3,7 @@ package cn.octopusyan.alistgui; import cn.octopusyan.alistgui.config.Constants; import cn.octopusyan.alistgui.config.Context; import cn.octopusyan.alistgui.manager.ConfigManager; +import cn.octopusyan.alistgui.manager.SystemTrayManager; import cn.octopusyan.alistgui.manager.http.HttpConfig; import cn.octopusyan.alistgui.manager.http.HttpUtil; import cn.octopusyan.alistgui.manager.thread.ThreadPoolManager; @@ -84,6 +85,13 @@ public class Application extends javafx.application.Application { primaryStage.setScene(scene); primaryStage.show(); + // 静默启动 + if (ConfigManager.silentStartup()) { + Platform.setImplicitExit(false); + primaryStage.hide(); + SystemTrayManager.show(); + } + logger.info("application start over ..."); } diff --git a/src/main/java/cn/octopusyan/alistgui/controller/RootController.java b/src/main/java/cn/octopusyan/alistgui/controller/RootController.java index 02265f7..4f27e78 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/RootController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/RootController.java @@ -3,10 +3,13 @@ package cn.octopusyan.alistgui.controller; import atlantafx.base.controls.ModalPane; import cn.octopusyan.alistgui.base.BaseController; import cn.octopusyan.alistgui.config.Context; +import cn.octopusyan.alistgui.manager.ConfigManager; +import cn.octopusyan.alistgui.manager.SystemTrayManager; import cn.octopusyan.alistgui.util.WindowsUtil; import cn.octopusyan.alistgui.viewModel.RootViewModel; import com.gluonhq.emoji.EmojiData; import com.gluonhq.emoji.util.EmojiImageUtils; +import javafx.application.Platform; import javafx.css.PseudoClass; import javafx.fxml.FXML; import javafx.geometry.Pos; @@ -107,7 +110,15 @@ public class RootController extends BaseController { */ @Override public void initViewAction() { - closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().close()); + closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> { + if (ConfigManager.closeToTray()) { + SystemTrayManager.show(); + } else { + SystemTrayManager.hide(); + } + Platform.setImplicitExit(!ConfigManager.closeToTray()); + getWindow().close(); + }); minimizeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().setIconified(true)); alwaysOnTopIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> { boolean newVal = !getWindow().isAlwaysOnTop(); diff --git a/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java b/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java index 67847f3..7f3e299 100644 --- a/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java +++ b/src/main/java/cn/octopusyan/alistgui/controller/SetupController.java @@ -37,6 +37,8 @@ public class SetupController extends BaseController implements I @FXML public CheckBox silentStartupCheckBox; @FXML + public CheckBox closeToTrayCheckBox; + @FXML public ComboBox languageComboBox; @FXML public ComboBox themeComboBox; @@ -92,6 +94,7 @@ public class SetupController extends BaseController implements I // autoStartCheckBox.selectedProperty().bindBidirectional(viewModel.autoStartProperty()); silentStartupCheckBox.selectedProperty().bindBidirectional(viewModel.silentStartupProperty()); + closeToTrayCheckBox.selectedProperty().bindBidirectional(viewModel.closeToTrayProperty()); proxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty()); proxyPort.textProperty().bindBidirectional(viewModel.proxyPortProperty()); diff --git a/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java b/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java index c6a7404..c652c55 100644 --- a/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java +++ b/src/main/java/cn/octopusyan/alistgui/manager/ConfigManager.java @@ -237,6 +237,16 @@ public class ConfigManager { guiConfig.setSilentStartup(startup); } +// --------------------------------{ 最小化到托盘 }------------------------------------------ + + public static boolean closeToTray() { + return guiConfig.getCloseToTray(); + } + + public static void closeToTray(boolean check) { + guiConfig.setCloseToTray(check); + } + // --------------------------------{ 版本检查 }------------------------------------------ public static UpgradeConfig upgradeConfig() { diff --git a/src/main/java/cn/octopusyan/alistgui/manager/SystemTrayManager.java b/src/main/java/cn/octopusyan/alistgui/manager/SystemTrayManager.java new file mode 100644 index 0000000..966279f --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/manager/SystemTrayManager.java @@ -0,0 +1,186 @@ +package cn.octopusyan.alistgui.manager; + +import cn.octopusyan.alistgui.Application; +import cn.octopusyan.alistgui.config.Constants; +import cn.octopusyan.alistgui.config.Context; +import cn.octopusyan.alistgui.util.WindowsUtil; +import cn.octopusyan.alistgui.view.PopupMenu; +import javafx.application.Platform; +import javafx.scene.control.MenuItem; +import javafx.stage.Stage; +import lombok.extern.slf4j.Slf4j; + +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.net.URL; +import java.util.List; + +/** + * 系统托盘管理 + * + * @author octopus_yan + */ +@Slf4j +public class SystemTrayManager { + // 托盘工具 + private static final SystemTray systemTray; + private static TrayIcon trayIcon; + private static PopupMenu popupMenu; + + static { + //检查系统是否支持托盘 + if (!SystemTray.isSupported()) { + //系统托盘不支持 + log.info("{}:系统托盘不支持", Thread.currentThread().getStackTrace()[1].getClassName()); + systemTray = null; + } else { + systemTray = SystemTray.getSystemTray(); + } + } + + public static void toolTip(String toptip) { + if (trayIcon == null) return; + + trayIcon.setToolTip(toptip); + } + + public static void icon(String path) { + if (trayIcon == null) return; + icon(WindowsUtil.class.getResource(path)); + } + + public static void icon(URL url) { + if (trayIcon == null) return; + icon(Toolkit.getDefaultToolkit().getImage(url)); + } + + public static void icon(Image image) { + if (trayIcon == null) return; + trayIcon.setImage(image); + } + + public static boolean isShowing() { + if (systemTray == null) return false; + + return List.of(systemTray.getTrayIcons()).contains(trayIcon); + } + + public static void show() { + + // 是否启用托盘 + if (!ConfigManager.closeToTray() || systemTray == null) { + if (trayIcon != null && isShowing()) { + hide(); + } + return; + } + + initTrayIcon(); + + try { + if (!isShowing()) + systemTray.add(trayIcon); + } catch (AWTException e) { + //系统托盘添加失败 + log.error("{}:系统添加失败", Thread.currentThread().getStackTrace()[1].getClassName(), e); + } + } + + public static void hide() { + if (systemTray == null) return; + + systemTray.remove(trayIcon); + } + +//========================================={ private }=========================================== + + private static void initTrayIcon() { + if (trayIcon != null) return; + + // 系统托盘图标 + Image image = Toolkit.getDefaultToolkit().getImage(WindowsUtil.class.getResource("/assets/logo-disabled.png")); + trayIcon = new TrayIcon(image); + + // 设置图标尺寸自动适应 + trayIcon.setImageAutoSize(true); + + // 弹出式菜单组件 +// trayIcon.setPopupMenu(getMenu()); + + // 鼠标移到系统托盘,会显示提示文本 + toolTip(Constants.APP_TITLE); + + // 鼠标监听 + trayIcon.addMouseListener(new MouseAdapter() { + + @Override + public void mouseReleased(MouseEvent event) { + maybeShowPopup(event); + } + + @Override + public void mousePressed(MouseEvent event) { + maybeShowPopup(event); + } + + private void maybeShowPopup(MouseEvent event) { + // popup menu trigger event + if (event.isPopupTrigger()) { + // 弹出菜单 + Platform.runLater(() -> { + initPopupMenu(); + popupMenu.show(event); + }); + } else if (event.getButton() == MouseEvent.BUTTON1) { + // 显示 PrimaryStage + Platform.runLater(() -> Application.getPrimaryStage().show()); + } + } + }); + } + + /** + * 构建托盘菜单 + */ + private static void initPopupMenu() { + if (popupMenu != null) return; + + MenuItem start = PopupMenu.menuItem(getString("main.control.start"), _ -> AListManager.openScheme()); + MenuItem browser = PopupMenu.menuItem(getString("main.more.browser"), _ -> AListManager.openScheme()); + browser.setDisable(true); + + AListManager.runningProperty().addListener((_, _, newValue) -> { + start.setText(getString(STR."main.control.\{newValue ? "stop" : "start"}")); + browser.disableProperty().set(!newValue); + toolTip(STR."AList \{newValue ? "running" : "stopped"}"); + icon(STR."/assets/logo\{newValue ? "" : "-disabled"}.png"); + }); + + popupMenu = new PopupMenu() + .addItem(new MenuItem(Constants.APP_TITLE), _ -> stage().show()) + .addSeparator() + .addCaptionItem("AList") + .addItem(start, _ -> { + if (AListManager.isRunning()) { + AListManager.stop(); + } else { + AListManager.start(); + } + }) + .addItem(getString("main.control.restart"), _ -> AListManager.restart()) + .addMenu(getString("main.control.more"), browser, + PopupMenu.menuItem(getString("main.more.open-config"), _ -> AListManager.openConfig()), + PopupMenu.menuItem(getString("main.more.open-log"), _ -> AListManager.openLogFolder())) + .addSeparator() + .addExitItem(); + } + + private static String getString(String key) { + return Context.getLanguageBinding(key).get(); + } + + private static Stage stage() { + return WindowsUtil.getStage(); + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/model/GuiConfig.java b/src/main/java/cn/octopusyan/alistgui/model/GuiConfig.java index a82a895..a9bf845 100644 --- a/src/main/java/cn/octopusyan/alistgui/model/GuiConfig.java +++ b/src/main/java/cn/octopusyan/alistgui/model/GuiConfig.java @@ -18,6 +18,7 @@ public class GuiConfig { private Boolean autoStart = false; private Boolean silentStartup = false; + private Boolean closeToTray = true; @JsonProperty("proxy") private ProxyInfo proxyInfo; @JsonProperty("proxy.testUrl") diff --git a/src/main/java/cn/octopusyan/alistgui/util/WindowsUtil.java b/src/main/java/cn/octopusyan/alistgui/util/WindowsUtil.java index 9dc750b..33c5479 100644 --- a/src/main/java/cn/octopusyan/alistgui/util/WindowsUtil.java +++ b/src/main/java/cn/octopusyan/alistgui/util/WindowsUtil.java @@ -2,17 +2,25 @@ package cn.octopusyan.alistgui.util; import cn.octopusyan.alistgui.Application; import javafx.scene.layout.Pane; +import javafx.stage.Screen; import javafx.stage.Stage; import java.util.HashMap; import java.util.Map; /** + * 工具 + * * @author octopus_yan */ public class WindowsUtil { - public static final Map paneXOffset = new HashMap<>(); - public static final Map paneYOffset = new HashMap<>(); + // 获取系统缩放比 + public static final double scaleX = Screen.getPrimary().getOutputScaleX(); + public static final double scaleY = Screen.getPrimary().getOutputScaleY(); + + + private static final Map paneXOffset = new HashMap<>(); + private static final Map paneYOffset = new HashMap<>(); public static void bindShadow(Pane pane) { pane.setStyle(""" @@ -29,6 +37,13 @@ public class WindowsUtil { bindDragged(pane, stage); } + public static void unbindDragged(Pane pane) { + pane.setOnMousePressed(null); + pane.setOnMouseDragged(null); + paneXOffset.remove(pane); + paneYOffset.remove(pane); + } + public static void bindDragged(Pane pane, Stage stage) { pane.setOnMousePressed(event -> { paneXOffset.put(pane, stage.getX() - event.getScreenX()); @@ -40,11 +55,15 @@ public class WindowsUtil { }); } + public static Stage getStage() { + return Application.getPrimaryStage(); + } + public static Stage getStage(Pane pane) { try { return (Stage) pane.getScene().getWindow(); } catch (Throwable e) { - return Application.getPrimaryStage(); + return getStage(); } } } diff --git a/src/main/java/cn/octopusyan/alistgui/view/PopupMenu.java b/src/main/java/cn/octopusyan/alistgui/view/PopupMenu.java new file mode 100644 index 0000000..bf15bd9 --- /dev/null +++ b/src/main/java/cn/octopusyan/alistgui/view/PopupMenu.java @@ -0,0 +1,117 @@ +package cn.octopusyan.alistgui.view; + +import atlantafx.base.controls.CaptionMenuItem; +import cn.octopusyan.alistgui.config.Constants; +import cn.octopusyan.alistgui.util.WindowsUtil; +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.Scene; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.layout.Region; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +/** + * 托盘图标 菜单 + * + * @author octopus_yan + */ +public class PopupMenu { + // 用来隐藏弹出窗口的任务栏图标 + private static final Stage utilityStage = new Stage(); + // 菜单栏 + private final ContextMenu root = new ContextMenu(); + + static { + utilityStage.initStyle(StageStyle.UTILITY); + utilityStage.setScene(new Scene(new Region())); + utilityStage.setOpacity(0); + } + + public PopupMenu() { + + root.focusedProperty().addListener((_, _, focused) -> { + if (!focused) + Platform.runLater(() -> { + root.hide(); + utilityStage.hide(); + }); + }); + } + + public PopupMenu addItem(String label, EventHandler handler) { + return addItem(new MenuItem(label), handler); + } + + public PopupMenu addItem(MenuItem node, EventHandler handler) { + node.setOnAction(handler); + return addItem(node); + } + + public PopupMenu addSeparator() { + return addItem(new SeparatorMenuItem()); + } + + public PopupMenu addCaptionItem() { + return addCaptionItem(null); + } + + public PopupMenu addCaptionItem(String title) { + return addItem(new CaptionMenuItem(title)); + } + + public PopupMenu addMenu(String label, MenuItem... items) { + return addMenu(new Menu(label), items); + } + + public PopupMenu addMenu(Menu menu, MenuItem... items) { + menu.getItems().addAll(items); + return addItem(menu); + } + + public PopupMenu addTitleItem() { + return addTitleItem(Constants.APP_TITLE); + } + + public PopupMenu addTitleItem(String label) { + return addExitItem(label); + } + + public PopupMenu addExitItem() { + return addExitItem("Exit"); + } + + public PopupMenu addExitItem(String label) { + return addItem(label, _ -> Platform.exit()); + } + + private PopupMenu addItem(MenuItem node) { + root.getItems().add(node); + return this; + } + + public void show(java.awt.event.MouseEvent event) { + // 必须调用show才会隐藏任务栏图标 + utilityStage.show(); + + if (root.isShowing()) + root.hide(); + + root.show(utilityStage, + event.getX() / WindowsUtil.scaleX, + event.getY() / WindowsUtil.scaleY + ); + // 获取焦点 (失去焦点隐藏自身) + root.requestFocus(); + } + + public static MenuItem menuItem(String label, EventHandler handler) { + MenuItem menuItem = new MenuItem(label); + menuItem.setOnAction(handler); + return menuItem; + } +} diff --git a/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java b/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java index c563c57..1ec2381 100644 --- a/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java +++ b/src/main/java/cn/octopusyan/alistgui/viewModel/SetupViewModel.java @@ -26,6 +26,7 @@ import java.util.Locale; public class SetupViewModel extends BaseViewModel { private final BooleanProperty autoStart = new SimpleBooleanProperty(ConfigManager.autoStart()); private final BooleanProperty silentStartup = new SimpleBooleanProperty(ConfigManager.silentStartup()); + private final BooleanProperty closeToTray = new SimpleBooleanProperty(ConfigManager.closeToTray()); private final ObjectProperty theme = new SimpleObjectProperty<>(ConfigManager.theme()); private final StringProperty proxyHost = new SimpleStringProperty(ConfigManager.proxyHost()); private final StringProperty proxyPort = new SimpleStringProperty(ConfigManager.proxyPort()); @@ -50,6 +51,18 @@ public class SetupViewModel extends BaseViewModel { } ConfigManager.autoStart(newValue); }); + silentStartup.addListener((_, _, newValue) -> { + // 开启时检查托盘选项 + if (newValue && !closeToTray.get()) closeToTray.set(true); + + ConfigManager.silentStartup(newValue); + }); + closeToTray.addListener((_, _, newValue) -> { + // 开启时检查托盘选项 + if (!newValue && silentStartup.get()) silentStartup.set(false); + + ConfigManager.closeToTray(newValue); + }); proxySetup.addListener((_, _, newValue) -> ConfigManager.proxySetup(newValue)); proxyTestUrl.addListener((_, _, newValue) -> ConfigManager.proxyTestUrl(newValue)); proxyHost.addListener((_, _, newValue) -> ConfigManager.proxyHost(newValue)); @@ -69,6 +82,10 @@ public class SetupViewModel extends BaseViewModel { return silentStartup; } + public BooleanProperty closeToTrayProperty() { + return closeToTray; + } + public ObjectProperty languageProperty() { return language; } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index f43f846..9d4dbf6 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,4 +1,5 @@ module cn.octopusyan.alistgui { + requires java.desktop; requires java.net.http; requires javafx.controls; requires javafx.fxml; diff --git a/src/main/resources/assets/logo_disabled.png b/src/main/resources/assets/logo-disabled.png similarity index 100% rename from src/main/resources/assets/logo_disabled.png rename to src/main/resources/assets/logo-disabled.png diff --git a/src/main/resources/fxml/setup-view.fxml b/src/main/resources/fxml/setup-view.fxml index 4f4e4b1..f8d9c19 100644 --- a/src/main/resources/fxml/setup-view.fxml +++ b/src/main/resources/fxml/setup-view.fxml @@ -12,6 +12,7 @@ +