Appending to NSTextView - objective-c

I've got an NSTask (with an NSPipe set up) running in the background and I want to output the contents, as they're coming in, in an NSTextView (output).
The code I'm using is :
NSMutableAttributedString* str = [[NSMutableAttributedString alloc] initWithString:s];
//[str addAttribute:NSForegroundColorAttributeName value:[NSColor whiteColor] range:NSMakeRange(0, [str length])];
[[output textStorage] appendAttributedString:str];
[output scrollRangeToVisible:NSMakeRange([[output string] length], 0)];
Issues :
When there is a lot of data appending, the view seems like "flashing"... and not working properly.
Given that the NSTextView is on a Sheet, NO CONTENTS seem to be appearing when the mouse pointer is elsewhere other than hovering above the NSTextView
Why is that, although I've set the color/insertion color/etc of the NSTextView, this doesn't seem to apply to newly inserted text?
What's the suggested way of appending (+scrolling) on an NSTextView?
Thanks!

Remember that user interface elements, and this includes NSTextView, do their magic on the main thread. If you're attempting to add information to the text view, that's where you'd best be doing it. Here's how:
[[output textStorage] performSelectorOnMainThread:#selector(appendAttributedString:)
withObject:str
waitUntilDone:YES];
I'd address your third point, but frankly, that's a thing of which I'm still very much a student.
To address your fourth point, it would appear you've got that figured out; just combine your append and scroll actions. But just like changing the contents of textStorage, you want to be sure you're doing this on the main thread. Since -scrollRangeToVisible: doesn't take an object for its argument, you have to do this a bit differently:
dispatch_async(dispatch_get_main_queue(), ^{
[output scrollRangeToVisible:NSMakeRange([[output string] length], 0)];
});
My first example notwithstanding, you could place your call to -appendAttributedString: inside that block as well:
dispatch_async(dispatch_get_main_queue(), ^{
[[output textStorage] appendAttributedString:str];
[output scrollRangeToVisible:NSMakeRange([[output string] length], 0)];
});

Regarding the recommended way of appending to the NSTextView: You're doing quite well with appendAttributedString:, but it's recommended to shield it inside shouldChangeTextInRange, then a beginEditing, appendAttributedString, and finally endEditing:
textStorage = [textView textStorage];
if([textView shouldChangeTextInRange:range replacementString:string])
{
[textStorage beginEditing];
[textStorage replaceCharactersInRange:range withAttributedString:attrStr];
// or if you've already set up the attributes (see below)...
// [textStorage replaceCharactersInRange:range withString:str];
[textStorage endEditing];
}
I'd strongly suggest replacing scrollRangeToVisible: by scrollToPoint:, as scrollRangeToVisible: will cause a lot of flickering and it will also gradually become slower as you move 'down the range'.
A quick-and-dirty way could be something like this:
- (void)scrollToBottom
{
NSPoint pt;
id scrollView;
id clipView;
pt.x = 0;
pt.y = 100000000000.0;
scrollView = [self enclosingScrollView];
clipView = [scrollView contentView];
pt = [clipView constrainScrollPoint:pt];
[clipView scrollToPoint:pt];
[scrollView reflectScrolledClipView:clipView];
}
I let constrainScrollPoint do all the calculation work.
I do this, because my calculations failed anyway (those suggested by Apple and others, that used visRect/docRect coordinates, produced unreliable results).
reflectScrolledClipView is also important; it updates the scroll bar so it has the correct proportion and position.
You might also find it interesting to know when scrolling has occurred. If so, subscribe to both NSViewBoundsDidChangeNotification and NSViewFrameDidChangeNotification. When one of them occurs, the scroll bar position most likely changed (investigate [textView visibleRect] and [textView bounds]).
I see you also have trouble with the text-attributes. So did I for a long time.
I found that appending an attributed string would help quite a lot, but it still wasn't enough for the text being typed.
..Then I found out about typingAttributes.
When setting up your NSTextView, for instance in an -awakeFromNib, you can pick what you like from the following...
NSMutableParagraphStyle *paragraphStyle;
float characterWidth;
NSFont *font;
uint32_t tabWidth;
NSMutableDictionary *typingAttributes;
tabWidth = 4;
font = [NSFont fontWithName:#"Monaco" size:9.0];
paragraphStyle = [[textView defaultParagraphStyle] mutableCopy];
if(NULL == paragraphStyle)
{
paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
// or maybe:
// paragraphStyle = [NSParagraphStyle new];
}
characterWidth = [[font screenFontWithRenderingMode:NSFontDefaultRenderingMode] advancementForGlyph:(NSGlyph)' '].width;
[paragraphStyle setDefaultTabInterval:(characterWidth * (float) tabWidth];
[paragraphStyle setTabStops:[NSArray array]];
typingAttributes = [[textView typingAttributes] mutableCopy];
if(NULL == typingAttributes)
{
typingAttributes = [NSMutableDictionary new];
}
[typingAttributes setObject:paragraphStyle forKey:NSParagraphStyleAttributeName];
[typingAttributes setObject:font forKey:NSFontAttributeName];
[textView setTypingAttributes:attributes];
...It's way more than you probably need, but it shows how you can set the font, the tab width and the typing attributes.
NSForegroundColorAttributeName might also be interesting for you (as well as some other attributes, type NSForegroundColorAttributeName in Xcode and option-double-click on it, then you'll see some more attributes (you can command-double-click as well; this takes you to the definition in the header file).

Related

Objective c UIFont systemFontOfSize does not work

I am trying to change text font size with using NSAttributedString. But it's size doesn't change.
NSDictionary *attrDict = #{NSFontAttributeName : [UIFont boldSystemFontOfSize:22], NSForegroundColorAttributeName : [UIColor orangeColor]};
NSAttributedString *newAttString = [[NSAttributedString alloc] initWithString:mytext attributes:attrDict];
[result appendAttributedString:newAttString];
Only text color changes. Size of result string is not 22 and also it is not bold.
Instead of applying the attributes with the alloc, init, try doing it after with something like (with a mutable NSAttributedString):
NSMutableAttributedString *newAtt = [[NSMutableAttributedString alloc] initWithString:mytext]; // Allocate a new NSMutableAttributedString with `mytext`
[newAtt addAttributes:#{NSFontAttributeName : [UIFont boldSystemFontOfSize:20],
NSForegroundColorAttributeName: [UIColor orangeColor]}
range:NSMakeRange(0, [result length])]; // add new attributes for the length of the string `mytext`
[result setAttributedText:newAtt];
This answer would vary depending on what result is, I tested it on a UITextView and it worked fine, there is also an attributedText
property on UILabels.
Hope this helps.
You didn't mention what result means at the end of your code. Are you sure you want to "append" it?
Besides, I use this code for setting fonts
[UIFont fontWithName:#"Arial-BoldMT" size:22.0f]
This can be used to set different fonts and sizes respectively.
Hope this helps:)

NSTextStorage - How to find return chars (new line) in a text

In an NSTextStorage I insert time strings at the current pointer location like this :
NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString: #"00:00:00"];
[attrString autorelease];
int pos = [self selectedRange].location;
[[self textStorage] insertAttributedString: attrString atIndex:pos];
So far so good, it works perfect. But now I want to position the pointer at the beginning of the next line. Obviously this is right after the next return char.
Now how to find the next return char in textStorage and position the pointer there ?
I have not found any hint in the web for this task. Please help ...
You won't find a method to set the cursor (more precise: selection with zero length) in NSTextStorage's API, because it is a storage, having no selection. The selection is a property of the text view. This is the result of MVC. Simply do a check: You can have many text views displaying the same text. Obviously each one needs its own selection.
What you have to do is to get the position of the next paragraph (this is better than searching for \n) and set this as a selection for the text view.
NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString: #"00:00:00"];
[attrString autorelease]; // You should *really* use ARC
int pos = [self selectedRange].location;
[[self textStorage] insertAttributedString: attrString atIndex:pos];
// Get the next paragraph
NSString *text = self.textStorage.string;
NSRange restOfStringRange = NSMakeRange( pos, [text length]-pos );
NSUInteger nextParagraphIndex;
// You have to look to the start of the next paragraph
[text getParagraphStart:&nextParagraphIndex end:NULL contentsEnd:NULL forRange:restOfStringRange];
NSTextView *view = self.textView; // Or where ever you get it from
view.selectedRange = NSMakeRange(nextParagraphIndex, 0);
Typed in Safari, not tested, just to show the basic approach.

Is there a way to do half of a newline?

I'm wondering, is there a way to do half of a new line character (\n) in an NSString in objective C, i.e. so that it only skips about half the space? Or anyway else to accomplish this in an NSString?
Like Wain said, setting NSParagraphStyle on an NSAttributedString might be what you are looking for. UILabel supports NSAttributedStrings in iOS 6, but anything before that you will have to use a third party component. TTTAttributedLabel is very good and well documented.
NSMutableAttributedString* attrStr = [NSMutableAttributedString attributedStringWithString:#"Hello World!"];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
[style setLineSpacing:24]; //Just a random value, you'll have to play with it till you are hhappy
[attrStr addAttribute:NSParagraphStyleAttributeName
value:style
range:NSMakeRange(0, [myString length])];
label.attributedText = attrStr;
if you end up using TTTAttributedLabel you would use label.text = attrStr; or one of the helper methods (Taken from TTTAttributedLabel docs:
[label setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:^ NSMutableAttributedString *(NSMutableAttributedString *mutableAttributedString) {
NSRange boldRange = [[mutableAttributedString string] rangeOfString:#"ipsum dolar" options:NSCaseInsensitiveSearch];
NSRange strikeRange = [[mutableAttributedString string] rangeOfString:#"sit amet" options:NSCaseInsensitiveSearch];
// Core Text APIs use C functions without a direct bridge to UIFont. See Apple's "Core Text Programming Guide" to learn how to configure string attributes.
UIFont *boldSystemFont = [UIFont boldSystemFontOfSize:14];
CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)boldSystemFont.fontName, boldSystemFont.pointSize, NULL);
if (font) {
[mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(id)font range:boldRange];
[mutableAttributedString addAttribute:kTTTStrikeOutAttributeName value:[NSNumber numberWithBool:YES] range:strikeRange];
CFRelease(font);
}
return mutableAttributedString;
}];
Also, TTTAttributedLabel has a lineHeightMultiple (between 0.0 and 1.0) property that you might be able to fiddle with to get the desired effect. That way, you'll still be able to use an NSString and not mess with the sometimes ugly NSAttributedString.
While this answer arguably won't help the asker, some historical perspective on the half-linefeed concept may be of general use.
The concept of half-reverse and half-forward linefeeds was useful on teletypes to combine, say, the o and " characters to approximate the German "o umlaut", รถ. The Unix col command recognized ESC-8 (0x1b 0x38) and ESC-9 (0x1b 0x39) as these sequences, respectively, and this standard was adopted by asciitiff.
The printer control language PCL recognized the forward half-linefeed.
An OKI PCL4.5 printer, the OL600e, accepted ESC= (0x1b 0x3d) as half-linefeed, and PCL5 extended that to ESC&= (0x1b 0x26 0x3d).
I'm having difficulty locating older examples.

Book layout NSTextFields -- going back pages?

I have two NSTextfields in a book layout, and I can't figure out a fast way to go back to the previous 'page'. Book size, font size, line size all change, so the string of text for the previous page has to be calculated on the fly. Picture:
The NSTextfields each have one NSTextContainer, and they share a NSLayoutManager and NSTextStorage.
Going forward is easy: I take the character range of the visible text, and then create a substring starting from the next character along.
My going back method is a kludge. I figure out the maximum amount of characters that can be visible at once. I then make a string to that length, with the last character the one I want in the bottom right corner of the book. I then loop: removing characters from the start, checking what is visible each time until the character I want is in the bottom right. This is very, very, slow.
Can anyone suggest a faster way to do what I want to achieve? I had the thought of using scrollRangeToVisible, but I couldn't figure out how to set up a NSScrollView for this layout.
Can anyone help?
Textcontainers are set up like this:
-(void)setupTextViews {
articleString = [[NSAttributedString alloc] init];
articleStringPortion = [[NSAttributedString alloc] init];
bookTextStorage = [[NSTextStorage alloc] init];
bookLayoutManager = [[NSLayoutManager alloc] init];
[[self bookTextStorage] addLayoutManager:bookLayoutManager];
leftColumnRect = NSZeroRect;
rightColumnRect = NSZeroRect;
NSDivideRect(bookRect, &leftColumnRect, &rightColumnRect, NSWidth(bookRect) / 2, NSMinXEdge);
// First column
{
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:leftColumnRect.size];
leftColumnTextView = [[CRMouseOverTextView alloc] initWithFrame:leftColumnRect textContainer:textContainer];
[leftColumnTextView setDrawsBackground:NO];
[leftColumnTextView setEditable:NO];
[leftColumnTextView setup];
[bookView addSubview:leftColumnTextView];
[bookLayoutManager addTextContainer:textContainer];
[textContainer release];
[leftColumnTextView release];
}
// Second column
{
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:rightColumnRect.size];
rightColumnTextView = [[CRMouseOverTextView alloc] initWithFrame:rightColumnRect textContainer:textContainer];
[rightColumnTextView setDrawsBackground:NO];
[rightColumnTextView setEditable:NO];
[rightColumnTextView setup];
[bookView addSubview:rightColumnTextView];
[bookLayoutManager addTextContainer:textContainer];
[textContainer release];
[rightColumnTextView release];
}
}
There's no point posting my awful going backwards code, but I'm using this method I found to figure out what is visible each time:
-(NSRange)getViewableRange:(NSTextView *)tv {
NSLayoutManager *lm = [tv layoutManager];
NSRect visRect = [tv visibleRect];
NSPoint tco = [tv textContainerOrigin];
visRect.origin.x -= tco.x;
visRect.origin.y -= tco.y;
NSRange glyphRange = [lm glyphRangeForBoundingRect:visRect inTextContainer:[tv textContainer]];
NSRange charRange = [lm characterRangeForGlyphRange:glyphRange actualGlyphRange:nil];
return charRange;
}
I'm not sure this is the answer you're looking for, but if it were me, I'd probably just "cache" a bunch of those character ranges, for all the previous pages that have been viewed. You probably wouldn't even have any problem storing them all for a book with a lot of pages. Of course, then you still have to use your kludgy code for when the user re-sizes the text, or whatever. (Either that, or you could re-calculate from some suitable starting point... say the beginning of the book if it's fast enough, or the beginning of a chapter or something. Then you just find the page(range) that contains the text that is already being displayed and show the previous one.)

How to create a PDF in iOS programmatically?

I want to create a PDF file in iOS ,there should be one table in the PDF, which is filled from one array. I already searched on Google, but didn't succeed. Any help is appreciated
Something like this routine to render the text:
- (CFRange)renderTextRange:(CFRange)currentRange andFrameSetter:(CTFramesetterRef)frameSetter intoRect:(CGRect)frameRect {
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, currentRange, framePath, NULL);
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
CGContextTranslateCTM(currentContext, 0, 792);
CGContextScaleCTM(currentContext, 1.0, -1.0);
CTFrameDraw(frameRef, currentContext);
CGContextRestoreGState(currentContext);
CGPathRelease(framePath);
currentRange = CTFrameGetVisibleStringRange(frameRef);
currentRange.location += currentRange.length;
currentRange.length = 0;
CFRelease(frameRef);
return currentRange;
}
And the following code snippet which calls it, assuming you have the context and any fonts etc created and in the appropriate variables. The following loop simply builds up the text line by line into an NSMutableAttributedString which you can then render:
CTFontRef splainFont = CTFontCreateWithName(CFSTR("Helvetica"), 10.0f, NULL);
CGFloat margin = 32.0f;
CGFloat sink = 8.0f;
NSMutableAttributedString *mainAttributedString = [[NSMutableAttributedString alloc] init];
NSMutableString *mainString = [[NSMutableString alloc] init];
// Ingredients is an NSArray of NSDictionaries
// But yours could be anything, or just an array of text.
for (Ingredient *ingredient in ingredients) {
NSString *ingredientText = [NSString stringWithFormat:#"%#\t%#
\n",ingredient.amount,ingredient.name];
[mainString appendString:ingredientText];
NSMutableAttributedString *ingredientAttributedText =
[[NSMutableAttributedString alloc] initWithString:ingredientText];
[ingredientAttributedText addAttribute:(NSString *)(kCTFontAttributeName)
value:(id)splainFont
range:NSMakeRange(0, [ingredientText length])];
[mainAttributedString appendAttributedString:ingredientAttributedText];
[ingredientAttributedText release];
}
Now you have your array written out with newlines to one NSMutableAttributedString you can render it, depending on your text you might want to render it out in a loop until the rendered location matches your text length. Something like:
// Render Main text.
CTFramesetterRef mainSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mainAttributedString);
currentRange = [KookaDIS renderTextRange:currentRange
andFrameSetter:mainSetter
intoRect:pageRect];
// If not finished create new page and loop until we are.
while (!done) {
UIGraphicsBeginPDFPageWithInfo(pageRect, nil);
currentRange = [self renderTextRange:currentRange
andFrameSetter:mainSetter
intoRect:pageRect];
if (currentRange.location >= [mainString length]) {
done = TRUE;
}
}
The above code will need quite a bit of adapting I'm sure as it's hacked out of a project of my own so some variables (like the frame setter) won't exist and you need to close off the PDF context and release variables etc. Note how mainString is used to determine when the text has been rendered out.
It should give a clear enough indication of how to loop around an array or any other group to render an arbitrary length of text into a document.
Slight modifications to the while loop and the one render before entering it will let you render text out in multiple columns too.