MScholtes / VirtualDesktop

C# command line tool to manage virtual desktops in Windows 10
MIT License
574 stars 56 forks source link

Focus when switching to desktop #57

Closed widavies closed 1 year ago

widavies commented 1 year ago

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.

widavies commented 1 year ago

Here's my Windows version:

image

MScholtes commented 1 year ago

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

widavies commented 1 year ago

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:

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.

MScholtes commented 1 year ago

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.

widavies commented 1 year ago

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.

rotemgrim commented 1 year ago

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 😕

MScholtes commented 1 year ago

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 _ShellTrayWnd) 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

FuPeiJiang commented 1 year ago

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

MScholtes commented 1 year ago

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

MScholtes commented 1 year ago

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.

widavies commented 1 year ago

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 _ShellTrayWnd) 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?

MScholtes commented 1 year ago

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

MScholtes commented 1 year ago

Hello,

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

Greetings

Markus

MScholtes commented 1 year ago

Hello,

released new version 1.13 today that included the change.

Greetings

Markus

rotemgrim commented 1 year ago

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

MScholtes commented 1 year ago

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

MScholtes commented 1 year ago

Issue seems to be solved