0

I use Windows 10 virtual desktops to separate my activities. I have Chrome open on desktop 1 and MS Teams open on desktop 2.

------          -----
Chrome          Teams
------          -----
Desktop 1       Desktop 2

When I click a link in teams, I'd expect it to open in a new tab in the existing Chrome window on desktop 1, but instead it opens in a new Chrome window in desktop 2.

I thought this dialogue might help, but changing these settings make no difference: multi-tasking settings

Is there anything I can do about this?

5
  • 1
    This isn't a problem with --new-window generally because if Chrome is in the right virtual desktop then it does indeed open in a new tab in the existing window.
    – LondonRob
    Commented Apr 4, 2023 at 14:52
  • Fair enough! :) I will delete my comment. Commented Apr 4, 2023 at 15:11
  • 1
    @BlindSpots, yes this is a duplicate. The solution of using a non-Chromium browser (Firefox is the only serious example of this) works just fine.
    – LondonRob
    Commented Apr 5, 2023 at 10:42
  • 1
    I saw there was no solution for Chrome so I provided the solution below. Reviewed a number of solutions including 3rd party apps and leveraging 'IApplicationViewCollection' but they had various issues around usability and/or breaking changes. VirtualDesktop seemed the most promising, and includes patches for not only Windows 10 & 11, but also different builds. Might be a bit daunting if no AHK experience but pretty painless and doesn't need administrative access.
    – Blindspots
    Commented Apr 5, 2023 at 13:53

1 Answer 1

0

This solution will detect a new instance of Chrome browser (new window) and if it is on your Teams' virtual desktop will move it to your other virtual desktop. The virtual desktop that is in focus does not change during this move although the solution could be tweaked to do so.

Tools Used

  1. VirtualDesktop
    • MScholtes' Github repository VirtualDesktop contains a free open-source command line tool to manage virtual desktops in Windows 10 and Windows 11
    • The tool includes many features, including the ability to identify which virtual desktop is visible and to move specific Windows from one virtual desktop to another.
    • Installation:
      1. Download the tool (ZIP) from Github
      2. Unzip to a local folder
      3. In that folder run Compile.bat which will create several executables each for different versions of Windows.
  2. AutoHotKey
    • AutoHotkey (AHK) is a free, open-source well-documented scripting language for Windows that allows users to easily create small to complex scripts for all kinds of tasks.
    • For your use case, their WinHook command creates Windows Shell Hooks to monitor Windows events while using very few resources. This can be leveraged to detect whenever a new Chrome window is created.
    • Installation: navigate to AutoHotkey and follow the instructions on the site.
    • Also see Safe Browsing Note and Norton Safe Web Report vis-a-vis false positives about harmful programs.

Solution

NB: In this solution, the virtual desktop with the MS Teams application is arbitrarily named Teams and the other virtual desktop is named Main

  1. AutoHotKey script detect_chrome_window.ahk watches for new windows launched by Chrome
  2. When a new instance of Chrome is detected, the script retrieves its handle/unique ID (HWND)
  3. The script then runs a Windows batch file move_chrome_window.bat while passing the handle as a parameter
  4. Batch script uses VirtualDesktop to check if the current virtual desktop is Teams
  5. If it is, the script moves the new Chrome instance from Teams to Main

Detect_chrome_window.ahk

  • This script was adapted from a script by FanaticGuru. They included ~200 lines of comments which are an incredible resource.
  • You need only replace C:\path\to\batch\move_chrome_window.bat on 5th line with your own path and filename (if different)
WinHook.Shell.Add("NameOfFuncToRun",,, "chrome.exe",1) ; Chrome Window Created (1 at end means created)

NameOfFuncToRun(Win_Hwnd, Win_Title, Win_Class, Win_Exe, Win_Event)
{
  run C:\path\to\batch\move_chrome_window.bat "%Win_Hwnd%",,Hide   
}

