package com.ts3client.ui; import com.ts3client.ui.tray.AwtTray; import com.ts3client.ui.tray.TrayBackend; import com.ts3client.ui.tray.X11Tray; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JWindow; import javax.swing.SwingUtilities; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.awt.Frame; import java.awt.Image; import java.awt.MenuItem; import java.awt.PopupMenu; import java.util.function.IntFunction; /** * The system tray icon. It shows the local client's state on the active server — * the same badge the tree draws next to a nickname, so it turns green while * speaking and into the muted microphone or speaker when muted — and brings the * window back to the front when clicked. * *

The icon is docked by {@link X11Tray} where the panel can show a transparent * one, and by {@link AwtTray} everywhere else. On a desktop with no tray at all * nothing is installed and every call here is a no-op. */ final class TrayController { private final MainFrame frame; private final Runnable onQuit; private final TrayBackend tray; /** What is on screen now, so repeated updates don't touch the tray. */ private SelfState state; private String tooltip = ""; TrayController(MainFrame frame, Runnable onQuit) { this.frame = frame; this.onQuit = onQuit; this.tray = install(); } /** What happened when the icon was installed, for the diagnostics in the chat log. */ String status() { return tray == null ? "System tray: no icon could be installed (run with -Dts3j.tray.debug=true for why)." : "System tray: " + tray.description() + "."; } /** @return the backend that took the icon, or {@code null} when none could */ private TrayBackend install() { TrayBackend.Listener listener = new TrayBackend.Listener() { @Override public void activated() { SwingUtilities.invokeLater(TrayController.this::show); } @Override public void menuRequested(int x, int y) { SwingUtilities.invokeLater(() -> showMenu(x, y)); } }; // -Dts3j.tray=awt|x11 pins the backend; by default the transparent one is // tried first and AWT's picks up whatever it leaves. String choice = System.getProperty("ts3j.tray", "auto"); IntFunction initial = size -> SelfState.DISCONNECTED.icon(size).getImage(); TrayBackend backend = choice.equals("awt") ? null : X11Tray.create(listener, initial); if (backend == null && !choice.equals("x11")) { backend = AwtTray.create(listener, buildAwtMenu(), initial); } return backend; } /** Shows the state of the given server, named by {@code serverName} in the tooltip. */ void update(SelfState newState, String serverName) { if (tray == null) return; String newTooltip = "TS3J — " + (serverName == null || serverName.isBlank() ? newState.label() : serverName + ": " + newState.label()); if (newState == state && newTooltip.equals(tooltip)) return; state = newState; tooltip = newTooltip; tray.setIcon(size -> newState.icon(size).getImage()); tray.setTooltip(newTooltip); } /** Redraws after the icon pack changed. */ void refreshIcon() { if (tray == null || state == null) return; SelfState current = state; tray.setIcon(size -> current.icon(size).getImage()); } void dispose() { if (tray != null) tray.dispose(); } // ---- menus ---- /** The menu for AWT's icon, which shows one of its own. */ private PopupMenu buildAwtMenu() { PopupMenu menu = new PopupMenu(); MenuItem showItem = new MenuItem("Show TS3J"); showItem.addActionListener(e -> SwingUtilities.invokeLater(this::show)); MenuItem quitItem = new MenuItem("Quit"); quitItem.addActionListener(e -> SwingUtilities.invokeLater(onQuit)); menu.add(showItem); menu.addSeparator(); menu.add(quitItem); return menu; } /** * The same menu for backends that have none — a Swing popup needs a component to * hang off, so it is given an empty window at the pointer. */ private void showMenu(int x, int y) { JWindow anchor = new JWindow(frame); anchor.setLocation(x, y); anchor.setSize(1, 1); anchor.setVisible(true); JPopupMenu menu = new JPopupMenu(); JMenuItem showItem = new JMenuItem("Show TS3J", Icons.app()); showItem.addActionListener(e -> show()); JMenuItem quitItem = new JMenuItem("Quit", Icons.of("QUIT")); quitItem.addActionListener(e -> onQuit.run()); menu.add(showItem); menu.addSeparator(); menu.add(quitItem); menu.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { anchor.dispose(); } @Override public void popupMenuCanceled(PopupMenuEvent e) { anchor.dispose(); } }); menu.show(anchor, 0, 0); } /** Brings the window back from the taskbar or an iconified state. */ private void show() { frame.setVisible(true); frame.setExtendedState(frame.getExtendedState() & ~Frame.ICONIFIED); frame.toFront(); frame.requestFocus(); } }