NSLayoutManager glyph generation and caret position - objective-c

I have a text editor app, which uses Markdown-style formatting. Formatting characters are hidden when caret/selection is not on the corresponding line.
I'm successfully using shouldGenerateGlyphs: method in NSLayoutManagerDelegate to manipulate what is drawn on NSTextView. I'm using pure delegation and haven't subclassed the layout manager itself.
However, I can't understand how I can position the caret correctly after glyph regeneration. After selection has changed, I'm calling a method (here called hideAndShowMarkup) which regenerates glyphs for both the line currently edited and the one that was selected earlier. Because some glyphs are added on screen, caret gets rendered wrong.
-(void)hideAndShowMarkup {
[self.layoutManager invalidateGlyphsForCharacterRange:currentLine.range changeInLength:0 actualCharacterRange:nil];
[self.layoutManager invalidateGlyphsForCharacterRange:prevLine.range changeInLength:0 actualCharacterRange:nil];
[self.layoutManager ensureGlyphsForCharacterRange:currentLine.range];
[self.layoutManager ensureGlyphsForCharacterRange:prevLine.range];
[self updateInsertionPointStateAndRestartTimer:YES];
}
updateInsertionPoint... doesn't work here, as ensuring glyphs seems to run asynchronously. I've tried calling it in didCompleteLayoutForTextContainer: delegate method too, but to no effect.
Is there a way to detect when the glyphs have actually been drawn, and to ensure insertion point position after that?

NSLayoutManager is surprisingly poorly documented, and understanding its inner workings required looking through implementations in random public repositories.
To change glyphs in a range and have the caret position correctly, you first need to invalidate glyphs for both of the changed ranges, and then invalidate the layout for the range the caret is about to be positioned in.
Then, after making sure that there is a current graphic context, glyphs can be redrawn synchronously. After this, updating insertion point works normally.
In the example below, I have two objects, line and prevLine which contain ranges for both the line on which caret is positioned now, and the one caret moved away from.
-(void)hideAndShowMarkup {
[self.layoutManager invalidateGlyphsForCharacterRange:line.range changeInLength:0 actualCharacterRange:nil];
[self.layoutManager invalidateGlyphsForCharacterRange:prevLine.range changeInLength:0 actualCharacterRange:nil];
[self.layoutManager invalidateLayoutForCharacterRange:line.range actualCharacterRange:nil];
if (NSGraphicsContext.currentContext) {
[self.layoutManager drawGlyphsForGlyphRange:line.range atPoint:self.frame.origin];
[self.layoutManager drawGlyphsForGlyphRange:prevLine.range atPoint:self.frame.origin];
}
[self updateInsertionPointStateAndRestartTimer:NO];
}

Related

iOS7 Type into UITextView with Line Spacing and keep formatting using TextKit

I came across this great example which helped me understand how I can achieve line spacing / paragraph spacing as you type inside a UITextView with iOS7, however there is a problem and I am hoping someone can help me with resolving an issue as I am still learning about TextKit please.
This is the actual example
https://github.com/downie/DoubleSpacedTextView
I have attached 2 files:-
1- This is a video of the problem:-
http://1drv.ms/1o8Rpd2
2- This is the project I am testing with:
http://1drv.ms/1o8RtK0
The issue is when there are blank lines between the text lines (at least 2 blank lines) and then click inside the blank lines to activate the UITextView and show the keyboard, the text moves up / loses its formatting.
This is the core function which I modified slightly from the original and it does the formatting, it works perfectly but not when you click inside the UITextView the first time:-
- (void) formatText
{
__block CGFloat topOffset = 0;
NSRange lineGlyphRange = [self.layoutManager glyphRangeForTextContainer:self.textContainer];
[self.layoutManager
enumerateLineFragmentsForGlyphRange:lineGlyphRange usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop)
{
CGRect adjustedRect = rect;
CGRect adjustedUsedRect = usedRect;
adjustedRect.origin.y = topOffset;
adjustedUsedRect.origin.y = topOffset;
[self.layoutManager setLineFragmentRect:adjustedRect forGlyphRange:glyphRange usedRect:adjustedUsedRect];
topOffset += 30; // 30 is the space between the lines you can adjust this as you like
}];
CGRect adjustedExtraLineFragmentRect = self.layoutManager.extraLineFragmentRect;
CGRect adjustedExtraLineFragmentUsedRect = self.layoutManager.extraLineFragmentUsedRect;
adjustedExtraLineFragmentRect.origin.y = topOffset;
adjustedExtraLineFragmentUsedRect.origin.y = topOffset;
[self.layoutManager setExtraLineFragmentRect:adjustedExtraLineFragmentRect usedRect:adjustedExtraLineFragmentUsedRect textContainer:self.textContainer];
}
Can you please see if we can stop this from happening? so that the text is always formatted as we type into the UITextView.
Are there any other examples that show how we can type a formatted text (with line spacing) into a UITextView using TextKit or attributed Text?.
I have been struggling with this problem since iOS6.
Thanks,
Will
I don't have a full solution, just ideas of where to look for the solution.
All of the layout code fires off of the textChange event of the UITextView. Moving the cursor around doesn’t change any of the content of that text, so none of that code is called. However, even attaching it to textViewDidChangeSelection doesn’t make the issue go away.
I’m thinking it could be one of two things:
The NSLayoutManager is handling a blank line differently than a line with text. This either means it’s a TextKit bug, or it’s one of the many delegates/methods that just isn't being called.
Something about the rect math is wrong for a blank line. I believe the only key difference in layout if the line is blank vs not is that the usedRect for the lineFragmentRect has width 0, but maybe it’s also got height 0 or a bad offset that’s messing with the math. I don’t really see what’s causing that though.

How to animate a uibezierpath to make it stand out

Part of my iPad app allows users to draw paths to connect different parts of the screen. They all have the same color (white) and line width. Each path is represented as a UIBezierPath. Besides their locations, they look identical. Since users are only editing one path at a time, I want to make it so that they can visually see which path they are editing.
Is there a way to animate the path, so that the user has a visual queue about which path they are editing? I'm thinking that maybe the current path could glow or have moving dotted lines. I don't want to change the base color, since I use many colors in the other parts of application (pretty much all major colors except white).
I haven't done this in an animated way, but I make my currently drawing paths have dashed lines, and then solid once the drawing ends. I subclassed NSBezierPath, and added a selected property. The setSelected method looks like this:
-(void)setSelected:(BOOL) yes_no {
selected = yes_no;
if (yes_no == YES) {
CGFloat dashArray[2];
dashArray[0] = 5;
dashArray[1] = 2;
[self setLineDash:dashArray count:2 phase:0];
self.pathColor = [self.unselectedColor highlightWithLevel:.5];
} else {
[self setLineDash:nil count:2 phase:0];
self.pathColor = self.unselectedColor;
}
}
I set the property to YES in the mouseDragged: method, and then to NO in mouseUP:

How to generate grid in PDF using Cocoa?

I'm a beginner to Cocoa and Objective-C.
I want to make a Cocoa application that will generate a grid of boxes (used for practicing Chinese calligraphy) to export as a PDF, similar to this online generator: http://incompetech.com/graphpaper/chinesequarter/.
How should I generate the grid? I've tried to use Quartz with a CustomView, but didn't manage to get very far. Also, once the grid is drawn in the CustomView, what is the method for "printing" that to a PDF?
Thanks for the help.
How should I generate the grid?
Implement a custom view that draws it.
I've tried to use Quartz with a CustomView, …
That's one way; AppKit drawing is the other. Most parts of them are very similar, though; AppKit is directly based on PostScript, while Quartz is indirectly based on PostScript.
… but didn't manage to get very far.
You should ask a more specific question about your problem.
Also, once the grid is drawn in the CustomView, what is the method for "printing" that to a PDF?
Send it a dataWithPDFInsideRect: message, passing its bounds.
Note that there is no “once the grid is drawn in the CustomView”. Though there may be some internal caching, conceptually, a view does not draw once and hold onto it; it draws when needed, every time it's needed, into where it's needed. When the window needs to be redrawn, Cocoa will tell any views that are in the dirty area to (re)draw, and they will draw ultimately to the screen. When you ask for PDF data, that will also tell the view to draw, and it will draw into a context that records PDF data. This allows the view both to be lazy (draw only when needed) and to draw differently in different contexts (e.g., when printing).
Oops, you were asking about Cocoa and this is Cocoa Touch, but I'll leave it here as it may be some use (at least to others who find this later).
You can draw things in the view and then put what's there into a pdf.
This code will take what's drawn in a UIView (called sheetView here), put it into a pdf, then put that as an attachment in an email (so you can see it for now). You'll need to reference the protocol MFMailComposeViewControllerDelegate in your header.
if ([MFMailComposeViewController canSendMail]) {
//set up PDF rendering context
NSMutableData *pdfData = [NSMutableData data];
UIGraphicsBeginPDFContextToData(pdfData, sheetView.bounds, nil);
UIGraphicsBeginPDFPage();
//tell our view to draw (would normally use setNeedsDisplay, but need drawn now).
[sheetView drawRect:sheetView.bounds];
//remove PDF rendering context
UIGraphicsEndPDFContext();
//send PDF data in mail message as an attachment
MFMailComposeViewController *mailComposer = [[[MFMailComposeViewController alloc] init] autorelease];
mailComposer.mailComposeDelegate = self;If
[mailComposer addAttachmentData:pdfData mimeType:#"application/pdf" fileName:#"SheetView.pdf"];
[self presentModalViewController:mailComposer animated:YES];
}
else {
if (WARNINGS) NSLog(#"Device is unable to send email in its current state.");
}
You'll also need this method...
#pragma mark -
#pragma mark MFMailComposeViewControllerDelegate protocol method
//also need to implement the following method, so that the email composer can let
//us know that the user has clicked either Send or Cancel in the window.
//It's our duty to end the modal session here.
-(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error {
[self dismissModalViewControllerAnimated:YES];
}

NSTextView not showing red misspelling underlines when on a layer

When an NSTextView is a subview of an NSView that is layer-backed (-wantsLayer == YES), it does not render the squiggly red underlines for misspelled words. All it takes to reproduce this is to make an empty Cocoa project, open the nib, drag NSTextView into the window, and toggle the window's content view to want a layer. Boom - no more red underlines.
I've done some searching, and this appears to be a known situation and has been true since 10.5. What I cannot find, though, is a workaround for it. Is there no way to get the underlines to render when NSTextView is in a layer-backed view?
I can imagine overriding NSTextView's drawRect: and using the layout manager to find the proper rects with the proper temporary attributes set that indicate misspellings and then drawing red squiggles myself, but that is of course a total hack. I also can imagine Apple fixing this in 10.7 (perhaps) and suddenly my app would have double underlines or something.
[update] My Workaround
My current workaround was inspired by nptacek's mentioned spell checking delegate method which prompted me to dig deeper down a path I didn't notice before, so I'm going to accept that answer but post what I've done for posterity and/or further discussion.
I am running 10.6.5. I have a subclass of NSTextView which is the document view of a custom subclass of NSClipView which in turn is a subview of my window's contentView which has layers turned on. In playing with this, I eventually had all customizations commented out and still the spelling checking was not working correctly.
I isolated what, I believe, are two distinct problems:
#1 is that NSTextView, when hosted in a layer-backed view, doesn't even bother to draw the misspelling underlines. (I gather based on Google searches that there may have been a time in the 10.5 days when it drew the underlines, but not in the correct spot - so Apple may have just disabled them entirely to avoid that problem in 10.6. I am not sure. There could also be some side effect of how I'm positioning things, etc. that caused them not to appear at all in my case. Presently unknown.)
#2 is that when NSTextView is in this layer-related situation, it appears to not correctly mark text as misspelled while you're typing it - even when -isContinuousSpellCheckingEnabled is set to YES. I verified this by implementing some of the spell checking delegate methods and watching as NSTextView sent messages about changes but never any notifying to set any text ranges as misspelled - even with obviously misspelled words that would show the red underline in TextEdit (and other text views in other apps). I also overrode NSTextView's -handleTextCheckingResults:forRange:types:options:orthography:wordCount: to see what it was seeing, and it saw the same thing there. It was as if NSTextView was actively setting the word under the cursor as not misspelled, and then when the user types a space or moves away from it or whatever, it didn't re-check for misspellings. I'm not entirely sure, though.
Okay, so to work around #1, I overrode -drawRect: in my custom NSTextView subclass to look like this:
- (void)drawRect:(NSRect)rect
{
[super drawRect:rect];
[self drawFakeSpellingUnderlinesInRect:rect];
}
I then implemented -drawFakeSpellingUnderlinesInRect: to use the layoutManager to get the text ranges that contain the NSSpellingStateAttributeName as a temporary attribute and render a dot pattern reasonably close to the standard OSX misspelling dot pattern.
- (void)drawFakeSpellingUnderlinesInRect:(NSRect)rect
{
CGFloat lineDash[2] = {0.75, 3.25};
NSBezierPath *underlinePath = [NSBezierPath bezierPath];
[underlinePath setLineDash:lineDash count:2 phase:0];
[underlinePath setLineWidth:2];
[underlinePath setLineCapStyle:NSRoundLineCapStyle];
NSLayoutManager *layout = [self layoutManager];
NSRange checkRange = NSMakeRange(0,[[self string] length]);
while (checkRange.length > 0) {
NSRange effectiveRange = NSMakeRange(checkRange.location,0);
id spellingValue = [layout temporaryAttribute:NSSpellingStateAttributeName atCharacterIndex:checkRange.location longestEffectiveRange:&effectiveRange inRange:checkRange];
if (spellingValue) {
const NSInteger spellingFlag = [spellingValue intValue];
if ((spellingFlag & NSSpellingStateSpellingFlag) == NSSpellingStateSpellingFlag) {
NSUInteger count = 0;
const NSRectArray rects = [layout rectArrayForCharacterRange:effectiveRange withinSelectedCharacterRange:NSMakeRange(NSNotFound,0) inTextContainer:[self textContainer] rectCount:&count];
for (NSUInteger i=0; i<count; i++) {
if (NSIntersectsRect(rects[i], rect)) {
[underlinePath moveToPoint:NSMakePoint(rects[i].origin.x, rects[i].origin.y+rects[i].size.height-1.5)];
[underlinePath relativeLineToPoint:NSMakePoint(rects[i].size.width,0)];
}
}
}
}
checkRange.location = NSMaxRange(effectiveRange);
checkRange.length = [[self string] length] - checkRange.location;
}
[[NSColor redColor] setStroke];
[underlinePath stroke];
}
So after doing this, I can see red underlines but it doesn't seem to update the spelling state as I type. To work around that problem, I implemented the following evil hacks in my NSTextView subclass:
- (void)setNeedsFakeSpellCheck
{
if ([self isContinuousSpellCheckingEnabled]) {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(forcedSpellCheck) object:nil];
[self performSelector:#selector(forcedSpellCheck) withObject:nil afterDelay:0.5];
}
}
- (void)didChangeText
{
[super didChangeText];
[self setNeedsFakeSpellCheck];
}
- (void)updateInsertionPointStateAndRestartTimer:(BOOL)flag
{
[super updateInsertionPointStateAndRestartTimer:flag];
[self setNeedsFakeSpellCheck];
}
- (void)forcedSpellCheck
{
[self checkTextInRange:NSMakeRange(0,[[self string] length]) types:[self enabledTextCheckingTypes] options:nil];
}
It doesn't work quite the same way as the real, expected OSX behavior, but it's sorta close and it gets the job done for now. Hopefully this is helpful for someone else, or, better yet, someone comes here and tells me I was missing something incredibly simple and explains how to fix it. :)
Core Animation is awesome, except when it comes to text. I experienced this firsthand when I found out that subpixel antialiasing was not a given when working with layer-backed views (which you can technically get around by setting an opaque backgroundColor and making sure to draw the background). Subpixel anti-aliasing is just one of the many caveats encountered while working with text and layer-backed views.
In this case, you've got a couple of options. If at all possible, move away from layer-backed views for the parts of your program that utilize the text views. If you've already tried this, and can't avoid it, there is still hope!
Without going so far as overriding drawRect, you can achieve something that is close to the standard behavior with the following code:
- (NSArray *)textView:(NSTextView *)view didCheckTextInRange:(NSRange)range types:(NSTextCheckingTypes)checkingTypes options:(NSDictionary *)options results:(NSArray *)results orthography:(NSOrthography *)orthography wordCount:(NSInteger)wordCount
{
for(NSTextCheckingResult *myResult in results){
if(myResult.resultType == NSTextCheckingTypeSpelling){
NSMutableDictionary *attr = [[NSMutableDictionary alloc] init];
[attr setObject:[NSColor redColor] forKey:NSUnderlineColorAttributeName];
[attr setObject:[NSNumber numberWithInt:(NSUnderlinePatternDot | NSUnderlineStyleThick | NSUnderlineByWordMask)] forKey:NSUnderlineStyleAttributeName];
[[inTextView layoutManager] setTemporaryAttributes:attr forCharacterRange:myResult.range];
[attr release];
}
}
return results;
}
We're basically doing a quick-and-dirty delegate method for NSTextView (make sure to set the delegate in IB!) which checks to see if a word is flagged as incorrect, and if so, sets a colored underline.
Note that there are some issues with this code -- Namely that characters with descenders (g, j, p, q, y, for example) won't display the underline correctly, and it's only been tested for spelling errors (no grammar checking here!). The underline dot pattern (NSUnderlinePatternDot) does not match Apple's style for spellchecking, and the code is still enabled even when layer backing is disabled for the view. Additionally, I'm sure there are other problems, as this code is quick and dirty, and hasn't been checked for memory management or anything else.
Good luck with your endeavor, file bug reports with Apple, and hopefully this will someday be a thing of the past!
This is also a bit of a hack, but the only thing I could get working was to put an intermediate delegate on the NSTextView's layer, so that all selectors are passed through, but drawLayer:inContext: then calls the NSTextView's drawRect:. This works, and is probably a little more future proof, although I'm not sure if it will break any CALayer animations. It also seems you have to fix the CGContextRef's CTM (based on the backing layer frame?).
Edit:
You can get the drawing rect as in the drawInContext: documentation, with CGContextGetClipBoundingBox(ctx), but there might be an issue with flipped coordinates in the NSTextView.
I'm not entirely sure how to fix this as calling drawRect: as I did is a bit hackish, but I'm sure someone on the net has a tutorial on doing it. Perhaps I can make one if/when I have time and work it out.
It might be worthwhile looking for an NSCell backing the NSTextView, as it's probably a lot more appropriate to use this instead.

Clickable links within a custom NSCell

I have a custom NSCell with various elements inside of it (images, various text pieces) and one of those text blocks may have various clickable links inside of it. I have my NSAttributedString correctly identifying the links and coloring them blue however I can't figure out how to get the cursor to turn into a hand and allow a user to actually click on them.
Right now I have my attributed string drawn right to the cell which obviously isn't clickable, but I'm not sure how to add it any other way since NSCell doesn't inherit from NSView. Normally I'd just add an NSTextField as a subview but I can't do it like that in this case.
Any thoughts?
The only solution I can think of is via manual hit testing and mouse tracking within your NSCell. The hardest part (which I don't have the answer to) is how to determine the rect of the link text ... hopefully someone can answer that?
Once you know the rect of the url text it's possible to implement the clicking action by implementing hitTestForEvent. I think you'd do it something like this;
// If the event is a mouse down event and the point is inside the rect trigger the url
- (NSUInteger)hitTestForEvent:(NSEvent *)event inRect:(NSRect)frame ofView:(NSView *)controlView {
NSPoint point = [controlView convertPoint:[event locationInWindow] fromView:nil];
// Check that the point is over the url region
if (NSPointInRect(point, urlFrame)) {
// If event is mousedown activate url
// Insert code here to activate url
return NSCellHitTrackableArea;
} else {
return [super hitTestForEvent:event inRect:frame ofView:controlView];
}
}
Based on talks with Ira Cooke and other folks I've decided to go for the following solution:
Draw directly to the NSCell
When the mouse enters an NSCell, I will immediately add a custom NSView subview to the NSTableView at the same position as the NSCell that was hovered over.
Their designs match pixel for pixel so there's no discernible difference
This NSView will have an NSTextView (or Field, haven't decided) that will display the attributed string with links in it allowing it to be clickable.
When you hover out of the NSCell its mirror NSView is destroyed
If all goes according to plan then I should only have 1 NSView attached to the NSTableView at a time and most of the time none at all. I'll come back and report my results once I get it working.