Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can a solution be provided to prevent the window from stealing focus from the currently active window #4

Closed
zzzze opened this issue Feb 11, 2023 · 47 comments
Assignees
Labels
enhancement New feature or request

Comments

@zzzze
Copy link

zzzze commented Feb 11, 2023

Can a solution be provided to prevent the window from stealing focus from the currently active window, as mentioned in this issue?

I'd like to create a window/app that behaves like macOS Spotlight:

  1. It shows over fullscreen apps
  2. It doesn't deactivate the active app. If the window opens while you have Chrome focused, the Chrome menu bar is still shown and the Chrome window looks like it's still focused.

On macOS I think this kind of window is an NSPanel

@ahkohd
Copy link
Owner

ahkohd commented Feb 12, 2023

You don't need to worry about stealing focus if you're going to restore the focus.

I have implemented a solution that ensures the active window remains in focus. You can see this in the commit 03a3077.

The new logic ensures that when the dock window loses focus, the application automatically restores focus to the previously active window, providing an uninterrupted user experience.

See the demo:
Screen Recording 2023-02-12 at 1 31 39 AM

Here's how it works:

  • Upon pressing the shortcut to open the dock window, the application captures and stores the bundle URL of the currently active window or app.
  • Then, it displays the dock window.
  • When the dock window loses focus, the stored bundle URL is retrieved, and the previously active window is re-opened, regaining focus for the user.

@ahkohd ahkohd self-assigned this Feb 12, 2023
@ahkohd ahkohd added the enhancement New feature or request label Feb 12, 2023
@elanzini
Copy link

One problem with this. If for example I have VS Code in focus, I open the Tauri app and then I click on a different app (e.g. Firefox), it will give the focus back on VS Code instead of focusing on Firefox.

Screen.Recording.2023-02-12.at.10.15.27.mov

@zzzze
Copy link
Author

zzzze commented Feb 12, 2023

One problem with this. If for example I have VS Code in focus, I open the Tauri app and then I click on a different app (e.g. Firefox), it will give the focus back on VS Code instead of focusing on Firefox.

Screen.Recording.2023-02-12.at.10.15.27.mov

This problem can be resolved by moving the logic from the "backdrop" function to the "register_shortcut" function, so that it will only be affected by the shortcut that hides the app.

image

I have an even trickier problem. The workaround uses the "open" API to reopen the previously active application, but each app has its own unique logic for reopening, and some of them hide the app.

@zzzze zzzze closed this as completed Feb 12, 2023
@zzzze zzzze reopened this Feb 12, 2023
@zzzze
Copy link
Author

zzzze commented Feb 12, 2023

@ahkohd I resolved the issue by using the Object-C API [app activateWithOptions:NSApplicationActivateAllWindows]. Your solution seems to be even better. I'm looking forward to your solution.

@ahkohd
Copy link
Owner

ahkohd commented Feb 13, 2023

Fixed; see commit 303913c.

This commit effectively resolves the bugs detected in the previous commit. It employs the Objective-C "Copy Window List" API to retrieve the list of windows displayed on the screen, locates the spotlight window, and subsequently activates the next window in the list, excluding any windows designated as a menubar or always-on-top.

@zzzze zzzze closed this as completed Feb 13, 2023
@elanzini
Copy link

This is unfortunately still not behaving as expected. I am typing in a text box and then I open the application with Command + K. I then click on the browser that is in the background to get the focus back to the text box but that is not what is happening.

As a first step, would it be possible to register the escape key to close the application? So it simulates how Spotlight works on that aspect as well?

@ahkohd
Copy link
Owner

ahkohd commented Feb 13, 2023

Hi, I don't understand what you mean in the first paragraph, is the focus not restoring to the window you stole focus from?

You can create an issue for the escape key to tuck away the spotlight light window.

@elanzini
Copy link

Created #5 - it will be easier to reproduce what I mean once the escape feature is implemented. Looking forward to it ☺️

@zzzze
Copy link
Author

zzzze commented Feb 14, 2023

I found a bug where if there are notifications on the screen, it is not possible to activate the previous window. To reproduce this issue, you can use the following osascript to show a fake notification:
osascript -e 'display notification "Lorem ipsum dolor sit amet" with title "Hello World"'
image

PS: If you set the notification to "Alerts," it will remain on the screen until you manually close it.
image

