Page 4 of 4

Re: 4 options to change the current folder in Windows Explorer

Posted: 26 Jul 2017, 15:51
by JnLlnd
Thanks for the references, jeeswg. I used Navigate2 with an integer parameter (for example 17 = My Computer) but I did not know about object parameters. Interesting.

Re: 4 options to change the current folder in Windows Explor

Posted: 03 Apr 2018, 13:59
by Eureka
Just registered to say THANK YOU!! for all this fundamental research you all have done.
Very valuable, as I''m writing my first AHK (to navigate to folders using Everything, a very reliable and fast file/folder search utility). This thread helps enormously with that :)

BTW:
JnLlnd wrote:Unfortunately, there does not seem to be a solution to this:

Code: Select all

For pExp in ComObjCreate("Shell.Application").Windows
    if (pExp.hwnd = strWinId)
        try pExp.Navigate(myPath)
This will fail if myPath includes a hash (# as in C:\C#Projects).
No (longer?) such problems with AHK 1.1.27.07 @ Win10 1709 (both x64)

Re: 4 options to change the current folder in Windows Explorer

Posted: 03 Apr 2018, 16:44
by JnLlnd
Hi Eureka,

Please read this post and the following:
https://autohotkey.com/boards/viewtopic ... 199#p25199

For me, it still does not work if there is a slash after the hash. My message and example should have been: "This will fail if myPath includes a hash and has a slash after it (as in C:\C#Projects\)."

Re: 4 options to change the current folder in Windows Explorer

Posted: 03 Apr 2018, 17:50
by Eureka
JnLlnd wrote:Hi Eureka,

Please read this post and the following:
https://autohotkey.com/boards/viewtopic ... 199#p25199

For me, it still does not work if there is a slash after the hash. My message and example should have been: "This will fail if myPath includes a hash and has a slash after it (as in C:\C#Projects\)."
Ah, I see. Same here ...
That's probably because this method uses the URL syntax. And a hash/pound/number sign has special meaning in that case.
I tried to replace it with it's URL escape character: %23 (*), but in that case the % was replaced by it's URL escape character (%25).
It should be possible to feed a literal % by escaping it in AHK, but I'm not (yet) experienced enough in AHK to get that working.


(*) Drag the foldername to the address bar of your browser to see the converted filename

Re: 4 options to change the current folder in Windows Explorer

Posted: 27 Apr 2020, 07:22
by omareg94
jeeswg wrote:
13 Jun 2017, 15:54
I have what looks to be a working solution for this, which I've also posted here:

windows - Navigate Shell command not working when the path includes an hash - Stack Overflow
https://stackoverflow.com/questions/22868546/navigate-shell-command-not-working-when-the-path-includes-an-hash

Code: Select all

;links:
;Explorer Windows Manipulations - Page 5 - Scripts and Functions - AutoHotkey Community
;https://autohotkey.com/board/topic/19039-explorer-windows-manipulations/page-5#entry297581
;Navigate2 Method (IWebBrowser2)
;https://msdn.microsoft.com/en-us/library/aa752134(v=vs.85).aspx
;4 options to change the current folder in Windows Explorer - AutoHotkey Community
;https://autohotkey.com/boards/viewtopic.php?f=5&t=526
;windows - Navigate Shell command not working when the path includes an hash - Stack Overflow
;https://stackoverflow.com/questions/22868546/navigate-shell-command-not-working-when-the-path-includes-an-hash

;an AutoHotkey v1.1 script
;note: will create folder: %A_Desktop%\abc#def\abc#def
q:: ;explorer - navigate to folder (tested on Windows 7)
WinGet, hWnd, ID, A
WinGetClass, vWinClass, % "ahk_id " hWnd
if !(vWinClass = "CabinetWClass") && !(vWinClass = "ExploreWClass")
	return

vDir = %A_Desktop%\abc#def\abc#def
;vDir = %A_Desktop%\abc def\abc def
if !FileExist(vDir)
	FileCreateDir, % vDir

DllCall("shell32\SHParseDisplayName", WStr,vDir, Ptr,0, PtrP,vPIDL, UInt,0, Ptr,0)
for oWin in ComObjCreate("Shell.Application").Windows
	if (oWin.HWND = hWnd)
	{
		if !InStr(vDir, "#")
			oWin.Navigate(vDir)
		else
		{
			VarSetCapacity(SAFEARRAY, A_PtrSize=8?32:24, 0)
			NumPut(1, SAFEARRAY, 0, "UShort") ;cDims
			NumPut(1, SAFEARRAY, 4, "UInt") ;cbElements
			NumPut(vPIDL, SAFEARRAY, A_PtrSize=8?16:12, "Ptr") ;pvData
			NumPut(DllCall("shell32\ILGetSize", Ptr,vPIDL, UInt), SAFEARRAY, A_PtrSize=8?24:16, "Int") ;rgsabound[1]
			oWin.Navigate2(ComObject(0x2011,&SAFEARRAY))
			DllCall("shell32\ILFree", Ptr,vPIDL)
		}
		break
	}
return
Thanks a lot @jeeswg for your efforts and solutions you get to :) This really helps a lot :)
It's working efficiently with paths containing "#" :) .. Tested on Windows 10 build 18363


I've just packed in a simple function and all is working effectively:

Code: Select all

Explorer_Navigate(vDir, hWnd="") {
    ; WinGet, hWnd, ID, A
    hWnd := (hWnd="") ? WinExist("A") : hWnd
    WinGetClass, vWinClass, % "ahk_id " hWnd
    if !(vWinClass = "CabinetWClass") && !(vWinClass = "ExploreWClass")
        return

    ; vDir = %A_Desktop%\abc#def\abc#def
    ;vDir = %A_Desktop%\abc def\abc def
    ; if !FileExist(vDir)
    ;     FileCreateDir, % vDir

    DllCall("shell32\SHParseDisplayName", WStr,vDir, Ptr,0, PtrP,vPIDL, UInt,0, Ptr,0)
    for oWin in ComObjCreate("Shell.Application").Windows
        if (oWin.HWND = hWnd)
        {
            if !InStr(vDir, "#")
                oWin.Navigate(vDir)
            else
            {
                VarSetCapacity(SAFEARRAY, A_PtrSize=8?32:24, 0)
                NumPut(1, SAFEARRAY, 0, "UShort")
                NumPut(1, SAFEARRAY, 4, "UShort")
                NumPut(vPIDL, SAFEARRAY, A_PtrSize=8?16:12, "Ptr")
                NumPut(DllCall("shell32\ILGetSize", Ptr,vPIDL, UInt), SAFEARRAY, A_PtrSize=8?24:16, "Int")
                oWin.Navigate2(ComObject(0x2011,&SAFEARRAY))
                DllCall("shell32\ILFree", Ptr,vPIDL)
            }
            break
        }
    return
  }

Re: 4 options to change the current folder in Windows Explorer

Posted: 27 Oct 2022, 14:45
by JnLlnd
Windows 11 version 22H2, codenamed "Moment 1", released on October 18, 2022 (build 22621.675) has this new feature: "New tabbed browsing feature and refreshed layout of the left navigation pane in the File Explorer".

These new tabs will certainly be much appreciated. But they cause an issue with the code of the "Method 4" in the original post of this thread: it always change the folder in the first tab even when another tab is active.

Code: Select all

Explorer_Navigate(FullPath, hwnd="") {  ; by Learning one
    hwnd := (hwnd="") ? WinExist("A") : hwnd ; if omitted, use active window
    WinGet, ProcessName, ProcessName, % "ahk_id " hwnd
    if (ProcessName != "explorer.exe")  ; not Windows explorer
        return
    For pExp in ComObjCreate("Shell.Application").Windows
    {
        if (pExp.hwnd = hwnd) { ; matching window found
            pExp.Navigate("file:///" FullPath)
            return
        }
    }
}
The other methods sending keystrokes with Send (method 2) or using ControlSetText (method 3) are OK. Tey send their command to the active tab. But I think method 4 is largely used and lots of scripts will have to be adjusted. I'll follow up in the next post with some diag info from Win 11 Explorer with tabs.

Re: 4 options to change the current folder in Windows Explorer

Posted: 27 Oct 2022, 14:56
by JnLlnd
I used the following code to retrieve info from the instances of Windows File Explorer.

Code: Select all

#requires AutoHotkey v1.1
#SingleInstance,Force
#NoEnv

saExplorers := Object()
For pExplorer in ComObjCreate("Shell.Application").Windows
{
	strType := ""
	try strType := pExplorer.Type ; Gets the type name of the contained document object. "Document HTML" for IE windows. Should be empty for file Explorer windows.
	strWindowID := ""
	try strWindowID := pExplorer.HWND ; Try to get the handle of the window. Some ghost Explorer in the ComObjCreate may return an empty handle

	if !StrLen(strType) ; must be empty
		and StrLen(strWindowID) ; must not be empty
	{
		aaExplorer := Object()
		aaExplorer.strPosition := pExplorer.Left . "|" . pExplorer.Top . "|" . pExplorer.Width . "|" . pExplorer.Height

		aaExplorer.blnIsSpecialFolder := !StrLen(pExplorer.LocationURL) ; empty for special folders like Recycle bin

		if (aaExplorer.blnIsSpecialFolder)
		{
			aaExplorer.strLocationURL := pExplorer.Document.Folder.Self.Path
			aaExplorer.strLocationName := pExplorer.LocationName ; see http://msdn.microsoft.com/en-us/library/aa752084#properties
		}
		else
		{
			aaExplorer.strLocationURL := pExplorer.LocationURL
			aaExplorer.strLocationName :=  pExplorer.LocationURL
		}

		aaExplorer.strWindowId := strWindowID ; not used for Explorer windows, but keep it
		WinGet, intMinMax, MinMax, % "ahk_id " . pExplorer.HWND
		aaExplorer.intMinMax := intMinMax

		saExplorers.Push(aaExplorer) ; I was checking if StrLen(pExplorer.HWND) - any reason?
	}
}