;
;{============================
;
;   Class (Nested):     WinHook.Shell
;
;       Method:
;           Add(Func, wTitle:="", wClass:="", wExe:="", Event:=0)
;
;       Desc: Add Shell Hook
;
;       Parameters:
;       1) {Func}       Function name or Function object to call on event
;       2) {wTitle} window Title to watch for event (default = "", all windows)
;       3) {wClass} window Class to watch for event (default = "", all windows)
;       4) {wExe}       window Exe to watch for event (default = "", all windows)
;       5) {Event}      Event (default = 0, all events)
;
;       Returns: {Index}    index to hook that can be used to Remove hook
;
;               Shell Hook Events:
;               1 = HSHELL_WINDOWCREATED
;               2 = HSHELL_WINDOWDESTROYED
;               3 = HSHELL_ACTIVATESHELLWINDOW
;               4 = HSHELL_WINDOWACTIVATED
;               5 = HSHELL_GETMINRECT
;               6 = HSHELL_REDRAW
;               7 = HSHELL_TASKMAN
;               8 = HSHELL_LANGUAGE
;               9 = HSHELL_SYSMENU
;               10 = HSHELL_ENDTASK
;               11 = HSHELL_ACCESSIBILITYSTATE
;               12 = HSHELL_APPCOMMAND
;               13 = HSHELL_WINDOWREPLACED
;               14 = HSHELL_WINDOWREPLACING
;               32768 = 0x8000 = HSHELL_HIGHBIT
;               32772 = 0x8000 + 4 = 0x8004 = HSHELL_RUDEAPPACTIVATED (HSHELL_HIGHBIT + HSHELL_WINDOWACTIVATED)
;               32774 = 0x8000 + 6 = 0x8006 = HSHELL_FLASH (HSHELL_HIGHBIT + HSHELL_REDRAW)
;
;       Note: ObjBindMethod(obj, Method) can be used to create a function object to a class method
;                   WinHook.Shell.Add(ObjBindMethod(TestClass.TestNestedClass, "MethodName"), wTitle, wClass, wExe, Event)
;
; ----------
;
;       Desc: Function Called on Event
;           FuncOrMethod(Win_Hwnd, Win_Title, Win_Class, Win_Exe, Win_Event)
;
;       Parameters:
;       1) {Win_Hwnd}       window handle ID of window with event
;       2) {Win_Title}      window Title of window with event
;       3) {Win_Class}      window Class of window with event
;       4) {Win_Exe}            window Exe of window with event
;       5) {Win_Event}      window Event
;
;       Note: FuncOrMethod will be called with DetectHiddenWindows On.
;
; --------------------
;
;       Method:     Report(ByRef Object)
;
;       Desc:       Report Shell Hooks
;
;       Returns:    string report
;                       ByRef   Object[Index].{Func, Title:, Class, Exe, Event}
;
; --------------------
;
;       Method:     Remove(Index)
;       Method:     Deregister()
;
;{============================
;
;   Class (Nested):     WinHook.Event
;
;       Method:
;           Add(eventMin, eventMax, eventProc, idProcess, WinTitle := "")
;
;       Desc: Add Event Hook
;
;       Parameters:
;       1) {eventMin}       lowest Event value handled by the hook function
;       2) {eventMax}       highest event value handled by the hook function
;       3) {eventProc}      event hook function, call be function name or function object
;       4) {idProcess}      ID of the process from which the hook function receives events (default = 0, all processes)
;       5) {WinTitle}           WinTitle to identify which windows to operate on, (default = "", all windows)
;
;       Returns: {hWinEventHook}    handle to hook that can be used to unhook
;
;               Event Hook Events:
;               0x8012 = EVENT_OBJECT_ACCELERATORCHANGE
;               0x8017 = EVENT_OBJECT_CLOAKED
;               0x8015 = EVENT_OBJECT_CONTENTSCROLLED
;               0x8000 = EVENT_OBJECT_CREATE
;               0x8011 = EVENT_OBJECT_DEFACTIONCHANGE
;               0x800D = EVENT_OBJECT_DESCRIPTIONCHANGE
;               0x8001 = EVENT_OBJECT_DESTROY
;               0x8021 = EVENT_OBJECT_DRAGSTART
;               0x8022 = EVENT_OBJECT_DRAGCANCEL
;               0x8023 = EVENT_OBJECT_DRAGCOMPLETE
;               0x8024 = EVENT_OBJECT_DRAGENTER
;               0x8025 = EVENT_OBJECT_DRAGLEAVE
;               0x8026 = EVENT_OBJECT_DRAGDROPPED
;               0x80FF = EVENT_OBJECT_END
;               0x8005 = EVENT_OBJECT_FOCUS
;               0x8010  = EVENT_OBJECT_HELPCHANGE
;               0x8003 = EVENT_OBJECT_HIDE
;               0x8020 = EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED
;               0x8028 = EVENT_OBJECT_IME_HIDE
;               0x8027 = EVENT_OBJECT_IME_SHOW
;               0x8029 = EVENT_OBJECT_IME_CHANGE
;               0x8013 = EVENT_OBJECT_INVOKED
;               0x8019 = EVENT_OBJECT_LIVEREGIONCHANGED
;               0x800B = EVENT_OBJECT_LOCATIONCHANGE
;               0x800C = EVENT_OBJECT_NAMECHANGE
;               0x800F = EVENT_OBJECT_PARENTCHANGE
;               0x8004 = EVENT_OBJECT_REORDER
;               0x8006 = EVENT_OBJECT_SELECTION
;               0x8007 = EVENT_OBJECT_SELECTIONADD
;               0x8008 = EVENT_OBJECT_SELECTIONREMOVE
;               0x8009 = EVENT_OBJECT_SELECTIONWITHIN
;               0x8002 = EVENT_OBJECT_SHOW
;               0x800A = EVENT_OBJECT_STATECHANGE
;               0x8030 = EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED
;               0x8014 = EVENT_OBJECT_TEXTSELECTIONCHANGED
;               0x8018 = EVENT_OBJECT_UNCLOAKED
;               0x800E = EVENT_OBJECT_VALUECHANGE
;               0x0002 = EVENT_SYSTEM_ALERT
;               0x8016 = EVENT_SYSTEM_ARRANGMENTPREVIEW
;               0x0009 = EVENT_SYSTEM_CAPTUREEND
;               0x0008 = EVENT_SYSTEM_CAPTURESTART
;               0x000D = EVENT_SYSTEM_CONTEXTHELPEND
;               0x000C = EVENT_SYSTEM_CONTEXTHELPSTART
;               0x0020 = EVENT_SYSTEM_DESKTOPSWITCH
;               0x0011 = EVENT_SYSTEM_DIALOGEND
;               0x0010 = EVENT_SYSTEM_DIALOGSTART
;               0x000F = EVENT_SYSTEM_DRAGDROPEND
;               0x000E = EVENT_SYSTEM_DRAGDROPSTART
;               0x00FF = EVENT_SYSTEM_END
;               0x0003 = EVENT_SYSTEM_FOREGROUND
;               0x0007 = EVENT_SYSTEM_MENUPOPUPEND
;               0x0006 = EVENT_SYSTEM_MENUPOPUPSTART
;               0x0005 = EVENT_SYSTEM_MENUEND
;               0x0004 = EVENT_SYSTEM_MENUSTART
;               0x0017 = EVENT_SYSTEM_MINIMIZEEND
;               0x0016 = EVENT_SYSTEM_MINIMIZESTART
;               0x000B = EVENT_SYSTEM_MOVESIZEEND
;               0x000A = EVENT_SYSTEM_MOVESIZESTART
;               0x0013 = EVENT_SYSTEM_SCROLLINGEND
;               0x0012 = EVENT_SYSTEM_SCROLLINGSTART
;               0x0001 = EVENT_SYSTEM_SOUND
;               0x0015 = EVENT_SYSTEM_SWITCHEND
;               0x0014 = EVENT_SYSTEM_SWITCHSTART

