mouseDragged events get stolen by another view - objective-c

I have several views of class MyView (subclass of NSView) inside another NSView. MyView implements -mouseEntered:, -mouseExited:, -mouseDown:, -mouseDragged:, and -mouseUp:.
Almost always, when a MyView receives a mouse-down event, all subsequent mouse-dragged events are received by the same MyView until the next mouse-up event. Even if the cursor goes outside of the MyView. That is the expected behavior.
Occasionally, a MyView will receive a mouse-down event, but will only receive mouse-dragged and mouse-up events while the cursor remains inside the MyView. If the cursor moves onto a different MyView, then that MyView starts receiving mouse-dragged events (without first receiving a mouse-down event) and can receive the subsequent mouse-up event.
In case it matters, the mouse-down event creates a FooView (subclass of NSView) on top of the MyView, and the mouse-dragged events resize the frame of the FooView. This might be related, as I've only been able to reproduce the problem after one of these FooViews has been created. FooView does not implement any of the mouse event methods.
I've been messing with this for a while now and haven't been able to either purposely reproduce the problem or recreate the problem in a simple example. I'd be happy to answer any questions about my code, I'm just not sure what would be the relevant part to post.

Not sure what the root issue is (this Cocoa behavior seems inconsistent to me)... but here's one possible workaround:
In the superview, create an instance variable tracking the MyView instance in which the -mouseDown: occurred.
When you receive a -mouseDragged: in MyView, instead of operating on self, operate on the MyView instance reference stored in the superview.
...then you'll be able to consistently track which object is being dragged, without having to run your own event loop.

You need to run you own mouse tracking loop in the view until mouse is up. You can extend it to process more types of events by passing them into nextEventMatchingMask:.
- (void)mouseDown:(NSEvent*)event
{
CGPoint hitPoint = [self pointInViewSpaceFromEvent:event];
BOOL isDragging = NO;
BOOL isTracking = YES;
while (isTracking)
{
switch ([event type])
{
case NSLeftMouseDown:
[self singleMouseDownAtPoint:hitPoint withModifierFlags:[event modifierFlags]];
break;
case NSLeftMouseUp:
isTracking = NO;
if (isDragging)
[self mouseDraggingDidEndAtPoint:hitPoint];
else
[self singleMouseUpAtPoint:hitPoint withEvent:event];
break;
case NSLeftMouseDragged:
if (isDragging)
[self mouseDraggingAtPoint:hitPoint withModifierFlags:[event modifierFlags]];
else
isDragging = YES;
break;
default:
break;
}
if (isTracking)
{
event = [[self window] nextEventMatchingMask:NSLeftMouseDraggedMask | NSLeftMouseUpMask];
hitPoint = [self pointInViewSpaceFromEvent:event];
}
}
}

Related

How to send app to background

On tvOS, I've only been able to get the begin states of button presses from the Siri remote by overriding the pressesBegan method on the view. If I use gesture recognizers, it only returns the end state. The catch is, when I override pressesBegan, even if I only use it for the select button, it still overrides the default function of the menu button (to push the app to the background). So I was looking into how to send the app to the background and call that method for the menu button (as is default behavior), but it appears that it is not kosher per Apple's standards to do that.
Here is my code for reference:
-(void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
for (UIPress* press in presses) {
switch (press.type) {
case UIPressTypeSelect:
NSLog(#"press began");
break;
case UIPressTypeMenu:
// this is where I would call the send to background call if Apple would allow that
// removing this case also has no effect on what happens
break;
default:
break;
}
}
As an alternative, this ONLY sends button release signals, but nothing presses begin.
UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(gestureTap:)];
tapGesture.allowedPressTypes = #[[NSNumber numberWithInteger:UIPressTypeSelect]];
[view addGestureRecognizer:tapGesture];
When there's some behavior that happens if you don't override a method, and the behavior goes away in an empty override implementation, it stands to reason that behavior is provided by the superclass. (Cocoa is dynamic and complicated, so such inferences aren't true 100% of the time, but often enough.)
So, just call super for the cases where you don't want your override to change the default behavior:
case UIPressTypeMenu:
[super pressesBegan: presses withEvent: event];
break;

How can I close a NSWindow loaded modally as popup, by clicking outside of it?

How can I close a NSWindow loaded modally as popup, by clicking outside of it?
I would like to handle the mouse event, when the cursor is outside the modal window with the focus (but still inside the app).
You may implement the following delegate method of NSWindow to get the notification of window losing focus.
- (void)windowDidResignKey:(NSNotification *)notification
And inside check, if your application is the front most App. If yes then close accordingly.
While the application is in modal run loop, it does not respond to any
other events (including mouse, keyboard, or window-close events)
unless they are associated with the window. It also does not perform
any tasks (such as firing timers) that are not associated with the
modal run loop.
You can use nextEventMatchingMask:untilDate:inMode:dequeue: method.This will work in modal loop.
Both NSWindow and NSApplication define the method
nextEventMatchingMask:untilDate:inMode:dequeue:, which allows an
object to retrieve events of specific types from the event queue.
As mentioned above it is necessary to override [NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] method. In my case (plugin) I have to use the existing instance of a 3rd party unknown class derived from NSApplication. I can't just derive a new class from it. Thus I use method_exchangeImplementations to exchange the named method above with my own implementation
+ (void)hijack
{
Class appClass = [NSApplication class];
Method originalMethod = class_getInstanceMethod(appClass, #selector(nextEventMatchingMask:untilDate:inMode:dequeue:));
Method categoryMethod = class_getInstanceMethod(appClass, #selector(my_nextEventMatchingMask:untilDate:inMode:dequeue:));
method_exchangeImplementations(originalMethod, categoryMethod);
}
which looks as follows:
- (NSEvent *)my_nextEventMatchingMask:(NSUInteger)mask untilDate:(NSDate *)expiration inMode:(NSString *)mode dequeue:(BOOL)deqFlag
{
NSEvent *event = [self my_nextEventMatchingMask:mask untilDate:expiration inMode:mode dequeue:deqFlag];
NSEventType type = [event type]; // 0 if event is nil
if (type == NSLeftMouseDown || type == NSRightMouseDown)
{
if ([self modalWindow] != nil && [event window] != [self modalWindow])
{
[self stopModalWithCode:NSModalResponseCancel];
event = nil;
}
}
return event;
}
And finally the modal window is invoked as follows:
[NSApplication hijack];
[NSApp runModalForWindow:window];
[NSApplication hijack];
Obviously if you can just override NSApplication then you don't need to define and call hijack method.

-setNeedsDisplay not getting called to refresh drawRect

I have two methods that get called from within -drawRect:
- (void)drawRect:(CGRect)rect {
if(drawScheduleFlag) {
[self drawSchedule];
drawScheduleFlag = false;
}
else
[self drawGrid];
}
-drawGrid is called at initialization time. The other method (-drawSchedule) is called from this code, which is on the main thread:
- (void) calendarTouched: (CFGregorianDate) selectedDate {
// NSLog(#"calendarTouched - currentSelectDate: %d/%d/%d", selectedDate.month, selectedDate.day, selectedDate.year);
// NSLog(#"Main thread? %d", [NSThread isMainThread]);
// get data from d/b for this date (date, staff name, cust name, time, length, services required)
//------ stub -------
scheduledDate.year = 2012;
scheduledDate.month = 6;
scheduledDate.day = 20;
staffName = #"Saori";
custName = #"Brian";
startTime.hour = 11;
duration = 2;
servicesReqd = #"Nails";
drawScheduleFlag = true;
[self setNeedsDisplay];
return;
}
I know the code is being executed, but nothing happens to draw the schedule. Why is -[self setNeedsDisplay] not causing the -drawRect to be called?
UPDATE: I have put breaks in so I'm positive it's not being called. The original grid is drawn once; when the user taps a calendar date, -calendarTouched is called and completely executed. The drawScheduleFlag is set to true, and -[self setNeedsDisplay] gets called, but -drawRect does not. It appears that the UIView is not being invalidated (which -setNeedsDisplay is supposed to do), therefore -drawRect is not called.
UPDATE #2: I have a .xib for a UIViewController with two (2) UIViews in it. The first UIView takes about 1/3 of an iPad screen, the second UIView takes the bottom 2/3 of the screen. Each UIView has it's own specific class; the top UIView displays a calendar and is working correctly, capturing touches and changing the date selected.
The bottom UIView is supposed to show the schedule for the date picked in the top UIView. This is where the problem is. Since the top UIView is working, I will put up the code for the bottom UIView's class. It basically draws the schedule grid when -drawRect is first called. Once the user has selected a day, it is supposed to invalidate the UIView to draw the actual schedule on the grid.
Here is the code for Schedule.h: http://pastebin.com/NQpj0i07
Here is the code for Schedule.m: http://pastebin.com/YBbE8y0T
Here is the code for the controller: http://pastebin.com/nDqBCivj
Note that both pastebin's expire in 24 hours from Jun 28, 5:38 PM PST
Repeating what I said in the comments, the problem you are having is that your UIView subclass object is getting deallocated before the drawRect call gets executed, which means that whatever is holding the reference to the object, in this case your controller, is releasing it before you mean to, little issue that sometimes comes up with using ARC.
In the controller code that holds this view you should have something like this declared:
#property (weak, nonatomic) IBOutlet Schedule *scheduleView;
And synthesized:
#synthesize scheduleView = _scheduleView;
And in the controller's dealloc method:
self.scheduleView = nil;
If you're using storyboards, which you appear to not be using since you made a .xib file for this, you should have it hooked up properly. If not simply instantiate it and assign it.
Expanding on what I said before, I'm not sure what you're trying to do but taking a look at your code you are creating the view for a brief moment when the notification gets called and right there it's getting deallocated because no one is holding a reference to it. By doing what I said and control+click drag from the controller to the view in interface builder and hooking it up it will hold the reference to it, and you'll only have 1 Schedule object created.
After that you'll have to modify your code to work with this instance of Schedule:
- (void) testNotification:(NSNotification *) notification {
// was the calendar tapped?
if ([[notification name] isEqualToString:#"calendarUpdated"]) {
NSDictionary *passedData = notification.userInfo; // get passed data
CFGregorianDate currentSelectDate;
NSData *data = [passedData objectForKey:#"currentSelectDate"];
[data getBytes:&currentSelectDate length:sizeof(CFGregorianDate)];
[self.scheduleView calendarTouched:currentSelectDate];
}
return;
}
The other option you have if you're not using interface builder to set everything up is the following in your viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
self.scheduleView = [[Schedule alloc] init];
[self.view addSubView:self.scheduleView]; // Probably some extra code for positioning it where you want
// notify me when calendar has been tapped and CFGregorianDate has been updated
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(testNotification:) name:#"calendarUpdated" object:nil ];
}
As a final note, considering you're new to iOS development (as we all were) you can go to iTunes and look for Stanford's iPad and iPhone Development course (CS193P) on iTunes U, it's completely free and it will teach you most of what you need to know for developing in iOS.

QuickLook consumer as a delegate from an NSViewController

I am having some problems implementing QuickLook functionality from a table in an NSView. The limited documentation on QuickLook really doesn't help at all.
After reading through the Apple Docs (which are geared heavily towards custom generators and plugins), I ended up looking at the QuickLookDownloader sample code. This code is based upon a document-based application, but appears to be the right method for me (after all it is Apple's code and it does work in their project).
In my implementation I can get the QuickLook panel to show up just fine, and I can dismiss it just as easy. However, the panel itself never calls the delegate methods from within my NSViewController. As a result I never even get to displaying objects, just the wording "No items selected". And I am stumped.
I tried calling a setDelegate, but get warned about impending doom if I continue down that route...
[QL] QLError(): -[QLPreviewPanel setDelegate:] called while the panel has no controller - Fix this or this will raise soon.
See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
And then doom happens anyway with a dealloc when trying to respond to one of the delegate methods.
And yes I did read the header which confirms that I should be setting the delegate after I won the panel (see code below).
So here's my code, which pretty much matches the sample code with the exception of a) where I get my data from (I get it from an NSArrayController) and the b) where I get my preview item from (mine comes directly from my model object - or should anyway)
#interface MyViewController : NSViewController
<QLPreviewPanelDataSource, QLPreviewPanelDelegate> {
QLPreviewPanel * previewPanel;
NSArrayController * myArrayController;
NSTableView * myTable;
// [...] Other instance vars
}
#implementation MyViewController
// [...] all the other methods, init, dealloc etc...
-(IBAction)togglePreviewPanel:(id)previewPanel {
if ([QLPreviewPanel sharedPreviewPanelExists] &&
[[QLPreviewPanel sharedPreviewPanel] isVisible])
{
[[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
}
else
{
[[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
}
}
-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
{
return YES;
}
// This document is now responsible of the preview panel.
// It is allowed to set the delegate, data source and refresh panel.
-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
if (DEBUG) NSLog(#"QuickLook panel control did BEGIN");
previewPanel = [panel retain];
panel.delegate = self;
panel.dataSource = self;
}
// This document loses its responsisibility on the preview panel.
// Until the next call to -beginPreviewPanelControl: it must not change
// the panel's delegate, data source or refresh it.
-(void)endPreviewPanelControl:(QLPreviewPanel *)panel
{
[previewPanel release];
previewPanel = nil;
if (DEBUG) NSLog(#"QuickLook panel control did END");
}
// Quick Look panel data source
-(NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel
{
if (DEBUG) NSLog(#"QuickLook preview count called");
return [[myArrayController selectedObjects] count];
}
-(id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel
previewItemAtIndex:(NSInteger)index
{
if (DEBUG) NSLog(#"QuickLook preview selection of item called");
return [[displayAC selectedObjects] objectAtIndex:index];
}
-(BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event {
if (DEBUG) NSLog(#"QuickLook panel error handler called");
// redirect all key down events to the table view
if ([event type] == NSKeyDown) {
[myTable keyDown:event];
return YES;
}
return NO;
}
The issue seems to be that the acceptsPreviewPanelControl never gets called, so the delegates never get used (they definitely never get called).
I'm sure this is a simple step that I'm missing, but after dissecting the sample code and scouring over the docs I don't see the answer.
Is it because this is all from within an NSViewController (although I see no reason why that should even come into the equation)?
Any and all help much appreciated.
SOLUTION UPDATE
Thanks to Peter's observation, the fix was a quick one. Don't you hate it when the error message in the debugger means what it says? :-)
In my class that loaded MyViewController I simply needed to add three lines of code to fix the problem.
// mainWindow is an IBOutlet to my window because the calling class
// is a simple object and not an NSWindowController otherwise I could
// have used `self` instead of `mainWindow`
NSResponder * aNextResponder = [mainWindow nextResponder];
[mainWindow setNextResponder:myViewControllerInstance];
[myViewControllerInstance setNextResponder:aNextResponder];
Job done :-) Thanks Peter.
Why would you expect it to send you delegate messages if you aren't (yet) its delegate? If you want it to send you delegate messages, then you need to set yourself as its delegate.
I tried calling a setDelegate, but get warned about impending doom if I continue down that route...
[QL] QLError(): -[QLPreviewPanel setDelegate:] called while the panel has no controller - Fix this or this will raise soon. See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
“No controller”, it says. So, you need it to have a controller.
The comments on that header, particularly on acceptsPreviewPanelControl: and the QLPreviewPanel instance method updateController, suggest that the panel's controller, when it has one, is an object that is in the responder chain. Therefore, if your controller is not becoming the panel's controller, it's because your controller isn't in the responder chain.
So, fix that, and then it'll work.
I would imagine that your view controller should be in the responder chain whenever its view or any subview thereof is in the responder chain, but maybe this isn't the case. The documentation doesn't say. If all else fails, set yourself as some view's next responder explicitly (and its previous next responder as your next responder), then send the preview panel an updateController message.
After so many years, in the swift world, I found this line of code works as well.
Without rearrange the default response chain, just "push" your view controller to be the first responder in the window. I'm not sure if it works for every scenario:
view.window?.makeFirstResponder(self)
And the object setups are the same:
override func acceptsPreviewPanelControl(_ panel: QLPreviewPanel!) -> Bool {
return true
}
override func beginPreviewPanelControl(_ panel: QLPreviewPanel!) {
panel.dataSource = self
panel.delegate = self
panel.currentPreviewItemIndex = //your initial index
}
override func endPreviewPanelControl(_ panel: QLPreviewPanel!) {
panel.dataSource = nil
panel.delegate = nil
}

Sending an event to an NSControl underneath another NSControl if it does not handle it

I have multiple superposed controls which can handle a mouse click under certain conditions. What I want to be able to do is:
The top control receives the
mouseDown: event.
The top control decides if it handles the mouseDown: event.
If it does, do something and prevent other controls from receiving the mouseDown: event.
If it does not, send the event to the control that's underneath.
This control decides if it handles the event.
etc.
In essence I'm trying to send the event to the control whose "Z-Order" is just below the top control, without the top control needing to know about the other controls or needing some special setup at instantiation.
The first thing that came to my mind was to send the event to [topControl nextResponder] but it seems the nextResponder for all controls on the window is the window itself and not a chain of controls based on their Z-Order as I previously thought.
Is there a way to do this without resorting to setting the next responder manually? The goal is to get a control which is independent from the other controls and can just be dropped on a window and work as expected.
Thanks in advance!
All you have to do is call [super mouseDown:event]. Since Mac OS X 10.5 (this did not work the same way before) NSView knows how to handle overlapping views and will take care of event handling for you.
If you need to target releases before 10.5: This is a really bad idea. Not only does the event handling mechanism not know how to deal with overlapping subviews, neither does the drawing system and you can see some very strange artefacts. That said, if you're determined:
Override -[NSView hitTest:] in your custom control/view. AppKit uses this method to determine which view in the hierarchy to deliver mouse events to. If you return nil that point in your custom view is ignored and the event should get delivered to the next view covering that point.
Mind though, this is still a bad idea because of the reasons I outlined above. It just wasn't something formally supported by AppKit at the time. The more generally accepted workaround on 10.4 and earlier is to use a child window.
It's hard to know exactly the best approach because I don't know what your application does, but here's a thought. It sounds like you want to pass the messages up through the view hierarchy... somehow.
Regardless, a view would do one of two things:
handle the message
pass it to the "next view" (how you define "next view" depends on your application)
So. How would you do this? The default behavior for a view should be to pass the message to the next view. A good way of implementing this kind of thing is through an informal protocol.
#interface NSView (MessagePassing)
- (void)handleMouseDown:(NSEvent *)event;
- (NSView *)nextViewForEvent:(NSEvent *)event;
#end
#implementation NSView (MessagePassing)
- (void)handleMouseDown:(NSEvent *)event {
[[self nextView] handleMouseDown:event];
}
- (NSView *)nextViewForEvent:(NSEvent *)event {
// Implementation dependent, but here's a simple one:
return [self superview];
}
#end
Now, in the views that should have that behavior, you'd do this:
- (void)mouseDown:(NSEvent *)event {
[self handleMouseDown:event];
}
- (void)handleMouseDown:(NSEvent *)event {
if (/* Do I not want to handle this event? */) {
// Let superclass decide what to do.
// If no superclass handles the event, it will be punted to the next view
[super handleMouseDown:event];
return;
}
// Handle the event
}
You would probably want to create an NSView subclass to override mouseDown: that you would then base your other custom view classes on.
If you wanted to determine the "next view" based on actual z-order, keep in mind that z-order is determined by the order within the subviews collection, with later views appearing first. So, you could do something like this:
- (void)nextViewForEvent:(NSEvent *)event {
NSPoint pointInSuperview = [[self superview] convertPoint:[event locationInWindow] fromView:nil];
NSInteger locationInSubviews = [[[self superview] subviews] indexOfObject:self];
for (NSInteger index = locationInSubviews - 1; index >= 0; index--) {
NSView *subview = [[[self superview] subviews] objectAtIndex:index];
if (NSPointInRect(pointInSuperview, [subview frame]))
return subview;
}
return [self superview];
}
This might be way more than you wanted, but I hope it helps.