MScholtes/VirtualDesktop

Focus when switching to desktop

Closed this issue · 17 comments

To reproduce:

  1. Create two virtual desktops, open notepad on each.
  2. On the first desktop, click the notepad window to bring it to the foreground
  3. Use the windows short cut "ctrl+winkey+right arrow key" to go to second desktop
  4. Use Get-Desktop 0 | Switch-Desktop to go back to first virtual desktop

Focus isn't restored to the notepad window. Let me know if you are able to reduce this, it is somewhat intermittent.

Here's my Windows version:

image

Hello @widavies,

you are not the first to ask this question. VirtualDesktop is only changing the virtual desktop and does no window activation or change of keyboard focus.
And I think there is no satisfying programmable solution for this, but I'm open for ideas.

Greetings

Markus

Got it, I did a search of issues and didn't seem to find it. My bad.

I've done a fair amount of research on this myself and I have a few ideas:

  • The Windows virtual desktop appears to be implemented as one actual desktop. All that changes when "switching desktops" is the set of applications that appear. There is only one "foreground" application and it looks like this must be updated whenever a desktop switch occurs.
  • I noticed some really strange behavior when working on my hotkey based virtual desktop switcher. I use your API call VirtualDesktop.FromIndex(..).MakeVisible() to switch to a desktop upon detecting a particular keyboard shortcut. Strangely enough, when I run in debug mode, the foreground application is properly restored. However, when I run it in release mode the foreground application does not get restored. My explanation for this at first was that there must be some race condition that is only slowed down by the debugger running, but after some investigation I almost think that the debugger messes with the foreground state during the virtual desktop change and Windows will respond by correctly restoring the foreground application. Not entirely sure though.

It seems to be that we can write this off as a Windows bug and that for now we'll need to manually restore the foreground Window. I have two main ideas on how to accomplish this

Idea 1: Using IApplicationViewCollection

We could use the GetViewsByZOrder() function to get a listing of all applications by Z-order. My assumption is that IApplicationViewCollection is for all windows, not just the windows for a desktop, but I could be wrong. It also seems like GetViewInFocus(..) could be useful.

It then seems that we could loop through the views by z order, filter out any views not on the desktop we want to switch to be checking IVirtualDesktop.IsViewVisible and then using the SetForegroundWindow function to correct the foreground Window.

I did actually attempt this but GetViewsByZOrder would segfault for me (Windows 11 22H2)

Idea 2: Bookkeeping the foreground Window per desktop ourselves

The other alternative is a little sloppy, but we can always fetch the current foreground Window before switching desktops with GetForegroundWindow.

Basically, we'll just log the foreground Window prior to switching so that when we switch back, we'll restore it. It is a little sticky because if the user switches desktops using the Windows built-in UI, we'll have to hook into that and try to work out the foreground change.

Hello @widavies,

sorry, I should have explained the problems with setting the focus clearer: Setting a window active on a virtual desktop fails when there is a windows active (including the keyboard focus) on another virtual desktop!

I've already tried all possibilities I know to change the focus, They all did not work (some work sometimes, but I could not determine why). I tried (I'm mixing programming languages now):

::SendMessage(WindowHandle, WM_SYSCOMMAND, SC_HOTKEY, (LPARAM) WindowHandle);
::SendMessage(WindowHandle, WM_SYSCOMMAND, SC_RESTORE, (LPARAM) WindowHandle);
::ShowWindow(WindowHandle, SW_SHOW);
::SetForegroundWindow(WindowHandle);  //there seems to be only one ForegroundWindow over all virtual desktops
::SetFocus(WindowHandle);
ApplicationView.SetFocus();  // seems to do the same as the "classical" SetFocus()
ApplicationView.SwitchTo(); // seems to do nothing

I even tried to send

::SendMessage(wnd, WM_KILLFOCUS, 0, 0);

to the "old" window that still has the focus before switching.

Or activating the desktop before switching.
Nothing works. The main issue seems to be the keyboard focus.

The only way I got it working reliable was programmatically pressing the {WINKEY} twice before switching the virtual desktop. But this has so many side effects that this is not an option.

Greetings

Markus

Btw.: ApplicationView.GetNeediness(int); seems to determine whether a windows has a focus but is not activated.

Going to be a bit before I have time to look into this, but one hackish idea is we could simulate an alt+tab + release shortcut to trigger focus of the top window again.

Hey guys, did you try using AttachThreadInput API ?

I encountered a similar problem in the past when I wrote a program that switch to window with fuzzy search (in golang).

But the solution was to use something like this:

private static void ForceForegroundWindow(IntPtr hWnd)
{
    uint foreThread = GetWindowThreadProcessId(GetForegroundWindow(), 
        IntPtr.Zero);
    uint appThread = GetCurrentThreadId();
    const uint SW_SHOW = 5;

    if (foreThread != appThread)
    {
        AttachThreadInput(foreThread, appThread, true);
        BringWindowToTop(hWnd);
        ShowWindow(hWnd, SW_SHOW);
        AttachThreadInput(foreThread, appThread, false);
    }
    else
    {
        BringWindowToTop(hWnd);
        ShowWindow(hWnd, SW_SHOW);
    }
}

you can read more about it here:
https://shlomio.wordpress.com/2012/09/04/solved-setforegroundwindow-win32-api-not-always-works/
https://stackoverflow.com/questions/17370939/set-foreground-window-on-windows-8

**EDIT
I tried it myself - every time before I switch to other desktop I save the current HWND and the desktop index.
when I switch back I'm taking the last HWND for that desktop index and try to focus the window with a similar function to the above example.

It sometimes works and sometimes not 😕

Hello,

I think I have a working solution.

After reading a lot about this, especially in the issue at mzomparelli (who should no longer be supported "openly", as he makes closed software), @FuPeiJiang in combination with @rotemgrim's comment gave me a solution: before switching the desktop, the focus should be placed on a window that is available on each virtual desktop, so that the focus is simply retained when switching! In VD.ahk, the taskbar (window class Shell_TrayWnd) is used for this, but I think the desktop is more useful.
The idea to give away the focus with a message to minimize is a great idea (I don't know who had it initially).

With the following code blocks (in C++) it worked for me:

HWND wnd = FindWindow(NULL, "Program Manager");
DWORD DesktopThreadId = GetWindowThreadProcessId(wnd, NULL);
DWORD ForegroundThreadId = GetWindowThreadProcessId(GetForegroundWindow(), NULL);
DWORD CurrentThreadId = GetCurrentThreadId();

if ((DesktopThreadId != 0) && (ForegroundThreadId != 0) && (ForegroundThreadId != CurrentThreadId))
{
  AttachThreadInput(DesktopThreadId, CurrentThreadId, true);
  AttachThreadInput(ForegroundThreadId, CurrentThreadId, true);
  SetForegroundWindow(wnd);
  AttachThreadInput(ForegroundThreadId, CurrentThreadId, false);
  AttachThreadInput(DesktopThreadId, CurrentThreadId, false);
}

.
here is the code for switching the virtual desktop
.

HWND wnd = FindWindow(NULL, "Program Manager");
ShowWindow(wnd, SW_MINIMIZE);

I'm not entirely happy with it because it's probably not stable in every situation. I am still testing and experimenting...

Greetings

Markus

If I understand correctly
Your method is :
WinActivate desktopBackground
Switch VD
WinMinimize desktopBackground

my old method was :
WinActivate taskbar
Switch VD
WinMinimize taskbar

my current method is :
Switch VD
WinActivate firstWindow (I think I have a perfect replica of what windows appear in alt+tab list)

I have run into problems but I think I’ve patched them all now

My old method did not have an animation when switching virtual desktop

It’s better to have a reliable “WinActivate firstWindow” method because after you move the active window to a different desktop, you have focus on nothing, so you have to focus the next/new first window

my current method is :
Show a gui1 so that the active window is owned by my process
Switch VD (create gui2, moveVD gui2, winactivate gui2)
Loop until/Spin lock until current_VD_num==target_VD_num
WinActivate firstWindow
If no firstWindow {
WinActivate desktopBackground
}
Destroy gui1
Destroy gui2

It is insanely hacky but I tried the non-hacky way and am more comfortable with this way now

Hello @FuPeiJiang,

thank you very much for your comment.

It's interesting what you have to do to get a clean implementation. Since I only provide a text-oriented tool, the effort to create extra windows just to change the desktop cleanly is too high for me. I will therefore probably leave it at the simpler solution and accept individual error cases.

But I'm still researching.

Greetings

Markus

One idea I have is to act only if a window really has the problem, i.e. flashes.
The following code can be used to determine this for a window (here the foreground window), the code uses the declarations of my VirtualDesktop tool:

IntPtr hWnd = GetForegroundWindow();
var view = hWnd.GetApplicationView();
int neediness;
view.GetNeediness(out neediness);

In case the window is flashing neediness has the value 1, if not it has the value 0.

But I'm not really sure how to use it yet. Maybe not at all.

Hello,

I think I have a working solution.

After reading a lot about this, especially in the issue at mzomparelli (who should no longer be supported "openly", as he makes closed software), @FuPeiJiang in combination with @rotemgrim's comment gave me a solution: before switching the desktop, the focus should be placed on a window that is available on each virtual desktop, so that the focus is simply retained when switching! In VD.ahk, the taskbar (window class Shell_TrayWnd) is used for this, but I think the desktop is more useful. The idea to give away the focus with a message to minimize is a great idea (I don't know who had it initially).

