All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.systemTray.ui.swing._WindowsNativeTray Maven / Gradle / Ivy

Go to download

Cross-platform SystemTray support for Swing/AWT, GtkStatusIcon, and AppIndicator on Java 8+

The newest version!
/*
 * Copyright 2023 dorkbox, llc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dorkbox.systemTray.ui.swing;

import static com.sun.jna.platform.win32.WinDef.HWND;
import static com.sun.jna.platform.win32.WinDef.LPARAM;
import static com.sun.jna.platform.win32.WinDef.WPARAM;
import static com.sun.jna.platform.win32.WinUser.WM_QUIT;
import static dorkbox.jna.windows.Shell32.NIM_ADD;
import static dorkbox.jna.windows.Shell32.NIM_DELETE;
import static dorkbox.jna.windows.Shell32.NIM_MODIFY;
import static dorkbox.jna.windows.Shell32.Shell_NotifyIcon;
import static dorkbox.jna.windows.User32.WM_LBUTTONUP;
import static dorkbox.jna.windows.User32.WM_RBUTTONUP;
import static dorkbox.jna.windows.WindowsEventDispatch.WM_SHELLNOTIFY;
import static dorkbox.jna.windows.WindowsEventDispatch.WM_TASKBARCREATED;

import java.awt.Point;
import java.io.File;

import javax.swing.ImageIcon;

import com.sun.jna.platform.win32.Kernel32Util;
import com.sun.jna.platform.win32.WinDef.POINT;

import dorkbox.collections.ArrayMap;
import dorkbox.jna.windows.HBITMAPWrap;
import dorkbox.jna.windows.HICONWrap;
import dorkbox.jna.windows.Listener;
import dorkbox.jna.windows.Shell32;
import dorkbox.jna.windows.User32;
import dorkbox.jna.windows.WindowsEventDispatch;
import dorkbox.jna.windows.structs.NOTIFYICONDATA;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.Tray;
import dorkbox.systemTray.util.ImageResizeUtil;
import dorkbox.systemTray.util.SizeAndScaling;
import dorkbox.systemTray.util.SizeAndScalingWindows;
import dorkbox.util.ImageUtil;
import dorkbox.util.SwingUtil;


/**
 * Native implementation of a System tray on Windows with a Swing menu (the native menu is terrible)
 */
public
class _WindowsNativeTray extends Tray {
    private final Listener quitListener;
    private final Listener menuListener;
    private final Listener showListener;

    private volatile TrayPopup popupMenu;
    // is the system tray visible or not.
    private volatile boolean visible = false;
    private volatile File imageFile;
    private volatile String tooltipText = "";

    private final WindowsEventDispatch edt;

    // The image resources are cached, so that if someone is trying to create an animation, the image resource is re-used instead of
    // constantly created/destroyed -- which over time leads to issues.
    // This cache isn't anything fancy, it just lets us reuse what we have. It's cleared on hide(), and it will auto-grow as necessary.
    // If someone uses a different file every time, then this will cause problems. An error log is added if a different image is created 100x
    private final ArrayMap imageCache = new ArrayMap<>(false, 10);

