Resizing an NSView mid-animation - macos

I have a normal NSView that is resizable by dragging the window edges.
If the view is resized during an [NSView animator] animation, it continues to animate to the final size of the original animation, but does not take into account the new window size.
Here is a simple example project. Double click to begin the animation, then resize the window before it finishes.
What is the best way to make the animation take account of the new frame size?

IMHO, the best way would be to stop the animation as soon as the resizing phase begins.
During the resize phase, the user is in control and sets the size of the window manually.
When the resizing phase ends, the window is already set to the desired size, so there is non need to do more.

This kind of problem is best solved with an NSTimer instead of the animator function:
Let the timer call a function repeatedly, until the animation is "complete".
Once complete, end the timer (invalidate).
The function to be called repeatedly in each loop grabs the actual framesize of the window and the actual framesize of your view and simply adds the third of the difference of the two to the frame of the view, like:
frame.size.height += diffHeight/3.0;
So, no matter what happens, the view grows or shrinks closer and closer to its destination.
Once the abs(of the difference) is less then e.g. 0.2 you set the view directly to the desired size and end the timer.
This is direct, uses only little code and you need not listen to any events while it performs pretty well. :-)
Here are the critical codes to initiallize the animation (timer must be an instance of your class):
if(timer)return;
timer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(resizeView:) userInfo:[NSNumber numberWithBool:status] repeats:YES];
[timer setTolerance:0.02];
I use the word status instead of your word closed, the function to be repeatedly called might then look somewhat like:
- (void)resizeView:(id)userInfo;
{
BOOL status = [(NSNumber *)[userInfo userInfo] boolValue];
double startwid,stopwid;
NSRect newSizeRect = [[self window] frame];
stopwid = newSizeRect.size.width;
if(status){
stopwid -= 100.0;
}
NSRect cbgRect = [self frame];
startwid = cbgRect.size.width;
double diff = stopwid-startwid;
if(fabs(diff)<0.2){
diff = 0;
startwid = stopwid;
[timer invalidate];
timer = nil;
//NSLog(#"stop");
}
//NSLog(#"%f - %f = %f /10 = %f",stopwid,startwid,diff,diff/3.0);
cbgRect.size.width = startwid+diff/3.0;
[self setFrame:cbgRect];
}

Related

Cocos2d: Drawing on a UIImageView causes weird line jumps

I am implementing a CCLayer subclass that houses 2 UIImageViews. The views and layer are all the same size: I initialized the UIImageViews with the same frame, and set the contentSize of the layer to be the frame as well. Everything is working fine, but it seems as though something is going haywire with the first point when drawing. When the image is just tapped there is no line jump, but when I attempt to draw a stroke, as soon as I move my finger, the start of the line jumps down randomly(so in this screenshot I am drawing from left to right except for bottom-left line). I am not sure where I am going wrong in my code:
//_drawImageView is the UIImageView I am drawing in.
#pragma mark - Touch Methods
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
penStroked = NO;
_prevPoint = [touch locationInView:[[CCDirector sharedDirector]view]];
CGRect rect = self.boundingBox;
if (CGRectContainsPoint(rect, _prevPoint)) {
return YES;
}
return NO;
}
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
penStroked = YES;
_currPoint = [touch locationInView:_drawImageView];
UIGraphicsBeginImageContext(self.contentSize);
[_drawImageView.image drawInRect:CGRectMake(0, 0, _drawImageView.frame.size.width, _drawImageView.frame.size.height)];
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _prevPoint.x, _prevPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), _currPoint.x, _currPoint.y);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), penSize);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), penColor.r, penColor.g, penColor.b, 1.0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeNormal);
CGContextStrokePath(UIGraphicsGetCurrentContext());
_drawImageView.image = UIGraphicsGetImageFromCurrentImageContext();
[_drawImageView setAlpha: penOpacity];
UIGraphicsEndImageContext();
_prevPoint = _currPoint;
}
I was having a really hard time initially aligning the point at which the line would be draw n and the actual position of my finger, so that is why in the ccTouchBegan method the touch is taken from the CCDirector and in ccTouchMoved it is taken from the _drawImageView. This is the only way it seems to draw perfectly besides the initial wonky behavior.
The problem was definitely caused by the differing references for the touches. As I had said before though, it was the only way to have the line match the position my finger was in. I figured that the line jump was due to the line of code:
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _prevPoint.x, _prevPoint.y);
The problem was that on the first go around, or the first time that the pen/finger is stroked, the previous point was referenced from the CCDirectors view while the current point was from the imageView. I created an int variable, that was incremented every time the pen was stroked and when penStrokedInt == 1 (first stroke), I corrected the _prevPoint to the WorldSpace using the current point coordinates:
_prevPoint = [self convertToWorldSpace:_currPoint];
which basically got rid of the very first point in the line. It's not the best solution, but now it works! Make sure to reset the penStrokedInt back to 0 when the ccTouchEnded.

How to accept a mouse click for one portion and let click-though the rest of an NSWindow

