Home Technical Talk

[3DSMAX] Creating a History Window

polycounter lvl 13
Offline / Send Message
Pinned
Pdude2K7 polycounter lvl 13
Hey guys,
There was a feature I always wanted in 3dsmax but never actually found anything acceptable as a substitute for and that is an actions history window. I really like to see what actions I am doing
to prevent accidentally moving and doing collapses and welds as I try to work fast.

I know you can open the maxscript listener and see actions there but I'd much rather having
something like the right-click undo window permanently visible that I can casually glance at.

The question is whether its possible to have it either with some sort of hack(right now I have an autohotkey script that opens that window for me) or by tapping into the sdk or even via maxscript.

I'd love any input on this as its a feature I'd like to use on a regular basis.

Replies

  • Eric Chadwick
    There's a popup window you get if you right-click the Undo button. Or is the Redo button? Maybe script that into a persistent window.

    Hey look at this
    http://www.scriptspot.com/3ds-max/scripts/multipurpose-selection-bar
    ( I searched scriptspot for "undo history" )
  • Pdude2K7
    Offline / Send Message
    Pdude2K7 polycounter lvl 13
    Yeah, I'm having an autohotkey script to open that popup window when I press a key and toggle between undo and redo without moving the mouse but since I use it so much I'd like to have something persistent.

    Thanks for posting that script, it seems to do what I want however I think its bugged since the undo doesn't show either in max 2015 or 2017.

    I think I'll try to tap into the events that get called when there's an undo and read the label of that, I could use the script you posted as reference even if it doesn't seem to work per se.
  • Swordslayer
    Offline / Send Message
    Swordslayer interpolator
    Hey look at this
    http://www.scriptspot.com/3ds-max/scripts/multipurpose-selection-bar
    ( I searched scriptspot for "undo history" )
    That one registers a callback that's triggered only when you undo something or redo something - unless you undo every action you perform each and every time, it won't give the history you want. Not sure if there's anything better than what you have - the bad news is that unlike with other dialogs that are just hidden, the undo winow is destroyed and the window handle is different every time, so you'd have to repeatedly display it and parse the listbox contents if you wanted to keep track of it.
  • Pdude2K7
    Offline / Send Message
    Pdude2K7 polycounter lvl 13
    @Swordslayer, you're right. It seems a much bigger issue than I thought. From my research into it there's two possible ways to do it with the first one being easier but might not be entirely feasible. Firstly is by tapping into the events for move, rotate and scale along with possible others via maxscript and then just print these as they happen, only these events will be supported but its better than nothing. The other is the more proper way which is through the sdk by checking theHold variable and the undo system and getting action labels from that somehow.
  • Swordslayer
    Offline / Send Message
    Swordslayer interpolator
    SDK won't help you here, this part is a complete blackbox - it's a part of the codebase not included with the SDK with no relevant methods in the header files. If there's a way to keep track of the history, it will have to be some clever hack.
  • Pdude2K7
    Offline / Send Message
    Pdude2K7 polycounter lvl 13
    Hmm that's interesting, I wonder what can be done to make something that will be usable.
    There must be some array that I can access that will have the undo labels.

  • Swordslayer
  • Pdude2K7
    Offline / Send Message
    Pdude2K7 polycounter lvl 13
    Thanks for the link. I guess there must be some hack then by getting the window stuck on. Either that or back to the original way with maybe Maxscript but I haven't found callbacks to move, rotate or scale, let alone extrude or bevel or any editable poly actions.
  • Swordslayer
    Offline / Send Message
    Swordslayer interpolator
    They're no callbacks for extrude, bevel etc. per se, they all trigger the topologyChanged one. Same if you collapse a model, convert to editable poly, convert to editable mesh etc, that will be modelStructured. You can compile your own version of editable poly that will send some of this info to your tool if you're comfortable with C++ and max SDK, though. What I meant when I mentioned a hack was more like running a timer that periodically opens and destroys the undo window and parses the listbox contents, compares to items you already stored and if there're any new ones, adds them to the list.
  • Pdude2K7
    Offline / Send Message
    Pdude2K7 polycounter lvl 13
    Yeah I figured that callback will be grouped like that. Any idea about move, rotate and scale callbacks?
    Also when you say open the window and parse, you mean pixel read the text via something like autohotkey?
  • Swordslayer
    Offline / Send Message
    Swordslayer interpolator
    Move, rotate, scale are similar - in editable geometry subobjects, that would be geometryChanged, in object mode you could catch it with when transform obj changes, with modifier subobjects you'd have to register change handlers monitoring the subanim changes etc.
    No need for capturing pixels, once you have the window handle, get the child Listbox handle and enumerate its items.
  • Pdude2K7
    Offline / Send Message
    Pdude2K7 polycounter lvl 13
    I did a few tests and it seems like using node events is problematic since many things trigger the same events. For example if I move, rotate, scale, do any mesh ops or even just undo I get geometryChanged. Is there a way to isolate it so its clear at least when I actually moved, rotated or scale vs did any mesh ops? also not getting undo to trigger that would be good.
  • Swordslayer
    Offline / Send Message
    Swordslayer interpolator
    You'd get deep into the rabbithole with this approach, with each item you handle there will be multiple exceptions and different behaviors. Here, I made a barebones undo items collector - it will popup a rollout pre-filled with the contents of undo history listbox. Feel free to use that as a starting point, add a timer to add items to the history etc:

    (<br>	local items = #()<br><br>	fn showItems =<br>		createDialog (rollout undoItems "Undo Items" (listBox lbItems items:items;))<br><br>	fn sendRightClick hWnd =<br>	(<br>		local WM_RBUTTONDOWN = 0x0204<br>		windows.postMessage hWnd WM_RBUTTONDOWN 0 0<br>		windows.postMessage hWnd (WM_RBUTTONDOWN + 1) 0 0<br>	)<br><br>	fn getListBoxLength hWnd =<br>	(<br>		local LB_GETCOUNT = 0x18B<br>		windows.sendMessage hWnd LB_GETCOUNT 0 0<br>	)<br><br>	fn getListBoxItemText hWnd i =<br>	(<br>		local LB_GETTEXT = 0x189<br>		local LB_GETTEXTLEN = 0x18A<br>		local marshal = dotNetClass "System.Runtime.InteropServices.Marshal"<br><br>		local len = windows.sendMessage hWnd LB_GETTEXTLEN i 0<br>		local lParam = marshal.AllocHGlobal (2 * len + 2) asDotNetObject:on<br>		windows.sendMessage hWnd LB_GETTEXT i (lParam.ToInt64())<br><br>		local str = (marshal.PtrToStringAuto lParam asDotNetObject:on).ToString()<br>		marshal.FreeHGlobal lParam<br>		return str<br>	)<br><br>	fn collectUndoItems =<br>	(<br>		local hWnd = DialogMonitorOPS.getWindowHandle()<br><br>		if UIAccessor.getWindowText hWnd == "" and<br>		   UIAccessor.getWindowClassName hWnd == "Dialog" then<br>		(		<br>			local listBoxHWnd = for ctrl in (windows.getChildrenHWnd hWnd) where ctrl[4] == "ListBox" do exit with ctrl[1]<br>			local itemCount = getListBoxLength listBoxHWnd<br>			items = for i = 0 to itemCount - 1 collect getListBoxItemText listBoxHWnd i<br><br>			UIAccessor.closeDialog hWnd<br>			showItems()<br>			<br>			DialogMonitorOPS.enabled = off<br>			DialogMonitorOPS.unRegisterNotification id:#getUndoStack<br><br>			true<br>		)<br>		else false<br>	)<br><br>	local maxChildren = windows.getChildrenHWnd #max<br>	local undoBtnHWnd = for hWndData in maxChildren where UIAccessor.getWindowResourceID hWndData[1] == 50034 do exit with hWndData[1]<br><br>	if isKindOf undoBtnHWnd Number then<br>	(<br>		DialogMonitorOPS.unRegisterNotification id:#getUndoStack<br>		DialogMonitorOPS.registerNotification collectUndoItems id:#getUndoStack<br>		DialogMonitorOPS.enabled = on<br><br>		sendRightClick undoBtnHWnd<br>	)<br>	else messageBox "Couldn't find the undo button."<br>)
  • Pdude2K7
    Offline / Send Message
    Pdude2K7 polycounter lvl 13
    Swordslayer - I seriously can't believe I missed this post! Thanks a lot for taking the time to do this, it helps a whole bunch.

    I am gonna try to take this as a base and with my limited Maxscript knowledge make it so when I press a key or after an event fires it will update the list in the rollout
    .

    I will update once I have something working.
  • Pdude2K7
    Offline / Send Message
    Pdude2K7 polycounter lvl 13
    @Swordslayer - Okay I made the script to print the last 6 actions to the listener since I dont have enough experience with maxscript to make a rollout with a working update button.
    Its bare bones but it works. When I hit a key I have Autohotkey set up to clear the listener window and hit the Undo Script button in the UI to update the list.

    It makes it looks like this:


    Please let me know if you have an idea on how I can change this script to have an update button instead:

    macroScript Undoer
    category:"Undoer"
    buttontext:"UndoerGO"
    toolTip:"UndoerGOGO"
    --icon:#()
    
    (
    	(
    		--FUNCTIONS	
        	local items = #()
    
        	fn showItems =
            lbItems = items
        		--listBox lbItems items:items
    				
    				
        	fn sendRightClick hWnd =
        	(
        		local WM_RBUTTONDOWN = 0x0204
        		windows.postMessage hWnd WM_RBUTTONDOWN 0 0
        		windows.postMessage hWnd (WM_RBUTTONDOWN + 1) 0 0
        	)
    		
    		
    
        	fn getListBoxLength hWnd =
        	(
        		local LB_GETCOUNT = 0x18B
        		windows.sendMessage hWnd LB_GETCOUNT 0 0
        	)
    
        	fn getListBoxItemText hWnd i =
        	(
        		local LB_GETTEXT = 0x189
        		local LB_GETTEXTLEN = 0x18A
        		local marshal = dotNetClass "System.Runtime.InteropServices.Marshal"
    
        		local len = windows.sendMessage hWnd LB_GETTEXTLEN i 0
        		local lParam = marshal.AllocHGlobal (2 * len + 2) asDotNetObject:on
        		windows.sendMessage hWnd LB_GETTEXT i (lParam.ToInt64())
    
        		local str = (marshal.PtrToStringAuto lParam asDotNetObject:on).ToString()
        		marshal.FreeHGlobal lParam
        		return str
        	)
    
        	fn collectUndoItems =
        	(
        		local hWnd = DialogMonitorOPS.getWindowHandle()
    
        		if UIAccessor.getWindowText hWnd == "" and
        		   UIAccessor.getWindowClassName hWnd == "Dialog" then
        		(		
        			local listBoxHWnd = for ctrl in (windows.getChildrenHWnd hWnd) where ctrl[4] == "ListBox" do exit with ctrl[1]
        			local itemCount = getListBoxLength listBoxHWnd
        			items = for i = 0 to itemCount - 1 collect getListBoxItemText listBoxHWnd i
    
        			UIAccessor.closeDialog hWnd
        			showItems()
    								
    				ii=0
    				for v in items do
    				(
    					
    					if ii>6 do
    						exit
    					ii=ii+1
    					if v == "Move" or v == "Rotate" or v == "Scale" do
    						append v "<>"
    						print v			
    				)
        			
        			DialogMonitorOPS.enabled = off
        			DialogMonitorOPS.unRegisterNotification id:#getUndoStack
    
        			true
        		)
        		else false
        	)
    		
    		fn CallbackFn1 ev nd = 
    		(
    			/*
    			ii=0
    				for v in items do
    				(
    					
    					if ii>6 do
    						exit
    					ii=ii+1
    					--if v == "Move" do
    						print v			
    				)
    			*/
    		)
    				
    
        	local maxChildren = windows.getChildrenHWnd #max
        	local undoBtnHWnd = for hWndData in maxChildren where UIAccessor.getWindowResourceID hWndData[1] == 50034 do exit with hWndData[1]
    
        	if isKindOf undoBtnHWnd Number then
        	(
        		DialogMonitorOPS.unRegisterNotification id:#getUndoStack
        		DialogMonitorOPS.registerNotification collectUndoItems id:#getUndoStack
        		DialogMonitorOPS.enabled = on
    				
    			callbackItem = NodeEventCallback mouseUp:true delay:1000 subobjectSelectionChanged:CallbackFn1
    
        		sendRightClick undoBtnHWnd
        	)
        	else messageBox "Couldn't find the undo button."
    		
    		
        )
        
      
    )
    	
    	


Sign In or Register to comment.