for intIndex, oExplorer in saExplorers
	str .= "Explorer #" . A_Index . BuildStringObj(oExplorer) . "`n`n"
MsgBox, %str%

ExitApp

BuildStringObj(obj)
{
	str := "`n`n"
	for strKey, strValue in obj
		str .= strKey . " = " . strValue . "`n"

	return str
}
Here is the result with one instance of Explorer containing three tabs:

Explorer #1

blnIsSpecialFolder = 0
intMinMax = 0
strLocationName = file:///C:/Windows
strLocationURL = file:///C:/Windows
strPosition = 206|23|1200|900
strWindowId = 591696


Explorer #2

blnIsSpecialFolder = 0
intMinMax = 0
strLocationName = file:///C:/Dropbox/AutoHotkey/SandBox
strLocationURL = file:///C:/Dropbox/AutoHotkey/SandBox
strPosition = 206|23|1200|900
strWindowId = 591696


Explorer #3

blnIsSpecialFolder = 1
intMinMax = 0
strLocationName = Ce PC
strLocationURL = ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}
strPosition = 206|23|1200|900
strWindowId = 591696

As you can see, ComObjCreate("Shell.Application").Windows return three objects, which is good. But the pExplorer.HWND is the same for the three tabs, which is not good... This cause at least these two issues:

- changing the location of the active tab when it is not the first tab
- retrieving the location of the active tab (this issue is also discussed in this thread on the v2 forum).

But I'm sure a solution exists :-)

Re: 4 options to change the current folder in Windows Explorer

Posted: 28 Oct 2022, 02:01
by ntepa
Lexikos posted a solution to get the active tab. I only converted it to v1:

Code: Select all

GetActiveExplorerTab(hwnd:="") {
    if !hwnd
        hwnd := WinExist("A")
    activeTab := 0
    try ControlGet, activeTab, Hwnd,, % "ShellTabWindowClass1", % "ahk_id" hwnd
    for w in ComObjCreate("Shell.Application").Windows {
        if (w.hwnd != hwnd)
            continue
        if activeTab {
            static IID_IShellBrowser := "{000214E2-0000-0000-C000-000000000046}"
            shellBrowser := ComObjQuery(w, IID_IShellBrowser, IID_IShellBrowser)
            DllCall(NumGet(numGet(shellBrowser+0)+3*A_PtrSize), "Ptr", shellBrowser, "UInt*", thisTab)
            if (thisTab != activeTab)
                continue
        }
        return w
    }
}

^1::
    tab := GetActiveExplorerTab()
    MsgBox % tab.Document.Folder.Self.Path
Return

Re: 4 options to change the current folder in Windows Explorer

Posted: 28 Oct 2022, 20:04
by JnLlnd
ntepa wrote:
28 Oct 2022, 02:01
Lexikos posted a solution to get the active tab. I only converted it to v1:
I was trying to do it. I'm glad you did it before. Tested. It works perfectly.

I'll update the script in the OP with this code (not now but soon).

Thank you @ntepa !

Re: 4 options to change the current folder in Windows Explorer

Posted: 28 Oct 2022, 20:35
by ntepa
@JnLlnd I made a small edit. Added ObjRelease.
lexikos wrote:Also note that ComObjQuery returns a pointer value, not a ComObj, in v1. The pointer value can be used directly with NumGet and DllCall, but you must ObjRelease it afterward.

Code: Select all

GetActiveExplorerTab(hwnd:="") {
    if !hwnd
        hwnd := WinExist("A")
    activeTab := 0
    try ControlGet, activeTab, Hwnd,, % "ShellTabWindowClass1", % "ahk_id" hwnd
    for w in ComObjCreate("Shell.Application").Windows {
        if (w.hwnd != hwnd)
            continue
        if activeTab {
            static IID_IShellBrowser := "{000214E2-0000-0000-C000-000000000046}"
            shellBrowser := ComObjQuery(w, IID_IShellBrowser, IID_IShellBrowser)
            DllCall(NumGet(numGet(shellBrowser+0)+3*A_PtrSize), "Ptr", shellBrowser, "UInt*", thisTab)
            if (thisTab != activeTab)
                continue
            ObjRelease(shellBrowser)
        }
        return w
    }
}

^1::
    tab := GetActiveExplorerTab()
    MsgBox % tab.Document.Folder.Self.Path
Return

Re: 4 options to change the current folder in Windows Explorer

Posted: 03 Nov 2022, 12:35
by JnLlnd
I merged the GetActiveExplorerTab() code from ntepa with the existing function Explorer_Navigate() from Learning one into the new function Explorer_Navigate_Tab(). Tested on Win 10 and Win 11 build 22621.755. If there is not issue reported here, I'll add it to this thread OP.

Code: Select all

#Requires AutoHotkey v1.1
#NoEnv
#SingleInstance force

return

^1::
    Explorer_Navigate_Tab("C:\Windows")
Return

