Converting an NSPoint from window coordinates to view coordinates - cocoa

My application has a custom view that displays a timeline of events. This view is wrapped in an NSScrollView to support horizontal scrolling of the timeline. Using notifications, I've implemented a mechanism that should show another custom view which displays detail information about an event when the user clicks that event in the timeline. Below is the code that handles the event when it is received by the timeline:
NSEvent *anEvent = [aNotification object];
NSPoint aLocationInSelf = [self convertPoint: [anEvent locationInWindow] toView: self];
// Create callout view and display
NSRect aRect = NSMakeRect(aLocationInSelf.x, aLocationInSelf.y, 300, 200);
TimelineCallout *aCallout = [[TimelineCallout alloc] initWithFrame: aRect];
[self addSubview: aCallout];
In the above code I do a conversion of the point of the mouse click as registered by the event from window coordinates to view (timeline) coordinates.
However, when I step through this with the debugger no conversion is taking place and locationInSelf shows the same coordinates as the point I get from [anEvent locationInWindow]. As a result the callout is drawn at the wrong place or not visible at all.
I must be doing something wrong but I can't figure out what...

In order to convert from window coordinates to view coordinates, you have to use:
NSPoint aLocationInSelf = [self convertPoint: [anEvent locationInWindow] fromView: nil];
This will convert from window base coordinates to the receiver coordinates as the event is not originated from a particular view.
For more information, refer to the documentation.
More detailed explanation:
The conversion methods are a bit tricky to understand. There mainly two general cases (the other ones are variants):
Convert a point from one view to another view when they are located in the same window
Convert a point expressed in window coordinate into view coordinates
For the 1st case, you have two views (view1 and view2) located in the same window. If you want to convert a point from view2 into the view1's coordinate system the code will be:
NSPoint pointInView1 = [view1 convertPoint:pointInView2 fromView:view2];
For the 2nd case, you have a point expressed in window coordinates (from the event). As the point is expressed in window coordinates, you don't specify a "from" view and the code will be:
NSPoint pointInView1 = [view1 convertPoint:pointInWindow fromView:nil];
This is the fallback behavior of the method.

Related

NSScrollView starting at middle of the documentView

I have the following code:
[[ticketsListScrollView documentView] setFrame: NSMakeRect(0, 0, [ticketsListScrollView frame].size.width, 53 * [tickets count])];
[[ticketsListScrollView documentView] setFlipped:YES];
for(int i = 0; i < [tickets count]; i++) {
TicketsListViewController *viewController = [[TicketsListViewController alloc] initWithNibName:#"TicketsListViewController" bundle:nil];
viewController.dateLabelText = tickets[i][#"date"];
viewController.timeLabelText = tickets[i][#"time"];
viewController.subjectLabelText = tickets[i][#"title"];
NSRect frame = [[viewController view] frame];
frame.origin.y = frame.size.height * i;
[viewController view].frame = frame;
[[ticketsListScrollView documentView] addSubview:[viewController view]];
}
if the list is large enough (many views), the NSScrollView starts at top-left, which is great. For less views (the views do not take the whole documentView, then NSScrollView starts at the middle.
Any idea why?
Thank you!
Views are not flipped by default, which means your document view is being pinned to the lower-left corner (the default, non-flipped view origin) of the scroll view. What you're seeing is a view not tall enough to push the "top" subview to the top of the scroll view. I see you tried flipping this view, so you already know about this, but you're not doing it correctly.
I'm not sure why you're not getting an error or a warning when calling -setFlipped: since the isFlipped property is read-only. In your document view (the view that's scrolled, and in which you're placing all those subviews), you can override it:
- (BOOL)isFlipped {
return YES;
}
Of course you'll have to put this in a custom NSView subclass and set that as your scroll view's document view's class in IB if you're not creating it at runtime. You'll also need to adjust the frames you use for layout, since you're currently expressing them in the coordinate system of the scroll view's frame. You should be expressing them in your container/layout view's bounds coordinates, which will also be flipped, and so, likely different from your scroll view's coordinates. You'll also need to implement -intrinsicContentSize (and call -invalidateIntrinsicContentSize when adding/removing subviews) so auto-layout can size the container appropriately.

Why does a NSView keeps on receiving touch events after the cursor has left its frame?

I have an NSView with [self setAcceptsTouchEvents:YES];
After the cursor has left the NSView frame, this methods keeps on being called, until I click/begin a new gesture, I don't really understand when it stops.
-(void)touchesMovedWithEvent:(NSEvent *)theEvent {
[self doStuffs:theEvent];
}
with coordinates expressed in other windows coordinate systems.
Is there a way do prevent this, or convert the coordinates back the view coordinates system ?
NSResponder's touchesMovedWithEvent is called for touch events that start in your view, until they end (no matter if that's in your own view or elsewhere).
The event location is expressed in window coordinates. Converting to your view's coordinate system is easy:
- (void)touchesMovedWithEvent:(NSEvent *)event
{
NSPoint locInWindow;
NSPoint locInView;
locInWindow = [event locationInWindow];
locInView = [self convertPoint:locInWindow fromView:nil];
NSLog(#"Location in window: %#", NSStringFromPoint(locInWindow));
NSLog(#"Location in view: %#", NSStringFromPoint(locInView));
}
If you want to handle raw touches for your own multitouch gestures, this is likely not enough info. You'll want to use [event touchesMatchingPhase:NSTouchPhaseMoved inView:self], normalizedPosition, deviceSize etc. (see Apple's documentation).

NSPopover below caret in NSTextView

I know that in order to show a popover I need an NSView, but I don't think that there is one associated with the caret (inside the NSTextView). Is there a way to show a NSPopover below the caret?
I tried to alloc a NSView and position it using (NSRect)boundingRectForGlyphRange:(NSRange)glyphRange inTextContainer:(NSTextContainer *)container, but the popover will not appear (and there's a reason, that method returns NSRect: {{0, 0}, {0, 0}}).
I'm not sure if you are still looking for answer. I recently was working on a project which happens to need a very similar feature like you described.
You can do the following inside a subclass of NSTextView:
the function you are going to call is : showRelativeToRect:ofView:preferredEdge:
the rect will be a rect inside the NSTextView, using the NSTextView coordinate system, the ofView is the NSTextView, and the preferredEdge is the edge you want this popover thing to hook with.
now, you are saying that you want the PopOver thing to show under the caret, well you have to give him a Rect, a Point is not enough. The NSTextView has a selector called selectedRange, which gives you the range of the selected text, you can use that to locate your caret.
the next thing is to call firstRectForCharacterRange (the class must delegate NSTextInputClient), this method will return a screen coordinate of the selected text inside the NSTextView, then you convert them into the NSTextView coordinate system, you will be able to show the NSPopover thing at a correct position. Here's my code of doing this.
NSRect rect = [self firstRectForCharacterRange:[self selectedRange]]; //screen coordinates
// Convert the NSAdvancedTextView bounds rect to screen coordinates
NSRect textViewBounds = [self convertRectToBase:[self bounds]];
textViewBounds.origin = [[self window] convertBaseToScreen:textViewBounds.origin];
rect.origin.x -= textViewBounds.origin.x;
rect.origin.y -= textViewBounds.origin.y;
rect.origin.y = textViewBounds.size.height - rect.origin.y - 10; //this 10 is tricky, if without, my control shows a little below the text, which makes it ugly.
NSLog(#"rect %#", NSStringFromRect(rect));
NSLog(#"bounds %#", NSStringFromRect([self bounds]));
if([popover isShown] == false)
[popover showRelativeToRect:rect
ofView:self preferredEdge:NSMaxYEdge];
and this is the result.
All the thing I am wondering is that if there is a way to convert using System functions, although I tried the convertRect:toView, but since this method is written in a delegate, the NSTextView always has the coordinate system of (0,0), which makes this method useless.

NSTrackingArea works weird - Entire View, or nothing... no rectangles respected

In my "InitWithFrame" method of a view I'm setting a tracking area for which I want to capture mouse enter/exit events.
My problems are two fold:
Without NSTrackingInVisibleRect the events won't be called at all.
No matter what "rect" I put it, one that covers the entire view's frame or one that occupies just a small portion of it - the mouse enter/exited events are called for the entire view, regardless of where the mouse cursor is on the view.
this is how I initialize the tracking area:
trackingArea = [[NSTrackingArea alloc] initWithRect:rect
options: (NSTrackingMouseEnteredAndExited | NSTrackingInVisibleRect | NSTrackingActiveAlways )
owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
Any clues why this happens? I want the mouse enter/exit events to be called only for a small portion (the bottom part) of my view.
Mike Abdullah's answer explains point 2.
Here is a guess about why you don't receive events at all when not using the NSTrackingInVisibleRect flag:
Probably the variable rect you provide is not within the view's coordinate system. You could use the following code as the designated initializer of your NSView subclass to receive mouseEntered: and mouseExited: events for the whole area of your view:
- (id)initWithFrame:(NSRect)frame
{
if ((self = [super initWithFrame:frame]))
{
//by using [self bounds] we get our internal origin (0, 0)
NSTrackingArea* trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:(NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
[trackingArea release];
}
return self;
}
Apple's documentation says:
When creating a tracking-area object,
you specify a rectangle (in the view’s
coordinate system), ...
Straight from the docs for NSTrackingInVisibleRect:
The NSTrackingArea object is automatically synchronized with changes in the view’s visible area (visibleRect) and the value returned from rect is ignored.

How do I get the inner/client size of a NSView subclass?

I am doing manual layouting for my Cocoa application and at some point I need to figure out what the inner size of a NSView subclass is. (E.g. What is the height available for my child view inside of a NSBox?)
One of the reasons is that I am using a coordinate system with origin at the top-left and need to perform coordinate transformations.
I could not figure out a way to get this size so far and would be glad if somebody can give me a hint.
Another very interesting property I would like to know is the minimum size of a view.
-bounds is the one you're looking for in most views. NSBox is a bit of a special case, however, since you want to look at the bounds of the box's content view, not the bounds of the box view itself (the box view includes the title, edges, etc.). Also, the bounds rect is always the real size of the box, while the frame rect can be modified relative to the bounds to apply transformations to the view's contents (such as squashing a 200x200 image into a 200x100 frame).
So, for most views you just use [parentView bounds], and for NSBox you'll use [[theBox contentView] bounds], and you'll use [[theBox contentView] addSubview: myView] rather than [parentView addSubview: myView] to add your content.
Unfortunately, there is no standard way to do this for all NSView subclasses. In your specific example, the position and size of a child view within an NSBox can be computed as follows:
NSRect availableRect = [someNSBox bounds];
NSSize boxMargins = [someBox contentViewMargins];
availableRect = NSInsetRect(availableRect, boxMargins.width, boxMargins.height);
If you find yourself using this often, you could create a category on NSBox as follows:
// MyNSBoxCategories.h
#interface NSBox (MyCategories)
- (NSRect)contentFrame;
#end
// MyNSBoxCategories.m
#implementation NSBox (MyCategories)
- (NSRect)contentFrame
{
NSRect frameRect = [self bounds];
NSSize margins = [self contentViewMargins];
return NSInsetRect(frameRect, margins.width, margins.height);
}
#end
And you would use it like so:
#import "MyNSBoxCategories.h"
//...
NSRect frameRect = [someNSBox contentFrame];
[myContentView setFrame:frameRect];
[someNSBox addSubview:myContentView];
The bounds property of NSView returns an NSRect with the origin (usually (0,0)) and the size of an NSView. See this Apple Developer documentation page.
I'm not sure (I never had to go too deep in that stuff), but isn't it [NSView bounds]?
http://www.cocoadev.com/index.pl?DifferenceBetweenFrameAndBounds

Resources