UndoHistory

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
zcooler
Posts: 455
Joined: 11 Jan 2014, 04:59

UndoHistory

30 May 2016, 11:47

Hello!

I guess most folks has at least a few blank areas where they struggle and mine is definitely Associative Arrays :( In my defence I have to say what im trying to do here is quite intricate to begin with and Im coming up short. Im building an Undo feature in my GUI, where stuff like for example Undo Renaming of files and Undo copying files can be done. I have it working quite nicely, but there is a big drawback. There is no Undo queue, which implies only one Undo of the last action can be done. In comparison the shell for example queues up the Undo actions and you're abled to Undo as many actions you done since starting up the App. Replicate that is certainly no easy action or requires a big brain which seem to be in the freezer in my case ;)

So my thought was to have an UndoQueue associative array where the keys gets values from different arrays. When Undo is executed only the last added Key with values should be undone. If the UndoQueue contains other Undos (not executed yet) that one should be Undone next upon an Undo execution and so on. Im struggeling with how to sequence the keys to accomplish that. Maybe it's not possible doing it like that?
The second problem is how to retrieve the values from the UnRenameArray inside the UndoQueue AssocArray back to their original variables from an Object point of view, in order to do the actual Undo Rename?

So here is what I've got so far :)

Code: Select all

#NoEnv
; Create Associative Array UndoQueue
UndoQueue := {}
;-------------------------
; Undo renaming stuff
UndoOldName := "F:\10.jpg"
UndoNewName := "F:\555.jpg"
UndoOldID := "12447688"
UndoFileNameNoPath := "10.jpg"
UnRenameArray := [UndoOldName, UndoNewName, UndoOldID, UndoFileNameNoPath]
;-------------------------
; Undo copy stuff
UnCopyArray := []
UndoList=
(
F:\0.jpg
F:\888.jpg
)
UndoPath := "F:\TV" ; UndoList gets copied/pasted to UndoPath and gets deleted from there in case of an Undo 
Loop, Parse, UndoList, `n, `r
 {
   SplitPath, A_LoopField, UndoFilename
   UndoListCopy := UndoPath "\" UndoFilename ; Path swap for copied files
   UnCopyArray.Push(UndoListCopy)            ; Put the copied files in the UnCopyArray
 }
;-------------------------
UndoNameChange := True
UndoCopy := True
;-------------------------
If (UndoNameChange) {
  If (UnRenameArray.Length() = 0)     ; If no files in UnRenameArray key no action
    msgbox % "No Files in UnRenameArray"
  Else {                              ; UnRenameArray has content
    For Each, File in UnRenameArray {	  
	  UndoQueue[UnRenameArray, "UndoFile"] := File
	  UndoQueue[File] := {UnRenameArray: UndoFile}
	  UndoFile := UndoQueue[UnRenameArray, "UndoFile"]
	  msgbox % "UnRenameArrayFiles= " UndoFile 
	  ; How would one go about to get the values back into thier original variables? 
	  ; Restore the old filename here 
	  /*
	  If TVCache[UndoOldID, "IsDir"] {  ; folder
        DropEffect := 4   ; FO_RENAME SHFileOperation
        SHFileOperation_Internal([UndoNewName], UndoOldName, DropEffect)
        TV_Modify(UndoOldID, , UndoFileNameNoPath) ; Remove renamed file from TV if opened in other player
      }
      Else {       ; file
       DropEffect := 4   ; FO_RENAME SHFileOperation
       SHFileOperation_Internal([UndoNewName], UndoOldName, DropEffect)
       TV_Modify(UndoOldID, , UndoFileNameNoPath) ; Remove renamed file from TV if opened in other player
      }
	  */
	  ;UndoQueue.Delete(UnRenameArray) ; If Undo gets executed from context menu, UnRenameArray values gets deleted (same deal upon TV reload,
	  ;but there all keys are emptied). The Undo menu item gets disabled if no other UndoQueue key has content.
	  ;msgbox % UndoFile " files in UnMoveArray has been deleted"
	  UndoNameChange := False
    }
  }
} 
;-------------------------
If (UndoCopy) {
  If (UnCopyArray.Length() = 0)     ; If no files in UnCopyArray key no action
    msgbox % "No Files in UnCopyArray"
  Else {                              ; UnCopyArray has files
	For Each, File in UnCopyArray {
      UndoQueue[UnCopyArray, "UndoFile"] := File
      UndoQueue[File] := {UnCopyArray: UndoFile}
	  UndoFile := UndoQueue[UnCopyArray, "UndoFile"]
	  msgbox % "UnCopyArrayFiles= " UndoFile 
	  ;FileDelete, %UndoFile% ; Delete the files here
	  ;--------
	  ;UndoQueue.Delete(UnCopyArray) ; If Undo gets executed or upon TV reload values gets deleted 
	  ;from the UndoQueue and Undo menu item disabled if no other UndoQueue key has content
	  ;msgbox % UndoFile " files in UnCopyArray has been deleted"
	  UndoCopy := False
    }
  }
}
;-------------------------
Last edited by zcooler on 31 May 2016, 06:04, edited 1 time in total.
User avatar
waetherman
Posts: 112
Joined: 05 Feb 2016, 17:00

Re: Undo Queue Associative Array

30 May 2016, 12:55

Funny thing is, an undo queue is not a queue. A queue is FIFO, as in real life: https://en.wikipedia.org/wiki/Queue_%28 ... ta_type%29

Undo is FILO, just like a stack: https://en.wikipedia.org/wiki/Stack_%28 ... ta_type%29

In AHK, an array has a functionality of a stack. A normal array. An associative array perhaps as well, as long as you don't insert keys with numbers (I haven't really dived into array details in AHK so I'm not sure).

Why on Earth would you use an associative array to implement an undo queue, or as I like to call it, an undo history?

Here's how to implement undo:

Code: Select all

undoHistory := []
Here, done. :) Confused? Here's the UndoHistory class:

Code: Select all

class UndoHistory {
	history := []
	do( operation ) {
		this.history.push( operation )
	}
	undo() {
		return this.history.pop()
	}
}
It only gets complicated if you want to allow to redo, and even more complicated if you want to allow to redo once you did something after undoing (you need undo/redo trees for that).

For just undo/redo:

Code: Select all

class UndoManager {
	undo_history := []
	redo_history := []
	undo() {
		operation := this.undo_history.pop()
		this.redo_history.push( operation )
		return operation
	}
	redo() {
		operation := this.redo_history.pop()
		this.undo_history.push( operation )
		return operation
	}
	do( operation ) {
		this.undo_history.push( operation )
		this.redo_history := []
	}
}
Not hard at all! I assume your problem lies somewhere else than in the undo manager. You probably struggle in the process of:
1. While doing an operation, acquire the data to undo it (this is the data that you push to undo history)
2. When undoing, use the data from undo history to reverse the process.

The way to deal with these two points will be different for each program.
Image
zcooler
Posts: 455
Joined: 11 Jan 2014, 04:59

Re: Undo Queue Associative Array

30 May 2016, 14:38

Wow, thanks for this very comprehensive post :D It's not very easy finding such good information when not govern the programming lingo. Really nice class as well...I'll see if I can put it to use :) Having Redo seems over the top...then I'll have to spend a year on developing that one alone ;) It's sufficient with just Undo.

Many Thanks :wave:
zcooler
Posts: 455
Joined: 11 Jan 2014, 04:59

Re: UndoHistory

31 May 2016, 06:13

This works wonderful, thanks waetherman :D I had to learn how to use classes for this purpose :) Did some changes on the class, cuz I needed to know if the history array is empty in order to know when to disable the Undo Menu Item.

Code: Select all

class UndoHistory {
	history := []
	do( operation ) {
		this.history.push( operation )
	}
	undo() {
		return this.history.pop()
	}
	empty() {
	    loop % this.history.length()
          return this.history[a_index]
	}
}


Regards
zcooler :wave:
zcooler
Posts: 455
Joined: 11 Jan 2014, 04:59

Re: UndoHistory

01 Jun 2016, 14:18

Hello!

Ok, I now have the UndoHistory working perfectly and I can undo all operations made since script start. It's only one thing left...statusbar info on which UndoOperation is in the UndoHistory, before it gets deleted upon an uH.undo() call. It's possible to use true or false flags, but it's rather tricky to place them out when script is now rather huge and scattered. It's much more interesting to learn how to do it via the class :)

I decided to pass a string to the class instead of an array, cuz I don't exactly know how to do the latter and how to traverse it back to variables upon uH.undo(). Using the ParamToObj() function for traversing back now.

So how do I return the UndoOperation value "UndoMoveCopy" when calling msgbox % uH.do(param1)?

Code: Select all

#NoEnv
List1=
(
File1
File2
)
Loop, Parse, List1, `n, `r
  UndoFileList .= (A_Index > 1 ? "`n" : "") A_LoopField 
FilePath := "null"
NewFilePath := "null"
ItemID := "null"
DropEffect := 5
param1 := "UndoHistory -UndoOperation=UndoMoveCopy -FilePath=" FilePath " -NewFilePath=" NewFilePath " -ItemID=" ItemID " -DropEffect=" DropEffect " -UndoFileList=" UndoFileList
;----
uH := new UndoneHistory()
msgbox % uH.do(param1)  ; need to get the UndoOperation value (UndoMoveCopy) before it gets deleted in the next call.  
undoHistory := uH.undo()
;----
undoHistory := ParamToObj()
UndoOperation := RTrim(undoHistory.UndoOperation)
FilePath := RTrim(undoHistory.FilePath)
NewFilePath := RTrim(undoHistory.NewFilePath)
ItemID := RTrim(undoHistory.ItemID)
DropEffect := RTrim(undoHistory.DropEffect)
UndoFileList := RTrim(undoHistory.UndoFileList)

msgbox % "UndoOperation= " UndoOperation "`nFilePath= " FilePath "`nNewFilePath= " NewFilePath "`nItemID= " ItemID "`nDropEffect= " DropEffect "`nUndoFileList= " UndoFileList

return
class UndoneHistory {
	history := []
	do( operation ) {
		this.history.push( operation )
	}
	undo() {
		return this.history.pop()
	}
	empty() {
	    loop % this.history.length()
          return this.history[a_index]
	}
}
; ==================================================================================================================================
ParamToObj() {
  global undoHistory
  obj := {}
  Loop, Parse, undoHistory, -,
   {
	 newparam := "-" A_LoopField
	 If RegExMatch(newparam, "^(/|-)+(\w+)(\W(.+))?$", match) {
	   obj[match2] := match4
	   key := match2
	 }
	 Else If (key != "")
	   obj[key] := undoHistory, key := ""
   }
  Return obj
}
User avatar
waetherman
Posts: 112
Joined: 05 Feb 2016, 17:00

Re: UndoHistory

01 Jun 2016, 14:37

Code: Select all

uH := new UndoneHistory()
msgbox % uH.do(param1)  ; need to get the UndoOperation value (UndoMoveCopy) before it gets deleted in the next call.  
undoHistory := uH.undo()
;----
undoHistory := ParamToObj()
I don't get it. uH becomes an instance of UndoneHistory class. Then undoHistory becomes whatever undomethod of uH returns. Just after that undoHistory becomes a return value of ParamToObj function. What's the point in doing this:

Code: Select all

a := new ClassA()
a := b()
function b() {
   global a
   a.doSomething()
   return a
}
versus this:

Code: Select all

a := new ClassA()
b()
function b() {
global a
a.doSomething()
}
And why in heavens do you store undoHistory inside another undo[ne]History?
Image
zcooler
Posts: 455
Joined: 11 Jan 2014, 04:59

Re: UndoHistory

01 Jun 2016, 15:10

Sorry, im very new to this field and I could not get it to work the way you anticipated. After 20 hours of testing and failing I went with what works ;)
I may break a few rules, but this stipped down version works great in the big script.
User avatar
waetherman
Posts: 112
Joined: 05 Feb 2016, 17:00

Re: Undo Queue Associative Array

01 Jun 2016, 16:17

Well I kind of attacked you with OOP, but I thought You will understand what I mean in my first post (I forgot to subscribe to the thread too). So when I wrote:
waetherman wrote:Here's how to implement undo:

Code: Select all

undoHistory := []
Here, done. :) Confused? Here's the UndoHistory class:

Code: Select all

class UndoHistory {
	history := []
	do( operation ) {
		this.history.push( operation )
	}
	undo() {
		return this.history.pop()
	}
}
I basically meant, that instead of creating some complex undoHistory, you can just use a stack. So instead of

Code: Select all

uh := new UndoHistory
uh.do( something )
processUndoData( uh.undo() )
You could just do this:

Code: Select all

uh := []
uh.push( something )
processUndoData( uh.pop() )
But if you like OOP approach so much, here's how OOP nuts would deal with such a simple task:

Code: Select all

uh := new SimpleUndoHistory
value := 5

calc( "+3" )
calc( "*2" )
calc( "/1.5" )
calc( "-1" )

undo()
undo()
undo()
undo()
undo() ;too many undos
undo()

class SimpleUndoHistory {
	history := []
	do( operation ) {
		this.history.push( operation )
	}
	undo() {
		return this.history.pop()
	}
}

class Operation {
	static TYPE_ADD := "+" ; you can tell I've been scarred by OOP
	static TYPE_SUB := "-"
	static TYPE_MULTIPLY := "*"
	static TYPE_DIVIDE := "/"
	
	__New( type, operand1, operand2 ) {
		this.type := type
		this.operand1 := operand1
		this.operand2 := operand2
	}
}

calc(simpleExpression) {
	global uh
	global value
	
	operator := SubStr( simpleExpression, 1, 1 )
	operand2 := SubStr( simpleExpression, 2 )
	
	if ( operator = Operation.TYPE_ADD ) {
		result := value + operand2
	} else if ( operator = Operation.TYPE_SUB ) {
		result := value - operand2
	} else if ( operator = Operation.TYPE_MULTIPLY ) {
		result := value * operand2
	} else if ( operator = Operation.TYPE_DIVIDE ) {
		result := value / operand2
	} else {
		MsgBox FATAL NONDESCRIPTIVE ERROR THE WORLD WILL EXPLODE
		ExitApp
	}
	
	MsgBox % value operator operand2 "=" result
	uh.do( new Operation(operator, value, operand2) )
	value := result
	
}

undo() {
	global uh
	global value
	
	op := uh.undo()
	if ( op.type = Operation.TYPE_ADD ) {
		result := value - op.operand2 ; reverse operation aproach
	} else {
		result := op.operand1 ; state restore aproach - easier, but not always possible
	}
	MsgBox % value " ==> Undo: " op.operand1 op.type op.operand2 " ==> " result
	value := result
}

Image
zcooler
Posts: 455
Joined: 11 Jan 2014, 04:59

Re: UndoHistory

02 Jun 2016, 09:47

Thanks for the OOP example waetherman :) I realize I have much studying to do before grasping this concept.

Regards
zcooler :wave:

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: No registered users and 345 guests