NSTextView line break on character, not word - cocoa

I have an NSTextView for editing a long string without spaces, but with punctuation characters. I'd like it to wrap at whatever character falls at the end of the line instead of trying to split it into words where it finds punctuation, which results in uneven lines.
I thought it would be as easy as making a subclass of NSATSTypesetter to reimplement this method like so:
- (BOOL) shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex {
return YES;
}
This is not having any effect on the layout of the text view. It is being called once for every line break on every layout, but only where the line break would have occurred anyway.

The line breaking setting in a NSTextView is actually a attribute of the text in it's textStorage.
You need to get the NSTextStorage of that view which is a NSMutableAttributedString then set that string's style to your needed line break setting.
Keep in mind the text in the storage keeps changing and the text itself contains the setting so you need to keep setting it on each change.
Call this method below as [self setLineBreakMode:NSLineBreakByCharWrapping forView:txtView]; every time the text storage is changed, e.g.: KVO observe txtView.string
-(void)setLineBreakMode:(NSLineBreakMode)mode forView:(NSTextView*)view
{
NSTextStorage *storage = [view textStorage];
NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[style setLineBreakMode:mode];
[storage addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, [storage length])];
[style release];
}

Related

NSTextView undo/redo attribute changes (when not first responder)

I'm building a basic text editor with custom controls. For my text alignment control, I need to cover two user scenarios:
the text view is the first responder - make the paragraph attribute changes to textView.rangesForUserParagraphAttributeChange
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!
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...
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];
// ...

Cocoa NSScrollView starts at center

I have a NSScrollView item in my main window. Whenever I launch the program, there is text in the scroll view and it starts at the center. Shouldn't the user start reading at the top but why does the app launch it at the center? Thanks!
This can happen if you do not set your views up correctly.
Here's the information I have:
1: You use Interface Builder to set up your views.
2: You are using a NSTextView
3: The code you use for changing the text is:
[[_chapterContent insertText:[Book objectForKey:bookAndChapters]];
4: As you're using a NSTextView, we're not dealing with an issue caused by flipped coordinates.
I'm not sure whether you should use insertText for this task.
At least, you can use something like this...
range.location = 0;
range.length = 0;
[textView setSelectedRange:range];
...to set the position of the caret.
You can then do a...
[textView scrollRangeToVisible:range];
...if you like.
Note: You can use setSelectedRange with a length of 0 and any location within [textStorage length], to position the caret.
-Let me know if this makes the change you need.
I'd suggest using...
textStorage = [textView textStorage];
range.location = 0;
range.length = [textStorage length];
if([self shouldChangeTextInRange:range replacementString:string])
{
[textStorage beginEditing];
[textStorage replaceCharactersInRange:range withAttributedString:attrStr];
[textStorage endEditing];
}
... to replace the text with an attributed string.
If you don't need an attributed string, there's a simpler method available:
[textStorage replaceCharactersInRange:range withString:str];
It's worked well for me (and a number of other people) for many years; it's the recommended way of changing the contents of the NSTextView.
-If you just want to replace the entire contents with a string, you can use
[textView setString:string];

Cocoa Text - refreshing text on-the-fly

In an app I'm working on, the user inputs plain text, and the app reformats the text by transforming it to an NSAttributedString, and displays it. This all happens live.
Currently, I'm doing the following on my NSTextView's textDidChange delegate method:
- (void)textDidChange:(NSNotification *)notification {
// saving the cursor position
NSInteger insertionPoint = [[[self.mainTextView selectedRanges] objectAtIndex:0] rangeValue].location;
// this grabs the text view's contact as plain text
[self updateContentFromTextView];
// this creates an attributed strings and displays it
[self updateTextViewFromContent];
// resetting the cursor position
self.mainTextView.selectedRange = NSMakeRange(insertionPoint, 0);
}
While this mostly works, it's not ideal. The text seems to blink for a split second (you especially notice it on the red dots under spelling errors), and when the cursor was previously near one of the edges of the visible rect, it the scroll position gets reset. In my case, this is a very much undesirable side-effect.
So my question is: Is there a better way of doing what I'm trying to do?
I think you have a slight misconception of how an NSTextView works. The user never enters a "plain string", the data store of an NSTextView is always an NSTextStorage object, which is a subclass of NSMutableAttributedString.
What you need to do is add/remove attributes to the existing attributed string that the user is editing, rather than replacing the entire string.
You should also not make changes to the string in the ‑textDidChange: delegate method, as changing the string from that method can cause another change notification.
Instead, you should implement the delegate method ‑textStorageDidProcessEditing:. This is called whenever the text changes. You can then make modifications to the string like so:
- (void)textStorageDidProcessEditing:(NSNotification*)notification
{
//get the text storage object from the notification
NSTextStorage* textStorage = [notification object];
//get the range of the entire run of text
NSRange aRange = NSMakeRange(0, [textStorage length]);
//for example purposes, change all the text to yellow
//remove existing coloring
[textStorage removeAttribute:NSForegroundColorAttributeName range:aRange];
//add new coloring
[textStorage addAttribute:NSForegroundColorAttributeName
value:[NSColor yellowColor]
range:aRange];
}

Changing text selection color in NSTextView

I'm trying to write a "highlight" feature on an NSTextView. Currently, everything works great. You select a range of text and the background color of that text changes to yellow. However, while it's still selected, the background is that standard blue of selected text. How do I make that standard selection indicator color not show up in certain cases?
Thanks!
Use -[NSTextView setSelectedTextAttributes:...].
For example:
[textView setSelectedTextAttributes:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSColor blackColor], NSBackgroundColorAttributeName,
[NSColor whiteColor], NSForegroundColorAttributeName,
nil]];
You can simply pass an empty dictionary if you don't want the selection indicated in any way at all (short of hiding the insertion point).
Another option is to watch for selection changes and apply the "selection" using temporary attributes. Note that temporary attributes are used to show spelling and grammar mistakes and find results; so if you care about preserving these features of NSTextView then make sure only to add and remove temporary attributes, not replace them.
An example of this is (in a NSTextView subclass):
- (void)setSelectedRanges:(NSArray *)ranges affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag;
{
NSArray *oldRanges = [self selectedRanges];
for (NSValue *v in oldRanges) {
NSRange oldRange = [v rangeValue];
if (oldRange.length > 0)
[[self layoutManager] removeTemporaryAttribute:NSBackgroundColorAttributeName forCharacterRange:oldRange];
}
for (NSValue *v in ranges) {
NSRange range = [v rangeValue];
if (range.length > 0)
[[self layoutManager] addTemporaryAttributes:[NSDictionary dictionaryWithObject:[NSColor blueColor] forKey:NSBackgroundColorAttributeName]
forCharacterRange:range];
}
[super setSelectedRanges:ranges affinity:affinity stillSelecting:stillSelectingFlag];
}

Cocoa (Snow Leopard) NSTextView's textStorage -setAttributes:range: removes characters!

I'm not sure what I'm doing wrong. I have a NSTextView and am registered as the delegate for its textStorage attribute. When I receive -textStorageDidProcessEditing:notification: I'm trying to apply attributes to ranges of characters within the text. It certainly does "something" to the characters, but not what I expect... they just disappear!
A heavily distilled code example. This should make sure the second character in the text field is always red:
-(void)textStorageDidProcessEditing:(NSNotification *)notification {
NSTextStorage *textStorage = [textView textStorage];
if ([[textStorage string] length] > 1) {
NSColor *color = [NSColor redColor];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:color, NSForegroundColorAttributeName, nil];
[textStorage setAttributes:attributes range:NSMakeRange(1, 1)];
}
}
Instead, as I type the sequence "abcdefg" I get "a", then when I hit "b" seemingly nothing happens, then when I hit "cdefg" typing occurs as normal, making the end result "acdefg"... the "b" is missing!
If I start hitting backspace I have to hit backspace 7 times, as if the "b" is actually there, but just not being drawn (cursor stalls as it deletes the "b", then on the next backspace deletes the "a" as expected).
If I apply attributes to some default text in the view using the same -setAttributes:range: method before the view is drawn then it does exactly as I expect.
Any clues? It seems like a fairly normal use of a NSTextStorageDelegate :)
I've tried calling -setNeedsDisplay on the text field to no avail.
Figured it out. Using NSTextStorage's -addAttribute:value:range works. I still don't fully understand why but at least I can get over it and move on.
-(void)textStorageDidProcessEditing:(NSNotification *)notification {
// ... SNIP ...
[textStorage addAttribute:NSForegroundColorAttributeName
value:[NSColor redColor]
range:NSMakeRange(1, 1)];
}
Makes the code a bit less cluttered too.
I'm not sure how relevant this is for you after so many years but I think the reason for it was that you were setting attributes with a dictionary which does not contain NSFontAttributeName, effectively removing it from the textview.
So I think this should work:
-(void)textStorageDidProcessEditing:(NSNotification *)notification {
NSTextStorage *textStorage = [textView textStorage];
if ([[textStorage string] length] > 1) {
NSColor *color = [NSColor redColor];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:color, NSForegroundColorAttributeName, [NSFont ...whatever...], NSFontAttributeName, nil];
[textStorage setAttributes:attributes range:NSMakeRange(1, 1)];
}
}

Resources