With the following code blocks (in C++) it worked for me:

HWND wnd = FindWindow(NULL, "Program Manager");
DWORD DesktopThreadId = GetWindowThreadProcessId(wnd, NULL);
DWORD ForegroundThreadId = GetWindowThreadProcessId(GetForegroundWindow(), NULL);
DWORD CurrentThreadId = GetCurrentThreadId();

if ((DesktopThreadId != 0) && (ForegroundThreadId != 0) && (ForegroundThreadId != CurrentThreadId))
{
  AttachThreadInput(DesktopThreadId, CurrentThreadId, true);
  AttachThreadInput(ForegroundThreadId, CurrentThreadId, true);
  SetForegroundWindow(wnd);
  AttachThreadInput(ForegroundThreadId, CurrentThreadId, false);
  AttachThreadInput(DesktopThreadId, CurrentThreadId, false);
}

. here is the code for switching the virtual desktop .

HWND wnd = FindWindow(NULL, "Program Manager");
ShowWindow(wnd, SW_MINIMIZE);

I'm not entirely happy with it because it's probably not stable in every situation. I am still testing and experimenting...

Greetings

Markus

Excellent idea! I've been testing this as well and it actually seems to work well for me without the second part:

HWND wnd = FindWindow(NULL, "Program Manager");
ShowWindow(wnd, SW_MINIMIZE);

Does removing that work for you too?

Hello @widavies,

the code with the "minimise" message is only there to make the shell activate another window on the new desktop. If it's not a problem for you that the desktop has the focus, you don't need the code.

Greetings

Markus

Hello,

further research did not bring further success. Implemented my suggested solution to my companion project PSVirtualDesktop and will implement here soon.

Greetings

Markus

Hello,

released new version 1.13 today that included the change.

Greetings

Markus

@MScholtes Thank you.
Is the issue resolved completely? or is it a temporary fix?

Hello @rotemgrim,

this is the permanent solution. However, it may not work in all cases, as it requires a certain cooperation of the displayed windows. I hope, however, it will work in almost all cases.

Greetings

Markus

Issue seems to be solved