;
;       Note: ObjBindMethod(obj, Method) can be used to create a function object to a class method
;                   WinHook.Event.Add((eventMin, eventMax, ObjBindMethod(TestClass.TestNestedClass, "MethodName"), idProcess, WinTitle := "")
;
; ----------
;
;       Desc: Function Called on Event
;           FuncOrMethod(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime)
;
;       Parameters:
;       1) {hWinEventHook}      Handle to an event hook instance.
;       2) {event}                      Event that occurred. This value is one of the event constants
;       3) {hwnd}                       Handle to the window that generates the event.
;       4) {idObject}                   Identifies the object that is associated with the event.
;       5) {idChild}                    Child ID if the event was triggered by a child element.
;       6) {dwEventThread}      Identifies the thread that generated the event.
;       7) {dwmsEventTime}  Specifies the time, in milliseconds, that the event was generated.
;
;       Note: FuncOrMethod will be called with DetectHiddenWindows On.
;
; --------------------
;
;       Method: Report(ByRef Object)
;
;       Returns:    string report
;                       ByRef   Object[hWinEventHook].{eventMin, eventMax, eventProc, idProcess, WinTitle}
;
; --------------------
;
;       Method:     UnHook(hWinEventHook)
;       Method:     UnHookAll()
;
;{============================
class WinHook
{
    class Shell
    {
        Add(Func, wTitle:="", wClass:="", wExe:="", Event:=0)
        {
            if !WinHook.Shell.Hooks
            {
                WinHook.Shell.Hooks := {}, WinHook.Shell.Events := {}
                DllCall("RegisterShellHookWindow", UInt, A_ScriptHwnd)
                MsgNum := DllCall("RegisterWindowMessage", Str, "SHELLHOOK")
                OnMessage(MsgNum, ObjBindMethod(WinHook.Shell, "Message"))
            }
            if !IsObject(Func)
                Func := Func(Func)
            WinHook.Shell.Hooks.Push({Func: Func, Title: wTitle, Class: wClass, Exe: wExe, Event: Event})
            WinHook.Shell.Events[Event] := true
            return WinHook.Shell.Hooks.MaxIndex()
        }
        Remove(Index)
        {
            WinHook.Shell.Hooks.Delete(Index)
            WinHook.Shell.Events[Event] := {}   ; delete and rebuild Event list
            For key, Hook in WinHook.Shell.Hooks
                WinHook.Shell.Events[Hook.Event] := true
        }
        Report(ByRef Obj:="")
        {
            Obj := WinHook.Shell.Hooks
            For key, Hook in WinHook.Shell.Hooks
                Display .= key "|" Hook.Event "|" Hook.Func.Name "|" Hook.Title "|" Hook.Class "|" Hook.Exe "`n"
            return Trim(Display, "`n")
        }
        Deregister()
        {
            DllCall("DeregisterShellHookWindow", UInt, A_ScriptHwnd)
            WinHook.Shell.Hooks := "", WinHook.Shell.Events := ""
        }
        Message(Event, Hwnd)  ; Private Method
        {
            DetectHiddenWindows, On
            If (WinHook.Shell.Events[Event] or WinHook.Shell.Events[0])
            {

                WinGetTitle, wTitle, ahk_id %Hwnd%
                WinGetClass, wClass, ahk_id %Hwnd%
                WinGet, wExe, ProcessName, ahk_id %Hwnd%
                for key, Hook in WinHook.Shell.Hooks
                    if ((Hook.Title = wTitle or Hook.Title = "") and (Hook.Class = wClass or Hook.Class = "") and (Hook.Exe = wExe or Hook.Exe = "") and (Hook.Event = Event or Hook.Event = 0))
                        return Hook.Func.Call(Hwnd, wTitle, wClass, wExe, Event)
            }
        }
    }
    class Event
    {
        Add(eventMin, eventMax, eventProc, idProcess := 0, WinTitle := "")
        {
            if !WinHook.Event.Hooks
            {
                WinHook.Event.Hooks := {}
                static CB_WinEventProc := RegisterCallback(WinHook.Event.Message)
                OnExit(ObjBindMethod(WinHook.Event, "UnHookAll"))
            }
            hWinEventHook := DllCall("SetWinEventHook"
                , "UInt",   eventMin                        ;  UINT eventMin
                , "UInt",   eventMax                        ;  UINT eventMax
                , "Ptr" ,   0x0                             ;  HMODULE hmodWinEventProc
                , "Ptr" ,   CB_WinEventProc                 ;  WINEVENTPROC lpfnWinEventProc
                , "UInt" ,  idProcess                       ;  DWORD idProcess
                , "UInt",   0x0                             ;  DWORD idThread
                , "UInt",   0x0|0x2)                        ;  UINT dwflags, OutOfContext|SkipOwnProcess
            if !IsObject(eventProc)
                eventProc := Func(eventProc)
            WinHook.Event.Hooks[hWinEventHook] := {eventMin: eventMin, eventMax: eventMax, eventProc: eventProc, idProcess: idProcess, WinTitle: WinTitle}
            return hWinEventHook
        }
        Report(ByRef Obj:="")
        {
            Obj := WinHook.Event.Hooks
            For hWinEventHook, Hook in WinHook.Event.Hooks
                Display .= hWinEventHook "|" Hook.eventMin "|" Hook.eventMax "|" Hook.eventProc.Name "|" Hook.idProcess "|" Hook.WinTitle "`n"
            return Trim(Display, "`n")
        }
        UnHook(hWinEventHook)
        {
                DllCall("UnhookWinEvent", "Ptr", hWinEventHook)
                WinHook.Event.Hooks.Delete(hWinEventHook)
        }
        UnHookAll()
        {
            for hWinEventHook, Hook in WinHook.Event.Hooks
                DllCall("UnhookWinEvent", "Ptr", hWinEventHook)
            WinHook.Event.Hooks := "", CB_WinEventProc := ""
        }
        Message(event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime)  ; 'Private Method
        {
            DetectHiddenWindows, On
            Hook := WinHook.Event.Hooks[hWinEventHook := this] ; this' is hidden param1 because method is called as func
            WinGet, List, List, % Hook.WinTitle
            Loop % List
                if  (List%A_Index% = hwnd)
                    return Hook.eventProc.Call(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime)
        }
    }
}

move_chrome_window.bat

  • VirtualDesktop will compile multiple versions of the executable because of breaking changes between Windows 10, Windows 11 as well as builds of each. I am using Windows 11, so my executable for VirtualDesktop is VirtualDesktop11.exe yours will likely be VirtualDesktop.exe. If it is something different the batch file should be updated to reflect that.
  • I added the path to VirtualDesktop.exe to my PATH environment variable so I don't need to specify the path in the batch file. If you don't do that you need to add the path in the batch file
  • If you use other names than Main and Teams for the virtual desktops, you will need to modify the batch file to reflect that.
@echo off
setlocal enabledelayedexpansion

rem Check if 'Teams' is visible
FOR /F "tokens=*" %%F IN ('VirtualDesktop /IsVisible:Teams') DO (
  
  set myvar=%%F
  
  if not "!myvar:is visible=!"=="!myvar!" (
  
    rem move Chrome window from 'Teams' to 'Main'  
    VirtualDesktop "/GetDesktop:main" "/MoveWindowHandle:%1"
  
  )

)

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .