How to make Redo work with a custom group

  • Hi-

    I understand how Redo works as described on page 145 of Hillegass. He
    makes his insertObject: and removeObject: methods the inverse of each
    other so that when you do one, it puts the other one on the undo stack.

    But in my situation, I don't seem to be able to implement it that way.

    I want to undo a drag that occurs in a custom view.

    I put this into my -mouseDown: method:

    //start custom undo grouping
    [[appController myUndo] setGroupsByEvent:NO];
    [[appController myUndo] beginUndoGrouping];

    then I put this into my -mouseDragged: method:

    [[[appController myUndo] prepareWithInvocationTarget:selectedOS]
    setStartTime:[selectedOS startTime]];

    This gets called every time the mouse moves of course.

    Finally, in my -mouseUp: method I close it up:

    //end custom undo grouping
    [[appController myUndo] setActionName:[NSString
    stringWithFormat:@"Move order step"]];
    [[appController myUndo] endUndoGrouping];
    [[appController myUndo] setGroupsByEvent:YES];

    So Undo works great, but I when I try Redo (which does show OK in the
    menu), nothing happens. This is I think because there is no undo code
    in the setStartTime: call that I wrap into the
    prepareWithInvocationTarget:

    But how do I go about it? Do I make a second -setStartTime method
    called something like -setStartTimeWithUndo that puts its inverse on
    the undo stack?

    Thank you for any insight
  • Paul,

    I find it generally useful to parameterize the operations (and their
    inverses) with an undo manager.  This avoids reliance on the undo
    manager of a specific view and allows the operations to exist cleanly
    at the model level.  I would go further and say that in any model for
    which you want undo support, your 'primitive' mutation methods should
    have an undo parameter.  I say this based on experience adding undo
    support to a mesh editing program: there the operations were quite
    complex and after much difficulty trying to synchronize the effects
    of undo and redo I ended up restructuring to code to use the undo
    manager at the lowest level.

    dave

    On 29-Oct-07, at 8:11 AM, Paul Bruneau wrote:

    > Hi-
    >
    > I understand how Redo works as described on page 145 of Hillegass.
    > He makes his insertObject: and removeObject: methods the inverse of
    > each other so that when you do one, it puts the other one on the
    > undo stack.
    >
    > But in my situation, I don't seem to be able to implement it that way.
    >
    > I want to undo a drag that occurs in a custom view.
    >
    > I put this into my -mouseDown: method:
    >
    > //start custom undo grouping
    > [[appController myUndo] setGroupsByEvent:NO];
    > [[appController myUndo] beginUndoGrouping];
    >
    >
    > then I put this into my -mouseDragged: method:
    >
    > [[[appController myUndo] prepareWithInvocationTarget:selectedOS]
    > setStartTime:[selectedOS startTime]];
    >
    > This gets called every time the mouse moves of course.
    >
    > Finally, in my -mouseUp: method I close it up:
    >
    > //end custom undo grouping
    > [[appController myUndo] setActionName:[NSString
    > stringWithFormat:@"Move order step"]];
    > [[appController myUndo] endUndoGrouping];
    > [[appController myUndo] setGroupsByEvent:YES];
    >
    > So Undo works great, but I when I try Redo (which does show OK in
    > the menu), nothing happens. This is I think because there is no
    > undo code in the setStartTime: call that I wrap into the
    > prepareWithInvocationTarget:
    >
    > But how do I go about it? Do I make a second -setStartTime method
    > called something like -setStartTimeWithUndo that puts its inverse
    > on the undo stack?
    >
    > Thank you for any insight
  • Thank you David-

    Coincidentally, I did in fact use the undo manager as a parameter in
    a different part of my application. I was considering removing that
    because this app is non-document based and I want just a single undo
    manager throughout the whole thing anyway. I'll think more about that.

    When you say "use the undo manager at the lowest level" you don't
    really mean the LOWEST level do you? By that I mean, you didn't
    implement undo into your ivar setter methods did you?

    What I did which seems to work well is to make the following method
    in my view's controller:

    - (void)moveOrderStep:(OrderStep *)orderStep
             toStartTime:(NSDate *)newStartTime;
    {
    //add the inverse of this operation to the undo stack
    [[[appController myUndo] prepareWithInvocationTarget:self]
      moveOrderStep:orderStep
             toStartTime:[orderStep startTime]];

    if (![[appController myUndo] isUndoing])
    {
      [[appController myUndo] setActionName:@"move order step"];
    }
    }

    This gives me a sort of an undo "middle man" that can be called by
    the higher level methods which in turn calls the setter of the order
    step's ivar.

    Thanks again,

    PB

    On Oct 29, 2007, at 11:12 AM, David Spooner wrote:

    > Paul,
    >
    > I find it generally useful to parameterize the operations (and
    > their inverses) with an undo manager.  This avoids reliance on the
    > undo manager of a specific view and allows the operations to exist
    > cleanly at the model level.  I would go further and say that in any
    > model for which you want undo support, your 'primitive' mutation
    > methods should have an undo parameter.  I say this based on
    > experience adding undo support to a mesh editing program: there the
    > operations were quite complex and after much difficulty trying to
    > synchronize the effects of undo and redo I ended up restructuring
    > to code to use the undo manager at the lowest level.
    >
    > dave
    >
    > On 29-Oct-07, at 8:11 AM, Paul Bruneau wrote:
    >
    >> Hi-
    >>
    >> I understand how Redo works as described on page 145 of Hillegass.
    >> He makes his insertObject: and removeObject: methods the inverse
    >> of each other so that when you do one, it puts the other one on
    >> the undo stack.
    >>
    >> But in my situation, I don't seem to be able to implement it that
    >> way.
    >>
    >> I want to undo a drag that occurs in a custom view.
    >>
    >> I put this into my -mouseDown: method:
    >>
    >> //start custom undo grouping
    >> [[appController myUndo] setGroupsByEvent:NO];
    >> [[appController myUndo] beginUndoGrouping];
    >>
    >>
    >> then I put this into my -mouseDragged: method:
    >>
    >> [[[appController myUndo] prepareWithInvocationTarget:selectedOS]
    >> setStartTime:[selectedOS startTime]];
    >>
    >> This gets called every time the mouse moves of course.
    >>
    >> Finally, in my -mouseUp: method I close it up:
    >>
    >> //end custom undo grouping
    >> [[appController myUndo] setActionName:[NSString
    >> stringWithFormat:@"Move order step"]];
    >> [[appController myUndo] endUndoGrouping];
    >> [[appController myUndo] setGroupsByEvent:YES];
    >>
    >> So Undo works great, but I when I try Redo (which does show OK in
    >> the menu), nothing happens. This is I think because there is no
    >> undo code in the setStartTime: call that I wrap into the
    >> prepareWithInvocationTarget:
    >>
    >> But how do I go about it? Do I make a second -setStartTime method
    >> called something like -setStartTimeWithUndo that puts its inverse
    >> on the undo stack?
    >>
    >> Thank you for any insight
    >
  • On 29-Oct-07, at 10:42 AM, Paul Bruneau wrote:

    > Coincidentally, I did in fact use the undo manager as a parameter
    > in a different part of my application. I was considering removing
    > that because this app is non-document based and I want just a
    > single undo manager throughout the whole thing anyway. I'll think
    > more about that.
    >
    > When you say "use the undo manager at the lowest level" you don't
    > really mean the LOWEST level do you? By that I mean, you didn't
    > implement undo into your ivar setter methods did you?

    The operations for which you want to provide an inverse should take
    an undo manager.  If these operations are written as a sequence of
    more primitive operations then it may be convenient to provide the
    undo manager as an argument to those primitive methods also.  For
    example, in a method which splits an edge of a triangle mesh you need
    to replace the selected edge and its two adjacent triangles with a
    new vertex attached to four new edges and four new triangles.  By
    adding the undo manager as parameter to the primitive methods for
    vertex/edge/face addition and removal (where the inverse operation is
    easy) there was no need to write an explicit inverse method.

    > What I did which seems to work well is to make the following method
    > in my view's controller:
    >
    > - (void)moveOrderStep:(OrderStep *)orderStep
    > toStartTime:(NSDate *)newStartTime;
    > {
    > //add the inverse of this operation to the undo stack
    > [[[appController myUndo] prepareWithInvocationTarget:self]
    > moveOrderStep:orderStep
    > toStartTime:[orderStep startTime]];
    >
    > if (![[appController myUndo] isUndoing])
    > {
    > [[appController myUndo] setActionName:@"move order step"];
    > }
    > }
    >
    > This gives me a sort of an undo "middle man" that can be called by
    > the higher level methods which in turn calls the setter of the
    > order step's ivar.

    I'm not sure I understand.  You say the caller of this method also
    actually effects the start time, or did you leave out the call to -
    setStartTime:?

    I found the following method useful for the many situations in which
    I wanted undo for setting object attributes...

    @implementation NSObject(...)
    - (void) setValue:(id)value forKeyPath:(NSString *)path undo:
    (NSUndoManager *)undo
      {
        if (undo != nil)
          [[undo prepareWithInvocationTarget:self] setValue:[self
    valueForKeyPath:path] forKeyPath:path undo:undo];
        [self setValue:value forKeyPath:path];
      }
    @end

    > Thanks again,
    >
    > PB

    Cheers,
    dave
  • On Oct 29, 2007, at 3:25 PM, David Spooner wrote:

    > The operations for which you want to provide an inverse should take
    > an undo manager.  If these operations are written as a sequence of
    > more primitive operations then it may be convenient to provide the
    > undo manager as an argument to those primitive methods also.  For
    > example, in a method which splits an edge of a triangle mesh you
    > need to replace the selected edge and its two adjacent triangles
    > with a new vertex attached to four new edges and four new
    > triangles.  By adding the undo manager as parameter to the
    > primitive methods for vertex/edge/face addition and removal (where
    > the inverse operation is easy) there was no need to write an
    > explicit inverse method.

    OK, thank you, I understand.

    >
    >> What I did which seems to work well is to make the following
    >> method in my view's controller:
    >>
    >> - (void)moveOrderStep:(OrderStep *)orderStep
    >> toStartTime:(NSDate *)newStartTime;
    >> {
    >> //add the inverse of this operation to the undo stack
    >> [[[appController myUndo] prepareWithInvocationTarget:self]
    >> moveOrderStep:orderStep
    >> toStartTime:[orderStep startTime]];
    >>
    >> if (![[appController myUndo] isUndoing])
    >> {
    >> [[appController myUndo] setActionName:@"move order step"];
    >> }
    >> }
    >>
    >> This gives me a sort of an undo "middle man" that can be called by
    >> the higher level methods which in turn calls the setter of the
    >> order step's ivar.
    >
    > I'm not sure I understand.  You say the caller of this method also
    > actually effects the start time, or did you leave out the call to -
    > setStartTime:?

    Gah! I did just that! this line follows those above:

    [orderStep setStartTime:newStartTime];

    > I found the following method useful for the many situations in
    > which I wanted undo for setting object attributes...
    >
    > @implementation NSObject(...)
    > - (void) setValue:(id)value forKeyPath:(NSString *)path undo:
    > (NSUndoManager *)undo
    > {
    > if (undo != nil)
    > [[undo prepareWithInvocationTarget:self] setValue:[self
    > valueForKeyPath:path] forKeyPath:path undo:undo];
    > [self setValue:value forKeyPath:path];
    > }
    > @end

    That is neat. I must admit a certain fuzziness for setValue:
    forKeyPath: that I hope to clear up over time. Is your undo != nil
    test just habit from another language or does it serve a real
    purpose? If undo is nil, no harm is caused by sending it a message, no?

    Thanks again for your wisdom
  • On 29-Oct-07, at 1:54 PM, Paul Bruneau wrote:

    > On Oct 29, 2007, at 3:25 PM, David Spooner wrote:
    >
    >> I found the following method useful for the many situations in
    >> which I wanted undo for setting object attributes...
    >>
    >> @implementation NSObject(...)
    >> - (void) setValue:(id)value forKeyPath:(NSString *)path undo:
    >> (NSUndoManager *)undo
    >> {
    >> if (undo != nil)
    >> [[undo prepareWithInvocationTarget:self] setValue:[self
    >> valueForKeyPath:path] forKeyPath:path undo:undo];
    >> [self setValue:value forKeyPath:path];
    >> }
    >> @end
    >
    > That is neat. I must admit a certain fuzziness for setValue:
    > forKeyPath: that I hope to clear up over time. Is your undo != nil
    > test just habit from another language or does it serve a real
    > purpose? If undo is nil, no harm is caused by sending it a message,
    > no?
    >

    It's just an optimization to avoid calling -valueForKeyPath:
    unnecessarily.

    dave
previous month october 2007 next month
MTWTFSS
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        
Go to today