Changing text selection color in NSTextView - cocoa

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

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

Change the Color of specific substrings in NSTextView

I want to change the color of specific text in NSTextView. The method should check that after a keydown event.
For example: the word void is finished and the string void changes the
color to blue. Like a code editor.
I searched for a long time but don't find anything.
My Code:
NSRange range = [text rangeOfString:#"void"];
NSString *substring = [[text substringFromIndex:NSMaxRange(range)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
//I think, here is the mistake.
NSAttributedString *now = [NSAttributedString initWithString:substring];
[now setTextColor:[[NSColor blueColor]]];
I have read that i have to use a NSAttributedString but i don't know how I can get this class from a string.
I'am quite new in cocoa programming.
Thanks for every help!
You can do this way:
NSString *str = #"Hello. That is a test attributed string.";
//in place of NSMakeRange put your range
[self.textview setRichText:YES];
[self.textview setString:str];
[self.textview setTextColor:[NSColor redColor] range:NSMakeRange(3,5)];

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

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