NSTextView not showing red misspelling underlines when on a layer - objective-c

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.

Related

How to remove or hide background in MKMapView when using custom tiles

Having tried many methods I still haven't found a good and full-proof way of preventing the usual "maps" from being shown behind custom map tiles that I am using. Ultimately I want my app to have a map page consisting only of a custom map.
I am really looking for a solution that is pure iOS and doesn't require any 3rd party software but it would appear difficult.
I have tried 3 methods already:
Number 1, hiding the background map via it's view:
NSArray *views = [[[self.mapView subviews] objectAtIndex:0] subviews];
[[views objectAtIndex:0] setHidden:YES];
this however doesn't work on a certain new operating system coming out very soon! The whole screen goes blank. The Apple Developer Forum hasn't provided a solution either
Number 2, Using another blank overlay (e.g. MKCircle) to cover the background map. This works however when scrolling or zooming out quickly, sometimes the overlay flickers off and you can briefly see the background map behind so not ideal.
Number 3, and this is what I have been working on for a few days now is to simply prevent the user from zooming out. Most documented methods tend to use regionDidChangeAnimated or regionWillChangeAnimated, however these do not seem to suddenly stop the map zooming out when pinching - they wait until the pinch movement has finished before taking effect so again it means the background map can be viewed briefly.
So now I am stumped, unless of course I have missed something with these other two methods.
So any help would be much appreciated!
Add this:
-(void)viewDidAppear:(BOOL)animated
{
MKTileOverlay *overlay = [[MKTileOverlay alloc] init];// initWithURLTemplate:tileTemplate];
overlay.canReplaceMapContent = YES;
[map addOverlay:overlay];
overlay = nil;
}
-(void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
NSData *tile =nil;
}
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay: (id<MKOverlay>)overlay
{
if ([overlay isKindOfClass:[MKTileOverlay class]])
{
MKTileOverlayRenderer *renderer = [[MKTileOverlayRenderer alloc] initWithOverlay:overlay];
[renderer setAlpha:0.5];
return renderer;
}
}
It will replace the map content from the background. It worked very well in my case where I am adding an overlay on the whole map and hiding the real map from the user.
You can't do this in current releases without a third-party library like MapBox. However, the future OS release that you speak of lets you do this.

Cocoa Application does not redraw after overlapping another application

After I overlay an application over my previous application, I go back to previous application and encounter a few errors:
certain components have disappeared
only way to make the components visible is to resize the window
that seems to redraw the whole canvas.
Weird thing is that there are only a couple of components and drawn images that are missing
It doesn't always happen but only a couple of times
I haven't found a solid way to reproduce the problem.
Anybody have an Idea why this is happening?
I experienced exactly the same issue (view was updated correctly only after resizing), except that I've used OpenGL drawing in OSX game.
My problem was solved by adding this:
GLint vblSynch = 1;
[[self openGLContext] setValues:&vblSynch forParameter:NSOpenGLCPSwapInterval];
in my custom NSOpenGLView init method.
Then I've implemented:
- (void)drawRect:(NSRect)rect
{
[self destroyFramebuffer]; // glDeleteFramebuffers..
[self createFramebuffer]; // [super prepareOpenGl], glGenFrame(Render)Buffers, bind buffers, etc
[self drawView]; // [[self openGLContext] makeCurrentContext], make some drawing, [[self openGLContext] flushBuffer]..
}
like this.
After these changes, when window gets focus it redraws itself (without any resizing stuff :) ).
Hope this helps!

NSCell redrawing issues

I'm creating a NSCell subclass that draws some objects directly onto the view (using drawInRect:fromRect:operation:fraction:respectFlipped:hints:) and also draws an NSButton instance simply using NSView's addSubview: selector.
While objects drawn using the first method all draw correclty, I'm having problems with drawing the NSButton correctly. The issue is that my NSButton instances will draw in the right places, but multiple times over.
I've researched this on the internet for a while and some people suggested using a cache, but I'm not sure if this is efficient. (going an array containing buttons using a for loop will definately cause slow scrolling since I display a lot of data...)
How would you do this? Am I barking up the wrong tree?
This is the relevant code:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSRect _controlRect = cellFrame;
float _Y = cellFrame.origin.y;
NSRect _accessoryRect = NSMakeRect(_controlRect.size.width - 70.0f, _Y + 9.0f, 50.0f, 23.0f);
_switch = [self _choiceSwitch];
[_switch setFrame:_accessoryRect];
[controlView addSubview:_switch];
}
Long story short: Friends don't let friends addSubview, while drawing.
This a fundamental, and not particularly well-explained aspect of managing control interfaces, but is important to come to grips with.
Let your controllers dictate the "order" of subviews, and you can sleep tight knowing that that button shouldn't get overtly mucked about (which is NOT the case if it's getting jostled around inside your custom drawing routines).
It's easy to get trapped in this alley, cause, like, hey, I added an NSImageView in my initWithFrame and everything seems to be okay… But it's just sort of not how you're supposed to do it, I guess… and when you start subclassing NSControl, etc. is when you start to realize why.
Updated: Here's a really good write up on designing custom controls with an equally as great sample project attached - which embodies the kind of code organization that can help avoid this type of issue. For example.. you'll notice in the controller class how he's keeping each button seperate, unique, and independent of other views' business…
for (int butts = 0; butts < 3; butts++) {
NSRect buttFrame = NSMakeRect(0, butts * 10, 69, 10);
ExampleButt *butt = [[ExampleButt alloc]initWithFrame:buttFrame];
[mainView addSubview:butt];
}
“Drawing” NSButton by adding its instance into the view hierarchy each time you draw the cell itself is definitely a bad idea. Instead, create an NSButtonCell and configure it up to your taste. Then, in your -[NSCell drawInteriorWithFrame:inView:] use a cell ivar to draw its appearance.
If you want to have a clickable NSButton instance in each cell of the table view, try to avoid a call to addSubview: when possible. Each time you do this, the control view may invalidate its layout and re-draw everything from scratch making some kind of a recursion in your case.

Mouse Enter/Exit events on partially hidden NSViews

I have a problem that I think is solvable with some hackery, but I'm very curious if there is an easier way to get the job done without having to do all of that.
I have a stack of NSViews (layer-backed, if that somehow helps provides some better solution), as shown below:
The thing here is that this is essentially a menu, but is hover-sensitive. If the user hovers over one of the exposed parts of the lower-level views, I need to perform an action depending on what that view is. It is a dynamic system so the number of stacked menu items like this may change, making static calculations more difficult. As you can see, they are basically all a copy (shape-wise) of the first item, but then rotated a bit the further you go down the stack via simple transform rotation.
My question to the SO community is what do you all think the best approach to getting mouseEntered: and mouseExited: events for just the literally visible portions of these views?
What I have attempted to do is use an NSTrackingArea on the visibleRect portion of these views, which sounds much more handy than it really is in this situation. In reality, the visibleRect seems to be "visible" for all of them, all the time. Nothing is explicitly blocked or hidden by anything more than just a partially overlapping NSView. All that happens is I get a spammed console from all of the views screaming out at once that a mouse entered their rect.
Something I am considering is making sub-NSView's of each menu item and having each of those be responsible for the tracking area... each menu item having a "strip" view along the right and bottom sides that could report, but that's still a bit of a hack and is icky.
Does anyone have a better idea? Perhaps one from experience?
Thanks!
I know you already have a solution, but I thought I would try a different approach, that didn't require getting tons of mouseMoved events. I created 3 custom views in code, added tracking rects for them and sent all mouseEntered and mouseExited messages to the same method that does a hitTest to determine which view is top most. This is the code for the content view of the window.
#implementation MainView
#synthesize oldView;
-(void)awakeFromNib {
oldView = nil;
Card *card1 = [[Card alloc]initWithFrame:NSMakeRect(150, 150, 200, 150) color:[NSColor redColor] name:#"Red Box"];
NSTrackingArea *area1 = [[NSTrackingArea alloc]initWithRect:card1.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:area1];
[self addSubview:card1];
Card *card2 = [[Card alloc]initWithFrame:NSMakeRect(180, 120, 200, 150) color:[NSColor yellowColor] name:#"Yellow Box"];
NSTrackingArea *area2 = [[NSTrackingArea alloc]initWithRect:card2.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:area2];
[self addSubview:card2];
Card *card3 = [[Card alloc]initWithFrame:NSMakeRect(210, 90, 200, 150) color:[NSColor greenColor] name:#"Green Box"];
NSTrackingArea *area3 = [[NSTrackingArea alloc]initWithRect:card3.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:area3];
[self addSubview:card3];
}
-(void)mouseEntered:(NSEvent *)theEvent {
[self reportTopView:theEvent];
}
-(void)mouseExited:(NSEvent *)theEvent {
[self reportTopView:theEvent];
}
-(void)reportTopView:(NSEvent *)theEvent {
id topView = [self hitTest:[theEvent locationInWindow]];
if (![topView isEqual:oldView]) {
oldView = topView;
([topView isKindOfClass:[Card class]])? NSLog(#"%#",[(Card *)topView name]):NULL;
}
}
This is the code for what I called cards (colored rectangles):
#implementation Card
#synthesize name,fillColor;
- (id)initWithFrame:(NSRect)frame color:(NSColor *)color name:(NSString *)aName{
self = [super initWithFrame:frame];
if (self) {
self.fillColor = color;
self.name = aName;
}
return self;
}
- (void)drawRect:(NSRect)rect {
[self.fillColor drawSwatchInRect:rect];
}
I finally came to a solution on Twitter via Steven Troughton-Smith. Here's how it works:
In each menu item, I am disregarding anything related to NSTrackingArea or direct mouse position interpretation. Instead, the parent controller view is handling all of the tracking and receiving mouse movement events.
Each menu item has an overridden hitTest: method that does the point conversion and returns whether or not the point being tested is within the background image (there are shadows and stuff in there, making it more difficult than the vanilla implementation).
I then setup a sort of "hover menu item changed" callback in the controller so that I can handle hover menu changes.
This was a pretty straightforward solution. Very glad I decided to stop and ask, rather than hack something together with my previous idea.
Thanks Steven!
Overlapping tracking-areas:
All you have to do is hitTest from view you are in. if this is true:
window.view.hitTest(window.mousePos) === self/*sudo code*/
What this code does is that it returns the view under the mouse position. Now all you have to do is setup a few "if" and "else" clauses to verify that your mouse is off or on the view.
Full code example:
https://gist.github.com/eonist/537ae53b86d5fc332fd3
Full description of the concept here: (perma link)
http://stylekit.org/blog/2015/12/20/Overlapping-tracking-areas/
VS the default enter and exit behaviour:
I had to add another answer to this question as this is another approach to solve the problem. This approach now also includes path assertion (think rects with round edges or other custom paths)
The answer is long winded but it works:
http://stylekit.org/blog/2016/01/28/Hit-testing-sub-views/
it involves using the apple provided method: CGPathContainsPoint(path,transform,point)
If you follow the link to that blog post and then from there check the styleKit repo on github. You will find the code need to achieve the gif animation example given above. Im providing this as a pointer to the answer as it may take you significantly less time than trying to research this on your own. I use this technique in all my UI elements and it works flawlessly.

Background image for a window in Cocoa framework

I am looking for a perfect solution to set a background image for a window in a cocoa application. I haven't found a solution to this, I am new in objective c, so please anyone help me...
A window in Cocoa has a root-level view called the "content view". This is the view that contains all the others in a window. By default, it's just a plain, blank NSView. But you could easily create your own custom NSView subclass, override the drawRect: method to draw your background image, and use that for your custom view.
However, it might just be easier to use a plain old NSImageView. The advantage of this is that you can set, for example, autosizing behavior to keep the image pinned to one corner (try this with Installer.app by resizing the installer window). You would also be able to make it semi-opaque so that the background shows through a bit. (Again, I'm thinking of Installer.app; your app could be totally different)
Hope that gets you going in the right direction!
Michael Vannorsdel suggests sublassing NSView for the purpose, and I quote:
You'd really be better off making an
NSView subclass and having it draw
the image you want in drawRect:.
- (void)awakeFromNib
{
myImage = [[NSImage alloc] init....
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)rect
{
NSSize isize = [myImage size];
[myImage drawInRect:[self bounds] fromRect:NSMakeRect(0.0, 0.0,
isize.width, isize.height) operation: NSCompositeCopy fraction:1.0];
}
Read that whole thread on cocoabuilder, it's quite instructive.