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

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

Related

Tooltips in NSTextView/NSTextfields

I want to display tooltips over parts of an attributed string, and thought that using an NSToolTipAttributeName attribute on the required ranges would do the trick but I can't get it to work.
I am trying in MACOS, Xcode (9.2)
NSString *fullString = #"Create a new tooltip";
NSString *compareString =#"tooltip";
NSRange tooltipRange = [fullString rangeOfString: compareString];
[senderTextView.textStorage addAttribute:NSToolTipAttributeName
value:#"Custom tooltip"
range:tooltipRange];
[senderTextView.textStorage addAttribute:NSForegroundColorAttributeName
value:[NSColor redColor] range:tooltipRange];
The word "tooltip" is shown in red as expected, but no tooltip appears when I hover over it. What am I missing?
Couple of ways to do this, using the NSToolTipAttributeName in combination with NSLinkAttributeName, e.g.:
NSMutableDictionary* labelAttributes = [NSMutableDictionary new];
labelAttributes[NSToolTipAttributeName] = #"Tooltip";
labelAttributes[NSLinkAttributeName] = #"Link";
NSAttributedString* labelString = [NSAttributedString
attributedString:#"Display"]
withAttributes:labelAttributes];
Being careful not to forget displaysLinkToolTips = YES on the NSTextView.
Alternately, you can use the NSView Tooltips API, which provides:
- (NSToolTipTag)addToolTipRect:(NSRect)rect owner:(id)owner userData:(void *)data;

Default NSRecessedBezelStyle NSButton visual bug?

I have a basic NSRecessedBezelStyle NSButton added via IB to an NSView. Why is the font messed up in its unselected state? Is this normal?
As you can see, when pushed the recessed button looks fine, but unpressed it's solid black with no shadow. Am I missing something really obvious somewhere? I tried messing around with setAttributedTitle and setAttributedAlternateTitle but that yielded odd results with the push on push off mechanic.
That is the expected behavior for NSRecessedBezelStyle with the default "Push On Push Off" Type, bezeled in On state, plain text in OFF, additionally you can change the Type so the bezel is only displayed when hovering, here is the code to make it gray.
NSMutableDictionary *attrsDictionary = [NSMutableDictionary dictionaryWithCapacity:1];
[attrsDictionary setObject:[NSColor grayColor] forKey:NSForegroundColorAttributeName];
[attrsDictionary setObject:[NSFont boldSystemFontOfSize:12.0] forKey:NSFontAttributeName];
NSMutableParagraphStyle *paragraph = [[[NSMutableParagraphStyle alloc] init] autorelease];
[paragraph setAlignment:NSCenterTextAlignment];
[attrsDictionary setObject:paragraph forKey:NSParagraphStyleAttributeName];
NSAttributedString *str = [[[NSAttributedString alloc] initWithString:#"Button" attributes:attrsDictionary] autorelease];
[button setAttributedTitle:str];

NSTextField's attributed string is delayed in rendering

I've got an Label (NSTextField) in IB that's bound to a controller.
The controller, on awakeFromNIB, sets the attributedStringValue of the label to contain some coloured text and a link or two.
When you see the label it contains the correct string value, but some of the formatting is lost - until you click on the label, and it updates to contain the correct formatting.
I'm using this code to set the value:
[self.testTextField setAllowsEditingTextAttributes:YES];
[self.testTextField setSelectable:YES];
NSMutableAttributedString *linkString = [[NSMutableAttributedString alloc] initWithString:#"hit this "];
[linkString beginEditing];
NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString:#"link"];
NSRange range = NSMakeRange(0, [attrString length]);
[attrString addAttribute:NSLinkAttributeName value:[[NSURL URLWithString:#"http://google.com"] absoluteString] range:range];
[attrString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSUnderlinePatternDot] range:range];
[attrString addAttribute:NSForegroundColorAttributeName value:[NSColor blackColor] range:range];
[linkString appendAttributedString:attrString];
[linkString appendAttributedString:[[NSAttributedString alloc] initWithString:#" to search"]];
[linkString addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:NSMakeRange(0, [linkString length])];
[linkString endEditing];
[self.testTextField setAttributedStringValue:linkString];
Based on this example, you'll see the string coloured red and in the default Label font.
Then when you click on the label the font changes size and face and the link magically renders.
Any ideas on how to get the string to render correctly the first time?
I ran into this same problem. The solution I found was to explicitly set the NSFontAttributeName on the attributed string. I created an NSFont object that matched the font I had set in IB for my textfield and set that attribute like so:
NSFont *font = [NSFont fontWithName:#"Lucida Grande" size:(CGFloat)13.0];
[attrString addAttribute:NSFontAttributeName value:font range:range];
As far as I know that's just normal AppKit weirdness.
I've had success using this custom class to render text fields as links, you just add it in interface builder and set its attributed string value like normal:
DSClickableURLTextField / Swift DSClickableURLTextField fork.
You also have the option of using an NSButton, though that's more of a pain, and you don't get the hand cursor without extra work.

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

NSTextView - using initWithHTML with tables

I'm trying to format some text in NSTextView to be placed in a single line with black background. One piece of text needs to be left aligned, the other piece (but still in the same line) needs to be centered.
NSString *header = [NSString stringWithFormat:
#""
"<table style=\"width: 100%; background: black; "
"color: white; font-family: Arial; "
"font-size: 16px;\"><tr><td>"
"1. zadatak"
"</td><td align=\"center\" width=\"100%\">"
""
"%#"
""
"</td></tr></table>"
"", [self.problemname.stringValue uppercaseString]];
Unfortunately, no matter what width I specify, NSTextView appears to ignore the table width.
Any workarounds, different approaches, or other suggestions?
More info on the background of the problem.
I've written an app for writing tasks for programming contests. Each task author submits content in different format, so getting them to standardize it in a simple bundle would be of great benefit.
I need this for a printing module. I'm taking several NSTextViews and stitching them together.
These contests have the following document formatting which we're supposed to follow - Example PDF (GoogleDocs)
:
Contest header
+------------------------------------------------+
| 1st Task TITLE 20 points |
+------------------------------------------------+
Introductory text here.
Input
Explanation of input data
Output
Explanation of output data
Sample Data
Input: | Input:
abcd | bcde
|
Output: | Output:
efgh | fghi
|
More info: |
info text |
Modification of Apple's sample code for programmatically adding tables to NSAttributedString:
- (NSMutableAttributedString *) printTitleTableAttributedString
{
NSMutableAttributedString *tableString = [[NSMutableAttributedString alloc] initWithString:#"\n\n"];
NSTextTable *table = [[NSTextTable alloc] init];
[table setNumberOfColumns:3];
[tableString appendAttributedString:[self printTitleTableCellAttributedStringWithString:#"1. zadatak\n"
table:table
textAlignment:NSLeftTextAlignment
row:0
column:0]];
[tableString appendAttributedString:[self printTitleTableCellAttributedStringWithString:[self.problemname.stringValue stringByAppendingString:#"\n"]
table:table
textAlignment:NSCenterTextAlignment
row:0
column:1]];
[tableString appendAttributedString:[self printTitleTableCellAttributedStringWithString:#"20 bodova\n"
table:table
textAlignment:NSRightTextAlignment
row:0
column:2]];
[table release];
return [tableString autorelease];
}
- (NSMutableAttributedString *) printTitleTableCellAttributedStringWithString:(NSString *)string
table:(NSTextTable *)table
textAlignment:(NSTextAlignment)textAlignment
row:(int)row
column:(int)column
{
NSColor *backgroundColor = [NSColor colorWithCalibratedRed:0
green:0
blue:0x80/255.
alpha:0xFF];;
NSColor *borderColor = [NSColor whiteColor];
NSTextTableBlock *block = [[NSTextTableBlock alloc]
initWithTable:table
startingRow:row
rowSpan:1
startingColumn:column
columnSpan:1];
[block setBackgroundColor:backgroundColor];
[block setBorderColor:borderColor];
[block setWidth:0.0 type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockBorder];
[block setWidth:2.0 type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding];
NSMutableParagraphStyle *paragraphStyle =
[[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[paragraphStyle setTextBlocks:[NSArray arrayWithObjects:block, nil]];
[paragraphStyle setAlignment:textAlignment];
[block release];
NSMutableAttributedString *cellString =
[[NSMutableAttributedString alloc] initWithString:string];
[cellString addAttribute:NSParagraphStyleAttributeName
value:paragraphStyle
range:NSMakeRange(0, [cellString length])];
[cellString addAttribute:NSForegroundColorAttributeName
value:[NSColor whiteColor]
range:NSMakeRange(0, [cellString length])];
[paragraphStyle release];
return [cellString autorelease];
}
Then I just append it into the textview:
[printText.textStorage setAttributedString:[self printTitleTableAttributedString]];
NSTextView's HTML support is only rudimentary. It does't hook into WebKit or anything to render full HTML.
The only way I know of that comes even close to accomplishing what you're trying to do with an NSTextView is tab stops. You can add an NSTextTab with an alignment of NSCenterTextAlignment to the line's paragraph style. Then use a tab to separate the left-aligned text from the center-aligned text.
The biggest problem with this is that you have to calculate the center point of your text view and create your tab stop at that location every time the size of your text view changes.
I suppose you could also subclass NSLayoutManager, but that's probably more heavy-handed than you were expecting.

Resources