Flipped NSView mouse coordinates - macos

I have a subclass of NSView that re-implements a number of the mouse event functions. For instance in mouseDown to get the point from the NSEvent I use:
NSEvent *theEvent; // <- argument to function
NSPoint p = [theEvent locationInWindow];
p = [self convertPoint:p fromView:nil];
However the coordinates seem to be flipped, (0, 0) is in the bottom left of the window?
EDIT: I have already overridden the isFlipped method to return TRUE, but it has only affected drawing. Sorry, can't believe I forgot to put that straight away.

What do you mean by flipped? Mac uses a LLO (lower-left-origin) coordinate system for everything.
EDIT I can't reproduce this with a simple project. I created a single NSView implemented like this:
#implementation FlipView
- (BOOL)isFlipped {
return YES;
}
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint p = [theEvent locationInWindow];
p = [self convertPoint:p fromView:nil];
NSLog(#"%#", NSStringFromPoint(p));
}
#end
I received the coordinates I would expect. Removing the isFlipped switched the orientation as expected. Do you have a simple project that demonstrates your problmem?

I found this so obnoxious - until one day I just sat down and refused to get up until I had something that worked perfectly . Here it is.. called via...
-(void) mouseDown:(NSEvent *)click{
NSPoint mD = [NSScreen wtfIsTheMouse:click
relativeToView:self];
}
invokes a Category on NSScreen....
#implementation NSScreen (FlippingOut)
+ (NSPoint)wtfIsTheMouse:(NSEvent*)anyEevent
relativeToView:(NSView *)view {
NSScreen *now = [NSScreen currentScreenForMouseLocation];
return [now flipPoint:[now convertToScreenFromLocalPoint:event.locationInWindow relativeToView:view]];
}
- (NSPoint)flipPoint:(NSPoint)aPoint {
return (NSPoint) { aPoint.x,
self.frame.size.height - aPoint.y };
}
- (NSPoint)convertToScreenFromLocalPoint:(NSPoint)point
relativeToView:(NSView *)view {
NSPoint winP, scrnP, flipScrnP;
if(self) {
winP = [view convertPoint:point toView:nil];
scrnP = [[view window] convertBaseToScreen:winP];
flipScrnP = [self flipPoint:scrnP];
flipScrnP.y += [self frame].origin.y;
return flipScrnP;
} return NSZeroPoint;
}
#end
Hope this can prevent just one minor freakout.. somewhere, someday. For the children, damnit. I beg of you.. for the children.

This code worked for me:
NSPoint location = [self convertPoint:theEvent.locationInWindow fromView:nil];
location.y = self.frame.size.height - location.y;

This isn't "flipped", necessarily, that's just how Quartz does coordinates. An excerpt from the documentation on Quartz 2D:
A point in user space is represented by a coordinate pair (x,y), where x represents the location along the horizontal axis (left and right) and y represents the vertical axis (up and down). The origin of the user coordinate space is the point (0,0). The origin is located at the lower-left corner of the page, as shown in Figure 1-4. In the default coordinate system for Quartz, the x-axis increases as it moves from the left toward the right of the page. The y-axis increases in value as it moves from the bottom toward the top of the page.
I'm not sure what your question is, though. Are you looking for a way to get the "flipped" coordinates? If so, you can subclass your NSView, overriding the -(BOOL)isFlipped method to return YES.

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).

Draw Lines Diagonally in a particular NSTableview cell

Is there a way to draw lines diagonally in NSTableview cell.Can u please post sample to do this.I am new to the Mac development.Please help me in this issue.
Thanks in advance.......
Yes, easily.
You need to create a subclass of NSTextFieldCell which is actually the type of cell a NSTableView uses to display text.
Subclassing an class creates a new version of that class that does all that the original class did plus more.
This is using Xcode 4. If you are using Xcode 3 let me know.
In Xcode, create a new file by choosing File > New > New File...
In the sheet that pops up choose Objective-C Class and hit Next.
Make it a subclass of NSTextFieldCell, which is what we will be making a modified copy of. Hit Next.
You can save it as anything you want, but for the purposes of this tutorial, save it as MyDiagonalLinedTextFieldCell. Hit Save.
Two new files should pop up.
Click on the .m file. This is the implementation file that tells what the methods in the class do.
Its contents should be similar to below:
//
// MyDiagonalLinedTextFieldCell.m
// CustomCell
//
// Created by spudwaffle on 7/4/11.
// Copyright 2011 __MyCompanyName__. All rights reserved.
//
#import "MyDiagonalLinedTextFieldCell.h"
#implementation MyDiagonalLinedTextFieldCell
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
}
return self;
}
#end
Below the init method add a drawInteriorWithFrame: inView: method.
The application calls the drawInteriorWithFrame: inView: method each time the cell needs to render on screen.
Your code should now look like this:
#implementation MyDiagonalLinedTextFieldCell
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
}
return self;
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
}
#end
The first thing you need to do is just draw a standard NSTextFieldCell.
This can be done by calling:
[super drawInteriorWithFrame:cellFrame inView:controlView];
This draws a normal NSTextFieldCell in the exact area the program wants it to.
Now, we need to draw our custom lines. Let's put them 5 pixels apart and make them 1 pixel wide.
This calls for a for loop!
for (int i = 0; i < cellFrame.size.width/5; i ++) {
}
This makes a int that equals 0,adds to that count every time the loop runs, and stops when i reaches the amount of lines that need to be drawn.
Next, put in the drawing code to draw the lines.
for (int i = 0; i < cellFrame.size.width/5; i ++) {
NSBezierPath *path = [NSBezierPath bezierPath];
[path moveToPoint:NSMakePoint(i * 5, cellFrame.origin.y)];
[path lineToPoint:NSMakePoint((i * 5) + 2, cellFrame.origin.y + cellFrame.size.height)];
[[NSColor grayColor]set];
[path setLineWidth:1];
[path stroke];
}
This:
Creates an NSBezierPath, which is used to draw lines and shapes.
Moves the start of the path to the bottom edge of the cell.
Draws a line to the top edge of the cell.
Sets the drawing color to gray.
Sets the drawing line width to 1.
Draws the line.
It does this over and over for each line thanks to the for loop.
Here is the completed MyDiagonalLinedTextFieldCell.m file. You don't need to worry about the .h one for now.
#import "MyDiagonalLinedTextFieldCell.h"
#implementation MyDiagonalLinedTextFieldCell
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
}
return self;
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
[super drawInteriorWithFrame:cellFrame inView:controlView];
for (int i = 0; i < cellFrame.size.width/5; i ++) {
NSBezierPath *path = [NSBezierPath bezierPath];
[path moveToPoint:NSMakePoint(i * 5, cellFrame.origin.y)];
[path lineToPoint:NSMakePoint((i * 5) + 2, cellFrame.origin.y + cellFrame.size.height)];
[[NSColor grayColor]set];
[path setLineWidth:1];
[path stroke];
}
}
#end
Now, we need to set the cells in the table view to use this class.
Click on your MainMenu.xib file.
Click on the cell in a row of your table view until it turns blue.
Then, hit the button in the right side bar that looks like so:
Change the Class to MyDiagonalLinedTextFieldCell and hit enter.
Now hit run and enjoy the fruits of your labor!
Mess with the drawing code until you get the exact kind of lines you want.
Feel free to contact me with any questions.
This is a beautiful answer, and very well presented. Still, I tried it, and it seems to be incomplete or inaccurate. I have 4 columns in my NSTableView, and apply the custom cell to just the right one - for some reason only the FIRST (left) column gets the special diagonals drawn, no matter what I do.
It seems that the logic in your drawing code is missing some step of "aligning to the positional column" which I really don't know how to do.
you could also improve the sample by only introducing the custom cell .m once - and adding the .h to accompany it - thus demonstrating the inheritance.

CAShapeLayer Slow User Interaction

I have a CAShapeLayer and it has to do a simple task of moving on the screen, guided by the user's finger.
The problem is that the movement is too slow. The layer does move, but there is a lag and it feels slow.
I have another test app where an UIImage is moved and there is no lag at all and the image moves instantly.
What can I do to overcome this?
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
currentPoint = [[touches anyObject] locationInView:self];
}
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
CGPoint activePoint = [[touches anyObject] locationInView:self];
CGPoint newPoint = CGPointMake(activePoint.x - currentPoint.x,activePoint.y - currentPoint.y);
curLayer.position = CGPointMake(shapeLayer.position.x+newPoint.x,shapeLayer.position.y+newPoint.y);
currentPoint = activePoint;
}
Thanks!
Keep in mind that when you set the position on a layer (assuming it's not the root layer of a UIView on which actions are disabled by default), it implicitly animates to the new position, which takes 0.25 seconds. If you want to make it snappier, temporarily disable actions on the layer like this:
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
CGPoint activePoint = [[touches anyObject] locationInView:self];
CGPoint newPoint = CGPointMake(activePoint.x -
currentPoint.x,activePoint.y - currentPoint.y);
[CATransaction begin];
[CATransaction setDisableActions:YES];
curLayer.position = CGPointMake(shapeLayer.position.x +
newPoint.x, shapeLayer.position.y + newPoint.y);
[CATransaction commit];
currentPoint = activePoint;
}
This should cause it to jump to the new position rather than animate. If that doesn't help, then let me take a look at your layer init code so I can see what properties and dimensions it has. Properties such as cornerRadius, for example, can affect performance.
Try setting shouldRasterize to YES on your CAShapeLayer, particularly if it is usually drawn at the same scale. If your app runs on high-DPI devices, you may also need to set rasterizationScale to match the layer’s contentsScale.
While rasterizing your shape can make it faster to move the shape around, you’ll probably want to temporarily disable rasterization while you’re animating the layer’s path or size.

removing/adding CALayers for GPU optimization

I have a layer backed view, I am trying to add subLayers roughly sized around 300 X 270 (in pixels) to it.
The sublayers' count may reach 1000 to 2000, not to mention each sublayer is again scalable to roughly 4280 X 1500 or more for starters.
So the problem is obviously that of a GPU constraint.
After adding around 100 subLayers sized 300 X 270 , there is a warning image is too large for GPU, ignoring and that is messing with the layer display.
The solution for such a problem (from some mailing lists) was to use CATiledLayer, but I can't make use of the tiledLayer due to the complex requirement of the subLayers' display.
Is there a possibility of removing the subLayers which don't fall under VisibleRect of the view?
I tried to removeFromSuperlayer and then add it whenever required, there's always a crash when I try to add the subLayer back.
How can I do this?
I am adding sublayer twice (I need to change it) but for now just for the gist of the code:
-(IBAction)addLayer:(id)sender
{
Layer *l = [[Layer alloc] init];
CALayer *layer = [l page];
[contentArray addObject:page];
[drawLayer addSublayer:layer];
[self layout];
}
-(void)layout
{
NSEnumerator *pageEnumr = [contentArray objectEnumerator];
float widthMargin = [self frame].size.width;
CGRect rect;
float zoom = [self zoomFactor];
while(obj = [contentEnmr nextObject] )
{
[obj setZoomFactor:zoom];
CALayer *pg =(CALayer *)[obj page] ;
rect = pg.bounds;
if ( x + pg.bounds.size.width > widthMargin )
{
x = xOffset;
y += rect.size.height + spacing ;
}
rect.origin = CGPointMake(x,y);
[obj changeBounds];
NSRect VisibleRect = [self visibleRect];
NSRect result = NSIntersectionRect(VisibleRect,NSRectFromCGRect( rect));
if( NSEqualRects (result ,NSZeroRect) )
{
[pg removeFromSuperlayer];
}else
{
[drawLayer addSublayer:pg];
[pg setFrame:rect];
[pg setNeedsDisplay];
}
x += ( rect.size.width + spacing);
}
NSRect viewRect = [self frame];
if(viewRect.size.height < ( y + rect.size.height + spacing ) )
viewRect.size.height = ( y + rect.size.height + spacing) ;
[self setFrameSize: viewRect.size];
}
#interface Layer : NSObject {
CALayer *page;
}
#property (retain) CALayer *page;
Have a look at the PhotoScroller application included as part of the WWDC conference. It demonstrates how to zoom and scroll through a very large image by loading only portions of that image that are currently visible.
Also check out this discussion.
You'll need to do what NSTableView and UITableView do, and manage the addition / removal of layers yourself whenever the visible rect changes. Subscribe to the boundsDidChange noitification of the enclosing scroll view's clip view (I'm assuming that the reason some of the layer is offscreen is that it's enclosed in a scroll view):
- (void) viewDidMoveToSuperview
{
NSClipView* clipView = [[self enclosingScrollView] contentView];
[clipView setPostsBoundsChangedNotifications:YES];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(clipViewBoundsDidChange:)
name:NSViewBoundsDidChangeNotification
object:clipView];
}
and then write a clipViewBoundsDidChange: method that adds and removes sublayers as needed. You may also want to cache and reuse invalidated layers to cut down on allocations. Take a look at the way UITableView and NSTableView interact with their dataSource object for some ideas about how to design the interface for this.
CATiledLayer solves this problem the content of a layer --- ie, whatever you set its contents property or draw into its graphics context directly. It won't do this for sublayers, in fact I think you're advised not to add sublayers to a CATiledLayer at all, as this interferes with its drawing behaviour.

Resources