@zzzze zzzze reopened this Feb 14, 2023
@ahkohd
Copy link
Owner

ahkohd commented Feb 14, 2023

I had to reimplement how to refocus the window behind the spotlight window when it's tucked away. See commit dde828c.

Despite trying to use NSRunningApplication:activateWith:options, I found that this approach wasn't working correctly on multiple screens. After some troubleshooting, I decided to re-implement the code using macOS accessibility API. This allowed me to focus on the window using the owner process id and the window id, which proved to be a more reliable solution.

@ahkohd
Copy link
Owner

ahkohd commented Feb 14, 2023

@elanzini @zzzze, please verify if all is correct. I have tested thoroughly on my end everything works fine. I tried against multiple screens, zoomed windows, push notifications, and floating windows.

@elanzini
Copy link

@ahkohd Thanks so much for all the effort. Unfortunately, I see that the focus still doesn't go back to the text box where I am typing.

focus_missing_on_textbox.mov

@ahkohd
Copy link
Owner

ahkohd commented Feb 15, 2023

@ahkohd Thanks so much for all the effort. Unfortunately, I see that the focus still doesn't go back to the text box where I am typing.

focus_missing_on_textbox.mov

Did you grant accessibility permission?

@elanzini
Copy link

Did you grant accessibility permission?

I didn't get any popup asking for it when running npm run tauri dev

@ahkohd
Copy link
Owner

ahkohd commented Feb 15, 2023

Did you grant accessibility permission?

I didn't get any popup asking for it when running npm run tauri dev

Can you grant it manually to your terminal? I'll look into the code that requests it, perhaps it's broken

@elanzini
Copy link

Can you grant it manually to your terminal? I'll look into the code that requests it, perhaps it's broken

I go to the Accessibility screen in Settings but not sure what to do after.
Screenshot 2023-02-15 at 08 12 49

@ahkohd
Copy link
Owner

ahkohd commented Feb 15, 2023

@elanzini
Copy link

See this guide https://support.apple.com/en-ng/guide/mac-help/mh43185/mac

Both my Terminal and VS Code (from which I am running the app) have Accessibility permissions.

@ahkohd
Copy link
Owner

ahkohd commented Feb 15, 2023

See this guide https://support.apple.com/en-ng/guide/mac-help/mh43185/mac

Both my Terminal and VS Code (from which I am running the app) have Accessibility permissions.

That explains why you did not get the prompt. Just for sanity checks can you revoke it? So that it can request again when you run the code. But I doubt that will fix your issue.

@elanzini
Copy link

See this guide https://support.apple.com/en-ng/guide/mac-help/mh43185/mac

Both my Terminal and VS Code (from which I am running the app) have Accessibility permissions.

That explains why you did not get the prompt. Just for sanity checks can you revoke it? So that it can request again when you run the code. But I doubt that will fix your issue.

Yeah, unfortunately I am still seeing the same issue.

@ahkohd
Copy link
Owner

ahkohd commented Feb 15, 2023

See this guide https://support.apple.com/en-ng/guide/mac-help/mh43185/mac

Both my Terminal and VS Code (from which I am running the app) have Accessibility permissions.

That explains why you did not get the prompt. Just for sanity checks can you revoke it? So that it can request again when you run the code. But I doubt that will fix your issue.

Yeah, unfortunately I am still seeing the same issue.

You got the prompt right?

@elanzini
Copy link

You got the prompt right?

Correct, a got a prompt asking to grant Accessibility to VS Code

@zzzze
Copy link
Author

zzzze commented Feb 16, 2023

image

@ahkohd It looks like the active logic might not be taking effect when there is a floating widget with a height greater than 80. It is caused by the following piece of code.

image

@ahkohd
Copy link
Owner

ahkohd commented Feb 16, 2023

Ahaha I always know that might not work! I'll push a fix soon, I have made some research, and I have the solution to fix this using the window layer.
image
image

  • layer 23 is for the push notifications
  • system preferences sit on layer 3
  • The normal window layer is 0, you see, that's my Wezterm which I want to refocus!

@ahkohd
Copy link
Owner

ahkohd commented Feb 16, 2023

Fixed! See commit bbf4d9b. I hope all is good this time. 😅

This refactor removes the hacks I wrote previously. The new implementation uses the window layer/level to correctly identify the window to focus.

@zzzze @elanzini please help test.

@elanzini
Copy link

Fixed! See commit bbf4d9b. I hope all is good this time. 😅

This refactor removes the hacks I wrote previously. The new implementation uses the window layer/level to correctly identify the window to focus.

@zzzze @elanzini please help test.

I tested the focus on the text box and unfortunately still behaves like #4 (comment)

@ahkohd
Copy link
Owner

ahkohd commented Feb 17, 2023

Fixed! See commit bbf4d9b. I hope all is good this time. 😅
This refactor removes the hacks I wrote previously. The new implementation uses the window layer/level to correctly identify the window to focus.
@zzzze @elanzini please help test.

I tested the focus on the text box and unfortunately still behaves like #4 (comment)

Is the app that you want to refocus in fullscreen? Can you try other windows? maybe probe a little bit. I can create a new branch for you where I can add debug logs to be able to understand where the issue lies.

@zzzze
Copy link
Author

zzzze commented Feb 19, 2023

I think we overcomplicated things, this is just a workaround. I believe that changing "open" to "activate" on the original plan would already be good enough. For example, using the following function instead of open:

fn active_another_app(bundle_url: &str) -> Result<(), Error> {
    let workspace = unsafe {
        if let Some(workspace_class) = Class::get("NSWorkspace") {
            let shared_workspace: *mut Object = msg_send![workspace_class, sharedWorkspace];
            shared_workspace
        } else {
            return Err(Error::FailedToGetNSWorkspaceClass);
        }
    };
    let running_apps = unsafe {
        let running_apps: *mut Object = msg_send![workspace, runningApplications];
        running_apps
    };
    let target_app = unsafe {
        let count = msg_send![running_apps, count];
        if let Some(ns_object_class) = Class::get("NSObject") {
            let mut target_app = msg_send![ns_object_class, alloc];
            for i in 0..count {
                let app: *mut Object = msg_send![running_apps, objectAtIndex: i];
                let app_bundle_url: id = msg_send![app, bundleURL];
                let path: id = msg_send![app_bundle_url, path];
                let app_bundle_url_str = nsstring_to_string!(path);
                if let Some(app_bundle_url_str) = app_bundle_url_str {
                    if app_bundle_url_str == bundle_url.to_string() {
                        target_app = app;
                        break;
                    }
                }
            }
            target_app
        } else {
            return Err(Error::FailedToGetNSObjectClass);
        }
    };
    unsafe {
        let _: () = msg_send![target_app, activateWithOptions: NSApplicationActivateIgnoringOtherApps];
    };
    Ok(())
}

The key statement here is msg_send![target_app, activateWithOptions: NSApplicationActivateIgnoringOtherApps].

Additionally, I turned this solution into a plugin and it works well in my own project.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

I think we overcomplicated things, this is just a workaround. I believe that changing "open" to "activate" on the original plan would already be good enough. For example, using the following function instead of open:

fn active_another_app(bundle_url: &str) -> Result<(), Error> {
    let workspace = unsafe {
        if let Some(workspace_class) = Class::get("NSWorkspace") {
            let shared_workspace: *mut Object = msg_send![workspace_class, sharedWorkspace];
            shared_workspace
        } else {
            return Err(Error::FailedToGetNSWorkspaceClass);
        }
    };
    let running_apps = unsafe {
        let running_apps: *mut Object = msg_send![workspace, runningApplications];
        running_apps
    };
    let target_app = unsafe {
        let count = msg_send![running_apps, count];
        if let Some(ns_object_class) = Class::get("NSObject") {
            let mut target_app = msg_send![ns_object_class, alloc];
            for i in 0..count {
                let app: *mut Object = msg_send![running_apps, objectAtIndex: i];
                let app_bundle_url: id = msg_send![app, bundleURL];
                let path: id = msg_send![app_bundle_url, path];
                let app_bundle_url_str = nsstring_to_string!(path);
                if let Some(app_bundle_url_str) = app_bundle_url_str {
                    if app_bundle_url_str == bundle_url.to_string() {
                        target_app = app;
                        break;
                    }
                }
            }
            target_app
        } else {
            return Err(Error::FailedToGetNSObjectClass);
        }
    };
    unsafe {
        let _: () = msg_send![target_app, activateWithOptions: NSApplicationActivateIgnoringOtherApps];
    };
    Ok(())
}

