NSPanel not receiving mousedragged event - objective-c

I have a borderless NSPanel, when I first launch it and it has focus I can drag it around and the mousedragged method gets triggered correctly, however when I switch focus to another app and then return to the NSPanel (which is set with an NSNonactivatingPanelMask) I no longer receive the mousedragged events.
I do still receive mouseup and mousedown events, so I'm at a loss of why the mousedragged event isn't executed.
Any ideas?
Here's how it gets initialized:
_panel = [[MyPanel alloc] initWithContentRect:frame
styleMask:NSBorderlessWindowMask | NSNonactivatingPanelMask
backing:NSBackingStoreBuffered
defer:NO];
I've also tried adding all these methods to my panel class:
- (BOOL)canBecomeKeyWindow {
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (BOOL)acceptsFirstMouse
{
return YES;
}
And making it first responder in the mouse down (which it still receives):
- (void)mouseDown:(NSEvent *)theEvent
{
[self makeFirstResponder:self];
[self makeKeyWindow];
[self setBackgroundColor:[NSColor redColor]];
[self display];
}
The mousedragged simply contains this:
- (void)mouseDragged:(NSEvent *)theEvent
{
[self setBackgroundColor:[NSColor greenColor]];
[self display];
NSLog(#"dragged");
}
I do not want the window to get focus. (The focus should remain on the third party app below).
Update: I've added a sample project, download here: http://users.telenet.be/prullen/MovingPanel.zip
As you will see, when you first run the app, and drag it will output "dragged" in the console.log continuously (and the background color will be green). If you then switch to another app and then back to the movingpanel app, dragging will not output anything any more. (and the background color will be red, which is set in the mousedown event handler).
Without the NSNonactivatingPanelMask this works as it should, but it is vital that the window below my panel remains active. If there is another way to accomplish this, please do share.
One thing I have also noticed, if you double click on the panel (fast), and then drag, it turns green (so the mousedragged: is being called), but it's not moving... Not sure what to think of that.

I also noticed that if I do [self setMovableByWindowBackground:NO]; then it will also work correctly. I am betting that the way setMovableByWindowBackground works is interfering with the mouseDragged being called. It's probably a bug that it is called at all.
I would guess one possible solution would be to implement your own window dragging.
If what you are really interested in is responding to when the window is moving, this question and answer may provide what you need.
How to receive notifications when moving Window by mouse?

Related

Cocoa: NSUserNotification appears behind NSPopover

I have no idea where to even start trying to fix this but maybe someone can point me in the right direction. I have a status item application that shows a popover when clicked, some user interactions produce an NSUserNotification to warn/inform about changes or errors. But when the notification appears it is shown behind the popover, so if they overlay the user can't read what it says.
Is there a way to force the notification to appear in front of the popover? Like order the notification to the front.
Edit: here's a picture showing part of the notification hidden behind the popover after a user gave bad input. I don't really want to hide the popover to show the error notification.
I've tried stuff lake makeKeyAndOrderFront, activateIgnoringOtherApps, the type of stuff you'd use on a window, but none of it applies to an NSUserNotification so it doesn't work.
Edit 2: I should note that to get the popover to appear in front of everything when called I use activateIgnoringOtherApps and becomeFirstResponder on the popover, but even getting rid of that still shows the notification behind the popover.
Edit 3: The popover is shown relative to a status item in the status bar using
[[self popover] showRelativeToRect:statusItem.view.bounds ofView:statusItem.view preferredEdge:NSMaxYEdge];
When clicked, the status item calls a method that does this:
- (void)openPopover:(id)sender {
if (![_popover isShown] | (_popoverIsClosing)) {
[_preferencesWindow close];
[NSApp activateIgnoringOtherApps:YES];
[_popover becomeFirstResponder];
[statusItemView setHighlighted:YES];
[[self popover] showRelativeToRect:statusItem.view.bounds ofView:statusItem.view preferredEdge:NSMaxYEdge];
[self performSelector:#selector(defaultTextField)];
NSLog(#"popover opened");
} else {
[_preferencesWindow close];
[NSApp activateIgnoringOtherApps:NO];
NSLog(#"popover closed");
[_popover close];
[_popover resignFirstResponder];
[[NSApplication sharedApplication] hide: self];
[statusItemView setHighlighted:NO];
}
}
Thanks for the help.
You want to adjust the window.level.
A notification is using level 18, so as long as your level is lower, you will not have this issue.
You can use these default NSWindow.Level
.normal = 0
.floating = 3
.submenu = 3
.tornOffMenu = 3
Or a custom level NSWindow.Level(rawValue: 17).
An important thing to note is you should set the windows level during or after the popover did show. Otherwise your setting will be reset.
You can do this from the popover.delegate using the function,
func popoverDidShow(_ notification: Notification)
Or listen to the notification directly with NSPopoverDidShowNotification.
This has been tested on macOS 10.15

Menu Bar App Never Becomes Reactivated

I'm building a Mac app that only sits in the menu bar with no dock item and no key window and no main menu (it's LSUIElement in the info.plist is set to YES). When I first launch the app, applicationDidBecomeActive: is called, as I expect. However, once another app gains focus, applicationDidBecomeActive: is never called again.
This prevents a text field I have within my app from becoming the first responder. When I first open the app, the text field is editable:
But after another app comes to the foreground, the text field is not editable:
What I've tried:
When the menu is opened, menuWillOpen: is called on the NSMenu's delegate. I've tried placing the following with no success:
[NSApp unhide];
[NSApp arrangeInFront:self];
[NSApp activateIgnoringOtherApps:YES];
[NSApp requestUserAttention:NSCriticalRequest];
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
[[NSRunningApplication currentApplication] unhide];
I think the issue is probably related to not having any windows to bring to the front. I feel like I'm grasping at straws here. Any help would be greatly appreciated.
I think the issue is with that how the runloop operates when a NSMenu is open, so you should try activating the app before you display the menu. If you're having the NSStatusItem display it, I'd suggest doing it yourself like this:
- (void)toggleMenu:(id)sender
{
// App might already be active
if ([NSApp isActive]) {
[self.statusItem popUpStatusItemMenu:self.menu];
} else {
[NSApp activateIgnoringOtherApps:YES];
}
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
[self.statusItem popUpStatusItemMenu:self.menu];
}
That should work, but I think though in general you'll have better luck with an actual window instead of a menu.
You probably need to allow your input to -becomeFirstResponder, maybe by overriding -canBecomeFirstResponder or by calling the become method yourself.
You'd likely have to implement/call these methods for whatever view is housing your text input, or maybe tell your input view to become the first responder.
Either way, it smells like a responder chain issue.
Try calling -makeFirstResponder: on your window. NSWindow is usually the start of the NSResponder chain.
- (void)menuWillOpen:(NSMenu *)menu {
[[NSApp mainWindow] makeFirstResponder:yourTextInputField];
}
I'm assuming your text field already accepts first responder since you said your app launches initially with it as the first responder. If not, make sure your text field overrides -acceptsFirstResponder: to return YES
- (BOOL)acceptsFirstResponder {
return YES;
}
Edit: Ah, see that you don't have a key window. It looks like NSMenu actually has a window associated with it though, and it's safe to call -makeFirstResponder:. Some discussion here suggests overriding -viewDidMoveToWindow: on your view containing your text field in the NSMenu like so:
- (void)viewDidMoveToWindow {
[super viewDidMoveToWindow];
[[self window] makeFirstResponder:yourTextInputField];
}

Show Window without activating (keep application below it active)

I need to show a window (without title bar) above third party applications without my window taking focus.
I have tried using an NSPanel and setting enabling non-activating, but that didn't help.
I tried orderFront:self, but that didn't help either.
I always needed to add [NSApp activateIgnoringOtherApps:YES]; because the window wouldn't show otherwise.
I have here a sample project for just this functionality:
http://users.telenet.be/prullen/TopW2.zip
UIElement is set to true in the application's plist file, so there is no dock. You can activate the window by pressing ALT + SPACE at the same time. You will see that the app below it looses focus. Any thoughts on how to fix this? I've seen other apps do it so I know it's possible.
Edit: here's the code so far. Remember the window is a non-activating NSPanel.
I still need that last NSApp activateIgnoringOtherApps line or otherwise it doesn't display. But of course that makes the window the active one.
_windowController = [[MyWindowController alloc] initWithWindowNibName:#"MyWindowController"];
[[_windowController window] setLevel:NSNormalWindowLevel+1];
[[_windowController window] orderFrontRegardless];
[_windowController showWindow:self];
[NSApp activateIgnoringOtherApps:YES];
I've also subclassed NSPanel and added two methods:
- (BOOL)canBecomeKeyWindow
{
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
Edit: OK, unchecking setHidesOnDeactivate fixes this, but now the window will never hide. I need it to hide when the user presses the app below it or switches to another app.
Edit 2: OK, this seems to fix the above issue:
- (void)awakeFromNib
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(hideWindow) name:NSWindowDidResignKeyNotification object:nil];
}
- (void)hideWindow {
[self setHidesOnDeactivate:YES];
}
Not sure if there's a better way.
And for those that want to know how to display the window:
[[_windowController window] setLevel:NSPopUpMenuWindowLevel];
[[_windowController window] orderFrontRegardless];
[[_windowController window] makeKeyWindow];
[_windowController showWindow:self];
Either one of these should do the trick:
Use -[NSWindow orderFrontRegardless] to get a normal level window to the front without activating the corresponding app, or
Use -[NSWindow setLevel:] to increase the window level to something higher than NSNormalWindowLevel
Not to take away from #puzzle's useful answer, but it sounds like your problem has something to do with using an NSPanel instead of an NSWindow.
The "How Panels Work" docs say:
Onscreen panels, except for alert dialogs, are removed from the screen when the application isn’t active and are restored when the application again becomes active. This reduces screen clutter.
Specifically, the NSWindow implementation of the hidesOnDeactivate method returns NO, but the NSPanel implementation of the same method returns YES.
So perhaps you could override hidesOnDeactivate to return NO, or change to NSWindow

subview from UIWindow and sending it back to UIwindow

I have an UIWebView loaded with div, act as editor to write. Now i am adding UIWebView as sub view on UIWindow to set the frame equal to full screen and to hide the UIKeyboard, but at some button method, i need to get UIWebview from UIWindow and sent it back to UIKeyboard. Here is my code which is not working:
keyboardWindowFrame= nil;
for (UIWindow *testWindow in [[UIApplication sharedApplication] windows])
{
if (![[testWindow class] isEqual:[UIWindow class]])
{
keyboardWindowFrame = testWindow;
[webViewForEditing setFrame:CGRectMake(5, 63, 310, 400)];
[webViewForEditing.scrollView setContentSize:CGSizeMake(310, 400)];
[keyboardWindowFrame addSubview:webViewForEditing];
break;
}
}
- (IBAction)keyboardButtonSelected:(id)sender
{
[keyboardWindowFrame sendSubviewToBack:webViewForEditing]; //need to send UIWebView back to UIWindow so i can write
}
I think I am trying to do the same thing as you and so although your question is not particularly clear hopefully my answer will help.
It seems that you have a view 'webViewForEditing' which you want to add and bring in front of the keyboard. When you click a button you want to put this view behind the keyboard again.
I have also tried using the sendSubViewToBack code with no joy.
In the end though I managed to get it to work using:
[[self view] exchangeSubviewAtIndex: withSubviewAtIndex: ];
(Credit goes to Sunny with it adapted from a question here)
I used the following code below, with to switch between a UIView and the keyboard:
- (IBAction)toggleButtonPressed:(id)sender {
UIWindow * window = [UIApplication sharedApplication].windows.lastObject;
if (window.subviews.count == 1) {
[window addSubview:_menuView];
}
else {
[window exchangeSubviewAtIndex: 0 withSubviewAtIndex: 1];
}
}
I am only working with two views (_menuView and the keyboard) which means I check how many subviews my window has to make sure I only add _menuView once.
Then it is easy to exchange the two views.
Even if you had more subviews in your window I am sure you could use a modified version of this. As long as none of your other views change places then exchanging them will always switch the same two views.
NOTE: As an aside. I am not sure if this code works if called when the keyboard is not first responder. I get the variable for Window using the last object in the window, which is always the keyboard if it has been made first responder. It might need tweaking to get it working at other times.
I formulated another, more eloquent, answer while working on this:
Add the views you want to the window view that holds the keyboard view:
#interface BViewController : UIViewController {
UIWindow * window;
}
// Add the code when the keyboard is showing to ensure it is added while we have a keyboard and to make sure it is only added once
-(void) keyboardDidShow: (NSNotification *) notification {
window = [UIApplication sharedApplication].windows.lastObject;
if (window.subviews.count == 1) {
[window addSubview:_menuView];
[window addSubview:_gameView];
[window addSubview:_stickerView];
[self hideViews];
}
}
- (void)hideViews {
_menuView.hidden = YES;
_gameView.hidden = YES;
_stickerView.hidden = YES;
}
So now we have our UIWindow view which contains our views and also our keyboard. They are all hidden so our keyboard appears at the front and can be typed on.
Now use a simple function to decide which view to bring in front of the keyboard:
- (void)bringViewToFront: (UIView *)view {
[self hideViews];
view.hidden = NO;
}
Hiding the views makes sure we only have our correct view at the front.
I spent a lot of time thinking how we could move different views forward and back, using the exchange function. Actually using hide and reveal means we can get our view immediately and still easily have access to the keyboard by hiding all the views.

UIPageViewController Traps All UITapGestureRecognizer Events

It's been a long day at the keyboard so I'm reaching out :-)
I have a UIPageViewController in a typical implementation that basically follows Apple's standard template. I am trying to add an overlay that will allow the user to do things like touch a button to jump to certain pages or dismiss the view controller to go to another part of the app.
My problem is that the UIPageViewController is trapping all events from my overlay subview and I am struggling to find a workable solution.
Here's some code to help the example...
In viewDidLoad
// Page creation, pageViewController creation etc....
self.pageViewController.delegate = self;
[self.pageViewController setViewControllers:pagesArray
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:NULL];
self.pageViewController.dataSource = self;
[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
// self.overlay being the overlay view
if (!self.overlay)
{
self.overlay = [[MyOverlayClass alloc] init]; // Gets frame etc from class init
[self.view addSubview:self.overlay];
}
This all works great. The overlay gets created, it gets show over the top of the pages of the UIPageViewController as you would expect. When pages flip, they flip underneath the overlay - again just as you would expect.
However, the UIButtons within the self.overlay view never get the tap events. The UIPageViewController responds to all events.
I have tried overriding -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch per the suggestions here without success.
UIPageViewController Gesture recognizers
I have tried manually trapping all events and handling them myself - doesn't work (and to be honest even if it did it would seem like a bit of a hack).
Does anyone have a suggestion on how to trap the events or maybe a better approach to using an overlay over the top of the UIPageViewController.
Any and all help very much appreciated!!
Try to iterate through UIPageViewController.GestureRecognizers and assign self as a delegate for those gesture and implement
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
Your code may be like this:
In viewDidLoad
for (UIGestureRecognizer * gesRecog in self.pageViewController.gestureRecognizers)
{
gesRecog.delegate = self;
}
And add the following method:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if (touch.view != self.pageViewController.view]
{
return NO;
}
return YES;
}
The documented way to prevent the UIPageViewController from scrolling is to not assign the dataSource property. If you assign the data source it will move into 'gesture-based' navigation mode which is what you're trying to prevent.
Without a data source you manually provide view controllers when you want to with setViewControllers:direction:animated:completion method and it will move between view controllers on demand.
The above can be deduced from Apple's documentation of UIPageViewController (Overview, second paragraph):
To support gesture-based navigation, you must provide your view controllers using a data source object.