Cocoa/OSX - NSWindow standardWindowButton behaving strangely once copied and added again - objective-c

In my app I change the position of the standardWindowButtons close / miniturize / expand like so:
//Create the buttons
NSButton *minitButton = [NSWindow standardWindowButton:NSWindowMiniaturizeButton forStyleMask:window.styleMask];
NSButton *closeButton = [NSWindow standardWindowButton:NSWindowCloseButton forStyleMask:window.styleMask];
NSButton *fullScreenButton = [NSWindow standardWindowButton:NSWindowZoomButton forStyleMask:window.styleMask];
//set their location
[closeButton setFrame:CGRectMake(7+70, window.frame.size.height - 22 - 52, closeButton.frame.size.width, closeButton.frame.size.height)];
[fullScreenButton setFrame:CGRectMake(47+70, window.frame.size.height - 22 -52, fullScreenButton.frame.size.width, fullScreenButton.frame.size.height)];
[minitButton setFrame:CGRectMake(27+70, window.frame.size.height - 22 - 52, minitButton.frame.size.width, minitButton.frame.size.height)];
//add them to the window
[window.contentView addSubview:closeButton];
[window.contentView addSubview:fullScreenButton];
[window.contentView addSubview:minitButton];
Now when the window appears with the buttons there is two problems:
1. They are grey and not their correct color
2. when the mouse is over them they do not show the + - or x sign
can anyone tell me what I am doing wrong. Thanks.

Here is the mechanics of this hover magic: Before drawing itself standard circled button (such as NSWindowMiniaturizeButton) calls their superview undocumented method _mouseInGroup:. If this method returns YES circled button draws itself with icon inside. That's all.
If you place these buttons inside your own view, you can simply implement this method and control this mouse-hover-appearance as you want. If you just move or relayout these buttons and they still be subviews of NSThemeFrame (or something similar), you have to swizzle method _mouseInGroup: for this class, and probably it doesn't worth it because we have perfectly simple previous method.
In my case I have custom NSView that contains my standard buttons as subviews and this code makes all described above magic:
- (void)updateTrackingAreas
{
NSTrackingArea *const trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:(NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect) owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
}
- (void)mouseEntered:(NSEvent *)event
{
[super mouseEntered:event];
self.mouseInside = YES;
[self setNeedsDisplayForStandardWindowButtons];
}
- (void)mouseExited:(NSEvent *)event
{
[super mouseExited:event];
self.mouseInside = NO;
[self setNeedsDisplayForStandardWindowButtons];
}
- (BOOL)_mouseInGroup:(NSButton *)button
{
return self.mouseInside;
}
- (void)setNeedsDisplayForStandardWindowButtons
{
[self.closeButtonView setNeedsDisplay];
[self.miniaturizeButtonView setNeedsDisplay];
[self.zoomButtonView setNeedsDisplay];
}

