HumbleUI / JWM

Cross-platform window management and OS integration library for Java
Apache License 2.0
546 stars 44 forks source link

Add Window bringToFront & isFront, Windows impl #266

Closed Quezion closed 1 year ago

Quezion commented 1 year ago

These are not sufficient to steal foreground application focus in Windows, which will require another commit to add attaching the JWM window to the current foreground thread.

I'll mark this as Draft until subsequent commit but I wanted to open the floor for feedback on the first commit.

The subsequent commit will need to add functionality from the top answer of https://stackoverflow.com/questions/916259/win32-bring-a-window-to-top , which needs FNs for GetForegroundWindow, GetCurrentThreadId, GetWindowThreadProcessId, and AttachThreadInput. I'll first try just adding these to the Window class; there might be a better architecture to refactor this to, but I'll look to @tonsky's guidance once I actually get this working.

Quezion commented 1 year ago

Something I've been considering is that Linux & Mac APIs aren't going to have equivalent functions for the Windows specific ones like setForegroundWindow. This means my first commit will also require some refactor.

Should the shared/Window class just have an abstracted method named stealFocus & better hide the OS-specific hacks that're required to achieve this? We could do this & leave the operating-system specific calls as public (or private?) methods on the Java classes, which still allows consumers to leverage them if necessary.

Quezion commented 1 year ago

Still missing attachThread Windows workaround code, but added a 2nd commit with window/stealFocus. The specific Windows methods of setActiveWindow() & setForegroundWindow() now only exist in WindowWin32.java & will still be invoked as part of the full solution.

Quezion commented 1 year ago

The latest commit completes the Windows stealFocus implementation via attachThreadInput. The set of calls (attach, setForegroundWindow, detach) are the minimum ones to accomplish the working behavior on my Windows 10 installation.

I'd appreciate some guidance to help finish this PR; any recommendations on better symbol names, better places to put the thread-ID manipulation functions, thoughts on the stealFocus() function, or anything else? Whether you want to make the edits yourself or guide me there, I'll be happy as long as we can get this PR merged.

@tonsky First, thanks for putting this repo together; it was a very educational deep dive into some unfamiliar corners of the "Clojure" ecosystem. Second, no rush as I'm happy to consume my local version of HumbleUI + JWM for several weeks while I keep extending my app.

tonsky commented 1 year ago

Sorry, I’ve been distracted with me dayjob last few weeks. I’ll take a look once I’m back to my normal schedule

tonsky commented 1 year ago

Hi, I finally get the time to look at the PR. Unfortunately, I’m not sure I understand what stealFocus does (or is supposed to do). Can you elaborate? E.g., how is it different from Window::focus()? An example would help (how many windows, who owns them, are they all from the same app, etc)

Quezion commented 1 year ago

@tonsky From testing on Windows 10, I found that focus() does not work to bring a HumbleUI window to the front if it didn't already have "operating system" level foreground focus from the system.

Let's say you have a Windows app, like Excel, on top of the screen. Your background HumbleUI app can focus() but will still remain in the background of the OS. If you call it, maybe the Windows taskbar will popup and flash, but your HumbleUI will remain in the background & invisible to the user.

So then, how can we sidestep this limitation and force foreground focus off Excel & onto the HumbleUI app? The StackOverflow Answer is correct. I double-checked this via testing & looking at how some other codebases use of WinAPI.

To summarize the StackOverflow answer, you must first attach the HumbleUI's graphics thread's input to the foreground process's (Excel) graphics thread input. This has a side-effect of allowing your program's windows to be rendered on top of the foreground process, so then calling foreground() will fully take OS-level focus.


While this is the only way to achieve this in Windows, it feels a bit awkward and I didn't want to expose the thread-manipulation methods on the generic Window class. Hence stealFocus

I've also updated the PR by adding some extra text to stealFocus() docstring.

tonsky commented 1 year ago

Does it make sense to have both focus() and stealFocus()? From what you’ve described, focus() seems useless? Is it a normal practice to steal focus like you described? Or is it something only very special apps do? Like, why would you want to steal focus in the first place?

Quezion commented 1 year ago

