How to restrict the location of an NSWindow when dragging from a non key state - objective-c

I am trying to restrict the movement of an NSWindow to the top of my screen. I have implemented mouse and window event handlers for click and drag. It does work when the window has focus and is key but if I click on another application the click drag on my window I can move it anywhere on the screen and it will flicker between where I am dragging and the top of the screen.
I would really like to capture mouse movement events for the window when it doesn't have focus.
Here is a summary of my project:
#interface CustomWindow : NSWindow <NSWindowDelegate>
- (id)init
[self setDeligate:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(gainedFocus:) name:NSWindowDidBecomeKeyNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(lostFocus:) name:NSWindowDidResignKeyNotification object:nil];
-(void) gainedFocus:(NSNotification*)notification
{
hasFocus = true;
[self setMovableByWindowBackground:NO];
}
-(void) lostFocus:(NSNotification*)notification
{
hasFocus = false;
[self setMovableByWindowBackground:YES];
}
- (void)mouseDragged:(NSEvent *)theEvent // Only called if window has focus (is key)
{
[self positionWindow]; // Works only if window started drag with focus (key)
}
- (void)mouseMoved:(NSEvent *)event // Only called if window has focus (is key)
- (void)mouseUp:(NSEvent *)event // Only called if window has focus (is key)
- (void)mouseDown:(NSEvent *)event // Only called if window has focus (is key)
// Deligate functions
- (void)windowWillMove:(NSNotification *)notification // Is called when not key
- (void)windowDidMove:(NSNotification *)notification // Is called when not key but only when the movement stops and the window flickers between where i’m dragging and where the code moves it to.
{
[self positionWindow];
}
I can a function that moves the window to a desired region with:
-(void) positionWindow {
[self setFrameOrigin: ...];
}
Thanks

Related

Cordova - Implementing the Privacy Screen functionality with the ASWebAuthenticationSession usage

I'm adding the privacy screen functionality to the hybrid Cordova app via a plugin and following the approach adviced by apple.
Though it leads to unexpected issues when I open ASWebAuthenticationSession window I use for the OAuth authentication. What happens is, when system dialogue appears with a text "Your app wants to use xxx for Sign In", it makes the app to lose a focus and the privacy screen appears behind the overlay. After I choose "Yes", the app gains focus back and the code removing the privacy screen fires, the same code also closes the freshly opened ASWebAuthenticationSession window.
The code in PrivacyScreenPlugin.m:
UIViewController *blankViewController;
#interface PrivacyScreenPlugin ()
#end
#implementation PrivacyScreenPlugin
- (void)pluginInitialize
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onAppDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onPageDidLoad) name:CDVPageDidLoadNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onAppWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
}
-(UIViewController *)createViewWithGradient {
UIViewController *viewController;
viewController = [UIViewController new];
viewController.modalPresentationStyle = UIModalPresentationOverFullScreen;
return viewController;
}
-(void) applyPrivacyScreen
{
if (blankViewController == NULL) {
blankViewController = [self createViewWithGradient];
}
blankViewController.view.window.hidden = NO;
[self.viewController.view.window.rootViewController presentViewController:blankViewController animated:NO completion:NULL];
}
#pragma mark - Explicit Commands
- (void) hidePrivacyScreen:(CDVInvokedUrlCommand*)command
{
[self removePrivacyScreen];
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
- (void) showPrivacyScreen:(CDVInvokedUrlCommand*)command
{
[self applyPrivacyScreen];
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
#pragma mark - Triggered functions
- (void) onPageDidLoad
{
[self removePrivacyScreen];
}
- (void)onAppDidBecomeActive:(UIApplication *)application
{
[self removePrivacyScreen];
}
- (void)onAppWillResignActive:(UIApplication *)application
{
[self applyPrivacyScreen];
}
#pragma mark - Helper functions
-(void) removePrivacyScreen
{
[self.viewController dismissViewControllerAnimated:NO completion:nil];
}
#end
So far I got that issue is related to the way view is dismissed, namely dismissViewControllerAnimated which dismisses the stack of modal windows:
[self.viewController dismissViewControllerAnimated:NO completion:nil];
Can it be helped or worked around? Maybe instead of removing a security screen, it can be hidden? Or is there a different way to draw the overlay which is free of the issue?
P.S. I tried to listen to UIApplicationDidEnterBackgroundNotification event, but it's not what I want. I'd like the app screen to be covered as soon as it's sent to the list of apps (via a double tap on the home button or long swipe).
To accomplish it, it's needed to render the privacy screen in a subview, instead of a modal view. It enables to hide/show a privacy screen view instead of adding/removing it:
-(void) removePrivacyScreen
{
blankViewController.view.hidden=YES;
}
-(void) applyPrivacyScreen
{
if (blankViewController == NULL) {
blankViewController = [self createViewWithGradient];
}
blankViewController.view.hidden = NO;
[self.viewController.view.window addSubview:blankViewController.view];
}

NSWindow is not receiving any notification when it loses focus

I have a custom NSWindow class that has the following methods:
- (void)setupWindowForEvents{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(windowDidResignKey:) name:NSWindowDidResignMainNotification object:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(windowDidResignKey:) name:NSWindowDidResignKeyNotification object:self];
}
-(void)windowDidResignKey:(NSNotification *)note {
NSLog(#"notification");
[self close];
}
I call [_window setupWindowForEvents]; but the windowDidResignKey never gets called.
This is how I call my NSWindow: when the status bar item is clicked I makeKeyAndOrderFront and the Window is displayed right beneath the status bar item, like this:
Any ideas why the I don't get any notification when the window loses focus? I've used both NSWindowDidResignMainNotification and NSWindowDidResignKeyNotification to see if any of these worked, but none is working.
You're probably not getting the notification because you actually are never key in the first place. Your window appears to be borderless, and borderless windows don't grab key window status by default.
In your window subclass, be sure to return YES on the following methods:
- (BOOL)canBecomeKeyWindow {
return YES;
}
- (BOOL)canBecomeMainWindow {
return YES;
}

NSTextField: end editing when user clicks outside of the text field

I have an NSTextField that I'm setting editable depending on a user action. I'd like to end editing when the user clicks anywhere outside of the text field inside the window.
Seems simple, but I could not get this to work. I implemented controlTextDidEndEditing and textDidEndEditing, but no luck, especially when I click on a user interface element that does not accept the first responder status.
Every NSEvent is pass through NSWindow's sendEvent: method.
You can create a custom NSWindow and override the sendEvent: method. If there is a mouse down event, broadcast it by the NSNotificationCenter:
- (void)sendEvent:(NSEvent *)event {
[super sendEvent:event];
if (event.type == NSLeftMouseDown) {
[[NSNotificationCenter defaultCenter] postNotificationName:kCustomWindowMouseDown object:self userInfo:#{#"event": event}];
}
}
In the ViewController which reference the NSTextField, observer this notification:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(customWindowMouseDown:)
name:kCustomWindowMouseDown
object:self.view.window];
End the editing if the mouse down event's location is outside of the text field:
- (void)customWindowMouseDown:(id)sender {
NSNotification *notification = (NSNotification *) sender;
NSEvent *event = notification.userInfo[#"event"];
NSPoint locationInWindow = event.locationInWindow;
if ([self.view.window.firstResponder isKindOfClass:NSTextView.class]) {
NSTextView *firstResponder = (NSTextView *) self.view.window.firstResponder;
//we only care about the text field referenced by current ViewController
if (firstResponder.delegate == (id <NSTextViewDelegate>) self.textField) {
NSRect rect = [self.textField convertRect:self.textField.bounds toView:nil];
//end editing if click out side
if (!NSPointInRect(locationInWindow, rect)) {
[self.view.window makeFirstResponder:nil];
}
}
}
}
You can write a subclass for the NSView and write the below method and change the class of the NSView in the NSWindow of the nib file to that subclass.
- (void)mouseDown:(NSEvent *)event
{
[text setEditable:NO];
NSLog(#"mouseDown");
}
May be a bit dirty but you could create a big transparent button on the "outside of the text field" area. Show it when editing starts and hide it when editing ends. If user taps this button you stop editing (and hide the button).
Solved that for me when I needed a fast solution.
I'd improve the answer of vignesh kumar for the cases when you can't subclass the window that contains the view.
For all sub-views/controls that handle mouseDown, including the super view itself, implement:
- (void)mouseDown:(NSEvent *)event
{
[[self window] makeFirstResponder:self];
[super mouseDown:event];
}
For some controls, like buttons, you could change to
- (void)mouseDown:(NSEvent *)event
{
[[self window] makeFirstResponder:[self superview]];
[super mouseDown:event];
}
otherwise a focus ring may appear

UIKeyboard avoidance and Auto Layout

Given the focus on Auto Layout in iOS 6, and the recommendation by Apple engineers (see WWDC 2012 videos) that we no longer manipulate a views' frame directly, how would one go about avoiding the keyboard using only Auto Layout and NSLayoutConstraint?
Update
This looks like a reasonable solution: An example of keyboard sensitive layout (GitHub source) but one potential issue I see is what happens when a user rotates the device and the keyboard is already on screen?
That blog post is great, but I'd like to suggest some improvements to it. First, you can register to observe frame changes, so you don't need to register to observe both show and hide notifications. Second, you should convert the CGRects for the keyboard from screen to view coordinates. Last, you can copy the exact animation curve used by iOS for the keyboard itself, so the keyboard and the tracking views move in synchrony.
Putting it all together, you get the following:
#interface MyViewController ()
// This IBOutlet holds a reference to the bottom vertical spacer
// constraint that positions the "tracking view",i.e., the view that
// we want to track the vertical motion of the keyboard
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomVerticalSpacerConstraint;
#end
#implementation MyViewController
-(void)viewDidLoad
{
[super viewDidLoad];
// register for notifications about the keyboard changing frame
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
object:self.view.window];
}
-(void)keyboardWillChangeFrame:(NSNotification*)notification
{
NSDictionary * userInfo = notification.userInfo;
UIViewAnimationCurve animationCurve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue];
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// convert the keyboard's CGRect from screen coords to view coords
CGRect kbEndFrame = [self.view convertRect:[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]
fromView:self.view.window];
CGRect kbBeginFrame = [self.view convertRect:[[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]
fromView:self.view.window];
CGFloat deltaKeyBoardOrigin = kbEndFrame.origin.y - kbBeginFrame.origin.y;
// update the constant factor of the constraint governing your tracking view
self.bottomVerticalSpacerConstraint.constant -= deltaKeyBoardOrigin;
// tell the constraint solver it needs to re-solve other constraints.
[self.view setNeedsUpdateConstraints];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:duration];
[UIView setAnimationCurve:animationCurve];
[UIView setAnimationBeginsFromCurrentState:YES];
// within this animation block, force the layout engine to apply
// the new layout changes immediately, so that we
// animate to that new layout. We need to use old-style
// UIView animations to pass the curve type.
[self.view layoutIfNeeded];
[UIView commitAnimations];
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillChangeFrameNotification
object:nil];
}
#end
This will work, as long as you don't change orientation while the keyboard is up.
It was an answer on How to mimic Keyboard animation on iOS 7 to add "Done" button to numeric keyboard? showed how to mimic the keyboard animation curve correctly.
One last thing to beware of with respect to all these notification-based solutions: they can produce unexpected effects if some other screen in your app also uses the keyboard, because your view controller will still receive the notifications as long as it has not been deallocated, even if it's views are unloaded. One remedy for this is to put a conditional in the notification handler to ensure it only operates when the view controller is on screen.
Using the KeyboardLayoutConstraint in the Spring framework is the simplest solution I've found so far.
My idea is to create a UIView, let's call it keyboard view, and place it to your view controller's view. Then observe keyboard frame change notifications UIKeyboardDidChangeFrameNotification and match the frame of the keyboard to the keyboard view (I recommend to animate the change). Observing this notification handles the rotation you mentioned and also moving keyboard on iPad.
Then simply create your constraints relative to this keyboard view. Don't forget to add the constraint to their common superview.
To get the keyboard frame correctly translated and rotated to your view coordinates check out the docs for UIKeyboardFrameEndUserInfoKey.
I created a view like this that would watch the keyboard and change its own constraints when the keyboard comes on/off the screen.
#interface YMKeyboardLayoutHelperView ()
#property (nonatomic) CGFloat desiredHeight;
#property (nonatomic) CGFloat duration;
#end
#implementation YMKeyboardLayoutHelperView
- (id)init
{
self = [super init];
if (self) {
self.translatesAutoresizingMaskIntoConstraints = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillShow:) name:#"UIKeyboardWillShowNotification" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillHide:) name:#"UIKeyboardWillHideNotification" object:nil];
}
return self;
}
- (void)keyboardWillShow:(NSNotification *)notification
{
// Save the height of keyboard and animation duration
NSDictionary *userInfo = [notification userInfo];
CGRect keyboardRect = [userInfo[#"UIKeyboardBoundsUserInfoKey"] CGRectValue];
self.desiredHeight = CGRectGetHeight(keyboardRect);
self.duration = [userInfo[#"UIKeyboardAnimationDurationUserInfoKey"] floatValue];
[self setNeedsUpdateConstraints];
}
- (void)keyboardWillHide:(NSNotification *)notification
{
// Reset the desired height (keep the duration)
self.desiredHeight = 0.0f;
[self setNeedsUpdateConstraints];
}
- (void)updateConstraints
{
[super updateConstraints];
// Remove old constraints
if ([self.constraints count]) {
[self removeConstraints:self.constraints];
}
// Add new constraint with desired height
NSString *constraintFormat = [NSString stringWithFormat:#"V:[self(%f)]", self.desiredHeight];
[self addVisualConstraints:constraintFormat views:#{#"self": self}];
// Animate transition
[UIView animateWithDuration:self.duration animations:^{
[self.superview layoutIfNeeded];
}];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#end
Ive written a library that will do it all for you (supports Auto Layout and Springs & Struts)
IHKeyboardAvoiding https://github.com/IdleHandsApps/IHKeyboardAvoiding
Just call [IHKeyboardAvoiding setAvoidingView:self.myView];
For auto layout with keyboard case, I use static table view. This keeps your codes much simpler and not need to keep track of keyboard height. One thing I learned about table view is to keep each table row as narrow as possible. If you put too many UIs vertically in one row, you may get keyboard overlap.

How to draw custom window controls (close, minimize, and zoom buttons)

I've made an attempt to draw custom NSButtons, but it seems I'm reinventing the wheel here. Is there a way to just replace the default images used for the close, minimize and zoom buttons?
Several apps already do it:
OSX 10.8's Reminders app (they appear dark grey when the window is not key, vs most appear light grey)
Tweetbot (All buttons look totally custom)
More info:
I can generate the system defaults as such standardWindowButton:NSWindowCloseButton. But from there the setImage setter doesn't change the appearance of the buttons.
Edit: Since I wrote this, INAppStore has implemented a pretty nice way to do this with INWindowButton. If you're looking for a drag and drop solution check there, but the code below will still help you implement your own.
So I couldn't find a way to alter the standardWindowButtons. Here is a walkthrough of how I created my own buttons.
Note: There are 4 states the buttons can be in
Window inactive
Window active - normal
Window active - hover
Window active - press
On to the walkthrough!
Step 1: Hide the pre-existing buttons
NSButton *windowButton = [self standardWindowButton:NSWindowCloseButton];
[windowButton setHidden:YES];
windowButton = [self standardWindowButton:NSWindowMiniaturizeButton];
[windowButton setHidden:YES];
windowButton = [self standardWindowButton:NSWindowZoomButton];
[windowButton setHidden:YES];
Step 2: Setup the view in Interface Builder
You'll notice on hover the buttons all change to their hover state, so we need a container view to pick up the hover.
Create a container view to be 54px wide x 16px tall.
Create 3 Square style NSButtons, each 14px wide x 16px tall inside the container view.
Space out the buttons so there is are 6px gaps in-between.
Setup the buttons
In the attributes inspector, set the Image property for each button to the window-active-normal image.
Set the Alternate image property to the window-active-press image.
Turn Bordered off.
Set the Type to Momentary Change.
For each button set the identifier to close,minimize or zoom (Below you'll see how you can use this to make the NSButton subclass simpler)
Step 3: Subclass the container view & buttons
Container:
Create a new file, subclass NSView. Here we are going to use Notification Center to tell the buttons when they should switch to their hover state.
HMTrafficLightButtonsContainer.m
// Tells the view to pick up the hover event
- (void)viewDidMoveToWindow {
[self addTrackingRect:[self bounds]
owner:self
userData:nil
assumeInside:NO];
}
// When the mouse enters/exits we send out these notifications
- (void)mouseEntered:(NSEvent *)theEvent {
[[NSNotificationCenter defaultCenter] postNotificationName:#"HMTrafficButtonMouseEnter" object:self];
}
- (void)mouseExited:(NSEvent *)theEvent {
[[NSNotificationCenter defaultCenter] postNotificationName:#"HMTrafficButtonMouseExit" object:self];
}
Buttons:
Create a new file, this time subclass NSButton. This one's a bit more to explain so I'll just post all the code.
HMTrafficLightButton.m
#implementation HMTrafficLightButton {
NSImage *inactive;
NSImage *active;
NSImage *hover;
NSImage *press;
BOOL activeState;
BOOL hoverState;
BOOL pressedState;
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self setup];
}
return self;
}
- (id)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
// Setup images, we use the identifier to chose which image to load
active = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-active",self.identifier]];
hover = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-hover",self.identifier]];
press = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-press",self.identifier]];
inactive = [NSImage imageNamed:#"window-button-all-inactive"];
// Checks to see if window is active or inactive when the `init` is called
if ([self.window isMainWindow] && [[NSApplication sharedApplication] isActive]) {
[self setActiveState];
} else {
[self setInactiveState];
}
// Watch for hover notifications from the container view
// Also watches for notifications for when the window
// becomes/resigns main
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(setActiveState)
name:NSWindowDidBecomeMainNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(setInactiveState)
name:NSWindowDidResignMainNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(hoverIn)
name:#"HMTrafficButtonMouseEnter"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(hoverOut)
name:#"HMTrafficButtonMouseExit"
object:nil];
}
- (void)mouseDown:(NSEvent *)theEvent {
pressedState = YES;
hoverState = NO;
[super mouseDown:theEvent];
}
- (void)mouseUp:(NSEvent *)theEvent {
pressedState = NO;
hoverState = YES;
[super mouseUp:theEvent];
}
- (void)setActiveState {
activeState = YES;
if (hoverState) {
[self setImage:hover];
} else {
[self setImage:active];
}
}
- (void)setInactiveState {
activeState = NO;
[self setImage:inactive];
}
- (void)hoverIn {
hoverState = YES;
[self setImage:hover];
}
- (void)hoverOut {
hoverState = NO;
if (activeState) {
[self setImage:active];
} else {
[self setImage:inactive];
}
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#end
In IB set the Custom Class of the container view and all 3 buttons to their respective classes that we just created.
Step 4: Set the button actions
These methods, called from the view controller, are the same as the standardWindowButtons'. Link them to the buttons in IB.
- (IBAction)clickCloseButton:(id)sender {
[self.view.window close];
}
- (IBAction)clickMinimizeButton:(id)sender {
[self.view.window miniaturize:sender];
}
- (IBAction)clickZoomButton:(id)sender {
[self.view.window zoom:sender];
}
Step 5: Add the view to the window
I have a separate xib and view controller setup specifically for the window controls. The view controller is called HMWindowControlsController
(HMWindowControlsController*) windowControlsController = [[HMWindowControlsController alloc] initWithNibName:#"WindowControls" bundle:nil];
NSView *windowControlsView = windowControlsController.view;
// Set the position of the window controls, the x is 7 px, the y will
// depend on your titlebar height.
windowControlsView.frame = NSMakeRect(7.0, 10.0, 54.0, 16.0);
// Add to target view
[targetView addSubview:windowControlsView];
Hope this helps. This is a pretty lengthy post, if you think I've made a mistake or left something out please let me know.