Premature line wrapping in NSTextView when tabs are used - macos

I have a strange problem with NSTextView linewrapping after the 51st column if I enter a line of tabs. This only happens with tabs, not with any other character, which wrap correctly at the edge of the text view, not after the 51st character.
This is easy to repeat. Create a blank project in XCode with a single window and just one NSTextView. The only non-default settings are that I have removed constraints, and used the old style autosize to autosize the textview so that it fills the window. I have written no code. Now run the application, open up the window so that is much wider than 51 characters, hold down the tab key and it will wrap early.
Thanks in advance.

The issue here is that NSTextView has a default NSMutableParagraphStyle object which has a list of attributes such as line wrapping, tab stops, margins, etc... You can see this by going to the Format menu, text subview, and select the "Show Ruler" menu. (You get this menu for free with any NSTextView).
Once you show the ruler you will see all of your tab stops and this will explain why your tabs are wrapping once you reach the last tab stop.
So the solution you need is to create an array of tabs that you want for your paragraph style object and then set that to be the style for the NSTextView.
Here is a method to create tabs. In this example, it will create 5 left aligned tabs, each 1.5 inches apart:
-(NSMutableAttributedString *) textViewTabFormatter:(NSString *)aString
{
float columnWidthInInches = 1.5f;
float pointsPerInch = 72.0f;
NSMutableArray * tabArray = [NSMutableArray arrayWithCapacity:5];
for(NSInteger tabCounter = 0; tabCounter < 5; tabCounter++)
{
NSTextTab * aTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:(tabCounter * columnWidthInInches * pointsPerInch)];
[tabArray addObject:aTab];
}
NSMutableParagraphStyle * aMutableParagraphStyle = [[NSParagraphStyle defaultParagraphStyle]mutableCopy];
[aMutableParagraphStyle setTabStops:tabArray];
NSMutableAttributedString * attributedString = [[NSMutableAttributedString alloc] initWithString:aString];
[attributedString addAttribute:NSParagraphStyleAttributeName value:aMutableParagraphStyle range:NSMakeRange(0,[aString length])];
return attributedString;
}
Then you invoke it before you add any text to your NSTextView in order to set the default paragraph style with those tab stops in it:
[[mainTextView textStorage] setAttributedString:[self textViewTabFormatter:#" "]];
You can find a additional informatio here, if you want a deeper tutorial:
http://www.mactech.com/articles/mactech/Vol.19/19.08/NSParagraphStyle/index.html

I'm sharing my experience since I recently had kind of the same type of problem(s)
- when pressing tabs, cursor jumps to the next line after about 10-12 tabs
- when there are multiple lines of text, when pressing tabs the whole paragraph turns into bulleted lines
I used the above method by "Ed Fernandez" and could only resolve the problem when there is no text in the NSTextView initially but when existing saved text is loaded it had above problems
For this I tried the below code from below link (It really worked and solved the both problems)
http://www.cocoabuilder.com/archive/cocoa/159692-nstextview-and-ruler-tab-settings.html
You don't need to call "release" if you are using automatic reference counting.
- (IBAction)formatTextView:(NSTextView *)editorTextView
{
int cnt;
int numStops = 20;
int tabInterval = 40;
NSTextTab *tabStop;
NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init];
//attributes for attributed String of TextView
NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
// This first clears all tab stops, then adds tab stops, at desired intervals...
[paraStyle setTabStops:[NSArray array]];
for (cnt = 1; cnt <= numStops; cnt++) {
tabStop = [[NSTextTab alloc] initWithType:NSLeftTabStopType location: tabInterval * (cnt)];
[paraStyle addTabStop:tabStop];
}
[attrs setObject:paraStyle forKey:NSParagraphStyleAttributeName];
[[editorTextView textStorage] addAttributes:attrs range:NSMakeRange(0, [[[editorTextView textStorage] string] length])];
}
This tab limit is there due to the "Ruler" concept where it's limited to about 12 tabstops. You can see the Ruler by calling
[editorTextView setRulerVisible:YES];

Related

Why does NSTextView not always wrap text fluently while resizing?

I've stumbled upon a behavior in NSTextView that does not seem intended, or that I at least do not understand the reasoning behind.
When you have a large body of text in an NSTextView and you resize the control/window, the wrapping of words only happens fluently and immediately while resizing when the text is scrolled near the top. If you scroll far down in the text, it does not, and it doesn't seem to "commit" the wrapping until you release and finish resizing.
Is there some internal limitation, or is this a bug?
The issue seems to be reproducible:
macOS 10.15.4, Xcode 11.4.1
Create a new macOS App project
Put an NSTextView on the default generated view controller (doesn't matter which of the 3: rich, plain or default) and constrain it so that it resizes with the window (top, bottom, leading, trailing)
Run the application and paste a large body of text into the text view (for example: http://www.gutenberg.org/cache/epub/12281/pg12281.txt)
Scroll to the top of the NSTextView and observe how the text wraps while resizing the window
Scroll to the bottom and observe how it only wraps after resizing the window
Hoping there's any Cocoa detectives out there who can provide some enlightenment on this one.
EDIT:
As per the docs, it states that "the layout manager reserves the right to perform layout for larger ranges". I take it that this means it is indeed intended as a performance consideration.
Is there any way to determine what the limit is, though?
EDIT: You could try subclassing NSScrollView to render the text into multiple containers.
NSTextStorage *storage = [[NSTextStorage alloc] initWithString:string];
NSLayoutManager *manager = [[NSLayoutManager alloc] init];
[storage addLayoutManager:manager];
NSInteger i = 0;
while (YES) {
NSTextContainer *container = [[NSTextContainer alloc] initWithSize:CGSizeMake(width, height)];
[manager addTextContainer:container];
NSTextView *textView = [[NSTextView alloc] initWithFrame:CGRectMake(x, y, width, height) textContainer:container];
[self.contentView addSubview:textView];
i++;
NSRange range = [manager glyphRangeForTextContainer:container];
if ( range.length + range.location == string.length )
break;
}
Then, while resizing the window, you can call NSLayoutManager to ensure the layout only for visible containers.

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];

NSTextView line break on character, not word

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];
}

Is there a "right" way to have NSTextFieldCell draw vertically centered text?

I have an NSTableView with several text columns. By default, the dataCell for these columns is an instance of Apple's NSTextFieldCell class, which does all kinds of wonderful things, but it draws text aligned with the top of the cell, and I want the text to be vertically centered in the cell.
There is an internal flag in NSTextFieldCell that can be used to vertically center the text, and it works beautifully. However, since it is an internal flag, its use is not sanctioned by Apple and it could simply disappear without warning in a future release. I am currently using this internal flag because it is simple and effective. Apple has obviously spent some time implementing the feature, so I dislike the idea of re-implementing it.
So; my question is this: What is the right way to implement something that behaves exactly like Apple's NStextFieldCell, but draws vertically centered text instead of top-aligned?
For the record, here is my current "solution":
#interface NSTextFieldCell (MyCategories)
- (void)setVerticalCentering:(BOOL)centerVertical;
#end
#implementation NSTextFieldCell (MyCategories)
- (void)setVerticalCentering:(BOOL)centerVertical
{
#try { _cFlags.vCentered = centerVertical ? 1 : 0; }
#catch(...) { NSLog(#"*** unable to set vertical centering"); }
}
#end
Used as follows:
[[myTableColumn dataCell] setVerticalCentering:YES];
The other answers didn't work for multiple lines. Therefore I initially continued using the undocumented cFlags.vCentered property, but that caused my app to be rejected from the app store. I ended up using a modified version of Matt Bell's solution that works for multiple lines, word wrapping, and a truncated last line:
-(void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSAttributedString *attrString = self.attributedStringValue;
/* if your values can be attributed strings, make them white when selected */
if (self.isHighlighted && self.backgroundStyle==NSBackgroundStyleDark) {
NSMutableAttributedString *whiteString = attrString.mutableCopy;
[whiteString addAttribute: NSForegroundColorAttributeName
value: [NSColor whiteColor]
range: NSMakeRange(0, whiteString.length) ];
attrString = whiteString;
}
[attrString drawWithRect: [self titleRectForBounds:cellFrame]
options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin];
}
- (NSRect)titleRectForBounds:(NSRect)theRect {
/* get the standard text content rectangle */
NSRect titleFrame = [super titleRectForBounds:theRect];
/* find out how big the rendered text will be */
NSAttributedString *attrString = self.attributedStringValue;
NSRect textRect = [attrString boundingRectWithSize: titleFrame.size
options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin ];
/* If the height of the rendered text is less then the available height,
* we modify the titleRect to center the text vertically */
if (textRect.size.height < titleFrame.size.height) {
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - textRect.size.height) / 2.0;
titleFrame.size.height = textRect.size.height;
}
return titleFrame;
}
(This code assumes ARC; add an autorelease after attrString.mutableCopy if you use manual memory management)
Overriding NSCell's -titleRectForBounds: should do it -- that's the method responsible for telling the cell where to draw its text:
- (NSRect)titleRectForBounds:(NSRect)theRect {
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - titleSize.height) / 2.0;
return titleFrame;
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSRect titleRect = [self titleRectForBounds:cellFrame];
[[self attributedStringValue] drawInRect:titleRect];
}
For anyone attempting this using Matt Ball's drawInteriorWithFrame:inView: method, this will no longer draw a background if you have set your cell to draw one. To solve this add something along the lines of
[[NSColor lightGrayColor] set];
NSRectFill(cellFrame);
to the beginning of your drawInteriorWithFrame:inView: method.
FYI, this works well, although I haven't managed to get it to stay centered when you edit the cell... I sometimes have cells with large amounts of text and this code can result in them being misaligned if the text height is greater then the cell it's trying to vertically center it in. Here's my modified method:
- (NSRect)titleRectForBounds:(NSRect)theRect
{
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
// test to see if the text height is bigger then the cell, if it is,
// don't try to center it or it will be pushed up out of the cell!
if ( titleSize.height < theRect.size.height ) {
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - titleSize.height) / 2.0;
}
return titleFrame;
}
No. The right way is to put the Field in another view and use auto layout or that parent view's layout to position it.
Though this is pretty old question...
I believe default style of NSTableView implementation is intended strictly for single line text display with all same size & font.
In that case, I recommend,
Set font.
Adjust rowHeight.
Maybe you will get quietly dense rows. And then, give them padding by setting intercellSpacing.
For example,
core_table_view.rowHeight = [NSFont systemFontSizeForControlSize:(NSSmallControlSize)] + 4;
core_table_view.intercellSpacing = CGSizeMake(10, 80);
Here what you'll get with two property adjustment.
This won't work for multi-line text, but very good enough for quick vertical center if you don't need multi-line support.
I had the same problem and here is the solution I did :
1) In Interface Builder, select your NSTableCellView. Make sure it as big as the row height in the Size Inspector. For example, if your row height is 32, make your Cell height 32
2) Make sure your cell is well placed in your row (I mean visible)
3) Select your TextField inside your Cell and go to your size inspector
4) You should see "Arrange" item and select "Center Vertically in Container"
--> The TextField will center itself in the cell

Resources