Explorer_Navigate_Tab(FullPath, hwnd="") {
; originally from Learning one (https://www.autohotkey.com/boards/viewtopic.php?p=4480#p4480)
; adapted by JnLlnd for tabbed browsing new to Windows 11 version 22H2 (build 22621.675)
; with code from ntepa (https://www.autohotkey.com/boards/viewtopic.php?p=488735#p488735)
; also works with previous versions of Windows Explorer
    hwnd := (hwnd="") ? WinExist("A") : hwnd ; if omitted, use active window
    WinGet, ProcessName, ProcessName, % "ahk_id " hwnd
    if (ProcessName != "explorer.exe")  ; not Windows explorer
        return
    For pExp in ComObjCreate("Shell.Application").Windows
    {
        if (pExp.hwnd = hwnd) { ; matching window found
			activeTab := 0
			try ControlGet, activeTab, Hwnd,, % "ShellTabWindowClass1", % "ahk_id" hwnd
			if activeTab {
				static IID_IShellBrowser := "{000214E2-0000-0000-C000-000000000046}"
				shellBrowser := ComObjQuery(pExp, IID_IShellBrowser, IID_IShellBrowser)
				DllCall(NumGet(numGet(shellBrowser+0)+3*A_PtrSize), "Ptr", shellBrowser, "UInt*", thisTab)
				if (thisTab != activeTab) ; matching active tab
					continue
				ObjRelease(shellBrowser)
			}
			pExp.Navigate("file:///" FullPath)
			return
		}
    }
}

Re: 4 options to change the current folder in Windows Explorer

Posted: 21 Nov 2022, 12:26
by JnLlnd
I've updated the OP with code for Win 11 Explorer with tabs (see method 5).
viewtopic.php?f=76&t=526

Re: 4 options to change the current folder in Windows Explorer

Posted: 25 Sep 2023, 05:54
by windfancy3
Thank you for the code, it really helped me a lot to open frequently used folders easily when using Explorer.
Can you please add a function to make it possible to switch to the target tab when it already exists instead of opening a new one.
I am sorry for making such a request, without basic knowledge of AHK, I am not able to implement what I need on this code.
JnLlnd wrote:
03 Nov 2022, 12:35
I merged the GetActiveExplorerTab() code from ntepa with the existing function Explorer_Navigate() from Learning one into the new function Explorer_Navigate_Tab(). Tested on Win 10 and Win 11 build 22621.755. If there is not issue reported here, I'll add it to this thread OP.

Code: Select all

#Requires AutoHotkey v1.1
#NoEnv
#SingleInstance force

return

^1::
    Explorer_Navigate_Tab("C:\Windows")
Return

Explorer_Navigate_Tab(FullPath, hwnd="") {
; originally from Learning one (https://www.autohotkey.com/boards/viewtopic.php?p=4480#p4480)
; adapted by JnLlnd for tabbed browsing new to Windows 11 version 22H2 (build 22621.675)
; with code from ntepa (https://www.autohotkey.com/boards/viewtopic.php?p=488735#p488735)
; also works with previous versions of Windows Explorer
    hwnd := (hwnd="") ? WinExist("A") : hwnd ; if omitted, use active window
    WinGet, ProcessName, ProcessName, % "ahk_id " hwnd
    if (ProcessName != "explorer.exe")  ; not Windows explorer
        return
    For pExp in ComObjCreate("Shell.Application").Windows
    {
        if (pExp.hwnd = hwnd) { ; matching window found
			activeTab := 0
			try ControlGet, activeTab, Hwnd,, % "ShellTabWindowClass1", % "ahk_id" hwnd
			if activeTab {
				static IID_IShellBrowser := "{000214E2-0000-0000-C000-000000000046}"
				shellBrowser := ComObjQuery(pExp, IID_IShellBrowser, IID_IShellBrowser)
				DllCall(NumGet(numGet(shellBrowser+0)+3*A_PtrSize), "Ptr", shellBrowser, "UInt*", thisTab)
				if (thisTab != activeTab) ; matching active tab
					continue
				ObjRelease(shellBrowser)
			}
			pExp.Navigate("file:///" FullPath)
			return
		}
    }
}

Re: 4 options to change the current folder in Windows Explorer

Posted: 25 Sep 2023, 14:49
by ntepa
windfancy3 wrote: Can you please add a function to make it possible to switch to the target tab when it already exists instead of opening a new one.
I have a function to switch to a tab if it exists. It requires UIA library to be included.
The script is for v2 though. I wasn't able to convert it to v1:

Code: Select all

#Requires AutoHotkey v2.0

#Include <UIA>

^1::{
    ExplorerFindTab("C:\Windows")
}

; looks for an existing tab with the path.
ExplorerFindTab(pathToFind) {
    shellWindows := ComObject("Shell.Application").Windows
    Found := 0
    for w in shellWindows {
        try ctrl := ControlGetHwnd("Windows.UI.Composition.DesktopWindowContentBridge1", w)
        catch
            continue
        Path := w.Document.Folder.Self.Path
        if (pathToFind = Path) {
            TabName := w.Document.Folder.Title
            topPane := UIA.ElementFromHandle(ctrl)
            tabEl := topPane.FindElement({T:"TabItem", N:TabName})
            WinActivate(w)
            tabEl.Click()
            Found := 1
            break
        }
    }
    if !found
        Run("explorer.exe " pathToFind)
}

Re: 4 options to change the current folder in Windows Explorer

Posted: 26 Sep 2023, 11:48
by windfancy3
Thank you very much! But I'm getting the error when the C:\Windows tab already exists , and when it doesn't it opens from a new window. I'm not sure if this has anything to do with the way I imported UIA, but I changed the import code to #include UIA.ahk, which I downloaded from https://github.com/Descolada/UIA-v2.
The specific error message is

Code: Select all

Error: An element matching the condition was not found
	020: TabName := w.Document.
	021: topPane := UIA.ElementFromHandle(ctrl)
▶ 022: tabEl := topPane.FindElement({T: "TabItem", N:TabName})[quote=ntepa post_id=540745 time=1695671345 user_id=149849]
windfancy3 wrote: Can you please add a function to make it possible to switch to the target tab when it already exists instead of opening a new one.
I have a function to switch to a tab if it exists. It requires UIA library to be included.
The script is for v2 though. I wasn't able to convert it to v1:

Code: Select all

#Requires AutoHotkey v2.0

#Include <UIA>

^1::{
    ExplorerFindTab("C:\Windows")
}

; looks for an existing tab with the path.
ExplorerFindTab(pathToFind) {
    shellWindows := ComObject("Shell.Application").Windows
    Found := 0
    for w in shellWindows {
        try ctrl := ControlGetHwnd("Windows.UI.Composition.DesktopWindowContentBridge1", w)
        catch
            continue
        Path := w.Document.Folder.Self.Path
        if (pathToFind = Path) {
            TabName := w.Document.Folder.Title
            topPane := UIA.ElementFromHandle(ctrl)
            tabEl := topPane.FindElement({T:"TabItem", N:TabName})
            WinActivate(w)
            tabEl.Click()
            Found := 1
            break
        }
    }
    if !found
        Run("explorer.exe " pathToFind)
}
[/quote]

Re: 4 options to change the current folder in Windows Explorer

Posted: 26 Sep 2023, 13:18
by ntepa
Did you copy it correctly? line 20 is cut off in your error message.

Re: 4 options to change the current folder in Windows Explorer

Posted: 28 Jan 2024, 13:20
by jbscout
I wanted to leave the following code to help someone else in the future.

It is a v1 script that does not use UIA, but will:
  • monitor and record open tabs
  • open previously used tabs
  • open saved favorites tabs
  • activate an already open tab (if you try to open it)
  • really only works if 1 FE window is open (gets confused with 2+ FE windows)
It used v1 because my IT department isn't upgrading to v2.
Also I know it could use a lot of polish, but I am limited to the IDE I have at work :(

Thanks for the core work that others had done, and I am sorry if I forgot to credit anyone.

Code: Select all

#Requires AutoHotkey v1.1.33
#NoEnv
#SingleInstance Force
; #Persistent
; #InstallKeybdHook ; needed for $ HotKey modifier (no trigger self) and others (e.g, Inactivity)
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%


/*
;==============================================================================
;========== What This Script Does  ============================================
;==============================================================================
; Script that monitors what Tabs are open in File Explorer in Win 11
; NOTE:  Only works if 1 FE window is open (not 2+ windows).
;==============================================================================
*/

; =========================================
; ===== Includes
; =========================================
;BeginRegion
#Include %A_ScriptDir%\Library\SettingsClass.ahk
#Include %A_ScriptDir%\Library\getSupportFilename.ahk 

;EndRegion

;==============================================================================
;========== Pre-Main Script  ==================================================
;==============================================================================

------------------------------------------------------------------------
; Files to include during a compile
;BeginRegion

imageDir := A_ScriptDir "\Script_Images"
IconName := getSupportFilename("ico") ; use the older simple filename version of getSupportFilename("ico")
FileInstall, .\Script_Images\Icons\File_Explorer_Manager.ico, %imageDir%\Icons\%IconName%
if(A_IsCompiled)
{
	IconName = %imageDir%\Icons\%IconName%
}
Else
{
	IconName := getSupportFilename2("ico") ; now use the updated version of getSupportFilename2("ico")
}

;EndRegion

; ------------------------------------------------------------------------
; setup Settings variable
;BeginRegion


settingsArray := []
; new SettingsProperty(propertyName, iniName="", defaultValue="", isWriteOnClose=false, isArray=false, arraySplitChar="", isBoolean=false)
settingsArray.push( new SettingsProperty("Save_Every_X_Minutes", ,    2, false) )
settingsArray.push( new SettingsProperty("TabList_OnClose", ,        "", false, true, "|") )
settingsArray.push( new SettingsProperty("TabList_Favourites", ,     "", false, true, "|") )
settingsArray.push( new SettingsProperty("TabList_1", ,              "", false, true, "|") )
settingsArray.push( new SettingsProperty("TabList_2", ,              "", false, true, "|") )
settingsArray.push( new SettingsProperty("TabList_3", ,              "", false, true, "|") )
settingsArray.push( new SettingsProperty("TabList_4", ,              "", false, true, "|") )
settingsArray.push( new SettingsProperty("TabList_5", ,              "", false, true, "|") )
settingsArray.push( new SettingsProperty("TabList_Preserved", ,      "", false, true, "|") )

global Settings := new SettingsClass(settingsArray)

;EndRegion

; ------------------------------------------------------------------------
; Set up SystemTray Menu
;BeginRegion

FileGetTime, ScriptLastModified, %A_ScriptFullPath%, M
FormatTime, ScriptLastModifiedX, %ScriptLastModified%
TrayTipString  =
				( LTrim
					---- %A_ScriptName% ----

					Script last modified:
					%ScriptLastModifiedX%
				)
Menu, Tray, Tip, %TrayTipString% ; Create system tray Mouse-over ToolTip
IfExist %IconName%
{
	Menu, Tray, Icon, %IconName%
}
Menu, tray, NoStandard ; Remove all standard menu items
	GoSub Menu_Create_SubMenu_Favourites
Menu, tray, add, Favourite tabs, :SubMenu_FavouriteTabsMenuItems
Menu, Tray, add ; Creates a separator line.
	GoSub Menu_Create_SubMenu_OpenTabs
Menu, tray, add, Open tabs, :SubMenu_OpenTabsMenuItems
Menu, Tray, add ; Creates a separator line.
Menu, Tray, add, Open all tabs that were open when Explorer was closed , TabList_Open_Last
Menu, tray, default, Open all tabs that were open when Explorer was closed
	SubMenuNumber := 0
	GoSub Menu_Create_SubMenu_PriorTabs
Menu, Tray, add, Preserved tab list, :SubMenu_PriorTabsMenuItems_0
	SubMenuNumber := 1
	GoSub Menu_Create_SubMenu_PriorTabs
	Menu, SubMenu_PriorTabListsMenuItems, add, 1st most recent tab list, :SubMenu_PriorTabsMenuItems_1
	SubMenuNumber := 2
	GoSub Menu_Create_SubMenu_PriorTabs
	Menu, SubMenu_PriorTabListsMenuItems, add, 2nd most recent tab list, :SubMenu_PriorTabsMenuItems_2
	SubMenuNumber := 3
	GoSub Menu_Create_SubMenu_PriorTabs
	Menu, SubMenu_PriorTabListsMenuItems, add, 3rd most recent tab list, :SubMenu_PriorTabsMenuItems_3
	SubMenuNumber := 4
	GoSub Menu_Create_SubMenu_PriorTabs
	Menu, SubMenu_PriorTabListsMenuItems, add, 4th most recent tab list, :SubMenu_PriorTabsMenuItems_4
	SubMenuNumber := 5
	GoSub Menu_Create_SubMenu_PriorTabs
	Menu, SubMenu_PriorTabListsMenuItems, add, 5th most recent tab list, :SubMenu_PriorTabsMenuItems_5
Menu, tray, add, Prior tab lists, :SubMenu_PriorTabListsMenuItems
Menu, Tray, add ; Creates a separator line.
	Menu, SubMenu_StandardMenuItems, Standard
	Menu, tray, add, Standard menu, :SubMenu_StandardMenuItems
Menu, Tray, add  ; Creates a separator line.
Menu, Tray, add, Open INI file, Menu_OpenINI
;	GoSub Menu_Create_SubMenu_PriorTabList
;Menu, tray, add, Test, :SubMenu_PriorTabs1MenuItems




; Menu, Tray, add, About..., TabList_Open

;EndRegion

; ------------------------------------------------------------------------
; Setup the Log file
; global Log := createLogInstance()
; Log.isLogClassTurnedOff := true

; ------------------------------------------------------------------------
; Setup the ability to receive messages from other programs/scripts
;BeginRegion

RegisterShellHook() ; if FE is NOT running or for when it closes

;EndRegion

; When the script ends, perform this function
; OnExit("ExitFunction")

;EndRegion

;==============================================================================
;========== Main Script  ======================================================
;==============================================================================
;BeginRegion

Settings.Update_Tab_Menu_Every_X_Minutes := 0.5
if ( getFileExplorerHWND() )
{ ; if File Explorer is already running
	; update periodically
	timerPeriod := Settings.Save_Every_X_Minutes * 60 * 1000
	SetTimer, TabList_Update, %timerPeriod%

	timerPeriod2 := Settings.Update_Tab_Menu_Every_X_Minutes * 60 * 1000
	SetTimer, Menu_Create_SubMenu_OpenTabs, %timerPeriod2%
}

return

;EndRegion

; =========================================
; ===== Menu Subroutines / Labels
; =========================================
;BeginRegion

TabList_Update:
{
	newPathArray := getAllTabLocationsFromFileExplorer()
	oldPathArray := Settings.TabList_1
	if (arrayToString(newPathArray) != arrayToString(oldPathArray))
	{
		Settings.TabList_5 := Settings.TabList_4
		Settings.TabList_4 := Settings.TabList_3
		Settings.TabList_3 := Settings.TabList_2
		Settings.TabList_2 := Settings.TabList_1
		Settings.TabList_1 := newPathArray
		Settings.writeINIFile()
	}
	; MsgBox % arrayToString(newPathArray) . "`n`n`n" . arrayToString(oldPathArray)
}
return

TabList_Save_Preserved:
{
	newPathArray := getAllTabLocationsFromFileExplorer()
	oldPathArray := Settings.TabList_Preserved
	if (arrayToString(newPathArray) != arrayToString(oldPathArray))
	{
		Settings.TabList_Preserved := newPathArray
		Settings.writeINIFile()

		; rederaw the menu 
		SubMenuNumber := 0
		GoSub Menu_Create_SubMenu_PriorTabs
	}
}
return

TabList_Open_Last:
	reopenTabArray(Settings.TabList_OnClose)
return

Menu_OpenINI:
{ ; opens an INI file for editing. 
	IniName := Settings.iniFile
	IfExist %IniName%
	{
		Run, open %IniName%
	}
}
return

Menu_Create_SubMenu_PriorTabs:
{
	; SubMenuNumber needs to be defined prior to entering the subroutine (valid values are 0-5, 0 is the Preserved tab list)
	SubMenuName := "SubMenu_PriorTabsMenuItems_" . SubMenuNumber
	PriorTabsArray := getTheRightTabListArray(SubMenuNumber)

	Menu, %SubMenuName%, add
	Menu, %SubMenuName%, DeleteAll

	Menu, %SubMenuName%, add, Open All, Menu_Open_PriorTabsArray 
	Menu, %SubMenuName%, add,  ; Separator
	if (PriorTabsArray.Length() < 1)
	{
		Menu, %SubMenuName%, add, No Tabs in this list yet, TabList_Open_Last ; just a dummy target label
		Menu, %SubMenuName%, disable, No Tabs in this list yet
	}
	else
	{
		for index, value in PriorTabsArray
		{
			; the Tab_Open_Favourite subroutine works just fine here
			Menu, %SubMenuName%, add, %value% , Tab_Open_Favourite
		}
	}

	if(SubMenuNumber == 0)
	{
		Menu, %SubMenuName%, add ; Creates a separator line.
		Menu, %SubMenuName%, add, Set Current tab list as the Preserved tab list, TabList_Save_Preserved
	}
}
return

Menu_Open_PriorTabsArray:
{
	SubMenuNumber := SubStr(A_ThisMenu , 0, 1)
	PriorTabsArray := getTheRightTabListArray(SubMenuNumber)
	reopenTabArray(PriorTabsArray)
}
return

Menu_Create_SubMenu_Favourites:
{
	Menu, SubMenu_FavouriteTabsMenuItems, add
	Menu, SubMenu_FavouriteTabsMenuItems, DeleteAll

	Menu, SubMenu_FavouriteTabsMenuItems, add, Add Active Tab as Favourite tab  (Ctrl+Shift+F), Tab_Save_Favourite
	Menu, SubMenu_FavouriteTabsMenuItems, add, 
	if (Settings.TabList_Favourites.Length() < 1)
	{
		Menu, SubMenu_FavouriteTabsMenuItems, add, No Favourites yet, TabList_Open_Last ; just a dummy target label
		Menu, SubMenu_FavouriteTabsMenuItems, disable, No Favourites yet
	}
	else
	{
		for index, value in Settings.TabList_Favourites
		{
			; BoundMenuFunction := Func("Explorer_Navigate_Tab").Bind(value)
			Menu, SubMenu_FavouriteTabsMenuItems, add, %value% , Tab_Open_Favourite
		}
	}
}
return

Tab_Save_Favourite:
{
	activePath := GetActiveExplorerTab()
	; msgbox, % activePath
    
    wasAddedToArray := false
	if (Settings.TabList_Favourites.Length() < 1)
	{
		Settings.TabList_Favourites := [activePath]
		wasAddedToArray := true
        ; msgbox, "nothing"
    
	}
	else
	{
        ; msgbox, "something"
        isInArray := false
		for index, value in Settings.TabList_Favourites
		{
			if(value == activePath)
			{
				isInArray := true
			}
		}
		if (!isInArray)
		{
            ; msgbox, "not in array"
            wasAddedToArray := true
			Settings.TabList_Favourites.push(activePath)
		}
	}
	if (wasAddedToArray)
	{
        ; msgbox, "writing"
		Settings.writeINIFile()
		GoSub Menu_Create_SubMenu_Favourites

		; notify the user of a change in status 
		TrayTip, File Explorer Tab Manager, New Favourite folder saved.`n%activePath%,, 16
		SetTimer, HideTrayTip, -4000
	}
}
return

Tab_Open_Favourite:
	reopenTabArray([A_ThisMenuItem])
return

Menu_Create_SubMenu_OpenTabs:
{
	Menu, SubMenu_OpenTabsMenuItems, add
	Menu, SubMenu_OpenTabsMenuItems, DeleteAll

	Menu, SubMenu_OpenTabsMenuItems, add, Set Current tab list as the Preserved tab list, TabList_Save_Preserved
    Menu, SubMenu_OpenTabsMenuItems, add ; Creates a separator line.

    openTabsArray := getAllTabLocationsFromFileExplorer()
    Settings.OpenTabs := openTabsArray
    if (openTabsArray.Length() < 1)
	{
		Menu, SubMenu_OpenTabsMenuItems, add, No Open Tabs, TabList_Open_Last ; just a dummy target label
		Menu, SubMenu_OpenTabsMenuItems, disable, No Open Tabs
	}
	else
	{
		for index, value in openTabsArray
		{
			Menu, SubMenu_OpenTabsMenuItems, add, %value% , Menu_ActivateAnOpenTab
		}
	}
}
return

Timer_Create_SubMenu_OpenTabs:
{
	newPathArray := getAllTabLocationsFromFileExplorer()
	oldPathArray := Settings.OpenTabs
	if (arrayToString(newPathArray) != arrayToString(oldPathArray))
	{
		GoSub Menu_Create_SubMenu_OpenTabs
	}
}
return

Menu_ActivateAnOpenTab:
	activateAnOpenTab(A_ThisMenuItem)
return


;EndRegion



; =========================================
; ===== Functions
; =========================================
;BeginRegion
ShellMessage( wParam, lParam )
{
	static WindowHandle
	if ( wParam = (HSHELL_WINDOWCREATED:=1) )
	{
		; WinGet, WindowProcessName, ProcessName, ahk_id %lParam%
		if ( lParam = getFileExplorerHWND() )
		{
			WindowHandle := lParam
			OnExplorerOpen(lParam)
		}
	}
	else if ( wParam = (HSHELL_WINDOWDESTROYED:=2) )
	{
		if ( lParam = WindowHandle )
		{
			OnExplorerExit(lParam)
		}
	}
	return
}

RegisterShellHook()
{
	Gui, +LastFound
	ScriptHandle := WinExist()
	DllCall( "RegisterShellHookWindow", "UInt", ScriptHandle )
	ShellHookMessageIdentifier := DllCall( "RegisterWindowMessage", "Str", "SHELLHOOK" )
	OnMessage( ShellHookMessageIdentifier, "ShellMessage" )
	return
}

OnExplorerOpen(winHWHD)
{
	; notify the user of a change in status 
	TrayTip, File Explorer Tab Manager, The File Explorer app has opened.`nThe helper program will start to monitor the open tabs.
	SetTimer, HideTrayTip, -3000

	; update periodically
	timerPeriod := Settings.Save_Every_X_Minutes * 60 * 1000
	SetTimer, TabList_Update, %timerPeriod%
	timerPeriod2 := Settings.Update_Tab_Menu_Every_X_Minutes * 60 * 1000
	SetTimer, Menu_Create_SubMenu_OpenTabs, %timerPeriod2%

	return winHWHD
}

OnExplorerExit(winHWHD)
{
	; save the last open tabs
	if(Settings.TabList_1.Length() >= 1)
    {
        Settings.TabList_OnClose := Settings.TabList_1
    }
	else
    {
        Settings.TabList_OnClose := Settings.TabList_2
    }
	Settings.writeINIFile()

	if (getFileExplorerHWND())
	{ ; only if there are no more open FE windows

		; turn off TabList updates
		SetTimer, TabList_Update, Off
		SetTimer, Menu_Create_SubMenu_OpenTabs, Off

		; notify the user of a change in status 
		tmpStr := "The File Explorer app has exited." . "`n"
		tmpStr .= "The helper program will no longer monitor the open tabs."
		TrayTip, File Explorer Tab Manager, %tmpStr%
		SetTimer, HideTrayTip, -3000
	}
	else
	{ ; if another FE window is open (not that this program handles multiple FE windows well at all)
	
		; notify the user of a change in status 
		tmpStr := "A File Explorer window has closed." . "`n"
		tmpStr .= "But, at least one File Explorer window is still open." . "`n"
		tmpStr .= "The helper program will continue to monitor the open tabs."
		TrayTip, File Explorer Tab Manager, %tmpStr%
		SetTimer, HideTrayTip, -3000
	}

	return
}

HideTrayTip() 
{ ; from https://www.autohotkey.com/docs/v1/lib/TrayTip.htm
    TrayTip  ; Attempt to hide it the normal way.
    if SubStr(A_OSVersion,1,3) = "10." {
        Menu Tray, NoIcon
        Sleep 200  ; It may be necessary to adjust this sleep.
        Menu Tray, Icon
    }
}

; =========================================

activateAnOpenTab(targetFullPath)
{
    hwnd := openFileExplorer()
	; hwnd := getFileExplorerHWND()
	; WinActivate, ahk_id %hwnd%

	if ( isPathInFileExplorer(targetFullPath, hwnd) )
	{
		; OK this is painful but the best I can do.  Tab through the open tabs until you hit the desired one
        
        openTabsArray := getAllTabLocationsFromFileExplorer() ; I need to know how many tabs are open
        Loop, % openTabsArray.Length()
        {
            ;  move to next tab
            Send, ^{Tab} ; move to newxt tab (cntl+Tab)
                ; Note 1: This hotkey works in English , don't know for other localizations
                ; Note 2: AFAIK, this hotkey does nothing in earlier versions of Windows Explorer but it would be safer to check the version before sending it
            Sleep, 100 ; for safety

            canidateFullPath := GetActiveExplorerTab(hwnd) 
                tmpStr := "A_Index := " . A_Index . "`n"
                tmpStr .= "targetFullPath := " . targetFullPath . "`n"
                tmpStr .= "canidateFullPath := " . canidateFullPath . "`n"
                ; msgbox, % tmpStr
            if (canidateFullPath == targetFullPath)
            {
                return 0
            }
        }
	}

    ; if we can't find the Tab open it anew
    reopenTabArray([targetFullPath])
	return 1
}

reopenTabArray(pathArray)
{
    hwnd := openFileExplorer()
	; hwnd := getFileExplorerHWND()
	; WinActivate, ahk_id %hwnd%

	for index, pathCurrent in pathArray
	{
		; MsgBox, % pathCurrent
		if ! isPathInFileExplorer(pathCurrent)
		{
			WinActivate, ahk_id %hwnd%
			Explorer_Navigate_New_Tab(pathCurrent)
		}
	}
    
    ; update the menu
    GoSub Menu_Create_SubMenu_OpenTabs

	return
}

getTheRightTabListArray(whichNumber)
{
	switch whichNumber
	{
		case 0:
			tmpArr := Settings.TabList_Preserved
		case 1:
			tmpArr := Settings.TabList_1
		case 2:
			tmpArr := Settings.TabList_2
		case 3:
			tmpArr := Settings.TabList_3
		case 4:
			tmpArr := Settings.TabList_4
		case 5:
			tmpArr := Settings.TabList_5
		default:
			tmpArr := Settings.TabList_1
	}
	return tmpArr
}

getAllTabLocationsFromFileExplorer()
{ ; selects the FileExplorer window(s) and lists all open tabs
	tabPaths := []
	; https://www.autohotkey.com/boards/viewtopic.php?p=28761&sid=bf16c7462b523f4f0944d69383ea9a0e#p28761
	For Window in ComObjCreate("Shell.Application").Windows ; see http://msdn.microsoft.com/en-us/library/windows/desktop/aa752084(v=vs.85).aspx
	{
		if (Folder := Window.Document.Folder.Self) ; && Window.Visible
		{
			tabPaths.Push(Folder.Path)
			; MsgBox, 0, Shell Windows
				; , % ++Count . "`n"
				; . "Window.HWND: !"    . Window.HWND . "!`n"
				; . "Window.Visible: !" . Window.Visible . "!`n"
				; . "Folder.Path: !"    . Folder.Path . "!`n"  ; <--- this is what you want
				; . "Folder.Name: !"    . Folder.Name . "!`n"
				; . "Folder.Type: !"    . Folder.Type . "!"

		}
	}
	return tabPaths
}

isPathInFileExplorer(targetFullPath, targetHWND="")
{
	_targetHWND := targetHWND ; create a stable version that ignores changes we make in the loop

	tabNumber := 0
	For Window in ComObjCreate("Shell.Application").Windows ; see http://msdn.microsoft.com/en-us/library/windows/desktop/aa752084(v=vs.85).aspx
	{
		if (Folder := Window.Document.Folder.Self) ; && Window.Visible
		{
			tabNumber := tabNumber + 1
			; MsgBox, 0, Shell Windows
				; , % ++Count . "`n"
				; . "Window.HWND: !"    . Window.HWND . "!`n"
				; . "Window.Visible: !" . Window.Visible . "!`n"
				; . "Folder.Path: !"    . Folder.Path . "!`n"  ; <--- this is what you want
				; . "Folder.Name: !"    . Folder.Name . "!`n"
				; . "Folder.Type: !"    . Folder.Type . "!"
			canidateFullPath := Folder.Path
			canidateHWND := Window.HWND
			if (_targetHWND == "")
				targetHWND := canidateHWND

			if (canidateFullPath == targetFullPath) && (canidateHWND == targetHWND)
				return tabNumber
		}
	}
	return 0
}

Explorer_Navigate_New_Tab(FullPath, hwnd="")
{ ; opens a new tab and then sets that tab's path to FullPath

	; error checking / hwnd == "" try to make Active Window
	hwnd := (hwnd="") ? getFileExplorerHWND("") : hwnd ; if omitted, use active window
	if !(getFileExplorerHWND(hwnd))
	{ ; exit if no  Explorer window
		ErrorLevel := 1
		return 0
	}

	WinWaitActive, ahk_id %hwnd%

	; Windows Explorer is the active window
	Send, ^t ; add a new tab
				; Note 1: This hotkey works in English and French, don't know for other localizations
				; Note 2: AFAIK, this hotkey does nothing in earlier versions of Windows Explorer but
				; it would be safer to check the version before sending it
	Sleep, 100 ; for safety

	; the new tab is the active tab
	return Explorer_Navigate_Tab(FullPath, hwnd)
}

Explorer_Navigate_Tab(FullPath, hwnd="")
{ ; changes the active tab to FullPath (over-writing the existing value)
	; see https://www.autohotkey.com/boards/viewtopic.php?p=489575#p489575
	; originally from Learning one (https://www.autohotkey.com/boards/viewtopic.php?p=4480#p4480)
	; adapted by JnLlnd for tabbed browsing new to Windows 11 version 22H2 (build 22621.675)
	; with code from ntepa (https://www.autohotkey.com/boards/viewtopic.php?p=488735#p488735)
	; also works with previous versions of Windows Explorer

	; error checking / hwnd == "" try to make Active Window
	hwnd := (hwnd="") ? getFileExplorerHWND("") : hwnd ; if omitted, use active window
	if !(getFileExplorerHWND(hwnd))
	{ ; exit if no  Explorer window
		ErrorLevel := 1
		return 0
	}
    ; msgbox, % "Explorer_Navigate_Tab(" . FullPath . ", " . hwnd . ")"
	For pExp in ComObjCreate("Shell.Application").Windows
	{
		if (pExp.hwnd = hwnd)
		{ ; matching window found
			activeTab := 0
			try ControlGet, activeTab, Hwnd,, % "ShellTabWindowClass1", % "ahk_id" hwnd
			if activeTab {
				static IID_IShellBrowser := "{000214E2-0000-0000-C000-000000000046}"
				shellBrowser := ComObjQuery(pExp, IID_IShellBrowser, IID_IShellBrowser)
				DllCall(NumGet(NumGet(shellBrowser+0)+3*A_PtrSize), "Ptr", shellBrowser, "UInt*", thisTab)
					tmpStr := "HWND:`t" . hwnd . "`n"
					tmpStr .= "thisTab:`t" . thisTab . "`n"
					tmpStr .= "activeTab:`t" . activeTab . "`n"
					tmpStr .= "Loc:`t" . pExp.LocationURL . "`n"
					tmpStr .= "Loc2:`t" . GetFullPathName(pExp.LocationURL) . "`n"
					; msgbox, % tmpStr
				if (thisTab != activeTab) ; matching active tab
					continue
				ObjRelease(shellBrowser)
			}
			pExp.Navigate("file:///" FullPath)
			return 1
		}
	}
	return 0 ; window not found
}

openFileExplorer()
{ ; if a window is already open does nothing (but activates the window)
    hwnd := getFileExplorerHWND()
    if( hwnd == 0) 
    { ; no window open
        Run, Explorer.exe /n`,/e`
        Sleep, 1000
        hwnd := getFileExplorerHWND()
    }
    WinActivate, ahk_id %hwnd%
    return hwnd
}

getFileExplorerHWND(hwnd="")
{ ; get a File Explorer window OR error check that HWND is a File Explorer window

	if (hwnd == "")
	{ ;  get a File Explorer window (preference for the Active Window)
		hwnd := WinExist("A")
		if ( getFileExplorerHWND(hwnd) )
		{ ; see if Active Window is a file explorer window
			ErrorLevel := 0
			return hwnd
		}
		else if WinExist("ahk_Class CabinetWClass")
		{ ;  now grab any File Explorer window (explorer.exe is the Desktop process too.  So you cannot check ahk_exe)
			hwnd := WinExist("ahk_Class CabinetWClass")
			ErrorLevel := 0
			return hwnd
		}
		else
		{ ; fail
			ErrorLevel := 1
			return 0
		}
	}
	else
	{ ; perform error checking
		WinGet, ProcessName, ProcessName, % "ahk_id " hwnd
		WinGetClass, ClassName, % "ahk_id " hwnd
		if ((ProcessName == "explorer.exe") AND (ClassName == "CabinetWClass") )
		{
			ErrorLevel := 0
			return hwnd
		}
		else
		{
			ErrorLevel := 1
			return 0
		}
	}
}

GetFullPathName(path) {
	cc := DllCall("GetFullPathName", "str", path, "uint", 0, "ptr", 0, "ptr", 0, "uint")
	VarSetCapacity(buf, cc*(A_IsUnicode?2:1))
	DllCall("GetFullPathName", "str", path, "uint", cc, "str", buf, "ptr", 0, "uint")
	return buf
}

GetActiveExplorerTab(hwnd:="")
{ ; https://www.autohotkey.com/boards/viewtopic.php?p=544495#p544495  added case where hwnd==""
	; does both File Explorer & IE
	static IID_IShellBrowser := "{000214E2-0000-0000-C000-000000000046}"

	if(hwnd == "")
	{
		hwnd := getFileExplorerHWND()
	}
	; msgbox, % "HWND=" . hwnd
	activeTab := 0, hwnd := hwnd ? hwnd : WinExist("A")
	try
		ControlGet, activeTab, Hwnd,, ShellTabWindowClass1, ahk_id %hwnd%	; File Explorer (Windows 11)
	catch
		try
			ControlGet, activeTab, Hwnd,, TabWindowClass1, ahk_id %hwnd%	; IE
	for w in ComObjCreate("Shell.Application").Windows
	{
		; msgbox, %  "looking for w=" . w.hwnd . "==" . hwnd
		if (w.hwnd != hwnd)
			continue
		if (activeTab)
		{ ; The window has tabs, so make sure this is the right one.
			shellBrowser := ComObjQuery(w, IID_IShellBrowser, IID_IShellBrowser)
			DllCall(NumGet(NumGet(shellBrowser+0), 3*A_PtrSize), "UPtr",shellBrowser, "UIntP",thisTab:=0)
			if (thisTab != activeTab)
				continue
		}
		; return w ; you can just return the window object
		tab := w
        switch ComObjType(tab.Document, "Class")
		{
			case "ShellFolderView":
				; msgbox, %  tab.Document.Folder.Self.Path
                return tab.Document.Folder.Self.Path
			default: ;case "HTMLDocument":
				return tab.LocationURL
		}
	}
}

arrayToString(array, arraySplitChar="|")
{
	tmpVal := ""
	For Index, Value In array
		tmpVal .= arraySplitChar . Value ; Remove leading delimiters when the loop is done
	tmpVal := LTrim(tmpVal, arraySplitChar) ; Remove leading delimiters (,)
	return tmpVal
}

;EndRegion

; =========================================
; ===== HotKeys
; =========================================
;BeginRegion
; -------- Hotkey Definitions Reminder ----------
; # - Win
; ! - Alt
; ^ - Control
; + - Shift
; ~ - pass key command to OS
; example -- $>!v:: 		; $ (no trigger self)  > (right key of a pair) ---- Alt (!) + V

^+F:: ; Ctrl+Shft+F
	GoSub Tab_Save_Favourite
return

;EndRegion

; =========================================
; ===== Classes
; =========================================
;BeginRegion
;EndRegion

/*
;==============================================================================
;========== Version History ===================================================
;==============================================================================
;
; v0.9 2024-01-05
;    * First proof of concept
;
; v1.0 2024-01-07
;    * working functionality
;
; v1.1 2024-01-23
;    * added a submenu to list all open tabs
;   * added function to active an open tab
;   * fixed the bug where OnClose was empty
;   * opening a tab now starts FE if it isn't already running
;
; v1.2 2024-01-28
;   * added a submenus for each prior tab list (including Preserved)
;   * closing an FEno longer stops all monitoring, checks if another FE is open
;
;==============================================================================
*/

And the support files

SettingsClass.ahk

Code: Select all

; =========================================
; ===== Classes
; =========================================
;BeginRegion

class SettingsClass
{
	__New(aArrayOfProperties, aIniFile="", aSectionName="")
	{
		; Input variables
		; 	aArrayOfProperties -> an array of objects or associative arrays with the following keys:  propertyName, iniName, defaultValue, isWriteOnClose (class SettingsProperty, at the end of this file).  Properties are dynamically created from this array.
		; 	aIniFile -> the INI file to read/write (default is A_Scriptname.INI)
		; 	aSectionName -> the section of the INI file to use (default is A_ComputerName)

		; dynamically create the properties
		; this.settingsConfigutations := {}
		Loop, % aArrayOfProperties.Length()
		{
			currentSetting := aArrayOfProperties[A_Index]
			propertyName := currentSetting.propertyName
			this[propertyName] := currentSetting.defaultValue
			this.settingsConfigurations[propertyName] := currentSetting
			
			; tmpStr := "propertyName=" propertyName
			; msgbox, % tmpStr
		}
		
		; set the non-dynamic/universal properties
		if(aIniFile="")
		{
			aIniFile := this.getSupportFilename("ini")
		}
		this.iniFile := aIniFile

		if(aSectionName="")
		{
			this.SectionName := A_ComputerName . "-" . A_UserName
		}
		else
		{
			this.SectionName := aSectionName
		}

		; load the values of the settings from the INI file
		this.readINIFile()
		return this
	}

	; Properties
	; ---------------------------------------------
		; Properties are dynamically created from __New(aArrayOfProperties) 
		; the few that are universally defined in all version of the Class
		; this.iniFile -> the INI file name to be read/written
		; this.SectionName -> the section in the INI to read/write to
			; Properties hidden from the user
			; --------------------------------
			; this.settingsConfigurations[propertyName] -> a version of aArrayOfProperties keyed by the propertyName.  Used in the read/write methods.
		

	; Methods (of suitable importance)
	; ---------------------------------------------
	readINIFile(aIniFile="")
	{ ; get the INI variables from the file (various default values)
		; Input variables
		; 	aIniFile -> the INI file to read/write (default is this.iniFile set in _New())
		if(aIniFile="")
		{
			aIniFile := this.iniFile
		}
		
		retValF := FileExist(aIniFile) ; indicate if the INI file already existed (and hopefully the default values where NOT used)
		IniRead, OutputVarSectionNames, %aIniFile%
		secVar := this.SectionName
		retValS := InStr(OutputVarSectionNames, secVar) ; is there the Section we need?
		retVal := (retValF AND retValS)
		if (!retVal) ; write an INI file if we don't have one with the needed Section
		{
			this.writeDefaultINIFile(aIniFile)
		}
		
		for propertyName, propertyConfig in this.settingsConfigurations
		{
			; tmpStr := "propertyName = " propertyName "`n"
			; tmpStr .= "propertyConfig.iniName = " propertyConfig.iniName "`n"
			; tmpStr .= "propertyConfig.defaultValue = " propertyConfig.defaultValue "`n"
			; tmpStr .= "propertyConfig.isArray = " propertyConfig.isArray "`n"
			; tmpStr .= "propertyConfig.arraySplitChar = " propertyConfig.arraySplitChar "`n"
			; tmpStr .= "propertyConfig.isBoolean = " propertyConfig.isBoolean "`n"
			; msgbox, % tmpStr
			
			iniName := propertyConfig.iniName
			defaultValue := propertyConfig.defaultValue
			propertyValue := this.readINIFileValue(iniName, defaultValue, aIniFile)
			if (propertyConfig.isBoolean)
			{
				if (propertyConfig.isArray)
				{
					defaultValueArray := StrSplit(defaultValue, propertyConfig.arraySplitChar)
					for index, element in propertyValue
					{
						propertyValueTmp := this.validateBoolean(element, defaultValueArray[index])
						propertyValue[index] := propertyValueTmp
					}
				}
				else
				{
					propertyValue := this.validateBoolean(propertyValue, defaultValue)
				}
			}
			else if (propertyConfig.isArray)
			{
				propertyValue := StrSplit(propertyValue, propertyConfig.arraySplitChar)
			}
			else if (propertyConfig.isObject)
			{
				propertyValueTmp := this.jsonToObject(propertyValue)
				propertyValue := propertyValueTmp
			}
			this[propertyName] := propertyValue
		}

		return retVal
	}
	

	_writeINIFile(aIniFile="", isOnClose=false, isWriteDefaultValues=false, aSection="")
	{ ; write user changeable settings to the INI file
		; Input variables
		; 	aIniFile -> the INI file to read/write (default is this.iniFile set in _New())
		; Output variables
		; 	errorLevel (Boolean) -> did an error occur while writing to the INI file?
		if(aIniFile="")
		{
			aIniFile := this.iniFile
		}
		
		errLvl := false
		for propertyName, propertyConfig in this.settingsConfigurations
		{
			; tmpStr := "----- Write ----" isOnClose "-" isWriteDefaultValues "-`n"
			; tmpStr .= "aIniFile = " aIniFile "`n"
			; tmpStr .= "propertyName = " propertyName "`n"
			; tmpStr .= "propertyValue = " this[propertyName] "`n"
			; tmpStr .= "propertyConfig.iniName = " propertyConfig.iniName "`n"
			; tmpStr .= "propertyConfig.defaultValue = " propertyConfig.defaultValue "`n"
			; tmpStr .= "propertyConfig.isWriteOnClose = " propertyConfig.isWriteOnClose "`n"
			; tmpStr .= "propertyConfig.isArray = " propertyConfig.isArray "`n"
			; tmpStr .= "propertyConfig.arraySplitChar = " propertyConfig.arraySplitChar "`n"
			; tmpStr .= "propertyConfig.isBoolean = " propertyConfig.isBoolean "`n"
			; msgbox, % tmpStr

			tmpStr := "propertyName = " propertyName "`n"
			iniName := propertyConfig.iniName
			if(isWriteDefaultValues)
			{ ; if creating a default INI file
				errLvl_new  := this.writeINIFileValue(iniName, propertyConfig.defaultValue, aIniFile, aSection)
				tmpStr .= "written=" errLvl_new "`n"
			}
			else if (!isOnClose OR (isOnClose AND propertyConfig.isWriteOnClose))
			{ ; if a normal write, or writing on Close and allowed to write OnClose. 
				if (propertyConfig.isArray)
				{
					tmpVal := ""
					arraySplitChar := propertyConfig.arraySplitChar
					For Index, Value In this[propertyName]
						tmpVal .= arraySplitChar . Value ; Remove leading delimiters when the loop is done
					tmpVal := LTrim(tmpVal, arraySplitChar) ; Remove leading delimiters (,)
				}
				else if (propertyConfig.isObject)
				{
					tmpVal := this.objectToJson(this[propertyName])
				}
				else
				{
					tmpVal := this[propertyName]
				}
				errLvl_new  := this.writeINIFileValue(iniName, tmpVal, aIniFile, aSection)
				tmpStr .= "written=" errLvl_new "`n"
			}
			else
			{
				errLvl_new := false
			}
			errLvl := (errLvl_new OR errLvl)
			; msgbox, % tmpStr
		}
		return errLvl
	}


	writeINIFile(aIniFile="")
	{ ; write user changeable settings to the INI file
		; Input variables
		; 	aIniFile -> the INI file to read/write (default is this.iniFile set in _New())
		; Output variables
		; 	errorLevel (Boolean) -> did an error occur while writing to the INI file?
		
		return this._writeINIFile(aIniFile, false, false)
	}
	
	writeINIFileOnClose(aIniFile="")
	{ ; write non-user editable values on the close of the script.
		; Input variables
		; 	aIniFile -> the INI file to read/write (default is this.iniFile set in _New())
		; Output variables
		; 	errorLevel (Boolean) -> did an error occur while writing to the INI file?

		return this._writeINIFile(aIniFile, true, false)
	}
	
	writeDefaultINIFile(aIniFile="", aSection="")
	{ ; write default values ot the INI file, including the non-user changeable ones (i.e., installed_on)
		; Input variables
		; 	aIniFile -> the INI file to read/write (default is this.iniFile set in _New())
		; Output variables
		; 	errorLevel (Boolean) -> did an error occur while writing to the INI file?

		return this._writeINIFile(aIniFile, false, true, aSection)
	}

	; Methods (helpers)
	; ---------------------------------------------
	writeINIFileValue(aKey, aValue, aIniFile="", aSection="")
	{ ; a wrapper to IniWrite
		; Input variables
		; 	aKey/aValue -> the key/value to write to the INI file
		; 	aIniFile -> the INI file to read/write (default is this.iniFile set in _New())
		; 	aSectionName -> the section of the INI file to use (default is A_ComputerName)
		; Output variables
		; 	errorLevel (Boolean) -> did an error occur while writing to the INI file?
		if(aIniFile="")
		{
			aIniFile := this.iniFile
		}
		if(aSection="")
		{
			aSection := this.SectionName
		}
		IniWrite, %aValue%, %aIniFile%, %aSection%, %aKey%
		return ErrorLevel
	}
	
	readINIFileValue(aKey, aDefValue, aIniFile="", aSection="")
	{ ; a wrapper to IniRead (mostly a workaround for the fact that you can't use obj.var syntax in IniRead)
		; Input variables
		; 	aKey -> the key to read from the INI file
		; 	aDefValue -> if the aKey cannot be read, supply this as the returned value
		; 	aIniFile -> the INI file to read/write (default is this.iniFile set in _New())
		; 	aSectionName -> the section of the INI file to use (default is A_ComputerName)
		; Output variables
		; 	Value -> the value associated with the aKey
		
		if(aIniFile="")
		{
			aIniFile := this.iniFile
		}
		if(aSection="")
		{
			aSection := this.SectionName
		}
		IniRead, OutVar, %aIniFile%, %aSection%, %aKey%, %aDefValue%
		return OutVar
	}
	
	getSupportFilename_old(fileExten, addDot=true)
	{ ; provides a filename of a support file (e.g., ini, ico), regradless of whether the script is compiled or not
		; Input variables
		; 	fileExten -> the file extension of the support file (place in quotes)
		; 	addDot -> should a '.' be added before fileExten? (Default=true)
		; Output variables
		; 	a filename of the script with the extension replaced with fileExten

		return this.getSupportFilename(fileExten, addDot, "", False, "", False)
	}

	getSupportFilename(fileExten, addDot=true, parentFolder="", useDefaultFolder=True, filenameSuffix="", useFullPath=true)
	{ ; provides a filename of a support file (e.g., ini, ico), regradless of whether the script is compiled or not
		; Input variables
		; 	fileExten -> the file extension of the support file (place in quotes)
		; 	addDot -> should a '.' be added before fileExten? Allows you to use "log" instead of ".log" (Default=true)
		; 	parentFolder -> directory where the file should be located (Default="")
		; 	useDefaultFolder -> if parentFolder="" will place the file in a default folder based on fileExten (Default={"ini": "INI_Files", "ico": "Script_Images\Icons", "log": "Logs"})
		; 	filenameSuffix -> a string added between the filenameNoExt and the Exten (e.g., "_" . A_ComputerName) (Default="")
		; 	useFullPath -> return a FullPath or not.  If parentFolder isa fullpath this will be ignored. (Default=true)
		; Output variables
		; 	a filename of the script with the extension replaced with fileExten

		defaultArray := {"ini": "INI_Files", "ico": "Script_Images\Icons", "log": "Logs"}
		
		if(fileExten != "")
		{
			if(addDot)
			{
				replacementStr = .%fileExten%
			}
			else
			{
				replacementStr = %fileExten%
			}
		}
		else
		{
			replacementStr := ""
		}

		if(filenameSuffix != "")
		{
			replacementStr :=  filenameSuffix . replacementStr
		}
			
		if(A_IsCompiled)
		{
			StringReplace, fileName, A_ScriptName,.exe,%replacementStr%, All
		}
		else
		{
			StringReplace, fileName, A_ScriptName,.ahk,%replacementStr%, All
		}

		if(parentFolder == "")
		{
			if(useDefaultFolder)
			{
				1stChar := SubStr(fileExten, 1, 1)
				if (1stChar = ".")
				{
					fileExten1 := SubStr(fileExten, 2)
					; MsgBox, "dot: " . %fileExten1%
				}
				Else
				{
					fileExten1 := fileExten
					; MsgBox, "nul: " . %fileExten1%
				}
				For key, value in defaultArray
				{
					if( InStr(key, fileExten1) )
					{
						parentFolder := value
					}
				}
			}
		}

		if(parentFolder != "")
		{
			returnName := parentFolder . "\" . fileName
		}
		else
		{
			returnName := fileName
		}

		if(useFullPath)
		{
			; see https://www.autohotkey.com/boards/viewtopic.php?p=289536#p289536
			path := returnName
			cc := DllCall("GetFullPathName", "str", path, "uint", 0, "ptr", 0, "ptr", 0, "uint")
			VarSetCapacity(buf, cc*(A_IsUnicode?2:1))
			DllCall("GetFullPathName", "str", path, "uint", cc, "str", buf, "ptr", 0, "uint")
			returnName := buf
			; SplitPath, returnName, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			; if(OutDrive = "") ; is it already a FullPath?
			; {
			; 	SplitPath, A_ScriptFullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			; 	returnName := OutDir . "\" . returnName ; OutDir includes OutDrive
			; }
		}
		
		return returnName
	}

	validateBoolean(boolAsStr, defaultVal=false)
	{ ; checks a String for true/false, Yes/No, or a number ("" is false)
		; Input variables
		; 	boolAsStr -> a string representation of a Boolean (e.g. from an INI file/STDIN)
		; 	defaultVal -> a default Boolean that is returned if all else fails (default: false)
		; Output variables
		; 	boolAsBoolean -> a Boolean (true or false)

		if(boolAsStr = "true")
		{
			return true
		}
		else if(boolAsStr = "false")
		{
			return false
		}
		else if(boolAsStr = "")
		{
			return false
		}
		else if(SubStr(boolAsStr,1,1) = "t") ; "=" is case in-sensitive  ; "==" is case sensitive 
		{
			return true
		}
		else if(SubStr(boolAsStr,1,1) = "f")
		{
			return false
		}
		else if(SubStr(boolAsStr,1,1) = "y")
		{
			return true
		}
		else if(SubStr(boolAsStr,1,1) = "n")
		{
			return false
		}
		else if boolAsStr is number
		{
			if (boolAsStr != 0)
				return true
			else
				return false
		}
		; else
		if (defaultVal) ; very harsh error checking for defaultVal
		{
			return true
		}
		else
		{
			return false
		}
	}

	jsonToObject(ByRef src, args*) ; Jxon_Load()
	{ ; copied from jxon.ahk (https://github.com/cocobelgica/AutoHotkey-JSON/tree/master) in order to not have another file to include
		; Deserialize src (a JSON formatted string) to an AutoHotkey object

		; Syntax:
		; 	obj := Jxon_Load( ByRef src [ , object_base := "", array_base := "" ] )
		; 	Parameter(s):
		; 	src [in, ByRef] - JSON formatted string or path to the file containing JSON formatted string.
		; 	object_base [in, opt] - an object to use as prototype for objects( {} ) created during parsing.
		; 	array_base [in, opt] - an object to use as prototype for arrays( [] ) created during parsing.
		static q := Chr(34)
	
		key := "", is_key := false
		stack := [ tree := [] ]
		is_arr := { (tree): 1 }
		next := q . "{[01234567890-tfn"
		pos := 0
		while ( (ch := SubStr(src, ++pos, 1)) != "" )
		{
			if InStr(" `t`n`r", ch)
				continue
			if !InStr(next, ch, true)
			{
				ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
				col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))
	
				msg := Format("{}: line {} col {} (char {})"
				,   (next == "")      ? ["Extra data", ch := SubStr(src, pos)][1]
					: (next == "'")     ? "Unterminated string starting at"
					: (next == "\")     ? "Invalid \escape"
					: (next == ":")     ? "Expecting ':' delimiter"
					: (next == q)       ? "Expecting object key enclosed in double quotes"
					: (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
					: (next == ",}")    ? "Expecting ',' delimiter or object closing '}'"
					: (next == ",]")    ? "Expecting ',' delimiter or array closing ']'"
					: [ "Expecting JSON value(string, number, [true, false, null], object or array)"
					, ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
				, ln, col, pos)
	
				throw Exception(msg, -1, ch)
			}
	
			is_array := is_arr[obj := stack[1]]
	
			if i := InStr("{[", ch)
			{
				val := (proto := args[i]) ? new proto : {}
				is_array? ObjPush(obj, val) : obj[key] := val
				ObjInsertAt(stack, 1, val)
				
				is_arr[val] := !(is_key := ch == "{")
				next := q . (is_key ? "}" : "{[]0123456789-tfn")
			}
	
			else if InStr("}]", ch)
			{
				ObjRemoveAt(stack, 1)
				next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
			}
	
			else if InStr(",:", ch)
			{
				is_key := (!is_array && ch == ",")
				next := is_key ? q : q . "{[0123456789-tfn"
			}
	
			else ; string | number | true | false | null
			{
				if (ch == q) ; string
				{
					i := pos
					while i := InStr(src, q,, i+1)
					{
						val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
						static end := A_AhkVersion<"2" ? 0 : -1
						if (SubStr(val, end) != "\")
							break
					}
					if !i ? (pos--, next := "'") : 0
						continue
	
					pos := i ; update pos
	
						val := StrReplace(val,    "\/",  "/")
					, val := StrReplace(val, "\" . q,    q)
					, val := StrReplace(val,    "\b", "`b")
					, val := StrReplace(val,    "\f", "`f")
					, val := StrReplace(val,    "\n", "`n")
					, val := StrReplace(val,    "\r", "`r")
					, val := StrReplace(val,    "\t", "`t")
	
					i := 0
					while i := InStr(val, "\",, i+1)
					{
						if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
							continue 2
	
						; \uXXXX - JSON unicode escape sequence
						xxxx := Abs("0x" . SubStr(val, i+2, 4))
						if (A_IsUnicode || xxxx < 0x100)
							val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
					}
	
					if is_key
					{
						key := val, next := ":"
						continue
					}
				}
	
				else ; number | true | false | null
				{
					val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
				
				; For numerical values, numerify integers and keep floats as is.
				; I'm not yet sure if I should numerify floats in v2.0-a ...
					static number := "number", integer := "integer"
					if val is %number%
					{
						if val is %integer%
							val += 0
					}
				; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
				; SOMETIMES return strings due to certain optimizations. Since it
				; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
					else if (val == "true" || val == "false")
						val := %value% + 0
				; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
				; as it would raise an exception in AHK_H(overriding built-in var)
					else if (val == "null")
						val := ""
				; any other values are invalid, continue to trigger error
					else if (pos--, next := "#")
						continue
					
					pos += i-1
				}
				
				is_array? ObjPush(obj, val) : obj[key] := val
				next := obj==tree ? "" : is_array ? ",]" : ",}"
			}
		}
	
		return tree[1]
	}

	objectToJson(obj, indent:="", lvl:=1) ; Jxon_Dump(obj, indent:="", lvl:=1)
	{ ; copied from jxon.ahk (https://github.com/cocobelgica/AutoHotkey-JSON/tree/master) in order to not have another file to include
		; Serialize obj to a JSON formatted string
		; Syntax:
		; 	str := Jxon_Dump( obj [ , indent := "" ] )
		; 	Return Value:
		; 	A JSON formatted string.
		; Parameter(s):
		; 	obj [in] - this argument has the same meaning as in JSON.Dump()
		; 	indent [in, opt] - this argument has the same meaning as in JSON.Dump()

		static q := Chr(34)
	
		if IsObject(obj)
		{
			static Type := Func("Type")
			if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "")
				throw Exception("Object type not supported.", -1, Format("<Object at 0x{:p}>", &obj))
	
			is_array := 0
			for k in obj
				is_array := k == A_Index
			until !is_array
	
			static integer := "integer"
			if indent is %integer%
			{
				if (indent < 0)
					throw Exception("Indent parameter must be a postive integer.", -1, indent)
				spaces := indent, indent := ""
				Loop % spaces
					indent .= " "
			}
			indt := ""
			Loop, % indent ? lvl : 0
				indt .= indent
	
			lvl += 1, out := "" ; Make #Warn happy
			for k, v in obj
			{
				if IsObject(k) || (k == "")
					throw Exception("Invalid object key.", -1, k ? Format("<Object at 0x{:p}>", &obj) : "<blank>")
				
				if !is_array
					out .= ( ObjGetCapacity([k], 1) ? this.objectToJson(k) : q . k . q ) ;// key
						.  ( indent ? ": " : ":" ) ; token + padding
				out .= this.objectToJson(v, indent, lvl) ; value
					.  ( indent ? ",`n" . indt : "," ) ; token + indent
			}
	
			if (out != "")
			{
				out := Trim(out, ",`n" . indent)
				if (indent != "")
					out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1)
			}
			
			return is_array ? "[" . out . "]" : "{" . out . "}"
		}
	
		; Number
		else if (ObjGetCapacity([obj], 1) == "")
			return obj
	
		; String (null -> not supported by AHK)
		if (obj != "")
		{
				obj := StrReplace(obj,  "\",    "\\")
			, obj := StrReplace(obj,  "/",    "\/")
			, obj := StrReplace(obj,    q, "\" . q)
			, obj := StrReplace(obj, "`b",    "\b")
			, obj := StrReplace(obj, "`f",    "\f")
			, obj := StrReplace(obj, "`n",    "\n")
			, obj := StrReplace(obj, "`r",    "\r")
			, obj := StrReplace(obj, "`t",    "\t")
	
			static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]"
			while RegExMatch(obj, needle, m)
				obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0])))
		}
		
		return q . obj . q
	}
}

class SettingsProperty
{
	__New(propertyName, iniName="", defaultValue="", isWriteOnClose=false, isArray=false, arraySplitChar="", isBoolean=false, isObject=false)
	{
		this.propertyName := propertyName
		if(iniName="")
		{
			this.iniName := propertyName
		}
		else
		{
			this.iniName := iniName
		}
		this.defaultValue := defaultValue
		this.isWriteOnClose := isWriteOnClose
		this.isArray := isArray ; for backwards compatibility
		this.arraySplitChar := arraySplitChar ; for backwards compatibility
		this.isBoolean := isBoolean
		this.isObject := isObject
	}
}



;EndRegion

/*
;==============================================================================
;========== Version History ===================================================
;==============================================================================
;
; v1.0 2022-01-23
; * initial draft
; 
; v1.1 2022-11-12
; * changed writeDefaultINIFile(..., aSection="") and _writeINIFile(..., aSection="") so that you can write to any section (e.g., "DEFAULT_SETTINGS") and not just the Settings.SectionName value.
; 
; v1.2 2023-11-18
; * updated getSupportFilename("ico"), which sets default sub-directories for filetypes, and getSupportFilename_old("ico"), which is the old version
;
; v1.3 2024-01-08
; * added ability to store objects (via jxon.ahk functions), not tested
;
;==============================================================================
*/
getSupportFilename.ahk

Code: Select all

getSupportFilename(fileExten, addDot=true)
{ ; provides a filename of a support file (e.g., ini, ico), regradless of whether the script is compiled or not
	; Input variables
	; 	fileExten -> the file extension of the support file (place in quotes)
	; 	addDot -> should a '.' be added before fileExten? (Default=true)
	; Output variables
	; 	a filename of the script with the extension replaced with fileExten

	return getSupportFilename2(fileExten, addDot, "", False, "", False)
}

getSupportFilename2(fileExten, addDot=true, parentFolder="", useDefaultFolder=True, filenameSuffix="", useFullPath=true)
{ ; provides a filename of a support file (e.g., ini, ico), regradless of whether the script is compiled or not
	; Input variables
	; 	fileExten -> the file extension of the support file (place in quotes)
	; 	addDot -> should a '.' be added before fileExten? Allows you to use "log" instead of ".log" (Default=true)
	; 	parentFolder -> directory where the file should be located (Default="")
	; 	useDefaultFolder -> if parentFolder="" will place the file in a default folder based on fileExten (Default={"ini": "INI_Files", "ico": "Script_Images\Icons", "log": "Logs"})
	; 	filenameSuffix -> a string added between the filenameNoExt and the Exten (e.g., "_" . A_ComputerName) (Default="")
	; 	useFullPath -> return a FullPath or not.  If parentFolder isa fullpath this will be ignored. (Default=true)
	; Output variables
	; 	a filename of the script with the extension replaced with fileExten

	defaultArray := {"ini": "INI_Files", "ico": "Script_Images\Icons", "log": "Logs"}
	
	if(fileExten != "")
	{
		if(addDot)
		{
			replacementStr = .%fileExten%
		}
		else
		{
			replacementStr = %fileExten%
		}
	}
	else
	{
		replacementStr := ""
	}

	if(filenameSuffix != "")
	{
		replacementStr :=  filenameSuffix . replacementStr
	}
		
	if(A_IsCompiled)
	{
		StringReplace, fileName, A_ScriptName,.exe,%replacementStr%, All
	}
	else
	{
		StringReplace, fileName, A_ScriptName,.ahk,%replacementStr%, All
	}

	if(parentFolder == "")
	{
		if(useDefaultFolder)
		{
			1stChar := SubStr(fileExten, 1, 1)
			if (1stChar = ".")
			{
				fileExten1 := SubStr(fileExten, 2)
				; MsgBox, "dot: " . %fileExten1%
			}
			Else
			{
				fileExten1 := fileExten
				; MsgBox, "nul: " . %fileExten1%
			}
			For key, value in defaultArray
			{
				if( InStr(key, fileExten1) )
				{
					parentFolder := value
				}
			}
		}
	}

	if(parentFolder != "")
	{
		returnName := parentFolder . "\" . fileName
	}
	else
	{
		returnName := fileName
	}

	if(useFullPath)
	{
		; see https://www.autohotkey.com/boards/viewtopic.php?p=289536#p289536
		path := returnName
		cc := DllCall("GetFullPathName", "str", path, "uint", 0, "ptr", 0, "ptr", 0, "uint")
		VarSetCapacity(buf, cc*(A_IsUnicode?2:1))
		DllCall("GetFullPathName", "str", path, "uint", cc, "str", buf, "ptr", 0, "uint")
		returnName := buf
		; SplitPath, returnName, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
		; if(OutDrive = "") ; is it already a FullPath?
		; {
		; 	SplitPath, A_ScriptFullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
		; 	returnName := OutDir . "\" . returnName ; OutDir includes OutDrive
		; }
	}
	
	return returnName
}

getTempFilename(shortFilename)
{ ; creates a full path filename based on the Windows Temp directory
	; Input variables
	; 	shortFilename -> the short filename of the temp file you wish to use (e.g., demo.htm)
	; Output variables
	; 	fullFileName -> the full path filename of the temp file you wish to use (e.g., C:\Temp\demo.htm)

	SplitPath, shortFilename, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
	if(OutDrive != "") ; is it already a FullPath?
	{
		fullFilename := OutFileName
	}
	fullFilename = %A_Temp%\%fullFilename%
	return fullFilename
}



;==============================================================================
;========== Version History ===================================================
;==============================================================================
;BeginRegion
/*
; 1.0 2023-11-18
; * moved getSupportFilename(...), getSupportFilename2(...), and getTempFilename(filename) from Libray.ahk
;
;==============================================================================
*/
;EndRegion