diff --git a/SystemTray.iml b/SystemTray.iml index 9ef0454..919d9eb 100755 --- a/SystemTray.iml +++ b/SystemTray.iml @@ -32,6 +32,7 @@ + diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index 4ddcbb9..478660f 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -49,9 +49,11 @@ import dorkbox.systemTray.swingUI.SwingUIFactory; import dorkbox.systemTray.swingUI._SwingTray; import dorkbox.systemTray.util.ImageResizeUtil; import dorkbox.systemTray.util.JavaFX; +import dorkbox.systemTray.util.LinuxSwingUI; import dorkbox.systemTray.util.SizeAndScalingUtil; import dorkbox.systemTray.util.Swt; import dorkbox.systemTray.util.SystemTrayFixes; +import dorkbox.systemTray.util.WindowsSwingUI; import dorkbox.util.CacheUtil; import dorkbox.util.IO; import dorkbox.util.OS; @@ -751,6 +753,17 @@ class SystemTray { } + // have to make adjustments BEFORE the tray/menu image size calculations + if (AUTO_FIX_INCONSISTENCIES && isTrayType(trayType, TrayType.Swing) && SystemTray.SWING_UI == null && SwingUtil.isDefaultLookAndFeel()) { + if (isNix) { + SystemTray.SWING_UI = new LinuxSwingUI(); + } + else if (OS.isWindows()) { + SystemTray.SWING_UI = new WindowsSwingUI(); + } + } + + // initialize tray/menu image sizes. This must be BEFORE the system tray has been created int trayImageSize = SizeAndScalingUtil.getTrayImageSize(trayType); int menuImageSize = SizeAndScalingUtil.getMenuImageSize(trayType); diff --git a/src/dorkbox/systemTray/util/LinuxSwingUI.java b/src/dorkbox/systemTray/util/LinuxSwingUI.java new file mode 100644 index 0000000..94047ac --- /dev/null +++ b/src/dorkbox/systemTray/util/LinuxSwingUI.java @@ -0,0 +1,152 @@ +/* + * Copyright 2017 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.util; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Insets; + +import javax.swing.JComponent; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.border.EmptyBorder; +import javax.swing.plaf.MenuItemUI; +import javax.swing.plaf.PopupMenuUI; +import javax.swing.plaf.SeparatorUI; +import javax.swing.plaf.metal.MetalBorders; + +import dorkbox.systemTray.Entry; +import dorkbox.systemTray.Menu; +import dorkbox.systemTray.swingUI.SwingUIFactory; +import dorkbox.util.swing.DefaultMenuItemUI; +import dorkbox.util.swing.DefaultPopupMenuUI; +import dorkbox.util.swing.DefaultSeparatorUI; + +/** + * Factory to allow for Look & Feel of the Swing UI components in the SystemTray. + * + * This implementation is provided as an example of what looks reasonable on our systems for Nimbus. Naturally, everyone will have + * different systems and thus will want to change this based on their own, specified Swing L&F. + * + * NOTICE: components can ALSO have different sizes attached to them, resulting in different sized components + * mini + * myButton.putClientProperty("JComponent.sizeVariant", "mini"); + * small + * mySlider.putClientProperty("JComponent.sizeVariant", "small"); + * large + * myTextField.putClientProperty("JComponent.sizeVariant", "large"); + */ +public +class LinuxSwingUI implements SwingUIFactory { + + public static class Metal_MenuItemBorder extends MetalBorders.MenuItemBorder { + private final int verticalPadding; + + public + Metal_MenuItemBorder(int verticalPadding) { + this.verticalPadding = verticalPadding; + } + + @Override + public + Insets getBorderInsets(final Component c, final Insets newInsets) { + newInsets.set(verticalPadding, 2, verticalPadding, 2); + return newInsets; + } + } + + /** + * Allows one to specify the Look & Feel of the menus (The main SystemTray and sub-menus) + * + * @param jPopupMenu the swing JPopupMenu that is displayed when one clicks on the System Tray icon + * @param entry the entry which is bound to the menu, or null if it is the main SystemTray menu. + * + * @return the UI used to customize the Look & Feel of the SystemTray menu + sub-menus + */ + @Override + public + PopupMenuUI getMenuUI(final JPopupMenu jPopupMenu, final Menu entry) { + return new DefaultPopupMenuUI(jPopupMenu) { + @Override + public + void installUI(final JComponent c) { + super.installUI(c); + + JPopupMenu popupMenu = (JPopupMenu) c; + + // borderUI resource border type will get changed internally! + // setBorder(new BorderUIResource.EmptyBorderUIResource(0, 0, 0, 0)); + popupMenu.setBorder(new EmptyBorder(1, 1, 1, 1)); + } + }; + } + + /** + * Allows one to specify the Look & Feel of a menu entry + * + * @param jMenuItem the swing JMenuItem that is displayed in the menu + * @param entry the entry which is bound to the JMenuItem. Can be null during initialization. + * + * @return the UI used to customize the Look & Feel of the menu entry + */ + @Override + public + MenuItemUI getItemUI(final JMenuItem jMenuItem, final Entry entry) { + return new DefaultMenuItemUI(jMenuItem) { + @Override + public + void installUI(final JComponent c) { + super.installUI(c); + + JMenuItem menuItem = (JMenuItem) c; + menuItem.setIconTextGap(8); + // the original is hardcoded to always be 2 (top/bottom). We want this to be larger, so the vertical spacing looks like + // other menus + c.setBorder(new Metal_MenuItemBorder(4)); + } + }; + } + + /** + * Allows one to specify the Look & Feel of a menu separator entry + * + * @param jSeparator the swing JSeparator that is displayed in the menu + * + * @return the UI used to customize the Look & Feel of a menu separator entry + */ + @Override + public + SeparatorUI getSeparatorUI(final JSeparator jSeparator) { + return new DefaultSeparatorUI(jSeparator); + } + + + /** + * This saves a vector CheckMark to a correctly sized PNG file. The checkmark image will ALWAYS be centered in the targetImageSize + * (which is square) + * + * @param color the color of the CheckMark + * @param checkMarkSize the size of the CheckMark inside the image. (does not include padding) + * + * @return the full path to the checkmark image + */ + @Override + public + String getCheckMarkIcon(final Color color, final int checkMarkSize, final int targetImageSize) { + return HeavyCheckMark.get(color, checkMarkSize, targetImageSize); + } +} diff --git a/src/dorkbox/systemTray/util/SizeAndScalingUtil.java b/src/dorkbox/systemTray/util/SizeAndScalingUtil.java index fecc808..e67cef7 100644 --- a/src/dorkbox/systemTray/util/SizeAndScalingUtil.java +++ b/src/dorkbox/systemTray/util/SizeAndScalingUtil.java @@ -15,8 +15,11 @@ */ package dorkbox.systemTray.util; -import static dorkbox.util.jna.windows.Gdi32.GetDeviceCaps; -import static dorkbox.util.jna.windows.Gdi32.LOGPIXELSX; +import static com.sun.jna.platform.win32.WinDef.HDC; +import static com.sun.jna.platform.win32.WinDef.POINT; +import static com.sun.jna.platform.win32.WinUser.SM_CYMENUCHECK; +import static dorkbox.util.jna.windows.GDI32.GetDeviceCaps; +import static dorkbox.util.jna.windows.GDI32.LOGPIXELSX; import java.awt.Graphics2D; import java.awt.GraphicsDevice; @@ -36,12 +39,10 @@ import com.sun.jna.ptr.IntByReference; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.Tray; import dorkbox.systemTray.jna.linux.GtkTheme; -import dorkbox.systemTray.nativeUI._AwtTray; import dorkbox.systemTray.swingUI._SwingTray; import dorkbox.util.OS; import dorkbox.util.OSUtil; import dorkbox.util.SwingUtil; -import dorkbox.util.jna.windows.POINT; import dorkbox.util.jna.windows.ShCore; import dorkbox.util.jna.windows.User32; @@ -108,9 +109,9 @@ class SizeAndScalingUtil { public static int getWindowsLogicalDPI() { // get the logical resolution - Pointer screen = User32.GetDC(null); + HDC screen = User32.IMPL.GetDC(null); int logical_dpiX = GetDeviceCaps(screen, LOGPIXELSX); - User32.ReleaseDC(null, screen); + User32.IMPL.ReleaseDC(null, screen); if (SystemTray.DEBUG) { SystemTray.logger.debug("Windows logical DPI: '{}'", logical_dpiX); @@ -128,7 +129,7 @@ class SizeAndScalingUtil { IntByReference hardware_dpiX = new IntByReference(); // get the primary monitor handle - Pointer pointer = User32.MonitorFromPoint(new POINT(0, 0).asValue(), 1);// MONITOR_DEFAULTTOPRIMARY -> 1 + Pointer pointer = User32.IMPL.MonitorFromPoint(new POINT(0, 0), 1);// MONITOR_DEFAULTTOPRIMARY -> 1 ShCore.GetDpiForMonitor(pointer, 1, hardware_dpiX, new IntByReference()); // don't care about y @@ -164,6 +165,8 @@ class SizeAndScalingUtil { SystemTray.logger.debug("Windows version: '{}'", Arrays.toString(version)); } + // http://kynosarges.org/WindowsDpi.html + // 96 DPI = 100% scaling // 120 DPI = 125% scaling // 144 DPI = 150% scaling @@ -180,15 +183,16 @@ class SizeAndScalingUtil { int windowsLogicalDPI = getWindowsLogicalDPI(); if (!OSUtil.Windows.isWindows8_1_plus()) { + // < Windows 8.1 doesn't do scaling + DPI changes, they just "magnify" (but not scale w/ DPI) the icon + change the font size. // 96 DPI = 16 // 120 DPI = 20 (16 * 1.25) // 144 DPI = 24 (16 * 1.5) - TRAY_SIZE = (int) (16 * (windowsLogicalDPI / defaultDPI)); + TRAY_SIZE = 16; return TRAY_SIZE; } else { - // WINDOWS 8.1+ ONLY! Parts of this API were added in Windows 8.1, so this will not work at all for < 8.1 - int windowsPrimaryMonitorHardwareDPI = getWindowsPrimaryMonitorHardwareDPI(); + // Windows 8.1+ does proper scaling, so an icon at a higher resolution is drawn, instead of drawing the "original" + // resolution image and scaling it up to the new size // 96 DPI = 16 // 120 DPI = 20 (16 * 1.25) @@ -249,13 +253,8 @@ class SizeAndScalingUtil { // https://msdn.microsoft.com/en-us/library/dn742495.aspx // Use an icon with 16x16, 20x20, and 24x24 pixel versions. The larger versions are used in high-dpi display mode - - - - // no idea? -// TRAY_SIZE = 32; } else { - // no idea? + // reasonable default TRAY_SIZE = 32; } } @@ -263,7 +262,6 @@ class SizeAndScalingUtil { return TRAY_SIZE; } -// http://kynosarges.org/WindowsDpi.html public static int getMenuImageSize(final Class trayType) { if (TRAY_MENU_SIZE == 0) { @@ -277,48 +275,53 @@ class SizeAndScalingUtil { TRAY_MENU_SIZE = 16; } } - else if ((trayType == _SwingTray.class || trayType == _AwtTray.class)) { - // this covers windows (entirely) and 2 of the 4 options in linux + else if ((trayType == _SwingTray.class)) { // Java does not scale the menu item IMAGE **AT ALL**, we must provide the correct size to begin with - // Swing or AWT. While not "normal", this is absolutely a possible combination. - final AtomicInteger iconSize = new AtomicInteger(); + if (OS.isWindows()) { + // http://kynosarges.org/WindowsDpi.html - SwingUtil.invokeAndWaitQuietly(new Runnable() { - @Override - public - void run() { - JMenuItem jMenuItem = new JMenuItem(); - // do the same modifications that would also happen (if specified) for the actual displayed menu items - if (SystemTray.SWING_UI != null) { - jMenuItem.setUI(SystemTray.SWING_UI.getItemUI(jMenuItem, null)); + // image-size/menu-height + // 96 DPI = 100% actual size: 14/17 + // 144 DPI = 150% actual size: 24/29 + + // gets the height of the default checkmark size, adjusted + // This is the closest image size we can get to the actual size programmatically. This is a LOT closer that checking the + // largest size a JMenu image can be before the menu size changes. + TRAY_MENU_SIZE = User32.IMPL.GetSystemMetrics(SM_CYMENUCHECK) - 1; + + // image-size/menu-height + // 96 DPI = 100% mark size: 14/20 + // 144 DPI = 150% mark size: 24/30 + } else { + final AtomicInteger iconSize = new AtomicInteger(); + + SwingUtil.invokeAndWaitQuietly(new Runnable() { + @Override + public + void run() { + JMenuItem jMenuItem = new JMenuItem(); + + // do the same modifications that would also happen (if specified) for the actual displayed menu items + if (SystemTray.SWING_UI != null) { + jMenuItem.setUI(SystemTray.SWING_UI.getItemUI(jMenuItem, null)); + } + + // this is the largest size of an image used in a JMenuItem, before the size of the JMenuItem is forced to be larger + int height = SwingUtil.getLargestIconHeightForButton(jMenuItem); + iconSize.set(height); } - - // this is the largest size of an image used in a JMenuItem, before the size of the JMenuItem is forced to be larger - int height = SwingUtil.getLargestIconHeightForButton(jMenuItem); - - if (OSUtil.Windows.isWindows8_1_plus()) { - // windows 8.1 adjusts the menu differently that XP/Vista/Win7, which do not adjust the menu beyond a basic test - - // 96 DPI = 100% scaling - // 120 DPI = 125% scaling - // 144 DPI = 150% scaling - // 192 DPI = 200% scaling - final double defaultDPI = 96.0; - int windowsLogicalDPI = getWindowsLogicalDPI(); - - height = (int) (height * (windowsLogicalDPI / defaultDPI)); - } - - iconSize.set(height); - } - }); - TRAY_MENU_SIZE = iconSize.get(); + }); + TRAY_MENU_SIZE = iconSize.get(); + } } else if (OS.isLinux()) { // AppIndicator or GtkStatusIcon TRAY_MENU_SIZE = GtkTheme.getMenuEntryImageSize(); + } else { + // reasonable default + TRAY_MENU_SIZE = 16; } } diff --git a/src/dorkbox/systemTray/util/WindowsSwingUI.java b/src/dorkbox/systemTray/util/WindowsSwingUI.java new file mode 100644 index 0000000..f9187ed --- /dev/null +++ b/src/dorkbox/systemTray/util/WindowsSwingUI.java @@ -0,0 +1,120 @@ +/* + * Copyright 2017 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.util; + +import java.awt.Color; + +import javax.swing.JComponent; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.plaf.MenuItemUI; +import javax.swing.plaf.PopupMenuUI; +import javax.swing.plaf.SeparatorUI; + +import dorkbox.systemTray.Entry; +import dorkbox.systemTray.Menu; +import dorkbox.systemTray.swingUI.SwingUIFactory; +import dorkbox.util.swing.DefaultMenuItemUI; +import dorkbox.util.swing.DefaultPopupMenuUI; +import dorkbox.util.swing.DefaultSeparatorUI; + +/** + * Factory to allow for Look & Feel of the Swing UI components in the SystemTray. + * + * This implementation is provided as an example of what looks reasonable on our systems for Nimbus. Naturally, everyone will have + * different systems and thus will want to change this based on their own, specified Swing L&F. + * + * NOTICE: components can ALSO have different sizes attached to them, resulting in different sized components + * mini + * myButton.putClientProperty("JComponent.sizeVariant", "mini"); + * small + * mySlider.putClientProperty("JComponent.sizeVariant", "small"); + * large + * myTextField.putClientProperty("JComponent.sizeVariant", "large"); + */ +public +class WindowsSwingUI implements SwingUIFactory { + + /** + * Allows one to specify the Look & Feel of the menus (The main SystemTray and sub-menus) + * + * @param jPopupMenu the swing JPopupMenu that is displayed when one clicks on the System Tray icon + * @param entry the entry which is bound to the menu, or null if it is the main SystemTray menu. + * + * @return the UI used to customize the Look & Feel of the SystemTray menu + sub-menus + */ + @Override + public + PopupMenuUI getMenuUI(final JPopupMenu jPopupMenu, final Menu entry) { + return new DefaultPopupMenuUI(jPopupMenu) { + @Override + public + void installUI(final JComponent c) { + super.installUI(c); + } + }; + } + + /** + * Allows one to specify the Look & Feel of a menu entry + * + * @param jMenuItem the swing JMenuItem that is displayed in the menu + * @param entry the entry which is bound to the JMenuItem. Can be null during initialization. + * + * @return the UI used to customize the Look & Feel of the menu entry + */ + @Override + public + MenuItemUI getItemUI(final JMenuItem jMenuItem, final Entry entry) { + return new DefaultMenuItemUI(jMenuItem) { + @Override + public + void installUI(final JComponent c) { + super.installUI(c); + } + }; + } + + /** + * Allows one to specify the Look & Feel of a menu separator entry + * + * @param jSeparator the swing JSeparator that is displayed in the menu + * + * @return the UI used to customize the Look & Feel of a menu separator entry + */ + @Override + public + SeparatorUI getSeparatorUI(final JSeparator jSeparator) { + return new DefaultSeparatorUI(jSeparator); + } + + + /** + * This saves a vector CheckMark to a correctly sized PNG file. The checkmark image will ALWAYS be centered in the targetImageSize + * (which is square) + * + * @param color the color of the CheckMark + * @param checkMarkSize the size of the CheckMark inside the image. (does not include padding) + * + * @return the full path to the checkmark image + */ + @Override + public + String getCheckMarkIcon(final Color color, final int checkMarkSize, final int targetImageSize) { + return HeavyCheckMark.get(color, checkMarkSize, targetImageSize); + } +} diff --git a/test/dorkbox/CustomSwingUI.java b/test/dorkbox/CustomSwingUI.java index 7867e3e..1aaec98 100644 --- a/test/dorkbox/CustomSwingUI.java +++ b/test/dorkbox/CustomSwingUI.java @@ -16,13 +16,11 @@ package dorkbox; import java.awt.Color; -import java.awt.Insets; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; -import javax.swing.border.EmptyBorder; import javax.swing.plaf.MenuItemUI; import javax.swing.plaf.PopupMenuUI; import javax.swing.plaf.SeparatorUI; @@ -68,12 +66,6 @@ class CustomSwingUI implements SwingUIFactory { public void installUI(final JComponent c) { super.installUI(c); - - JPopupMenu popupMenu = (JPopupMenu) c; - - // borderUI resource border type will get changed internally! - // setBorder(new BorderUIResource.EmptyBorderUIResource(0, 0, 0, 0)); - popupMenu.setBorder(new EmptyBorder(1, 1, 1, 1)); } }; } @@ -94,12 +86,6 @@ class CustomSwingUI implements SwingUIFactory { public void installUI(final JComponent c) { super.installUI(c); - - - JMenuItem menuItem = (JMenuItem) c; - menuItem.setIconTextGap(8); - menuItem.setMargin(new Insets(2, -2, 2, 4)); - c.setBorder(new EmptyBorder(1, 1, 1, 1)); } }; }