Objective-C: Mouse location on IBAction - objective-c

I have an IBAction which should, when activated, wait until the mouse is clicked and then print the location. How do I do that properly? My attempt:
BOOL finished = NO;
while (!finished) {
if ([NSEvent pressedMouseButtons] == 1) {
finished = YES;
NSLog(#"%f | %f", [NSEvent mouseLocation].x, [NSEvent mouseLocation].y);
}
}
does work but will show a busy mouse pointer. So I guess there must be a much better way.

Both the IBaction and Click events occur on a Main Thread only. It seems you are blocking the main thread with a while loop in IBAction until the mouse click happens, which is like putting the Main thread busy.
You should run this while loop in a secondary thread so that Main thread will be free of this while loop checking.

Related

What's the relationship between UI animation and the main runloop

i have this code to wait for a loading task, showing a activityIndicator view
if (isLoading) {
self.tipView = [[BBTipsView alloc] initWithMessage:#"loading..." showLoading:YES parentView:self.view autoClose:NO];
self.tipView.needsMask = YES;
[self.tipView show];
while (isLoading) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
[self.tipView close];
}
the loading view will animate until the isLoading become false.here is my problem:
running a runloop in main thread will block the main thread until there is a source event comes or timer fire. but why the loading view keep animating while the main runloop didn't return?
-----edit by bupo----
I found that when timer fire the runloop won't return. This will make sense that the animation refresh ui by CADisplayLink timer fire.
Note that from the perspective of NSRunloop, NSTimer objects are not "input"—they are a special type, and one of the things that means is that they do not cause the run loop to return when they fire.
The NSRunLoop method runMode:beforeDate: runs until the given date OR until it finds a single event to deal with - after which the call returns. You're calling it on the main run loop ([NSRunLook currentRunLoop]). Hence, even though you think you're blocking the main run loop, you're not - you're causing events to be serviced. Hence, the animation timer can function, even though you might think you're 'blocking' the main run loop.
To confirm this, comment out the call to runMode:beforeDate: and you should see that the UI freezes until the operation completes.
Edit: see CodaFi's comment on your question. What actually happens if you comment out the call to runMode:beforeDate:, out of interest?
Original answer:
This style of code isn't recommended for starting and stopping UI animations. Don't mess around with run loops unless you have to. And having a tight loop that checks for boolean flag changing from elsewhere is often a code smell that means there's a better way.
Instead, do it asynchronously and without sitting on the main thread:
// on main thread
self.tipView = [[BBTipsView alloc] initWithMessage:#"loading..." showLoading:YES parentView:self.view autoClose:NO];
self.tipView.needsMask = YES;
[self.tipView show];
} // end of the method
- (void)loadingHasFinished {
// assuming this method called on main thread
[self.tipView close];
}
Obviously you'll have to ensure the loadingHasFinished is called as appropriate.
If loadingHasFinished is called on a background thread rather than the main thread, you'll want something like this:
- (void)loadingHasFinished {
dispatch_async(dispatch_get_main_queue(), ^{
[self.tipView close];
});
};

Modal NSAlert within block not displayed

In my document-based application, I have overridden the method openDocument: in my subclass of NSDocumentcontroller so that I can display my own openPanel. I pass the chosen URLs to the method openDocumentWithContentsOfURL:display:completionHandler:. I use this code for the call:
[self openDocumentWithContentsOfURL:[chosenFiles objectAtIndex:i] display:YES completionHandler:^(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error) {
if (document == nil)
{
NSAlert* alert = [NSAlert alertWithError:error];
[alert runModal];
}
}];
So I want to display the passed error if nil gets returned as a reference to the document. The problem is, that the program just "freezes" after I press the "Open" button in the open panel. Then I need to manually stop the program with the "stop" button in Xcode. No spinning beach ball appears, though. If I comment the line "[alert runModal]", the program does not freeze any more, but no alert is displayed, of course.
Now the strange thing: The code works sometimes. If I switch from Xcode to my browser and back and I run the program again, it sometimes works flawlessly and the error is displayed. After some time it stops working again. It is unpredictable, but most of the time it doesn't work.
All this sounds like a race-condition to me. It certainly has something to do with the block? But what do I do wrong?
Converting my comment to an answer:
runModel on the main thread.
[alert performSelectorOnMainThread:#selector(runModal) withObject:nil waitUntilDone:NO];
I think runModel needs to be called on the main thread because it's part of the AppKit framework, and it's estentially triggering UI graphics. I believe all calls to the AppKit framework or to any method that manipulates graphics needs to be on the main thread.

mouseEntered event disabled when mouseDown (NSEvents Mac)

I have created an NSButton class, and when rolling over my buttons it happily detects mouseEntered and mouseExited events. But as soon as the mouseDown event occurs, so long as the mouse it down, mouseEntered events are no longer called until the mouse button is lifted.
So, when the mouseDown event is called, mouseEntered or MouseExited events are no longer called, nor is mouseDown called when rolling over other buttons until I let go of the initial mouseDown.
So I would like to detect when my mouse enters while the mouse is down as well.
Turns out I just needed to add NSTrackingEnabledDuringMouseDrag to my NSTrackingAreaOptions. mouseEntered and mouseExited events now fire when dragging with mouse down.
When an NSButton receives a mouse down event, it enters a private tracking loop, handling all the mouse events that are posted until it gets a mouse up. You can set up your own tracking loop to do things based on the mouse location:
- (void) mouseDown:(NSEvent *)event {
BOOL keepTracking = YES;
NSEvent * nextEvent = event;
while( keepTracking ){
NSPoint mouseLocation = [self convertPoint:[nextEvent locationInWindow]
fromView:nil];
BOOL mouseInside = [self mouse:mouseLocation inRect:[self bounds]];
// Draw highlight conditional upon mouse being in bounds
[self highlight:mouseInside];
switch( [nextEvent type] ){
case NSLeftMouseDragged:
/* Do something interesting, testing mouseInside */
break;
case NSLeftMouseUp:
if( mouseInside ) [self performClick:nil];
keepTracking = NO;
break;
default:
break;
}
nextEvent = [[self window] nextEventMatchingMask:NSLeftMouseDraggedMask | NSLeftMouseUpMask];
}
}
When the left mouse button is down, dragging begins. If I remember correctly, mouse moved events aren't sent during drags, and that's probably one reason that you don't get mouseEntered and mouseExited messages. However, if you implement the NSDraggingDestination protocol and register your view as a possible recipient of the type of data being dragged, you'll get draggingEntered and draggingExited messages instead.
Read about it in the Dragging Destinations section of Drag and Drop Programming Topics.

Can notification from addGlobalMonitorForEventsMatchingMask be canceled?

I have simple global monitor for mouse clicks:
[NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask handler:^(NSEvent *event){
if ([event type] == NSLeftMouseDown) {
[self mouseDown];
if (self.lockMouse) {
// Cancel event
}
}
}];
Is there any way to cancel global mouse events, so clicking only notifies my app?
I.e.: After clicking on any button on screen (that don't belong to my app) while "locked", event goes here, but not to button that was below cursor. Something like event.preventDefault() in JavaScript.
Not with this API, no. From the documentation, it says that the block is:
The event handler block object. It is passed the event to monitor. You are unable to change the event, merely observe it.
If you want to intercept the event and prevent it from propagating, you need to use a CGEventTap instead.

Objective C: Deleting UIButtons (not just removeFromSuperview)

I'm making a game that involves buttons moving across the screen. When one button reaches the edge of the screen without being tapped, you lose a bar of health.
-(void) moveStickFig:(NSTimer *)timer {
UIButton *stick = (UIButton *)timer.userInfo;
CGPoint oldPosition = stick.center;
stick.center = CGPointMake(oldPosition.x + 1 , oldPosition.y);
if (oldPosition.x == 900) {
[stick removeFromSuperview];
healthCount--;
NSLog(#"%d", healthCount);
}
}
When you click on a button it disapears using [btn removeFromSuperview] The problem with this is the button still exists and continues moving across the screen. Is there a way to delete it completely? I've tried [stick release] but for some reason it just causes the app to freeze
It looks like you're using a repeating timer to move the button. If you don't explicitly end that timer, the timer is going to keep running, and moving the button.
Normally when you send the removeFromSuperview message to something like a button, it would deallocate or "delete" that object. This is because when the button is added to the superview, the superview retains the button, giving it a retain count of 1, and when it's removed from the superview, it releases it, giving it a retain count of 0.
However, because the button is stored as the userInfo of the timer, the timer also retains the object giving it a retain count of 2, and after you remove it from the superview it still has a retain count of 1. If you simply send the release message to the button, it will lower the retain count to 0 and it will deallocate the button, but it won't stop the timer. The next time the timer runs, it will cause problems because you're trying to access deallocated memory.
What you really want to do is to invalidate the timer: [timer invalidate]. This will stop the timer, and the timer will send a release message to the button, causing the button to be deallocated.
NSTimer retains its userInfo, which is the button object in your case. You should kill the timer using [timer invalidate].