I'm fully aware that this question is old and Valentin Shergin's answer is correct. It prevents the utilize of any Private API, unlike Google did in Chrome. Just wanted to share a method for those who don't feel like subclass NSView just to put those buttons in an existed view (such as self.window.contentView).
As I just wanted to reposition the NSWindowButtons via setFrame:, I found out that once the window was resized, the tracking areas seems to "fix" themselves automagically, without any Private API usage (at least in 10.11).
Thus, you can do things like the following to apply "fake resize" to the window that you repositioned your buttons:
NSRect frame = [self.window frame];
frame.size = NSMakeSize(frame.size.width, frame.size.height+1.f);
[self.window setFrame:frame display:NO animate:NO];
frame.size = NSMakeSize(frame.size.width, frame.size.height-1.f);
[self.window setFrame:frame display:NO animate:YES];
(I did it within my main window's NSWindowDelegate windowDidBecomeMain:. Should work as long as the window is loaded and visible.)

You're not adding them again. You're moving them to contentView. The buttons are originally in window.contentView.superview.
[window.contentView.superview addSubview:closeButton];
[window.contentView.superview addSubview:fullScreenButton];
[window.contentView.superview addSubview:minitButton];
Should get you the correct behaviour without requiring a trackingArea.

Call [button highlight:yes] for each button.

Related

Can't set view origin to negative

I have a simple KeyboardAdjuster class that is a property of my form views. If one of the form fields is hidden by the keyboard, then entering that field will have an animation to move the whole frame's origin.y up, so that the field appears above the keyboard. A very common approach. It also has a few complexities like calculating how much to scroll by when navigating between fields, but that's not important right now. . . I've been using this utility class since iOS5.
Example Form:
The problem:
On iOS8 it has simply stopped working.
When animating the frame starts by snapping in the opposite direction exactly the amount that its supposed to scroll by. And then scrolling back to the origin.
I tried commenting the animation part out, and simply setting the frame. No effect.
For example if the frame is supposed to be: {0, -127, 320, 480} then it will simply stay at {0, 0, 320, 480}
Why doesn't this work on iOS8? Has something changed that I've missed?
About the views:
My views are hand-coded, they're a sub-class of a simple form base-view. (Contains keyboard adjuster and a scroll-view). The other elements are added with initial frames of CGRectZero and then laid out manually in layoutSubviews
How the view/controller created:
There is a RootViewController that acts as a container controller (UIView containment). It:
Has a main navigation controller
Has a container to present / dismiss a hamburger menu for the nav controller's top views (these can change).
Has a container to present overlays with a custom bounce animation.
So the view is created as follows:
- (instancetype)initWithView:(INFAcceptGiftView *)view offerDao:(id <INFOfferDao>)offerDao
locationTracker:(INFLocationTracker *)locationTracker
{
self = [super init];
if (self)
{
self.view = view;
_offerDao = offerDao;
_locationTracker = locationTracker;
}
return self;
}
What triggers the keyboard animation?:
The UIView is a sub-class of form base view, which is a UITextFieldDelegate:
interface INFFormBaseView : UIView <UITextFieldDelegate, INFInputAccessoryDelegate>
{
UIResponder *_currentResponder;
INFInputValidator *_validator;
}
When a field is entered:
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
[_validator dismissMessages];
[_keyboardAdjuster scrollToAccommodateField:textField];
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
[_keyboardAdjuster scrollToAccommodateField:nil];
}
I have a different solution for you which works on both iOS 7 & 8 and Auto Layout.
In my example I have two UITextFields which I move and hide depending on their position and the position of the keyboard. In this particular case, I switch the UITextFields between them and hide the inactive one.
In viewDidLoad you register for the following notifications:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardShowed:)
name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardHidden:)
name:UIKeyboardWillHideNotification object:nil];
After that you grab a hold of the default frames of your views, in this case the two UITextFields:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
/*** FOR AUTOLAYOUT MODIFICATIONS & ADDITIONS # RUNTIME ***/
self.mailTextFieldDefaultFrame = self.mailTextField.frame;
self.passwordTextFieldDefaultFrame = self.passwordTextField.frame;
}
And when you receive UIKeyboardWillShowNotification you'll start moving your views:
- (void) keyboardShowed:(NSNotification*)notification {
//GET KEYBOARD FRAME
CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
//CONVERT KEYBOARD FRAME TO MATCH OUR COORDINATE SYSTEM (FOR UPSIDEDOWN ROTATION)
CGRect convertedFrame = [self.view convertRect:keyboardFrame fromView:self.view.window];
if ([self.mailTextField isFirstResponder]) {
[UIView transitionWithView:self.mailTextField
duration:.3f
options:UIViewAnimationOptionCurveLinear
animations:^{
self.mailTextField.alpha = 1.0f;
self.mailTextField.frame = CGRectMake(self.mailTextField.frame.origin.x,
convertedFrame.origin.y -
self.mailTextField.frame.size.height - 25,
self.mailTextField.frame.size.width,
self.mailTextField.frame.size.height);
self.passwordTextField.alpha = 0.0f;
}
completion:nil];
} else if ([self.passwordTextField isFirstResponder]) {
[UIView transitionWithView:self.passwordTextField
duration:.3f
options:UIViewAnimationOptionCurveLinear
animations:^{
self.passwordTextField.alpha = 1.0f;
self.mailTextField.frame = self.passwordTextField.frame;
self.passwordTextField.frame = CGRectMake(self.passwordTextField.frame.origin.x,
convertedFrame.origin.y -
self.passwordTextField.frame.size.height - 25,
self.passwordTextField.frame.size.width,
self.passwordTextField.frame.size.height);
self.mailTextField.alpha = 0.0f;
}
completion:(void (^)(BOOL finished)) ^{
}];
}
And when you hide the keyboard:
- (void) keyboardHidden:(NSNotification*)notification {
//RESTORE ORIGINAL STATE OF TEXTFIELDS
[UIView transitionWithView:self.view
duration:.3f
options:UIViewAnimationOptionCurveLinear
animations:^{
self.mailTextField.frame = self.mailTextFieldDefaultFrame;
self.passwordTextField.frame = self.passwordTextFieldDefaultFrame;
self.mailTextField.alpha = 1.0f;
self.passwordTextField.alpha = 1.0f;
}
completion:nil];
}
Here's the solution, posting in case it helps someone.
I mentioned above that I'm using UIView containment, so I have a root view controller that:
Contains a UINavigationController (the root view is replaceable).
Contains a Menu Controller (dealloc'd when not in use)
Presents an overlay with custom animation
My root view had layout subviews as follows:
- (void)layoutSubviews
{
[super layoutSubviews];
[_mainContentViewContainer setFrame:self.bounds];
}
This behaved the way that I wanted it to pre iOS8, but not afterwards. Technically it appears that iOS8 is doing the right thing - I should only be laying out the _mainConentViewContainer on startup or orientation change.

Programmatically add a close button to nsview

I am trying to create Borderless window with windows style mask set to "NSBorderlessWindowMask".
The window is subclassed and return "Yes" for canBecomeKeyWindow and canBecomeMainWindow.
I have to get only minimize button in my view at top left corner.
I am getting instance of minimise button object using "[NSWindow standardWindowButton:NSWindowMiniaturizeButton forStyleMask:NSBorderlessWindowMask]"
and adding it as subview .
The button is present in nsview. But is grayed out and is enabled and is not in Yellow also.
Once I click it , it turns yellow. And on mouse hover "-" that appears in minimise button is not turning in.
I tried [button setHighlighted:YES] which actually appears as clicked.
I have added tracking area to get mouse hover.
_minButton = [NSWindow standardWindowButton:NSWindowMiniaturizeButton forStyleMask:NSBorderlessWindowMask];
[_minButton setFrameOrigin:NSMakePoint(_minButton.frame.origin.x + 7, _minButton.frame.origin.y)];
[_minButton setTarget:self.view.window];
[_minButton setAction:#selector(miniaturize:)];
[self.view addSubview:_minButton];
NSTrackingArea* trackingArea = [[NSTrackingArea alloc]
initWithRect:[_minButton bounds]
options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways
owner:self userInfo:nil];
[_minButton addTrackingArea:trackingArea];
- (void)mouseEntered:(NSEvent *)theEvent{
[[_minButton cell]setHighlighted:YES];
}
- (void)mouseExited:(NSEvent *)theEvent{
[[_minButton cell]setHighlighted:NO];
}
try calling
[_minButton setNeedsDisplay:YES];
at the end of the method
This worked for me. It's a hack, but it worked.
- (void)windowDidBecomeKey:(NSNotification *)notification {
[_minButton highlight:YES];
[_minButton highlight:NO];

When showing the real time resizing of a pane, it struggles while dragging and top of the pane is hidden after released

I went through several posts on dragging but couldn't find an answer to my problem.
I can use the mouseDown and mouseUp events to track the current positions and redraw the resized pane. What I want is to show the real time movement of the pane. Everytime mouseDragged event is fired, y coordinate of the new location is taken and setFrame is called to redraw. The window seems to flicker and gets stuck finally (title bar goes out of bounds and hidden) as it seems to miss the final events in the run loop.
Is there a way to solve this problem?
The view has been implemented in the following way
NSSplitView (divided into sections left dock, right dock, etc.)
NSView is used to implement a sub view inside the dock
NSTableView is used inside the NSView to hold multiple "panels"
There can be several panels inside this table view (one below another)
I need to resize these panels by dragging the border line. For this I'm using an NSButton in the bottom.(I want to show a thicker separation line there)
Here is the code for mouseDown, mouseUp, mouseDragged callbacks, used to resize the panel
-(void)mouseDown:(NSEvent *)theEvent {
draggingInProgress = YES;
firstDraggingPointFound = NO;
}
-(void)mouseUp:(NSEvent *)theEvent {
if (!draggingInProgress) {
return;
}
NSPoint point = [self convertPoint: [theEvent locationInWindow] fromView: nil];
if (firstDraggingPointFound) {
[_delegate heightChanged:[NSNumber numberWithFloat:(point.y - previousDragPosition)]];
}
draggingInProgress = NO;
[_delegate heightChangingEnded]; //draggingInProgress is set to NO
}
-(void)mouseDragged:(NSEvent *)theEvent {
if (!draggingInProgress) {
return;
}
NSPoint point = [self convertPoint: [theEvent locationInWindow] fromView: nil];
if (firstDraggingPointFound) {
[_delegate heightChanged:[NSNumber numberWithFloat:(point.y - previousDragPosition)]];
} else {
firstDraggingPointFound = YES;
}
previousDragPosition = point.y;
}
//Delegate method
-(void)heightChanged:(NSNumber *)change {
NSRect f = [[self view] frame];
f.size.height += [change floatValue];
if (f.size.height < 100) {
f.size.height = 100;
}
[[self view] setFrame:f];
[self.panelViewModel setPanelHeight:f.size.height];
if (_heightChangeDelegate) {
[_heightChangeDelegate heightChangedForPanel:self];
}
[[self view] setFrame:f];
}
What would be the problem here?
Is there a better way to do this?
First off, I wouldn’t use the word “Panel” to describe what you want, since an “NSPanel” is a kind of “NSWindow”. Let’s call them “panes.”
Second, I wouldn’t use an NSTableView to contain your panes. NSTableView really isn’t designed for that, it’s for tabular data. You can use an NSStackView, which IS designed for it, or just use raw constraints and autolayout, manually setting the top of each pane to equal the bottom of the previous one.

Change cursor for full screen NSWindow

I am trying to make an overlay window that will allow drawing at the ShieldingWindowLevel, however when the window appears the cursor is still the default pointer. I would like to change it to the crosshairs. Having controller NSCursors before I am baffled why resetCursorRects is not ever called.
I manually create the window as follows (in my AppController class):
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Create the window
NSRect frame = [[NSScreen mainScreen] frame];
// Provide a small area on the right to move the cursor in-and-out of the window.
frame.size.width = frame.size.width - 20;
self.window = [[NSWindow alloc] initWithContentRect:frame
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
[self.window setAcceptsMouseMovedEvents:YES];
[self.window setOpaque:NO];
[self.window setLevel:CGShieldingWindowLevel()];
[self.window setBackgroundColor:[NSColor colorWithDeviceRed:0.0 green:0.0 blue:1.0 alpha:0.2]];
// Create the subview
ScreenOverlayView *subview = [[ScreenOverlayView alloc] initWithFrame:NSZeroRect];
[[self.window contentView] addSubview:subview];
// Add subview and show window
[self.window setContentView:subview];
[self.window makeFirstResponder:subview];
[self.window orderFrontRegardless];
}
With the following NSView subclass:
#implementation ScreenOverlayView
- (void) resetCursorRects {
[super resetCursorRects];
[self addCursorRect: [self bounds]
cursor: [NSCursor crosshairCursor]];
}
// ...
#end
I created a sample project to show this case and posted it to github, the most interesting files are ScreenOverlayView.m and AppDelegate.m.
I should point out that I have also spent a good deal of time trying to get this working with an NSTrackingArea, as you can see in the sample project. Tracking Area works if the mouse enters the view after it has appeared, but not if it is inside to start with. Using MouseEnter and MouseLeave would be fine if I had some way to set the initial cursor, but it will only change for a split second before changing back.
How can I get resetCursorRects to be invoked -OR- how can I set the cursor when I move it to the superview?
The key is that you really need to create a custom subclass of NSWindow, in order to counteract some of the default behavior that borderless windows (NSBorderlessWindowMask) have.
An updated version of your sample project is at http://www.markdouma.com/developer/full-screen-overlay.zip.
In it, I created a custom MDScreenOverlayWindow class that overrides NSWindow's canBecomeKeyWindow method like below:
// Windows created with NSBorderlessWindowMask normally can't be key,
but we want ours to be
- (BOOL)canBecomeKeyWindow {
return YES;
}
This will allow your view to become key and basically all your other stuff to work properly.
The other thing that may be of note is the drawRect: method. (It looks like you may be coming from iOS). You might want to look into NSBezierPath, as it could potentially simplify some of your drawing code. For example, I believe the drawing code you had could be consolidated into the following:
- (void)drawRect:(NSRect)rect {
// the color should probably be "pre-multiplied" by the alpha
// premultiplied version:
[[NSColor colorWithCalibratedRed:0.8 green:0.0 blue:0.0 alpha:0.8] set];
[NSBezierPath setDefaultLineWidth:2.0];
[NSBezierPath strokeLineFromPoint:currentLocation toPoint:downLocation];
}

Dismiss Custom Window for NSStatusItem

I've got a custom window popping up when an NSStatusItem is clicked. The code is based on MAAtachedwindow. Everything is working great but I can't figure out a way to dismiss the window when the user clicks on something else like another status bar item, or another app.
Here's my code for creating the window:
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:width] retain];
//setup custom status menu view
CGFloat height = [[NSStatusBar systemStatusBar] thickness];
NSRect viewFrame = NSMakeRect(0.0f, 0.0f, width, height);
statusMenuView = [[[_ISStatusMenuView alloc] initWithFrame:viewFrame] retain];
statusMenuView.offset = aOffset;
statusItem.view = statusMenuView;
//setup the window to show when clicked
NSRect contentRect = NSZeroRect;
contentRect.size = aView.frame.size;
statusMenuWindow = [[[NSWindow alloc] initWithContentRect:contentRect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO] retain];
[statusMenuWindow setLevel:NSPopUpMenuWindowLevel];
[statusMenuWindow setBackgroundColor:[NSColor clearColor]];
[statusMenuWindow setMovableByWindowBackground:NO];
[statusMenuWindow setExcludedFromWindowsMenu:YES];
[statusMenuWindow setOpaque:NO];
[statusMenuWindow setHasShadow:NO];
[statusMenuWindow useOptimizedDrawing:YES];
[[statusMenuWindow contentView] addSubview:aView];
[statusMenuWindow setDelegate:self];
statusMenuView.statusMenuWindow = statusMenuWindow;
And here is how I'm showing the window:
- (void)centerView{
NSRect menuFrame = self.window.frame;
NSRect windowFrame = self.statusMenuWindow.frame;
NSPoint menuPoint = NSMakePoint(NSMidX(menuFrame), NSMinY(menuFrame));
menuPoint.x -= windowFrame.size.width*0.5f;
menuPoint.y -= windowFrame.size.height+self.offset;
[self.statusMenuWindow setFrameOrigin:menuPoint];
[self.statusMenuWindow makeKeyAndOrderFront:self];
}
I was hoping the windowDidResignKey delegate method would do the trick but it doesn't go off with this configuration. The delegate is working because windowDidMove does run.
- (void)windowDidResignKey:(NSNotification *)notification{
NSLog(#"windowDidResignKey");
[statusMenuView hideView];
}
- (void)windowDidResignMain:(NSNotification *)notification{
NSLog(#"windowDidResignMain");
}
- (void)windowDidMove:(NSNotification *)notification{
NSLog(#"windowDidMove");
}
So to recap, how can I hide my custom window when the user clicks on anything else, the way the standard status bar menus work?
Edit
After looking at the Popup example the only thing I was missing was I had to subclass NSPanel and make it so it could become the key window.
#interface Panel : NSPanel
#end
#implementation Panel
- (BOOL)canBecomeKeyWindow{
return YES;
}
#end
You need to make sure your window can become the key window, and call your window's orderOut: method when it resigns key. You should be using a custom NSWindowController, if you are not already, in which case you would just call its "close" method to dismiss your window.
Instead of posting a bunch of code, I would suggest you just look at this excellent, recently-posted example of attaching a window to a status item:
Shpakovski Popup Window Example