I have the code below for a subclassed NSWindow, there is an animated view which can be scaled and I want to accept click when it is clicked at the right spot and reject (click through) if it is outside.
The code below works nice except the window does not let clicks through.
- (void)mouseDragged:(NSEvent *)theEvent {
if (allowDrag) {
NSRect screenVisibleFrame = [[NSScreen mainScreen] visibleFrame];
NSRect windowFrame = [self frame];
NSPoint newOrigin = windowFrame.origin;
// Get the mouse location in window coordinates.
NSPoint currentLocation = [theEvent locationInWindow];
// Update the origin with the difference between the new mouse location and the old mouse location.
newOrigin.x += (currentLocation.x - initialMouseLocation.x);
newOrigin.y += (currentLocation.y - initialMouseLocation.y);
if ((newOrigin.y + windowFrame.size.height) > (screenVisibleFrame.origin.y + screenVisibleFrame.size.height)) {
newOrigin.y = screenVisibleFrame.origin.y + (screenVisibleFrame.size.height - windowFrame.size.height);
}
// Move the window to the new location
[self setFrameOrigin:newOrigin];
}
}
- (void)mouseDown:(NSEvent *)theEvent
{
screenResolution = [[NSScreen mainScreen] frame];
initialMouseLocation = [theEvent locationInWindow];
float scale = [[NSUserDefaults standardUserDefaults] floatForKey:#"widgetScale"]/100;
float pX = initialMouseLocation.x;
float pY = initialMouseLocation.y;
float fX = self.frame.size.width;
float fY = self.frame.size.height;
if (pX>(fX-fX*scale)/2 && pX<(fX+fX*scale)/2 && pY>(fY+fY*scale)/2) {
allowDrag = YES;
} else {
allowDrag = NO;
}
}
In Cocoa, you have two basic choices: 1) you can make a whole window pass clicks through with [window setIgnoresMouseEvents:YES], or 2) you can make parts of your window transparent and clicks will pass through by default.
The limitation is that the window server makes the decision about which app to deliver the event to once. After it has delivered the event to your app, there is no way to make it take the event back and deliver it to another app.
One possible solution might be to use Quartz Event Taps. The idea is that you make your window ignore mouse events, but set up an event tap that will see all events for the login session. If you want to make an event that's going through your window actually stop at your window, you process it manually and then discard it. You don't let the event continue on to the app it would otherwise reach. I expect that this would be very tricky to do right. For example, you don't want to intercept events for another app's window that is in front of yours.
If at all possible, I recommend that you use the Cocoa-supported techniques. I would think you would only want clicks to go through your window where it's transparent anyway, since otherwise how would the user know what they are clicking on?
Please invoke an transparent overlay CHILD WINDOW for accepting control and make the main window -setIgnoresMouseEvents:YES as Ken directed.
I used this tricky on my app named "Overlay".

How to auto-resize NSWindow based on auto layout contentView's minimum size

At the moment the behaviour of my app allows me to called this extra function I wrote that hacks resizes the window to fittingSize.
- (void)fitToMinimumSize
{
NSRect frame = [self frame];
frame.size = [[self contentView] fittingSize];
int originalHeight = [self frame].size.height;
int diff = originalHeight - frame.size.height;
frame.origin.y += diff;
[self setFrame:frame display:YES];
}
But can I automate this behaviour through some built in auto layout code so that the window is always the size of the minimum of it's contentView instead of this almighty hack?
Edit: I've found out I can check [[self contentView] fittingSize], but how can I observe this incase it changes, or should I be triggering it myself?
Normally, everything should already be 'automatic' if your layout is fully and unambiguously determined by NSConstraint's set on your window's content view and its children.
In your case I'd try installing a height and a width constraints on the content view with sufficiently small dimensions (maybe even zero), but with priority lower than the 'content compression resistance priority' set on your subviews. To have everything working properly, you may have to play with constraint priorities and/or install additional constraints on the content view's children.
This should work without any other code, as long as your subviews are constrained to resist collapsing to zero. I'd also think that in this case the window should not be user-resizeable.

NSScrollView scroll bars are of the wrong length