The key statement here is msg_send![target_app, activateWithOptions: NSApplicationActivateIgnoringOtherApps].

Additionally, I turned this solution into a plugin and it works well in my own project.

This method may not work as expected in specific scenarios, such as when you have multiple windows from the same app open and visible on the same screen or across multiple screens. The method may focus on the wrong window or activate the wrong screen in these cases.

To address this issue, the method leverages the accessibility API to activate the app based on its process ID and bring a specific window to the front based on its window ID. Using these unique identifiers, the method can ensure that the correct window is brought into focus regardless of the number of windows or screens involved.

@zzzze
Copy link
Author

zzzze commented Feb 19, 2023

I think we overcomplicated things, this is just a workaround. I believe that changing "open" to "activate" on the original plan would already be good enough. For example, using the following function instead of open:

fn active_another_app(bundle_url: &str) -> Result<(), Error> {
    let workspace = unsafe {
        if let Some(workspace_class) = Class::get("NSWorkspace") {
            let shared_workspace: *mut Object = msg_send![workspace_class, sharedWorkspace];
            shared_workspace
        } else {
            return Err(Error::FailedToGetNSWorkspaceClass);
        }
    };
    let running_apps = unsafe {
        let running_apps: *mut Object = msg_send![workspace, runningApplications];
        running_apps
    };
    let target_app = unsafe {
        let count = msg_send![running_apps, count];
        if let Some(ns_object_class) = Class::get("NSObject") {
            let mut target_app = msg_send![ns_object_class, alloc];
            for i in 0..count {
                let app: *mut Object = msg_send![running_apps, objectAtIndex: i];
                let app_bundle_url: id = msg_send![app, bundleURL];
                let path: id = msg_send![app_bundle_url, path];
                let app_bundle_url_str = nsstring_to_string!(path);
                if let Some(app_bundle_url_str) = app_bundle_url_str {
                    if app_bundle_url_str == bundle_url.to_string() {
                        target_app = app;
                        break;
                    }
                }
            }
            target_app
        } else {
            return Err(Error::FailedToGetNSObjectClass);
        }
    };
    unsafe {
        let _: () = msg_send![target_app, activateWithOptions: NSApplicationActivateIgnoringOtherApps];
    };
    Ok(())
}

The key statement here is msg_send![target_app, activateWithOptions: NSApplicationActivateIgnoringOtherApps].
Additionally, I turned this solution into a plugin and it works well in my own project.

This method may not work as expected in specific scenarios, such as when you have multiple windows from the same app open and visible on the same screen or across multiple screens. The method may focus on the wrong window or activate the wrong screen in these cases.

To address this issue, the method leverages the accessibility API to activate the app based on its process ID and bring a specific window to the front based on its window ID. Using these unique identifiers, the method can ensure that the correct window is brought into focus regardless of the number of windows or screens involved.

My latest commit, located at zzzze/tauri-plugin-spotlight@3028d59, includes a trick to determine if the spotlight window is currently open after another window in the app is activated. If this condition is met and the spotlight window is subsequently hidden, window.set_focus is used to directly activate the previous window. However, if the previous window is located on a different screen, this action will result in focus being directed to that screen. Overall, I don't believe this should cause any issues.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

Hey @zzzze, and @elanzini could you help me by testing the latest refactor I made on the debug branch? I'd appreciate a second pair of eyes to make sure it works on your end.

I have added debug logs so it will be easy to see what's happening.

Regarding @elanzini's issue, I believe it may be an edge case with a full-screen window. I suspect it's an issue with detecting a full-screen window in the previous implementation.

See this commit on the debug branch 20f5837.

Here is a screenshot of my debug logs:
image

@zzzze
Copy link
Author

zzzze commented Feb 19, 2023

Hey @zzzze, and @elanzini could you help me by testing the latest refactor I made on the debug branch? I'd appreciate a second pair of eyes to make sure it works on your end.

I have added debug logs so it will be easy to see what's happening.

Regarding @elanzini's issue, I believe it may be an edge case with a full-screen window. I suspect it's an issue with detecting a full-screen window in the previous implementation.

Okay, I'll do some tests and get back to you later.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

So @zzzze, you're right about one thing! I don't need the accessibility API anymore.

Since I have correctly identified the owner_id of the window, the final piece of the implementation:

let app: id = unsafe {
            msg_send![
                class!(NSRunningApplication),
                runningApplicationWithProcessIdentifier: owner_id
            ]
        };
        unsafe {
            let _: () = msg_send![
                app,
                activateWithOptions:
                    NSApplicationActivationOptions::NSApplicationActivateIgnoringOtherApps
            ];

        };

See commit 2cd3d09.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

@elanzini pull the updates from debug and test. I'm 101% sure this time that this issue is fixed! I can bet my money on it 🤣

@zzzze, @elanzini, let me know if it works on your end so I can merge it to the main branch.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

@zzzze, excellent plugin! I'm excited to contribute. I read the source code; I could not understand the hack you did (#4 (comment)); I guess I need more attention so that I can understand. I could not build the example app as well.

@zzzze
Copy link
Author

zzzze commented Feb 19, 2023

@zzzze, excellent plugin! I'm excited to contribute. I read the source code; I could not understand the hack you did (#4 (comment)); I guess I need more attention so that I can understand. I could not build the example app as well.

The function (#4 (comment)) simply searches for the previously activated application from among all currently running apps using the app_url stored in the plugin's state. Once it locates the application, it activates it.

I have resolved the issue, and you should now be able to run the example.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

@zzzze, excellent plugin! I'm excited to contribute. I read the source code; I could not understand the hack you did (#4 (comment)); I guess I need more attention so that I can understand. I could not build the example app as well.

The function (#4 (comment)) simply searches for the previously activated application from among all currently running apps using the app_url stored in the plugin's state. Once it locates the application, it activates it.

I have resolved the issue, and you should now be able to run the example.

I have tested the example app, and it works!

I understand this. But how do you solve for when the frontmost window changes while you're using the spotlight window?

Edit: It's unimportant because if the frontmost window changes, the spotlight window will auto-hide.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

@zzzze Let me know when you're done with the plugin; there are some improvements I'll love to make.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

@zzzze, @elanzini, I pushed a new update to the main branch.

@zzzze have a look at the implementation; Usingprocess id and NSRunningApplicationrunningApplicationWithProcessIdentifier: owner_id suffice. See commit 42e38cb.

Let me know if this works on your end so we can close this issue.

@ahkohd
Copy link
Owner

ahkohd commented Feb 19, 2023

I've decided to maintain the window-level implementation in the experiment branch, as it has proven to be effective. It may also be useful for other applications.

@zzzze
Copy link
Author

zzzze commented Feb 20, 2023

@elanzini pull the updates from debug and test. I'm 101% sure this time that this issue is fixed! I can bet my money on it 🤣

@zzzze, @elanzini, let me know if it works on your end so I can merge it to the main branch.

I have tested it and it runs very well on my end.

@zzzze
Copy link
Author

zzzze commented Feb 20, 2023

@zzzze Let me know when you're done with the plugin; there are some improvements I'll love to make.

Now it's just version 0.1.0, and the API may still need some adjustments. Your code contributions are welcome.

@elanzini
Copy link

@elanzini pull the updates from debug and test. I'm 101% sure this time that this issue is fixed! I can bet my money on it 🤣

@zzzze, @elanzini, let me know if it works on your end so I can merge it to the main branch.

Tested from latest master at 430dcb0 and it works! Thanks so much for all the work

@ahkohd
Copy link
Owner

ahkohd commented Feb 20, 2023

I'll close the issue then; thanks, everyone.

@ahkohd ahkohd closed this as completed Feb 20, 2023
@ahkohd
Copy link
Owner

ahkohd commented Mar 5, 2023

Hey @zzzze and @elanzini, I have some exciting news! After a lot of hard work and experimentation, I have finally managed to create a fully functional NSPanel from Tauri's NSWindow. This is a major breakthrough that will help us address the issue we've been facing, no more hacks! Please take a look at #6 for more details. It would be great to get your feedback on this new development.

@elanzini
Copy link

elanzini commented Mar 5, 2023

Checked out locally and tested. Works like a charm 😍

@ahkohd
Copy link
Owner

ahkohd commented Mar 5, 2023

Checked out locally and tested. Works like a charm 😍

Great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants