Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

Recover accidentally closed Windows Explorer windows


  • Please log in to reply
2 replies to this topic
thinkstorm
  • Members
  • 40 posts
  • Last active: Sep 10 2014 06:06 PM
  • Joined: 17 Aug 2004
I sometimes click ALT-F4 or CTRL-w too fast and close a Windows Explorer window. So I wrote a "Kill Ring" for Windows Explorer. it's basic (as always), and holds the last five window paths that got closed. A few notes:
[*:22fc51wl] One of the obstacles was that I didn't want to use timers to look for windows, so I used a windows SHELLHOOK, which is not really future-proof. Also, I noticed that in case of a DESTROY, the hidden file path text would also be destroyed, so I am also reacting on UPDATE messages to update the file path
[*:22fc51wl] I created a more complex object structure in case you want to store more information in the objects that you closed - just add more fields to the Onbject(key, value, key, value ....) field and make sure you copy the values into the kill ring
[*:22fc51wl] There are some special folders like Desktop, Workstation, Recycling bin, etc. Unfortunately, I currently are running a German OS, so you will have to adjust the names by hand in C_SPECIALLOCATIONS. The easiest way is probably to copy the CSIDs in the RUN dialog of Windows and see what the names of the windows are that open ;)
[*:22fc51wl] UPDATE 2011-08-31: Added a nicer kill ring overview with keyboard navigation (up/down, enter). ESC exits the window
[*:22fc51wl] UPDATE two on 2011-08-31: oh man, my time functions were so far off, I'm really sorry, this was embarrassing... hopefully fixed now.
I use my simple debugger script "debug.ahk" that is #include'd:
_USE_DEBUG := true
_DEBUG_CONSOLE := "SCITE"

hDebugStd := -1
DllCall("AttachConsole", int, -1, int)
DllCall("AllocConsole", int)

_debug( text, term="`n" )
{
	global _USE_DEBUG, _USE_SCITE,_DEBUG_CONSOLE
    static hDebugStd=-1

	if not (_USE_DEBUG)
		Return
    if (hDebugStd = -1) {
        hDebugStd := DllCall("GetStdHandle", "UInt", -11) ; -11=STD_OUTPUT_HANDLE
        if ErrorLevel
            return 0
    }
    text .= term
	StringCaseSense, OFF
	if (_DEBUG_CONSOLE="SCITE") {
		FileAppend  %text%, *
	} else if (_DEBUG_CONSOLE="OUTPUTDEBUG") {
		OutputDebug, %text%
	} else if ((_DEBUG_CONSOLE="VS") || (_DEBUG_CONSOLE="DBGVIEW")) {
		OutputDebug, %text%
	} else {
		FileAppend %str%, CONOUT$
	}
	return ErrorLevel
}

The main program is here:
#SingleInstance Force
SetBatchLines, -1

;#include %A_ScriptDir%\lib\about.ahk
#include %A_ScriptDir%\lib\debug.ahk
_USE_DEBUG := false ; DEBUG OFF
_DEBUG_CONSOLE := "SCITE"
_debug("----- started --------")
APPNAME := "Windows Explorer Kill Ring"
WINTITLE := APPNAME . " ahk_class AutoHotkeyGUI"

C_OSDTIME_MS := 1000 ; the OSD menu is show for x milliseconds
C_KILLRINGSIZE := 10 ; number of last windows explore paths to store

; -------------------------------- HOTKEYS -----------------------------------
Hotkey, #esc, RestoreLastWindow
Hotkey, +#esc, ShowCurrentKillRing
Hotkey, IfWinActive, %WINTITLE%
Hotkey, up, SelectUp
Hotkey, down, SelectDown
; ----------------------------- TRAY MENU ICON -------------------------------
Menu, Tray, NoStandard
Menu, Tray, add, Exit, gtfo
; Menu, Tray, add, About, showAbout
Menu, Tray, Default, Exit
; Menu, Tray, Icon, %A_WorkingDir%\WindowsExplorerKillRing.ico
; ----------------------------- CONSTANTS ------------------------------------
col_window := "cFCFCFC"
col_killpath := "c1A1A1A"
col_datetime := "c00AA00"
col_killpathselect := "cFF0088"
col_datetimeselect := "c444444"
HSHELL_WINDOWDESTROYED := 2
HSHELL_REDRAW := 6
C_EXPLORER := "C:\WINDOWS\explorer.exe"
C_SPECIALLOCATIONS := Object("Arbeitsplatz","::{20d04fe0-3aea-1069-a2d8-08002b30309d}","Eigene Dateien","::{450d8fba-ad25-11d0-98a8-0800361b1103}", "Netzwerkumgebung","::{208d2c60-3aea-1069-a2d7-08002b30309d}", "Netzwerkverbindungen","::{7007acc7-3202-11d1-aad2-00805fc1270e}", "Drucker und Faxgeräte","::{2227a280-3aea-1069-a2de-08002b30309d}", "Papierkorb","::{645ff040-5081-101b-9f08-00aa002f954e}", "Geplante Tasks","::{d6277990-4c6a-11cf-8d87-00aa0060f5bf}")
C_HIGHLIGHT := "1px_1A1A1A.bmp"
; ----------------------------- VARIABLES ------------------------------------
explorerWindows := Object() ;stores all explorer objects
ringpos := 0
killRing := Object()
killRingSelected := 0
loop, %C_KILLRINGSIZE% {
	killRing[ringpos] := Object("path","","datetime",A_Now)
	_debug("initialized killRing[" . ringpos . "]")
	ringpos := MOD(ringpos + 1, C_KILLRINGSIZE)
}
; -------------------------- WINDOW MESSAGING HOOK ---------------------------
DetectHiddenWindows, On
guiid := WinExist(A_ScriptFullPath)
DllCall( "RegisterShellHookWindow", UInt,guiid)
MsgNum := DllCall( "RegisterWindowMessage", Str,"SHELLHOOK" )
OnMessage( MsgNum, "ShellMessage" )
Return ; end of auto execution


; ============================== WORK ========================================

ShellMessage(wParam,lParam) {
	global HSHELL_WINDOWDESTROYED, HSHELL_REDRAW
	
	WinGetClass, winclass, ahk_id %lParam%
	if (winclass <> "CabinetWClass") {
		Return ; not a windows explorer window
	}
	if (wParam = HSHELL_REDRAW) {
		CreateOrUpdatePath(lParam)
	}
	if (wParam = HSHELL_WINDOWDESTROYED) {
		StorePathInKillRing(lParam)
	}
}

CreateOrUpdatePath(lParam) {
	global explorerWindows
	
	SetTitleMatchMode, slow
	DetectHiddenText, OFF
	WinGetText, thepath, ahk_id %lParam% ; I found out through Autohotkey's WindowSpy that this holds the path of the window, but in two lines
	if (thepath = "") 
	{
		_debug("### REDRAW: no path for '" . lParam . "'") ; happens...
		Return
	}
	pos := InStr(thepath,"`r`n",1,1) ; get the linebreak, if any
	if (pos) {
		thepath := SubStr(thepath, 1, InStr(thepath,"`r`n",1,1)-1) ; get first line
	}
	If (isObject(explorerWindows[lParam])) { ; update path
		explorerWindows[lParam]["path"] := thepath
		_debug("explorerWindows[" . lParam . "]: '" . explorerWindows[lParam]["path"] . "' (update)")
	} else { ; create new object and store path
		explorerWindows.Insert(lParam,Object("path",thepath, "datetime", ""))
		_debug("explorerWindows[" . lParam . "]: '" . explorerWindows[lParam]["path"] . "' (NEW)")
	}
}

StorePathInKillRing(lParam) {
	global explorerWindows, killRing, ringpos, C_KILLRINGSIZE
	if not (isObject(explorerWindows[lParam])) {
		_debug("### DESTROY: 'explorerWindows[" . lParam . "]': no such object")
		Return
	}
	if (explorerWindows[lParam]["path"] = "") {
		_debug("### DESTROY: empty path for 'explorerWindows[" . lParam . "]'")
		Return
	}
	_debug("found explorerWindows[" . lParam . "][""path""] '" . explorerWindows[lParam]["path"] . "'") 
	killRing[ringpos]["path"] := explorerWindows[lParam]["path"]
	killRing[ringpos]["datetime"] := A_Now
	_debug(">>> killRing[" . ringpos . "] = '" . killRing[ringpos]["path"] . "' (" . killRing[ringpos]["datetime"] . ")")
	if not (explorerWindows.Remove(lParam)) {
		_debug("### couldn't remove the object at '" . lParam . "'")
	}
	ringpos := Mod(ringpos + 1, C_KILLRINGSIZE)
}

; ============================= RESTORE LAST WINDOW ===========================
RestoreLastWindow:
	ringpos := ringpos - 1
	if (ringpos < 0) {
		ringpos := C_KILLRINGSIZE-1
	}
	If (killRing[ringpos]["path"]="") {
		_debug("killRing[" . ringpos . "][""path""] is empty")
		ShowOSD("--- no more old explorer windows to restore ---")
		Return
	}
	command := killRing[ringpos]["path"]
	;datetime := killRing[ringpos]["datetime"]
	killRing[ringpos]["path"] := ""
	killRing[ringpos]["datetime"] := ""
	StringCaseSense, OFF
	for localname, csid in C_SPECIALLOCATIONS {
		if (command=localname) {
			showOSD("restoring " . localname)
			Run, %csid%
			Return
		}
	}
	If (InStr(FileExist(command), "D")) {
		showOSD("restoring " . command)
		_debug("restoring " . command)
		Run, %C_EXPLORER% "%command%"
	} else {
		showOSD("directory '" . command . "' does not exist anymore")
		_debug("directory '" . command . "' does not exist anymore")
	}
Return

; =============================== OSD =========================================
Gui1:=0
showOSD(text) {
	global Gui1, C_OSDTIME
	SetTimer, hideOSD, OFF
	Gui, 1: Color, 006600,FCFCFC
	Gui, 1: +Toolwindow -Resize -SysMenu -Border -Caption +AlwaysOnTop +LastFound
	WinSet, Transparent, 0
	transp := 0
	Gui1 := WinExist()
	Gui, 1: Font,cF9F9F9 s24,Corbel ; preferred font (as it is the last one set, if available)
	Gui, 1: Add, Text,center w700, %text%
	Gui, 1: Show, NoActivate Center AutoSize
	Loop, 10 {
		transp := transp + 23
		WinSet, Transparent, %transp%
		sleep, 15
	}
	SetTimer, hideOSD, %C_OSDTIME_MS%
}

hideOSD: 
	SetTimer, hideOSD, OFF
	transp := 220
	WinSet, Transparent, 250, ahk_id %Gui1%
	sleep, 80
	Loop, 10
	{
		transp := transp - 12
		WinSet, Transparent, %transp%, ahk_id %Gui1%
		sleep, 25
	}
	Loop, 18
	{
		transp := transp - 5
		WinSet, Transparent, %transp%, ahk_id %Gui1%
		sleep,15
	}
	Gui1 := 0
	Gui, 1:Hide
	Gui, 1:Destroy
Return

; ========================== SHOW CURRENT KILL RING ===========================

ShowCurrentKillRing:
	Gui2:=0
	Gui, 2: Color, %col_window%, %col_killpath%
	Gui, 2: +Toolwindow -Resize -SysMenu -Border -Caption +AlwaysOnTop +LastFound
	WinSet, Transparent, 0
	Gui2 := WinExist()
	killRingSelected := C_KILLRINGSIZE ; indicates "no selection yet"
	i:= ringpos
	entries := 0
	Loop, %C_KILLRINGSIZE% {
		i := i - 1
		if (i<0)
			i := C_KILLRINGSIZE - 1
		if not (isObject(killRing[i])) {
			_debug("### showKillRing: killRing[" . i . "] is not an object")
			continue ; continue with next killRing item
		}
		if (killRing[i]["path"] = "") {
			_debug("showKillRing: found empty path")
			break ; break the loop, we're done
		}
		entries += 1
		y:=5+(42*(entries-1))
		killpath := killRing[i]["path"]
		if (strlen(killpath) > 40) {
			killpath := SubStr(killpath,1,18) . " ... " . SubStr(killpath,-18)
		}
		datetime := MyDateTimeFormat(killRing[i]["datetime"])
		Gui, 2: Add, Picture, hidden w735 h35 x5 y%y% vgui_mark%i%,  %C_HIGHLIGHT%
		Gui, 2: Font, %col_killpath% normal s20, Corbel
		Gui, 2: Add, Text, w500 h35 x5 y%y% -wrap vgui_kp%i% BackgroundTrans +Right, %killpath%
		Gui, 2: Font, %col_datetime% normal, Corbel
		Gui, 2: Add, Text, w220 h35 x515 y%y% -wrap vgui_dt%i% BackgroundTrans +Left, %datetime%
		_debug("showKillRing: " . killpath . "   (" . datetime . ")")
	}
	if (!entries) {
		Gui, 2: Font, %col_killpath% normal s28, Corbel
		Gui, 2: Add, Text, center w700 x5 y5, --- empty ---
		_debug("showKillRing: --- empty ---")
	}
	Gui, 2: Add, Button, x-10 y-10 w1 h1 +default hidden gCurrentKillRingSelect, Exit
	Gui, 2: Show, Activate Center AutoSize, %APPNAME%
	transp := 0
	Loop, 10 {
		transp := transp + 23
		WinSet, Transparent, %transp%
		sleep, 15
	}
	SetTimer, watchFocus_ShowCurrentKillRing, 250
Return

watchFocus_ShowCurrentKillRing:
	GuiControlGet, hasFocus, 2:Focus
	if (!hasFocus) {
		Goto HideCurrentKillRing
	}
Return

CurrentKillRingSelect:
	If (killRingSelected = C_KILLRINGSIZE) { ; indicates "no selection yet"
		_debug("enter was pressed, but nothing was selected")
		GoSub HideCurrentKillRing
		Return
	}
	SetTimer, watchFocus_ShowCurrentKillRing, OFF
	Gui2 := 0
	Gui, 2:Hide
	Gui, 2:Destroy
	If (killRing[killRingSelected]["path"]="") {
		_debug("killRing[" . killRingSelected . "][""path""] is empty")
		ShowOSD("--- sorry, cannot restore ---")
		Return
	}
	command := killRing[killRingSelected]["path"]
	datetime := killRing[killRingSelected]["datetime"]
	_debug("killRing[" . killRingSelected . "][""path""] is " . command)
	_debug("killRing[" . killRingSelected . "][""datetime""] is " . datetime)
	StringCaseSense, OFF
	for localname, csid in C_SPECIALLOCATIONS {
		if (command=localname) {
			showOSD("restoring " . localname)
			Run, %csid%
			Return
		}
	}
	If (InStr(FileExist(command), "D")) {
		showOSD("restoring " . command)
		Run, %C_EXPLORER% "%command%"
	} else {
		showOSD("directory '" . command . "' does not exist anymore")
	}
Return

hideCurrentKillRing:
	SetTimer, watchFocus_ShowCurrentKillRing, OFF
	transp := 220
	WinSet, Transparent, 250, ahk_id %Gui2%
	sleep, 80
	Loop, 10
	{
		transp := transp - 12
		WinSet, Transparent, %transp%, ahk_id %Gui2%
		sleep, 25
	}
	Loop, 18
	{
		transp := transp - 5
		WinSet, Transparent, %transp%, ahk_id %Gui2%
		sleep,15
	}
	Gui2 := 0
	Gui, 2:Hide
	Gui, 2:Destroy
Return

SelectUp:
	if ((!WinActive(WINTITLE)) || (!Gui2) || (!entries)) {
		_debug("up key not allowed right now")
		Return
	}
	if (killRingSelected = C_KILLRINGSIZE) { ; indicates nothing selected yet
		killRingSelected := ringpos
	} else {
		GoSub Gui2Unselect
		killRingSelected += 1 ; select newer kill ring entry
	}
	if (killRingSelected > (entries-1)) {
		killRingSelected := 0
	}
	_debug("selected kill ring entry " . killRingSelected) 
	GoSub Gui2Select
Return

SelectDown:
	if ((!WinActive(WINTITLE)) || (!Gui2) || (!entries)) {
		_debug("down key not allowed right now")
		Return
	}
	if (killRingSelected = C_KILLRINGSIZE) { ; indicates nothing selected yet
		_debug("nothing selected, setting selection to " . )
		killRingSelected := ringpos-1
	} else {
		GoSub Gui2Unselect
		killRingSelected -= 1 ; select older kill ring entry
		if (killRingSelected < 0) {
			killRingSelected := max(entries - 1, ringpos-1)
		}
	}
	_debug("selected kill ring entry " . killRingSelected) 
	GoSub Gui2Select
Return

Gui2Unselect:
	if (killRingSelected=C_KILLRINGSIZE) ; indicates "no selection yet"
		Return ; nothing yet selected
	GuiControl 2:Hide,gui_mark%killRingSelected%
	Gui, 2: Font, %col_killpath% normal s20, Corbel
	GuiControl 2:Font,gui_kp%killRingSelected%
	Gui, 2: Font, %col_datetime% normal, Corbel
	GuiControl 2:Font,gui_dt%killRingSelected%
Return

Gui2Select:
	if (killRingSelected=C_KILLRINGSIZE) ; indicates "no selection yet"
		Return ; nothing selected, should not happen ;)
	GuiControl 2:Show,gui_mark%killRingSelected%
	Gui, 2: Font, %col_killpathselect% normal s20, Corbel
	GuiControl 2:Font,gui_kp%killRingSelected%
	Gui, 2: Font, %col_datetimeselect% normal, Corbel
	GuiControl 2:Font,gui_dt%killRingSelected%
Return

2GuiClose:
2GuiEscape:
	GoSub HideCurrentKillRing
Return

; ========================= HELPER FUNCTIONS ==================================

MyDateTimeFormat(datetime) {
	_debug("#MyDateTimeFormat: datetime is " . datetime)
	lastfiveminutes := A_Now
	lastfiveminutes += -5, minutes
	_debug("#MyDateTimeFormat: lastfiveminutes is " . lastfiveminutes . " (vs. " . datetime . ")")
	if (datetime > lastfiveminutes)
		return "recently"
	lasthour := A_Now
	lasthour += -1, hours
	_debug("#MyDateTimeFormat: lasthour is " . lasthour . " (vs. " . datetime . ")")
	if (datetime > lasthour)
		return "last hour"
	; from here we see the bigger picture
	FormatTime, thedate, datetime, yyyyMMdd
	FormatTime, today,,yyyyMMdd
	_debug("#MyDateTimeFormat: today is " . today . " (vs. " . thedate . ")")
	if (today = thedate)
		return "today"
	yesterday := A_Now
	yesterday += -1, days
	FormatTime, yesterday, yesterday, yyyyMMdd
	if (yesterday = thedate)
		return "yesterday"
	FormatTime output, datetime, ddd MM/dd
	return output
}

max(a,b){
	Return % a > b ? a : b
}

; =========================== ABOUT DIALOG ====================================
/*
showAbout:
	_about(APPNAME)
Return

_aboutOpenLink:
	if (_aboutguid) {
		Run, http://www.thinkstorm.com
	}
Return

88GuiEscape:
88GuiClose:
	if (_aboutguid)  {
		_aboutHide()
	}
Return
*/
; ================================= THE END ===================================

gtfo:
ExitApp

Cheers,

Thorsten

thinkstorm
  • Members
  • 40 posts
  • Last active: Sep 10 2014 06:06 PM
  • Joined: 17 Aug 2004
added a nicer kill ring overview with up/down arrow navigation - not clickable, sorry. Window fades out if it loses focus or by pressing ESC. A selection is opened when pressing ENTER.
Cheers,

Thorsten

thinkstorm
  • Members
  • 40 posts
  • Last active: Sep 10 2014 06:06 PM
  • Joined: 17 Aug 2004
Sorry for the re-edit again, but the time function was so far off it wasn't even funny anymore. It looks like it's working now (on XP, who knows what happens on Windows 7 ...)
Cheers,

Thorsten