diff --git a/runelite-client/src/main/java/net/runelite/client/Notifier.java b/runelite-client/src/main/java/net/runelite/client/Notifier.java index cdb1d63cfe..ce5db852c1 100644 --- a/runelite-client/src/main/java/net/runelite/client/Notifier.java +++ b/runelite-client/src/main/java/net/runelite/client/Notifier.java @@ -160,9 +160,14 @@ public class Notifier return; } - if (runeLiteConfig.requestFocusOnNotification()) + switch (runeLiteConfig.notificationRequestFocus()) { - clientUI.requestFocus(); + case REQUEST: + clientUI.requestFocus(); + break; + case FORCE: + clientUI.forceFocus(); + break; } if (runeLiteConfig.enableTrayNotifications()) diff --git a/runelite-client/src/main/java/net/runelite/client/config/RequestFocusType.java b/runelite-client/src/main/java/net/runelite/client/config/RequestFocusType.java new file mode 100644 index 0000000000..728ce3f4b8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/config/RequestFocusType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.config; + +public enum RequestFocusType +{ + OFF, + REQUEST, + FORCE; +} diff --git a/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java b/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java index b9fa210bb9..b29a46a5df 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java @@ -150,12 +150,12 @@ public interface RuneLiteConfig extends Config @ConfigItem( keyName = "notificationRequestFocus", name = "Request focus on notification", - description = "Toggles window focus request", + description = "Configures the window focus request type on notification", position = 21 ) - default boolean requestFocusOnNotification() + default RequestFocusType notificationRequestFocus() { - return true; + return RequestFocusType.OFF; } @ConfigItem( diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java index 57a9906fc1..257c231077 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java @@ -27,6 +27,8 @@ package net.runelite.client.plugins.devtools; import java.awt.GridLayout; import java.awt.TrayIcon; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.swing.JButton; import javax.swing.JPanel; @@ -48,6 +50,7 @@ class DevToolsPanel extends PluginPanel private final VarInspector varInspector; private final ScriptInspector scriptInspector; private final InfoBoxManager infoBoxManager; + private final ScheduledExecutorService scheduledExecutorService; @Inject private DevToolsPanel( @@ -57,7 +60,8 @@ class DevToolsPanel extends PluginPanel VarInspector varInspector, ScriptInspector scriptInspector, Notifier notifier, - InfoBoxManager infoBoxManager) + InfoBoxManager infoBoxManager, + ScheduledExecutorService scheduledExecutorService) { super(); this.client = client; @@ -67,6 +71,7 @@ class DevToolsPanel extends PluginPanel this.scriptInspector = scriptInspector; this.notifier = notifier; this.infoBoxManager = infoBoxManager; + this.scheduledExecutorService = scheduledExecutorService; setBackground(ColorScheme.DARK_GRAY_COLOR); @@ -143,7 +148,7 @@ class DevToolsPanel extends PluginPanel final JButton notificationBtn = new JButton("Notification"); notificationBtn.addActionListener(e -> { - notifier.notify("Wow!", TrayIcon.MessageType.ERROR); + scheduledExecutorService.schedule(() -> notifier.notify("Wow!", TrayIcon.MessageType.ERROR), 3, TimeUnit.SECONDS); }); container.add(notificationBtn); diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java index cb4257b79e..5c6f2b8fe5 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java @@ -65,9 +65,6 @@ import net.runelite.api.Constants; import net.runelite.api.GameState; import net.runelite.api.Player; import net.runelite.api.Point; -import net.runelite.client.eventbus.EventBus; -import net.runelite.client.events.ClientShutdown; -import net.runelite.client.events.ConfigChanged; import net.runelite.api.events.GameStateChanged; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; @@ -77,7 +74,10 @@ import net.runelite.client.config.ConfigManager; import net.runelite.client.config.ExpandResizeType; import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.config.WarningOnExit; +import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ClientShutdown; +import net.runelite.client.events.ConfigChanged; import net.runelite.client.events.NavigationButtonAdded; import net.runelite.client.events.NavigationButtonRemoved; import net.runelite.client.input.KeyManager; @@ -90,6 +90,7 @@ import net.runelite.client.util.ImageUtil; import net.runelite.client.util.OSType; import net.runelite.client.util.OSXUtil; import net.runelite.client.util.SwingUtil; +import net.runelite.client.util.WinUtil; import org.pushingpixels.substance.internal.SubstanceSynapse; import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities; import org.pushingpixels.substance.internal.utils.SubstanceTitlePaneUtilities; @@ -663,12 +664,38 @@ public class ClientUI */ public void requestFocus() { - if (OSType.getOSType() == OSType.MacOS) + switch (OSType.getOSType()) { - OSXUtil.requestFocus(); + case MacOS: + // On OSX Component::requestFocus has no visible effect, so we use our OSX-specific + // requestUserAttention() + OSXUtil.requestUserAttention(); + break; + default: + frame.requestFocus(); + } + + giveClientFocus(); + } + + /** + * Attempt to forcibly bring the client frame to front + */ + public void forceFocus() + { + switch (OSType.getOSType()) + { + case MacOS: + OSXUtil.requestForeground(); + break; + case Windows: + WinUtil.requestForeground(frame); + break; + default: + frame.requestFocus(); + break; } - frame.requestFocus(); giveClientFocus(); } diff --git a/runelite-client/src/main/java/net/runelite/client/util/OSXUtil.java b/runelite-client/src/main/java/net/runelite/client/util/OSXUtil.java index f521349b94..de099a1646 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/OSXUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/util/OSXUtil.java @@ -49,16 +49,23 @@ public class OSXUtil } } + /** + * Request user attention on macOS + */ + public static void requestUserAttention() + { + Application app = Application.getApplication(); + app.requestUserAttention(true); + log.debug("Requested user attention on macOS"); + } + /** * Requests the foreground in a macOS friendly way. */ - public static void requestFocus() + public static void requestForeground() { - if (OSType.getOSType() == OSType.MacOS) - { - Application app = Application.getApplication(); - app.requestForeground(true); - log.debug("Requested focus on macOS"); - } + Application app = Application.getApplication(); + app.requestForeground(true); + log.debug("Forced focus on macOS"); } } diff --git a/runelite-client/src/main/java/net/runelite/client/util/WinUtil.java b/runelite-client/src/main/java/net/runelite/client/util/WinUtil.java new file mode 100644 index 0000000000..b300257690 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/util/WinUtil.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.util; + +import com.sun.jna.Native; +import com.sun.jna.platform.win32.User32; +import com.sun.jna.platform.win32.WinDef; +import com.sun.jna.platform.win32.WinUser; +import java.awt.Frame; + +public class WinUtil +{ + /** + * Forcibly set focus to the given component + * + */ + public static void requestForeground(Frame frame) + { + // SetForegroundWindow can't set iconified windows to foreground, so set the + // frame state to normal first + frame.setState(Frame.NORMAL); + + User32 user32 = User32.INSTANCE; + + // Windows does not allow any process to set the foreground window, but it will if + // the process received the last input event. So we send a F22 key event to the process. + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow + WinUser.INPUT input = new WinUser.INPUT(); + input.type = new WinDef.DWORD(WinUser.INPUT.INPUT_KEYBOARD); + input.input.ki.wVk = new WinDef.WORD(0x85); // VK_F22 + user32.SendInput(new WinDef.DWORD(1), (WinUser.INPUT[]) input.toArray(1), input.size()); + + // Now we may set the foreground window + WinDef.HWND hwnd = new WinDef.HWND(Native.getComponentPointer(frame)); + user32.SetForegroundWindow(hwnd); + } +}