    @SuppressWarnings("unused")
    public
    _WindowsNativeTray(final String trayName, final ImageResizeUtil imageResizeUtil, final Runnable onRemoveEvent) {
        super(onRemoveEvent);

        // setup some swing menu bits...
        // This creates the transparent icon
        SwingMenuItem.createTransparentIcon(SizeAndScaling.TRAY_MENU_SIZE, imageResizeUtil);
        SwingMenuItemCheckbox.createCheckedIcon(SizeAndScaling.TRAY_MENU_SIZE);

        edt = WindowsEventDispatch.start();

        // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization.
        final SwingMenu swingMenu = new SwingMenu(trayName) {
            @Override
            public
            void setImage(final MenuItem menuItem) {
                imageFile = menuItem.getImage();

                NOTIFYICONDATA nid = new NOTIFYICONDATA();
                nid.hWnd = edt.get();

                if (imageFile != null) {
                    HICONWrap imageIcon = convertImage(imageFile);
                    nid.setIcon(imageIcon);
                }

                if (!Shell32.Shell_NotifyIcon(NIM_MODIFY, nid)) {
                    SystemTray.logger.error("Error setting the image for the tray. {}", Kernel32Util.getLastErrorMessage());
                }

                // don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we
                // want to make sure keep the tooltip text the same as before.
                setTooltip_(tooltipText);

                SwingUtil.INSTANCE.invokeLater(()->{
                    if (popupMenu == null) {
                        TrayPopup popupMenu = (TrayPopup) _native;
                        popupMenu.pack();
                        popupMenu.setFocusable(true);
                        _WindowsNativeTray.this.popupMenu = popupMenu;
                    }

                    popupMenu.setTitleBarImage(imageFile);
                });
            }

            @Override
            public
            void setEnabled(final MenuItem menuItem) {
                boolean enabled = menuItem.getEnabled();

                if (visible && !enabled) {
                    // hide
                    hide();
                }
                else if (!visible && enabled) {
                    // show
                    show();
                }
            }

            @Override
            public
            void setText(final MenuItem menuItem) {
                // no op.
            }

            @Override
            public
            void setShortcut(final MenuItem menuItem) {
                // no op
            }

            @Override
            public
            void setTooltip(final MenuItem menuItem) {
                final String text = menuItem.getTooltip();

                setTooltip_(text);
            }

            @Override
            public
            void remove() {
                hide();
                super.remove();

                edt.stop();
            }
        };

        HWND hWnd = edt.get();
        if (hWnd == null) {
            throw new RuntimeException("The Windows System Tray is not supported! Please write an issue and include your OS type and configuration");
        }

        showListener = new Listener() {
            @Override
            public
            void run(final HWND hWnd, final WPARAM wParam, final LPARAM lParam) {
                show();
            }
        };
        quitListener = new Listener() {
            @Override
            public
            void run(final HWND hWnd, final WPARAM wParam, final LPARAM lParam) {
                edt.removeListener(showListener);
                edt.removeListener(quitListener);
                edt.removeListener(menuListener);

                edt.stop();
            }
        };


        menuListener = new Listener() {
            final POINT mousePosition = new POINT();

            @Override
            public
            void run(final HWND hWnd, final WPARAM wParam, final LPARAM lParam) {
                int lp = lParam.intValue();

                switch (lp) {
                    case WM_LBUTTONUP:
                    case WM_RBUTTONUP:
                        if (popupMenu != null && User32.User32.GetCursorPos(mousePosition)) {
                            double scale = SizeAndScalingWindows.getDpiScaleForMouseClick(mousePosition.x, mousePosition.y);
                            Point point = new Point((int) (mousePosition.x * scale), (int) (mousePosition.y * scale));
                            popupMenu.doShow(point, 0);
                        }
                        break;

                    default:
                        break;
                }
            }
        };

        edt.addListener(WM_TASKBARCREATED, showListener);
        edt.addListener(WM_QUIT, quitListener);
        edt.addListener(WM_SHELLNOTIFY, menuListener);

        show();

        bind(swingMenu, null, imageResizeUtil);
    }

    private
    void setTooltip_(final String text) {
        if (tooltipText != null && tooltipText.equals(text)) {
            // if it's the same, don't do anything
            return;
        }

        tooltipText = text;

        NOTIFYICONDATA nid = new NOTIFYICONDATA();
        nid.hWnd = edt.get();

        if (text != null) {
            nid.setTooltip(text);
        }

        Shell_NotifyIcon(NIM_MODIFY, nid);
    }

    private
    void hide() {
        synchronized (imageCache) {
            for (final HICONWrap value : imageCache.getValues()) {
                value.close();
            }
            imageCache.clear();
        }

        if (visible) {
            NOTIFYICONDATA nid = new NOTIFYICONDATA();
            nid.hWnd = edt.get();

            if (nid.hWnd != null && !Shell32.Shell_NotifyIcon(NIM_DELETE, nid)) {
                SystemTray.logger.error("Error hiding tray. {}", Kernel32Util.getLastErrorMessage());
            }
            visible = false;
        }
    }

    private
    void show() {
        if (visible) {
            // for some reason, it is trying to show twice. This can happen when changing the display scale or on startup)
            hide();
        }

        NOTIFYICONDATA nid = new NOTIFYICONDATA();
        nid.hWnd = edt.get();
        nid.setTooltip(tooltipText);
        if (imageFile != null) {
            HICONWrap imageIcon = convertImage(imageFile);
            if (imageIcon != null) {
                nid.setIcon(imageIcon);
            }
        }
        nid.setCallback(WM_SHELLNOTIFY);

        if (!Shell_NotifyIcon(NIM_ADD, nid)) {
            SystemTray.logger.error("Error showing tray. {}, {}", Kernel32Util.getLastErrorMessage(), Thread.currentThread().getStackTrace());
        }
        visible = true;
    }

    private
    HICONWrap convertImage(final File imageFile) {
        synchronized (imageCache) {
            HICONWrap hiconWrap = imageCache.get(imageFile);
            if (hiconWrap == null) {
                ImageIcon imageIcon = new ImageIcon(imageFile.getAbsolutePath());
                // fully loads the image and returns when it's done loading the image
                imageIcon = new ImageIcon(imageIcon.getImage());

                // for some reason, it's not possible to properly load the image.
                if (imageIcon.getIconHeight() <= 0 || imageIcon.getIconWidth() <= 0) {
                    SystemTray.logger.error("Error loading image for the system tray. {}", imageFile);
                    return null;
                }

                HBITMAPWrap hbitmapTrayIcon = new HBITMAPWrap(ImageUtil.getBufferedImage(imageIcon));
                hiconWrap = new HICONWrap(hbitmapTrayIcon);

                imageCache.put(imageFile, hiconWrap);

                if (imageCache.getSize() > 120) {
                    SystemTray.logger.error("More than 120 different images used for the SystemTray icon. This will lead to performance issues.");
                }
            }

            return hiconWrap;
        }
    }

    @Override
    public
    boolean hasImage() {
        return imageFile != null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy