2

I'm building a basic text editor with custom controls. For my text alignment control, I need to cover two user scenarios:

  1. the text view is the first responder - make the paragraph attribute changes to textView.rangesForUserParagraphAttributeChange

  2. the text view is not the first responder - make the paragraph attribute changes to the full text range.

Here's the method:

- (IBAction)changedTextAlignment:(NSSegmentedControl *)sender
{
    NSTextAlignment align;
    // ....

    NSRange fullRange = NSMakeRange(0, self.textView.textStorage.length);
    NSArray *changeRanges = [self.textView rangesForUserParagraphAttributeChange];

    if (![self.mainWindow.firstResponder isEqual:self.textView])
    {
        changeRanges = @[[NSValue valueWithRange:fullRange]];
    }

    [self.textView shouldChangeTextInRanges:changeRanges replacementStrings:nil];
    [self.textView.textStorage beginEditing];

    for (NSValue *r in changeRanges)
    {
        @try {
            NSDictionary *attrs = [self.textView.textStorage attributesAtIndex:r.rangeValue.location effectiveRange:NULL];
            NSMutableParagraphStyle *pStyle =  [attrs[NSParagraphStyleAttributeName] mutableCopy];
            if (!pStyle)
                pStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];

            [pStyle setAlignment:align];
            [self.textView.textStorage addAttributes:@{NSParagraphStyleAttributeName: pStyle}
                                             range:r.rangeValue];
        }
        @catch (NSException *exception) {
            NSLog(@"%@", exception);
        }
    }

    [self.textView.textStorage endEditing];
    [self.textView didChangeText];

    // ....

    NSMutableDictionary *typingAttrs = [self.textView.typingAttributes mutableCopy];
    NSMutableParagraphStyle *pStyle =  typingAttrs[NSParagraphStyleAttributeName];
    if (!pStyle)
        pStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
    [pStyle setAlignment:align];
    [typingAttrs setObject:NSParagraphStyleAttributeName forKey:pStyle];
    self.textView.typingAttributes = typingAttrs;

}

So both scenarios work fine... BUT undo/redo doesn't work when the change is applied in the 'not-first-responder' scenario. The undo manager pushes something onto its stack (i.e Undo is available in the Edit menu), but invoking undo doesn't change the text. All it does is visibly select the full text range.

How do I appropriately change text view attributes so that undo/redo works regardless of whether the view is first reponder or not?

Thank you in advance!

Kyle Truscott
  • 1,537
  • 1
  • 12
  • 18

2 Answers2

0

I'm not sure, but I have two suggestions. One, check the return value from shouldChangeTextInRanges:..., since perhaps the text system is refusing your proposed change; a good idea in any case. Two, I would try to make the not-first-responder case more like the first-responder case in order to try to get it to work; in particular, you might begin by selecting the full range, so that rangesForUserParagraphAttributeChange is then in fact the range that you change the attributes on. A further step in this direction would be to actually momentarily make the textview be the first responder, for the duration of your change. In that case, the two cases should really be identical, I would think. You can restore the first responder as soon as you're done. Not optimal, but it seems that AppKit is making some assumption behind the scenes that you probably just have to work around. Without getting into trying to reproduce the problem and play with it, that's the best I can offer...

bhaller
  • 1,803
  • 15
  • 24
0

The issue is a typo on my part in the code that updates the typingAttributes afterwards. Look here:

//...

NSMutableParagraphStyle *pStyle =  typingAttrs[NSParagraphStyleAttributeName];

// ...

Doh! Needs to be really mutable...

//...

NSMutableParagraphStyle *pStyle =  [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];

// ...
Kyle Truscott
  • 1,537
  • 1
  • 12
  • 18