Stealing focus in response to a background keypress is common for "launcher" style functionality. It's also leveraged by mature desktop apps -- example YouTube video demonstrating the Todoist app's use of this to add tasks from anywhere. https://youtu.be/SnBT7niGJZo?t=13

I haven't tried spawning multiple windows in HumbleUI, but I assume that plain focus() is still useful where you're rearranging several HumbleUI windows.

tonsky commented 1 year ago

Sorry to be such a drag, but I really try to understand. I feel you are right and there should be two methods. I’m happy to have this functionality in JWM, but it’ll really help me if I can imagine a use case for it.

In the example of Todoist, is it Todoist itself who’s handling the key shortcut? Or is it Windows that captures shortcut and brings the window forward?

What is your use-case?

Quezion commented 1 year ago

In the case of Todoist, I don't believe there's any Windows API that can do this. I use Mac OS & I'm familiar with the "system shortcuts" menu but I can't find a Win equivalent.

I can only speculate about Todoist src -- but I bet they're using the the same ThreadInput attachment trick because I spent a day testing alternaties & it was the only working one. Launchy is a older open-src program launcher that also does this via ThreadInput Attach+Focus+Detach.

My personal use case is a Clojure "program launcher" application with built-in productivity features, or a combination of the aforementioned apps. I have some Java code to listening to OS key-events and I stealFocus() onto my HumbleUI Window. This is a code-snippet:

(def ui-keybind-map
  {"Alt+Space" #(app/doui (#'launcher/toggle-open! @*window))})
(defn toggle-open!
  [$window]
  (let [foreground-hwnd (.getForegroundWindow $window)
        app-in-foreground? (= (.getWindowThreadProcessId $window foreground-hwnd)
                              (.getCurrentThreadId $window))]
    (window/set-visible $window (not app-in-foreground?))
    (when-not app-in-foreground?
      (window/steal-focus $window)
      (window/restore $window)
      (window/focus $window))))

I'd very much like to preserve support for this somehow since my app isn't effective without it. I can eventually contribute Linux & Mac implementations to achieve the same behavior -- though I haven't yet tested how the existing focus() behaves there.

Quezion commented 1 year ago

I went digging into Electron and didn't find the src-code responsible for focus. I did find this thread though, and it seems to imply that .show() is a lighter weight window method than ".focus()" https://github.com/electron/electron/issues/2867

One person linked a 2020 npm package called Force Focus, which led to this interesting workaround for spamming alt before running SetForegroundWindow into SetFocus. I did unsuccessfully try this trick earlier but only with SetForegroundWindow, not SetFocus... so might work?

This 2022 post implies that Electron .focus() still doesn't work correctly in 2023, but that users of Electron can run setAlwaysOnTop() beforehand as a workaround. This seems to have some unwanted effects in the WinAPI when running multiple Electron apps but seems a better workaround than fake-holding alt. https://stackoverflow.com/questions/70925355/why-does-win-focus-not-bring-the-window-to-the-front


Given the thread (& my personal biases), I'd like to see some type of support for this in JWM. Whether renaming the library's current focus() -> show() & stealFocus -> focus or exposing attach/detachThreadInput functions via JWM and letting callers invoke it themselves.

tonsky commented 1 year ago

Cool, let’s include it! Even just for Windows now is good.

I have a few notes:

void setActiveWindow();
HWND getForegroundWindow(); // Note asymmetry between get/setForegroundWindow
void setForegroundWindow();
long getCurrentThreadId();
long getWindowThreadProcessId(HWND hWnd);
void attachThreadInput(long parentThreadId, long childThreadId, bool attachOrDetach);

Do we need all these methods in Java? Are they realistically useful or is single stealFocus is enough? From what you’ve described, they seem like implementation detail and should be hidden?

Second, how do you about renaming stealFocus to something like bringToFront? Again, from what you described, it feels like it more accurately described what it does.

Third, regarding

I have some Java code to listening to OS key-events

Do you feel like it should be included into JWM, too? It seems to me that stealing focus is not that useful without an ability to bind on OS-level shortcut.

Quezion commented 1 year ago

bringToFront seems like a good name for this & I'll update the PR later today.


For the Java thread methods, I agree that these should be hidden. I've also painted myself into a corner b/c I'm using this app code to determine if the Window has foreground focus:

  (let [foreground-hwnd (.getForegroundWindow $window)
        app-in-foreground? (= (.getWindowThreadProcessId $window foreground-hwnd)
                              (.getCurrentThreadId $window))]
    (when-not app-in-foreground?
      ...)

Are you OK with adding one additional Java function like isFront(), which returns boolean representing whether window object has foreground OS focus?

Do you feel like it should be included into JWM, too? It seems to me that stealing focus is not that useful without an ability to bind on OS-level shortcut.

Yes, but I must confess that my Java code is just jnativehook. I'm interested in porting this to JWM at some point for API improvements & consistent key events after features like setTitleBar & other x-platform code are done.

Quezion commented 1 year ago

Just copying some notes from ChatGPT. It "knows" about the AttachThreadInput technique but didn't mention setAlwaysOnTop from the Electron thread.


Q: Write a C program that uses the Windows API to steal foreground focus onto my window

...truncated response using plain SetForegroundWindow(hwnd)...

Q: This doesn't work to steal focus onto my window unless it's already in the foreground. How can I steal focus from the current foreground process?

You are correct that the SetForegroundWindow function may not work if the window is not currently in the foreground. In order to steal focus from the current foreground process, you can use the AttachThreadInput function to attach your window's thread input to the foreground thread input, and then call SetForegroundWindow. Here's an updated example program:


#include <windows.h>

int main() {
    HWND hWnd = FindWindow(NULL, "Window Title"); // Replace "Window Title" with the title of your window
    if (hWnd != NULL) {
        DWORD dwCurrentThreadId = GetCurrentThreadId();
        DWORD dwForegroundThreadId = GetWindowThreadProcessId(GetForegroundWindow(), NULL);
        AttachThreadInput(dwCurrentThreadId, dwForegroundThreadId, TRUE);
        SetForegroundWindow(hWnd);
        AttachThreadInput(dwCurrentThreadId, dwForegroundThreadId, FALSE);
    }
    return 0;
}

In this program, we first get the current thread ID and the thread ID of the foreground window using the GetCurrentThreadId and GetWindowThreadProcessId functions, respectively. We then use the AttachThreadInput function to attach the input of our window's thread to the foreground thread's input. This allows our window to receive input from the user even if it is not in the foreground.

After that, we call SetForegroundWindow to bring our window to the foreground and give it the focus. Finally, we detach the thread input using AttachThreadInput with the last parameter set to FALSE. This ensures that our window's thread is no longer attached to the foreground thread's input.

Note that there are some security restrictions on using the AttachThreadInput function, and it may not work in all situations or on all versions of Windows. Use it with caution and test thoroughly before using it in production code.


I tried asking "Is there some other way to steal foreground focus onto my window?", but it gave the below invocation which it notes "does not steal the input focus away from the currently active window, but rather just brings your window to the front." Not quite right but including anway --

ShowWindow(hWnd, SW_SHOWMAXIMIZED);
tonsky commented 1 year ago

Are you OK with adding one additional Java function like isFront(), which returns boolean representing whether window object has foreground OS focus?

Absolutely

Yes, but I must confess that my Java code is just jnativehook. I'm interested in porting this to JWM at some point for API improvements & consistent key events after features like setTitleBar & other x-platform code are done.

No worries then, if anybody needs it, they can use the same library for now

Quezion commented 1 year ago

@tonsky PR updated -- renamed bringToFront and isFront & removed all extra passthrough/wrapper FNs. Does this look good?

I retested locally on Win10 after tweaking app code & this snippet works.

(let [in-front? (window/front? $window)]
  (window/set-visible $window (not in-front?))
  (when-not in-front?
    (-> $window
        window/bring-to-front
        window/restore
        window/focus)))

Once this merges, I'll open a small HumbleUI PR to expose window/ bring-to-front and front?

tonsky commented 1 year ago

Yeah, looks great, happy to merge it. Thanks for seeing it through. I just published 0.4.15, should get to Maven Central in 30-60 minutes