Use own implementation for tray icon

This commit is contained in:
crschnick 2023-10-21 08:54:14 +00:00
parent 03b09c9c8f
commit 48a43ba5a5
3 changed files with 353 additions and 95 deletions

View file

@ -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 {

View 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.");
}
}
}

View file

@ -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);
} }