I have a Cocoa window, whose content view contains an NSScrollView that, in turns, contains a fixed-size NSView.
Upon launching the program, the scroll bars displayed initially are too small, as if the content size was much larger than it actually is:
When I start playing with, e.g., the vertical scroll bar, and bring it back to the original position at the top, it gets resized to its expected size (which corresponds to the ratio of scroll view and content view sizes):
(Notice the horizontal bar, which still has incorrect size. If I then play with it, and bring it back to its leftmost position, it gets resized to the correct size.)
I also encountered the same problem, I have searched everywhere but it seems no one else experiences this problem. Fortunately I found a hack which solves the problem.
What I did notice was that when the window is resized or maximized the scrollbars resize to the expected size (autoresizing has to be enabled). This is because when the window resizes so does the scrollview and the length of the scroll bars gets recalculated and is calculated correctly. Possibly due to some bug the scroll bar lengths are not calculated correctly on initialization. Anyway to fix the problem, in your application delegate create an outlet to your window. Override the "applicationDidFinishLaunching" method and inside it call the method "frame" on the window outlet, which returns the current NSRect of the window. Using the returned value add one to the size.width and size.height. The call the method setFrame with display set to YES. This will resize the window and force the size of the scrollbars to be recalculated.
Here is the code for applicationDidFinishLaunching Below
(void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Get the current rect
NSRect windowRect = [_window frame];`
// add one to the width and height to resize window
windowRect.size.width += 1;
windowRect.size.height += 1;
// resize window with display:YES to redraw window subviews
[_window setFrame:windowSize display:YES];
}
I encountered this issue when modifying an NSTextView textContainer size to toggle line wrapping. Resizing the enclosing view does cause the correct scroll view height to be used, however its a brutal solution.
NSScrollView supports -reflectScrolledClipView. Calling this directly in my case had no effect except when delayed on the runloop:
[textScrollView performSelector:#selector(reflectScrolledClipView:) withObject:textScrollView.contentView afterDelay:0];
The scroller position is correct but there is a scroller redraw. So it looks as if part of the view geometry is calculated when drawing. A better solution is therefore:
NSDisableScreenUpdates();
[textScrollView display];
[textScrollView reflectScrolledClipView:textScrollView.contentView];
[textScrollView display];
NSEnableScreenUpdates();
Building on the answer from jstuxx above, if you don't want the window to visibly resize, try:
NSRect windowRect = [[[self view] window] frame];
windowRect.size.width += 1;
windowRect.size.height += 1;
[[[self view] window] setFrame:windowRect display:YES];
windowRect.size.width -= 1;
windowRect.size.height -= 1;
[[[self view] window] setFrame:windowRect display:YES];
I had to put this code after where I was programmatically adding the scroll view to my interface.

Explicit animation of NSView using core animation

I'm trying to slide in a NSView using core animation. I think I need to use explicit animation rather than relying on something like [[view animator] setFrame:newFrame]. This is mainly because I need to set the animation delegate in order to take action after the animation is finished.
I have it working just fine using the animator, but as I said, I need to be notified when the animation finishes. My code currently looks like:
// Animate the controlView
NSRect viewRect = [controlView frame];
NSPoint startingPoint = viewRect.origin;
NSPoint endingPoint = startingPoint;
endingPoint.x += viewRect.size.width;
[[controlView layer] setPosition:NSPointToCGPoint(endingPoint)];
CABasicAnimation *controlPosAnim = [CABasicAnimation animationWithKeyPath:#"position"];
[controlPosAnim setFromValue:[NSValue valueWithPoint:startingPoint]];
[controlPosAnim setToValue:[NSValue valueWithPoint:endingPoint]];
[controlPosAnim setDelegate:self];
[[controlView layer] addAnimation:controlPosAnim forKey:#"controlViewPosition"];
This visually works (and I get notified at the end) but it looks like the actual controlView doesn't get moved. If I cause the window to refresh, the controlView disappears. I tried replacing
[[controlView layer] setPosition:NSPointToCGPoint(endingPoint)];
with
[controlView setFrame:newFrame];
and that does cause the view (and layer) to move, but it is corrupting something such that my app dies with a seg fault soon afterwards.
Most of the examples of explicit animation seem to only be moving a CALayer. There must be a way to moving the NSView and also being able to set a delegate. Any help would be appreciated.
Changes made to views take effect at the end of the current run loop. The same goes for any animations applied to layers.
If you animate a view's layer, the view itself is unaffected which is why the view appears to jump back to its original position when the animation completes.
With these two things in mind, you can get the effect you want by setting the view's frame to what you want it to be when the animation is done and then adding an explicit animation to the view's layer.
When the animation begins, it moves the view to the starting position, animates it to the end position and when the animation is done, the view has the frame you specified.
- (IBAction)animateTheView:(id)sender
{
// Calculate start and end points.
NSPoint startPoint = theView.frame.origin;
NSPoint endPoint = <Some other point>;
// We can set the frame here because the changes we make aren't actually
// visible until this pass through the run loop is done.
// Furthermore, this change to the view's frame won't be visible until
// after the animation below is finished.
NSRect frame = theView.frame;
frame.origin = endPoint;
theView.frame = frame;
// Add explicit animation from start point to end point.
// Again, the animation doesn't start immediately. It starts when this
// pass through the run loop is done.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setFromValue:[NSValue valueWithPoint:startPoint]];
[animation setToValue:[NSValue valueWithPoint:endPoint]];
// Set any other properties you want, such as the delegate.
[theView.layer addAnimation:animation forKey:#"position"];
}
Of course, for this code to work you need to make sure both your view and its superview have layers. If the superview doesn't have a layer, you'll get corrupted graphics.
I think you need to call the setPosition at the end (after setting the animation).
Also, I don't think you should animate explicitely the layer of the view, but instead the view itself by using animator and setting the animations. You can use delegates too with animator :)
// create controlPosAnim
[controlView setAnimations:[NSDictionary dictionaryWithObjectsAndKeys:controlPosAnim, #"frameOrigin", nil]];
[[controlView animator] setFrame:newFrame];

Resources