package envoy.client.ui; import static java.awt.Image.SCALE_SMOOTH; import java.awt.*; import java.awt.TrayIcon.MessageType; import java.awt.image.BufferedImage; import javafx.application.Platform; import javafx.stage.Stage; import dev.kske.eventbus.core.Event; import dev.kske.eventbus.core.EventBus; import envoy.data.Message; import envoy.data.User.UserStatus; import envoy.client.data.*; import envoy.client.event.*; import envoy.client.helper.ShutdownHelper; import envoy.client.util.*; /** * A tray icon with the Envoy logo, an "Envoy" tool tip and a pop-up menu with menu items for * * * @author Kai S. K. Engelbart * @since Envoy Client v0.2-alpha */ public final class StatusTrayIcon { /** * The {@link TrayIcon} provided by the System Tray API for controlling the system tray. This * includes displaying the icon, but also creating notifications when new messages are received. */ private final TrayIcon trayIcon; /** * A received {@link Message} is only displayed as a system tray notification if this variable * is set to {@code true}. */ private boolean displayMessageNotification; /** * The size of the tray icon's image. */ private final Dimension size; /** * The Envoy logo on which the current user status and unread message count will be drawn to * compose the tray icon. */ private final Image logo; private static final Font unreadMessageFont = new Font("sans-serif", Font.PLAIN, 8); /** * @return {@code true} if the status tray icon is supported on this platform * @since Envoy Client v0.2-beta */ public static boolean isSupported() { return SystemTray.isSupported(); } /** * Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up menu. * * @param stage the stage whose focus determines if message notifications are displayed * @since Envoy Client v0.2-beta */ public StatusTrayIcon(Stage stage) { size = SystemTray.getSystemTray().getTrayIconSize(); logo = IconUtil.loadAWTCompatible("/icons/envoy_logo.png").getScaledInstance(size.width, size.height, SCALE_SMOOTH); final var popup = new PopupMenu(); // Adding the exit menu item final var exitMenuItem = new MenuItem("Exit"); exitMenuItem.addActionListener(evt -> ShutdownHelper.exit(true)); popup.add(exitMenuItem); // Adding the logout menu item final var logoutMenuItem = new MenuItem("Logout"); logoutMenuItem.addActionListener(evt -> Platform.runLater(UserUtil::logout)); popup.add(logoutMenuItem); // Adding the status change items final var statusSubMenu = new Menu("Change status"); for (final var status : UserStatus.values()) { final var statusMenuItem = new MenuItem(status.toString().toLowerCase()); statusMenuItem .addActionListener(evt -> Platform.runLater(() -> UserUtil.changeStatus(status))); statusSubMenu.add(statusMenuItem); } popup.add(statusSubMenu); // Initialize the icon trayIcon = new TrayIcon(createImage(), "Envoy", popup); // Only display messages if the stage is not focused and the current user status // is not BUSY (if BUSY, displayMessageNotification will be false) stage.focusedProperty() .addListener((ov, wasFocused, isFocused) -> displayMessageNotification = !displayMessageNotification && wasFocused ? false : !isFocused); // Listen to changes in the total unread message amount Chat.getTotalUnreadAmount().addListener((ov, oldValue, newValue) -> updateImage()); // Show the window if the user clicks on the icon trayIcon.addActionListener(evt -> Platform.runLater(() -> { stage.setIconified(false); stage.toFront(); stage.requestFocus(); })); // Start processing message events EventBus.getInstance().registerListener(this); } /** * Makes the icon appear in the system tray. * * @since Envoy Client v0.2-alpha */ public void show() { try { SystemTray.getSystemTray().add(trayIcon); } catch (AWTException e) {} } /** * Removes the icon from the system tray. * * @since Envoy Client v0.2-beta */ @Event(Logout.class) public void hide() { SystemTray.getSystemTray().remove(trayIcon); } @Event private void onOwnStatusChange(OwnStatusChange statusChange) { displayMessageNotification = !statusChange.get().equals(UserStatus.BUSY); trayIcon.getImage().flush(); trayIcon.setImage(createImage()); } @Event private void onMessage(Message message) { if (displayMessageNotification) trayIcon .displayMessage(message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received" : "New message received", message.getText(), MessageType.INFO); } /** * Updates the tray icon's image by first releasing the resources held by the current image and * then setting a new one generated by the {@link StatusTrayIcon#createImage()} method. * * @since Envoy Client v0.3-beta */ private void updateImage() { trayIcon.getImage().flush(); trayIcon.setImage(createImage()); } /** * Composes an icon that displays the current user status and the amount of unread messages, if * any are present. * * @since Envoy Client v0.3-beta */ private BufferedImage createImage() { // Create a new image with the dimensions of the logo var img = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB); // Obtain the draw graphics of the image and copy the logo var g = img.createGraphics(); g.drawImage(logo, 0, 0, null); // Draw the current user status switch (Context.getInstance().getLocalDB().getUser().getStatus()) { case ONLINE: g.setColor(Color.GREEN); break; case AWAY: g.setColor(Color.ORANGE); break; case BUSY: g.setColor(Color.RED); break; case OFFLINE: g.setColor(Color.GRAY); } g.fillOval(size.width / 2, size.height / 2, size.width / 2, size.height / 2); // Draw total amount of unread messages, if any are present if (Chat.getTotalUnreadAmount().get() > 0) { // Draw black background circle g.setColor(Color.BLACK); g.fillOval(size.width / 2, 0, size.width / 2, size.height / 2); // Unread amount in white String unreadAmount = Chat.getTotalUnreadAmount().get() > 9 ? "9+" : String.valueOf(Chat.getTotalUnreadAmount().get()); g.setColor(Color.WHITE); g.setFont(unreadMessageFont); g.drawString(unreadAmount, 3 * size.width / 4 - g.getFontMetrics().stringWidth(unreadAmount) / 2, size.height / 2); } // Finish drawing g.dispose(); return img; } }