Controlling Undo from Python

I’m working on a script that launches a dialog via Rhino.UI.Dialogs.ShowSemiModal(). Buttons in the interface let the user manipulate the document geometry. For example trimming lines, adding circles, etc.

I’d like to put an “Undo Last Operation” button in the UI to let the user undo the last change they made.

I tried the obvious call to rs.Command(“Undo”) but Rhino reports “Nothing to undo.”. Rhino is of course accumulating my changes because when I exit the script I can undo the entire thing.

Is there any way I can control the undo stack myself somehow? That is place begin and end points and jump back to previous begin points. Something like that?

Thanks,
Mark

Rhino’s Undo is command-based. That is, anything that happens within the context of a command can be undone.

When you are outside of a command, you have to let Rhino know what should be undo-able. This is done by calling RhinoDoc.BeginUndoRecord and RhinoDoc.EndUndoRecord.

For example:

def MyStuff():
    sn = scriptcontext.doc.BeginUndoRecord("My Stuff")
    pt = Rhino.Geometry.Point3d(0,0,0)
    scriptcontext.doc.Objects.AddPoint(pt)
    scriptcontext.doc.Views.Redraw()
    if (sn > 0):
        scriptcontext.doc.EndUndoRecord(sn)

If BeginUndoRecord returns 0, then an undo record is already been started (by a command), or undo has been disabled. If BeginUndoRecord returns a value >0, then you are responsible for calling EndUndoRecord.

Does this help?

Thanks for the quick response, Dale.

This works if I make the dialog modeless. So if I launch it like this:

ui.form.Show()

Then I can begin and end the undo records and undo using rs.Command(“Undo”). That works nicely.

If I launch with this:

Rhino.UI.Dialogs.ShowSemiModal(ui.form)

Then I’m still inside a command until that function returns (which happens when the dialog closes). So when I’m in a command then when I try the same thing as above it doesn’t do anything.

I hope that made sense. Do you have any suggestions for making it work if I do use ShowSemiModal?

(If not I can live with it as a modeless dialog).

Thanks,
Mark

Can you provide me a cheesy sample that repeats this?

Dale,

Yes I can send you a sample. I have one question first though - because if the answer is Yes I know what the issue is…

Is Edit > Undo disabled when ShowSemiModal is active? For example you can’t Delete layers when it is active (that is, when the dialog is up).

If the answer is No (undo is not disabled) then I’ll send you an example.

BTW… You can test this by launching anything that presents a UI using ShowSemiModal. Then draw a curve with the dialog up. Then attempt to undo it. It’ll report “Nothing to undo”.

Thanks for the help!

Mark
PS: The modeless version is working great - so thanks for your earlier help :slight_smile:

Yes. The only things that work outside of a semi-model dialog are view related operations.

– Dale

Hi Dale,
if I run this example separately, sn is always 0.
Is it somehow possible to run rs.Command(“Undo”) inside a script? Or record operations I would like to undo and then undo them?
When I try to run rs.Command(“Undo”) it has nothing to undo. I guess the record for undo ends after the script finishes?

Thanks,
Tomas

Python script are run by the RunPythonScript command. Thus, an undo record has already been created.

Yes, I guess so.
My example:
I create a set of lines (as face normals) on a mesh by rs.AddLine and then I ask the user, if he wants to keep them or delete. My guess is, that the easiest way to delete them is to record the set of operations where I create them and then undo.
Is it somehow possible?

Thanks,
Tomas

If the lines that you are creating are in a list, can’t you just delete them with rs.DeleteObjects(list) instead of having to try to undo the operations?

I guess the other way to tackle this would be to create virtual geometry in RhinoCommon (don’t actually add the lines to the document) and display it with a custom display - something I don’t know how to do, yet though - and then if the user decides they don’t want them, no problem, nothing to undo or delete. If the user decides they want the lines, THEN add them to the document.

–Mitch

Thanks Mitch. The first one is the one I would use. I can collect the GUIDs of these lines in a list and delete them afterwards.

Tomas

Hello,

I just needed to implement Undo/redo in one of my Python GUI Rhino script and your post helped me very much. I wrote a small helper class to use with the with Python statement to make undo/redo more robust in the face of exceptions (like the RAII C++ idiom). Here it is:

class ScopeUndoRecord():
    def __init__(self, undoName):
        self.name = undoName
    def __enter__(self):
        self.undoRecord = scriptcontext.doc.BeginUndoRecord(self.name)
    def __exit__(self,exctype,value,tb):
        if (self.undoRecord > 0): 
            scriptcontext.doc.EndUndoRecord(self.undoRecord)
        else:
            print "Operation could not be undone."

You would use it like this:

with ScopeUndoRecord("MySafeUndo") as undo:
    operation1()
    # lots of operations that could possibly throw exceptions
    operationN()

#Here, getting out of scope (by thrown exception or normal execution flow) automatically calls EndUndoRecord giving us a valid Undo record on the stack.

I was struggling with undo function within python script and finally have it to work: Before Dale’s lines, any active (Rhino will have undo on already) recording has to be stopped:

if sc.doc.UndoRecordingIsActive: sc.doc.EndUndoRecord(sc.doc.CurrentUndoRecordSerialNumber)

def MyStuff():…

end the code with rs.Command(‘_Undo’, True) or CTR+Z

1 Like