mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-09-20 06:11:13 +00:00
Use own implementation for tray icon
This commit is contained in:
parent
03b09c9c8f
commit
48a43ba5a5
|
@ -1,68 +1,25 @@
|
||||||
package io.xpipe.app.core;
|
package io.xpipe.app.core;
|
||||||
|
|
||||||
import com.dustinredmond.fxtrayicon.FXTrayIcon;
|
|
||||||
import io.xpipe.app.core.mode.OperationMode;
|
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.issue.ErrorHandler;
|
import io.xpipe.app.issue.ErrorHandler;
|
||||||
import io.xpipe.core.process.OsType;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import java.awt.*;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class AppTray {
|
public class AppTray {
|
||||||
|
|
||||||
private static AppTray INSTANCE;
|
private static AppTray INSTANCE;
|
||||||
private final FXTrayIcon icon;
|
private final AppTrayIcon icon;
|
||||||
@Getter
|
@Getter
|
||||||
private final ErrorHandler errorHandler;
|
private final ErrorHandler errorHandler;
|
||||||
private TrayIcon privateTrayIcon;
|
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
private AppTray() {
|
private AppTray() {
|
||||||
var image = switch (OsType.getLocal()) {
|
this.icon = new AppTrayIcon();
|
||||||
case OsType.Windows windows -> "img/logo/logo_16x16.png";
|
|
||||||
case OsType.Linux linux -> "img/logo/logo_24x24.png";
|
|
||||||
case OsType.MacOs macOs -> "img/logo/logo_24x24.png";
|
|
||||||
};
|
|
||||||
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, image).orElseThrow();
|
|
||||||
|
|
||||||
var builder = new FXTrayIcon.Builder(App.getApp().getStage(), url)
|
|
||||||
.menuItem(AppI18n.get("open"), e -> {
|
|
||||||
OperationMode.switchToAsync(OperationMode.GUI);
|
|
||||||
});
|
|
||||||
if (AppProperties.get().isDeveloperMode()) {
|
|
||||||
builder.menuItem("Throw exception", e -> {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
throw new RuntimeException("This is a test exception");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.menuItem("Throw terminal exception", e -> {
|
|
||||||
try {
|
|
||||||
throw new RuntimeException("This is a terminal exception");
|
|
||||||
} catch (Exception ex) {
|
|
||||||
ErrorEvent.fromThrowable(ex).terminal(true).build().handle();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.icon = builder.separator()
|
|
||||||
.menuItem(AppI18n.get("quit"), e -> {
|
|
||||||
OperationMode.close();
|
|
||||||
})
|
|
||||||
.toolTip("XPipe")
|
|
||||||
.build();
|
|
||||||
this.errorHandler = new TrayErrorHandler();
|
this.errorHandler = new TrayErrorHandler();
|
||||||
|
|
||||||
var tray = SystemTray.getSystemTray();
|
|
||||||
var f = icon.getClass().getDeclaredField("trayIcon");
|
|
||||||
f.setAccessible(true);
|
|
||||||
privateTrayIcon = (TrayIcon) f.get(this.icon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init() {
|
public static void init() {
|
||||||
|
@ -76,59 +33,10 @@ public class AppTray {
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public void show() {
|
public void show() {
|
||||||
icon.show();
|
icon.show();
|
||||||
|
|
||||||
// Remove functionality to show stage when primary clicked and replace it with our own
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
for (var l : Arrays.stream(privateTrayIcon.getActionListeners()).toList()) {
|
|
||||||
privateTrayIcon.removeActionListener(l);
|
|
||||||
}
|
|
||||||
privateTrayIcon.addActionListener(e -> {
|
|
||||||
if (OsType.getLocal() != OsType.MACOS) {
|
|
||||||
OperationMode.switchToAsync(OperationMode.GUI);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ugly fix to show a transparent background on Linux
|
|
||||||
if (OsType.getLocal().equals(OsType.LINUX)) {
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
try {
|
|
||||||
Field peerField;
|
|
||||||
peerField = TrayIcon.class.getDeclaredField("peer");
|
|
||||||
peerField.setAccessible(true);
|
|
||||||
var peer = peerField.get(this.privateTrayIcon);
|
|
||||||
|
|
||||||
// If tray initialization fails, this can be null
|
|
||||||
if (peer == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var canvasField = peer.getClass().getDeclaredField("canvas");
|
|
||||||
canvasField.setAccessible(true);
|
|
||||||
Component canvas = (Component) canvasField.get(peer);
|
|
||||||
canvas.setBackground(new Color(0, 0, 0, 0));
|
|
||||||
|
|
||||||
var frameField = peer.getClass().getDeclaredField("eframe");
|
|
||||||
frameField.setAccessible(true);
|
|
||||||
Frame frame = (Frame) frameField.get(peer);
|
|
||||||
frame.setTitle("XPipe");
|
|
||||||
} catch (Exception e) {
|
|
||||||
ErrorEvent.fromThrowable(e).omit().handle();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hide() {
|
public void hide() {
|
||||||
// Ugly fix to prevent platform exit in icon.hide()
|
icon.hide();
|
||||||
try {
|
|
||||||
var tray = SystemTray.getSystemTray();
|
|
||||||
EventQueue.invokeLater(() -> {
|
|
||||||
tray.remove(privateTrayIcon);
|
|
||||||
});
|
|
||||||
} catch (Exception ex) {
|
|
||||||
ErrorEvent.fromThrowable(ex).handle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TrayErrorHandler implements ErrorHandler {
|
private class TrayErrorHandler implements ErrorHandler {
|
||||||
|
|
349
app/src/main/java/io/xpipe/app/core/AppTrayIcon.java
Normal file
349
app/src/main/java/io/xpipe/app/core/AppTrayIcon.java
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
package io.xpipe.app.core;
|
||||||
|
|
||||||
|
import io.xpipe.app.core.mode.OperationMode;
|
||||||
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
|
import io.xpipe.core.process.OsType;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.event.MouseListener;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
public class AppTrayIcon {
|
||||||
|
|
||||||
|
private static final Integer winScale = 16;
|
||||||
|
private static final Integer coreScale = 22;
|
||||||
|
private boolean shown = false;
|
||||||
|
private ActionListener exitMenuItemActionListener;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default AWT SystemTray
|
||||||
|
*/
|
||||||
|
private final SystemTray tray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parent Stage of the FXTrayIcon
|
||||||
|
*/
|
||||||
|
private Stage parentStage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application's title, to be used
|
||||||
|
* as default tooltip text for the FXTrayIcon
|
||||||
|
*/
|
||||||
|
private String appTitle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AWT TrayIcon managed by FXTrayIcon
|
||||||
|
*/
|
||||||
|
private final TrayIcon trayIcon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AWT PopupMenu managed by FXTrayIcon
|
||||||
|
*/
|
||||||
|
private final PopupMenu popupMenu = new PopupMenu();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@code MouseListener} whose
|
||||||
|
* single-click action performs the passed
|
||||||
|
* JavaFX EventHandler
|
||||||
|
* @param e A JavaFX event to be performed
|
||||||
|
* @return A MouseListener fired by single-click
|
||||||
|
*/
|
||||||
|
private MouseListener getPrimaryClickListener(EventHandler<ActionEvent> e) {
|
||||||
|
return new MouseListener() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent me) {
|
||||||
|
Platform.runLater(() -> e.handle(new ActionEvent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent ignored) { }
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent ignored) { }
|
||||||
|
@Override
|
||||||
|
public void mouseEntered(MouseEvent ignored) { }
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent ignored) { }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppTrayIcon() {
|
||||||
|
ensureSystemTraySupported();
|
||||||
|
|
||||||
|
tray = SystemTray.getSystemTray();
|
||||||
|
|
||||||
|
var image = switch (OsType.getLocal()) {
|
||||||
|
case OsType.Windows windows -> "img/logo/logo_16x16.png";
|
||||||
|
case OsType.Linux linux -> "img/logo/logo_24x24.png";
|
||||||
|
case OsType.MacOs macOs -> "img/logo/logo_24x24.png";
|
||||||
|
};
|
||||||
|
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, image).orElseThrow();
|
||||||
|
|
||||||
|
this.trayIcon = new TrayIcon(loadImageFromURL(url), App.getApp().getStage().getTitle(), popupMenu);
|
||||||
|
this.trayIcon.setImageAutoSize(false);
|
||||||
|
this.trayIcon.setToolTip("XPipe");
|
||||||
|
|
||||||
|
{
|
||||||
|
var open = new MenuItem(AppI18n.get("open"));
|
||||||
|
open.addActionListener(e -> {
|
||||||
|
OperationMode.switchToAsync(OperationMode.GUI);
|
||||||
|
});
|
||||||
|
popupMenu.add(open);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var quit = new MenuItem(AppI18n.get("quit"));
|
||||||
|
quit.addActionListener(e -> {
|
||||||
|
tray.remove(trayIcon);
|
||||||
|
OperationMode.close();
|
||||||
|
});
|
||||||
|
popupMenu.add(quit);
|
||||||
|
}
|
||||||
|
|
||||||
|
trayIcon.addActionListener(e -> {
|
||||||
|
if (OsType.getLocal() != OsType.MACOS) {
|
||||||
|
OperationMode.switchToAsync(OperationMode.GUI);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the nested AWT {@link TrayIcon}. This is intended for extended
|
||||||
|
* instances of FXTrayIcon which require the access to implement
|
||||||
|
* custom features.
|
||||||
|
* @return The nest trayIcon within this instance of FXTrayIcon.
|
||||||
|
*/
|
||||||
|
public final TrayIcon getAwtTrayIcon() {
|
||||||
|
return trayIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureSystemTraySupported() {
|
||||||
|
if (!SystemTray.isSupported()) {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"SystemTray icons are not "
|
||||||
|
+ "supported by the current desktop environment.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Image loadImageFromURL(URL iconImagePath) {
|
||||||
|
try {
|
||||||
|
return ImageIO.read(iconImagePath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
|
return AppImages.toAwtImage(AppImages.DEFAULT_IMAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the FXTrayIcon to the system tray.
|
||||||
|
* This will add the TrayIcon with the image initialized in the
|
||||||
|
* {@code FXTrayIcon}'s constructor. By default, an empty popup
|
||||||
|
* menu is shown.
|
||||||
|
* By default, {@code javafx.application.Platform.setImplicitExit(false)}
|
||||||
|
* will be called. This will allow the application to continue running
|
||||||
|
* and show the tray icon after no more JavaFX Stages are visible. If
|
||||||
|
* this is not the behavior that you intend, call {@code setImplicitExit}
|
||||||
|
* to true after calling {@code show()}.
|
||||||
|
*/
|
||||||
|
public void show() {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
try {
|
||||||
|
tray.add(this.trayIcon);
|
||||||
|
shown = true;
|
||||||
|
fixBackground();
|
||||||
|
} catch (Exception e) {
|
||||||
|
ErrorEvent.fromThrowable("Unable to add TrayIcon", e).handle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fixBackground() {
|
||||||
|
// Ugly fix to show a transparent background on Linux
|
||||||
|
if (OsType.getLocal().equals(OsType.LINUX)) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
try {
|
||||||
|
Field peerField;
|
||||||
|
peerField = TrayIcon.class.getDeclaredField("peer");
|
||||||
|
peerField.setAccessible(true);
|
||||||
|
var peer = peerField.get(this.trayIcon);
|
||||||
|
|
||||||
|
// If tray initialization fails, this can be null
|
||||||
|
if (peer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvasField = peer.getClass().getDeclaredField("canvas");
|
||||||
|
canvasField.setAccessible(true);
|
||||||
|
Component canvas = (Component) canvasField.get(peer);
|
||||||
|
canvas.setBackground(new Color(0, 0, 0, 0));
|
||||||
|
|
||||||
|
var frameField = peer.getClass().getDeclaredField("eframe");
|
||||||
|
frameField.setAccessible(true);
|
||||||
|
Frame frame = (Frame) frameField.get(peer);
|
||||||
|
frame.setTitle("XPipe");
|
||||||
|
} catch (Exception e) {
|
||||||
|
ErrorEvent.fromThrowable(e).omit().handle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hide() {
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
tray.remove(trayIcon);
|
||||||
|
shown = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an info popup message near the tray icon.
|
||||||
|
* <p>NOTE: Some systems do not support this.</p>
|
||||||
|
* @param title The caption (header) text
|
||||||
|
* @param message The message content text
|
||||||
|
*/
|
||||||
|
public void showInfoMessage(String title, String message) {
|
||||||
|
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||||
|
showMacAlert(title, message,"Information");
|
||||||
|
} else {
|
||||||
|
EventQueue.invokeLater(() ->
|
||||||
|
this.trayIcon.displayMessage(
|
||||||
|
title, message, TrayIcon.MessageType.INFO));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an info popup message near the tray icon.
|
||||||
|
* <p>NOTE: Some systems do not support this.</p>
|
||||||
|
* @param message The message content text
|
||||||
|
*/
|
||||||
|
public void showInfoMessage(String message) {
|
||||||
|
this.showInfoMessage(null, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a warning popup message near the tray icon.
|
||||||
|
* <p>NOTE: Some systems do not support this.</p>
|
||||||
|
* @param title The caption (header) text
|
||||||
|
* @param message The message content text
|
||||||
|
*/
|
||||||
|
public void showWarningMessage(String title, String message) {
|
||||||
|
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||||
|
showMacAlert(title, message,"Warning");
|
||||||
|
} else {
|
||||||
|
EventQueue.invokeLater(() ->
|
||||||
|
this.trayIcon.displayMessage(
|
||||||
|
title, message, TrayIcon.MessageType.WARNING));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a warning popup message near the tray icon.
|
||||||
|
* <p>NOTE: Some systems do not support this.</p>
|
||||||
|
* @param message The message content text
|
||||||
|
*/
|
||||||
|
public void showWarningMessage(String message) {
|
||||||
|
this.showWarningMessage(null, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an error popup message near the tray icon.
|
||||||
|
* <p>NOTE: Some systems do not support this.</p>
|
||||||
|
* @param title The caption (header) text
|
||||||
|
* @param message The message content text
|
||||||
|
*/
|
||||||
|
public void showErrorMessage(String title, String message) {
|
||||||
|
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||||
|
showMacAlert(title, message,"Error");
|
||||||
|
} else {
|
||||||
|
EventQueue.invokeLater(() ->
|
||||||
|
this.trayIcon.displayMessage(
|
||||||
|
title, message, TrayIcon.MessageType.ERROR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an error popup message near the tray icon.
|
||||||
|
* <p>NOTE: Some systems do not support this.</p>
|
||||||
|
* @param message The message content text
|
||||||
|
*/
|
||||||
|
public void showErrorMessage(String message) {
|
||||||
|
this.showErrorMessage(null, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a popup message near the tray icon.
|
||||||
|
* Some systems will display FXTrayIcon's image on this popup.
|
||||||
|
* <p>NOTE: Some systems do not support this.</p>
|
||||||
|
* @param title The caption (header) text
|
||||||
|
* @param message The message content text
|
||||||
|
*/
|
||||||
|
public void showMessage(String title, String message) {
|
||||||
|
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||||
|
showMacAlert(title, message,"Message");
|
||||||
|
} else {
|
||||||
|
EventQueue.invokeLater(() ->
|
||||||
|
this.trayIcon.displayMessage(
|
||||||
|
title, message, TrayIcon.MessageType.NONE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a popup message near the tray icon.
|
||||||
|
* Some systems will display FXTrayIcon's image on this popup.
|
||||||
|
* <p>NOTE: Some systems do not support this.</p>
|
||||||
|
* @param message The message content text
|
||||||
|
*/
|
||||||
|
public void showMessage(String message) {
|
||||||
|
this.showMessage(null, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the system tray icon is supported on the
|
||||||
|
* current platform, or not.
|
||||||
|
* Just because the system tray is supported, does not mean that the
|
||||||
|
* current platform implements all system tray functionality.
|
||||||
|
* This will always return true on Windows or MacOS. Check the
|
||||||
|
* specific desktop environment for AppIndicator support when
|
||||||
|
* calling this on *nix platforms.
|
||||||
|
* @return false if the system tray is not supported, true if any
|
||||||
|
* part of the system tray functionality is supported.
|
||||||
|
*/
|
||||||
|
public static boolean isSupported() {
|
||||||
|
return Desktop.isDesktopSupported() && SystemTray.isSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a sliding info message. Behavior is similar to Windows, but without AWT
|
||||||
|
* @param subTitle The message caption
|
||||||
|
* @param message The message text
|
||||||
|
* @param title The message title
|
||||||
|
*/
|
||||||
|
private void showMacAlert(String subTitle, String message, String title) {
|
||||||
|
String execute = String.format(
|
||||||
|
"display notification \"%s\""
|
||||||
|
+ " with title \"%s\""
|
||||||
|
+ " subtitle \"%s\"",
|
||||||
|
message != null ? message : "",
|
||||||
|
title != null ? title : "",
|
||||||
|
subTitle != null ? subTitle : ""
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
Runtime.getRuntime()
|
||||||
|
.exec(new String[] { "osascript", "-e", execute });
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"Cannot run osascript with given parameters.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ public class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler
|
||||||
if (event.getThrowable() instanceof LicenseRequiredException lex) {
|
if (event.getThrowable() instanceof LicenseRequiredException lex) {
|
||||||
LicenseProvider.get().showLicenseAlert(lex);
|
LicenseProvider.get().showLicenseAlert(lex);
|
||||||
event.setShouldSendDiagnostics(true);
|
event.setShouldSendDiagnostics(true);
|
||||||
|
ErrorAction.ignore().handle(event);
|
||||||
} else {
|
} else {
|
||||||
ErrorHandlerComp.showAndTryWait(event, true);
|
ErrorHandlerComp.showAndTryWait(event, true);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue