NSButtonCell hover image, transparent background on click? - objective-c

I've implemented an NSButton with an image in it.
When the user hovers it, the image changes to something else, and then back on.
Normal Status :
On Hover :
The code I'm using for the NSButtonCell is :
Interface :
#import <Foundation/Foundation.h>
#interface DKHoverButtonCell : NSButtonCell
{
NSImage *_oldImage;
NSImage *hoverImage;
}
#property (retain) NSImage *hoverImage;
#end
Implementation :
#import "DKHoverButtonCell.h"
#interface NSButtonCell()
- (void)_updateMouseTracking;
#end
#implementation DKHoverButtonCell
#synthesize hoverImage;
- (void)mouseEntered:(NSEvent *)event {
if (hoverImage != nil && [hoverImage isValid]) {
_oldImage = [[(NSButton *)[self controlView] image] retain];
[(NSButton *)[self controlView] setImage:hoverImage];
}
}
- (void)mouseExited:(NSEvent *)event {
if (_oldImage != nil && [_oldImage isValid]) {
[(NSButton *)[self controlView] setImage:_oldImage];
[_oldImage release];
_oldImage = nil;
}
}
- (void)_updateMouseTracking {
[super _updateMouseTracking];
if ([self controlView] != nil && [[self controlView] respondsToSelector:#selector(_setMouseTrackingForCell:)]) {
[[self controlView] performSelector:#selector(_setMouseTrackingForCell:) withObject:self];
}
}
- (void)setHoverImage:(NSImage *)newImage {
[newImage retain];
[hoverImage release];
hoverImage = newImage;
[[self controlView] setNeedsDisplay:YES];
}
- (void)dealloc {
[_oldImage release];
[hoverImage release];
[super dealloc];
}
#end
Now, here is the issue :
although the above controls works 100% (with a "X" rounded image, and transparent background"), when the user clicks on it, it displays a "white"-ish background, and not retain my old "transparent" background
How should I go about resolving this?

You need to change button's type to Momentary Change. You can change it in Attributes inspector:
Or change programatically:
[button setButtonType:NSMomentaryChangeButton];

But for your case it should be sufficient to use showsBorderOnlyWhileMouseInside property of NSButtonCell which has been around since the beginning (OS X 10.0). This would show the button's border only when the mouse is hovering inside the button. Combine that with a border style that is filled and light gray, it would be pretty close to the result you've achieved.
There's no subclassing required nor using undocumented API functions (in your case, _updateMouseTracking).
closeButton.bezelStyle = .inline
closeButton.setButtonType(.momentaryPushIn)
if let buttonCell = closeButton.cell as? NSButtonCell {
buttonCell.showsBorderOnlyWhileMouseInside = true
}

Related

Cannot set my custom NSView as first responder

I've created a simple subclass of NSView, but calling becomeFirstResponder seems to have no effect - the framework doesn't even call my acceptsFirstResponder method.
However, if I click with my mouse on my view it seems to becomes the first responder and I can receive keyDown events.
Here is my NSView subclass:
#interface MyView : NSView
#end
#implementation MyView
- (void) keyDown: (NSEvent*) with {
NSLog(#"keyDown");
}
- (BOOL)acceptsFirstResponder {
NSLog(#"acceptsFirstResponder");
return YES;
}
- (BOOL)canBecomeKeyView {
NSLog(#"canBecomeKeyView");
return YES;
}
#end
And here is how I'm setting it up in my AppDelegate:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSRect frameRect = NSMakeRect(0,0,100,100);
MyView *myView = [[MyView alloc] initWithFrame:frameRect];
[_window.contentView addSubview:myView];
[myView becomeFirstResponder];
}
What am I doing wrong?
Whoops - the documentation says 'Never invoke this method directly'. So I should call
[_window makeFirstResponder:myView];
instead of
[myView becomeFirstResponder];
Thanks to Willeke for the hint.

Ignore gestures on parent view

I recently came across an issue where I had a superview that needed to be swipable and a subview that also needed to be swipable. The interaction was that the subview should be the only one swiped if the swipe occurred within its bounds. If the swipe happened outside of the subview, the superview should handle the swipe.
I couldn't find any answers that solved this exact problem and eventually came up with a hacky solution that I thought I'd post if it can help others.
Edit:
A better solution is now marked as the right answer.
Changed title from "Ignore touch events..." to "Ignore gestures..."
If you are looking for a better solution, you can use gestureRecognizer:shouldReceiveTouch: delegate method to ignore the touch for the parent view recognizer.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch{
UIView* swipeableSubview = ...; //set to the subview that can be swiped
CGPoint locationInSubview = [touch locationInView:swipeableSubview];
BOOL touchIsInSubview = [swipeableSubview pointInside:locationInSubview withEvent:nil];
return !touchIsInSubview;
}
This will make sure the parent only receives the swipe if the swipe does not start on the swipeable subview.
The basic premise is to catch when a touch happens and remove gestures if the touch happened within a set of views. It then re-adds the gestures after the gesture recognizer handles the gestures.
#interface TouchIgnorer : UIView
#property (nonatomic) NSMutableSet * ignoreOnViews;
#property (nonatomic) NSMutableSet * gesturesToIgnore;
#end
#implementation TouchIgnorer
- (id) init
{
self = [super init];
if (self)
{
_ignoreOnViews = [[NSMutableSet alloc] init];
_gesturesToIgnore = [[NSMutableSet alloc] init];
}
return self;
}
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint relativePt;
for (UIView * view in _ignoreOnViews)
{
relativePt = [view convertPoint:point toView:view];
if (!view.isHidden && CGRectContainsPoint(view.frame, relativePt))
{
for (UIGestureRecognizer * gesture in _gesturesToIgnore)
{
[self removeGestureRecognizer:gesture];
}
[self performSelector:#selector(rebindGestures) withObject:self afterDelay:0];
break;
}
}
return [super pointInside:point withEvent:event];
}
- (void) rebindGestures
{
for (UIGestureRecognizer * gesture in _gesturesToIgnore)
{
[self addGestureRecognizer:gesture];
}
}
#end

Progress spinner in status bar in Cocoa

I'd like to create a NSStatusItem that displays a progress spinner. My idea was to subclass NSProgressIndicator and use this as a NSView to pass to setView:.
// SpinnerView.h
//#import <Cocoa/Cocoa.h>
#interface SpinnerView : NSProgressIndicator {
NSStatusItem *_statusItem;
BOOL _isHighlighted;
}
- (id)initWithStatusItem:(NSStatusItem *)statusItem;
#end
// SpinnerView.m
#import "SpinnerView.h"
#implementation SpinnerView
- (id)initWithStatusItem:(NSStatusItem *)statusItem {
CGFloat thickness = [[NSStatusBar systemStatusBar] thickness];
NSRect frameRect = NSMakeRect(0.0, 0.0, thickness, thickness);
self = [super initWithFrame:frameRect];
[self setStyle:NSProgressIndicatorSpinningStyle];
[self setControlSize:NSSmallControlSize];
_statusItem = statusItem;
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
NSLog(#"drawRect");
[_statusItem drawStatusBarBackgroundInRect:dirtyRect withHighlight:_isHighlighted];
[super drawRect:dirtyRect];
}
#end
However, when I do this, the spinner is displayed but is surrounded by a white box:
Any idea why this happens, or how to fix it? If I comment out the entire drawRect: method the result is the same.
You need to just continually set the image for the status item.
Use a timer.

NSWindow set view

I have different sections to my application. I want to have buttons change the view. So, if I click the first button the windows's view will change to another view. Is there anyway to do this?
Use an NSTabView. You can make the tab view borderless so that no bezel or tabs are drawn (using [tabView setTabViewType:NSNoTabsNoBorder]) and then implement your own button actions to change the views programatically by switching the active tab using -selectTabViewItem: or -selectTabViewItemAtIndex:. This has the added benefit of being able to configure all of your views in Interface Builder.
Updated: Sample project that uses this code:
SwitchView.zip
You can use the following categories on NSWindow to implement switching from one view to another.
MDAppKitAdditions.h:
#interface NSWindow (MDAdditions)
- (CGFloat)toolbarHeight;
- (void)resizeToSize:(NSSize)newSize;
- (void)switchView:(NSView *)aView title:(NSString *)aString;
- (void)switchView:(NSView *)aView;
#end
MDAppKitAdditions.m:
static NSView *blankView() {
static NSView *view = nil;
if (view == nil) view = [[NSView alloc] init];
return view;
}
#implementation NSWindow (MDAdditions)
- (CGFloat)toolbarHeight {
NSToolbar *toolbar = self.toolbar;
CGFloat toolbarHeight = 0.0;
if (toolbar && toolbar.isVisible) {
NSRect windowFrame = [[self class] contentRectForFrameRect:self.frame
styleMask:self.styleMask];
toolbarHeight = NSHeight(windowFrame) - NSHeight([self.contentView frame]);
}
return toolbarHeight;
}
- (void)resizeToSize:(NSSize)newSize {
CGFloat newHeight = newSize.height + [self toolbarHeight];
CGFloat newWidth = newSize.width;
NSRect aFrame = [[self class] contentRectForFrameRect:self.frame
styleMask:self.styleMask];
aFrame.origin.y += aFrame.size.height;
aFrame.origin.y -= newHeight;
aFrame.size.height = newHeight;
aFrame.size.width = newWidth;
aFrame = [[self class] frameRectForContentRect:aFrame
styleMask:self.styleMask];
[self setFrame:aFrame display:YES animate:YES];
}
- (void)switchView:(NSView *)aView title:(NSString *)aTitle {
if (self.contentView != aView) {
[self setContentView:blankView()];
if (aTitle) [self setTitle:NSLocalizedString(aTitle, #"")];
[self resizeToSize:aView.frame.size];
[self setContentView:aView];
}
}
- (void)switchView:(NSView *)aView {
return [self switchView:aView title:nil];
}
#end
To use it, I assume you would have a controller class like the following, with IBOutlets for views and your main window:
#interface MDAppController : NSObject {
IBOutlet NSWindow *mainWindow;
IBOutlet NSView *firstView;
IBOutlet NSView *secondView;
}
- (IBAction)changeView:(id)sender;
#end
Then in your implementation, something like this:
#import "MDAppKitAdditions.h"
#implementation MDAppController
- (IBAction)changeView:(id)sender {
NSInteger tag = [sender tag];
NSView *targetView = nil;
if (tag == 0) {
targetView = firstView;
else if (tag == 1) {
targetView = secondView;
}
[mainWindow switchView:targetView title:#"New window title"];
}
#end
You could set it up so that the buttons you want to use to switch views each call the same changeView: method instead of defining separate methods for each one. In the nib file in Interface Builder, you can set the tag property of the buttons to differentiate between them. At runtime, when you click the button and it calls the changeView: method, the button is passed in as the generic sender parameter, so you then examine that to determine which view you should switch to.

How can I redraw an image in an custom NSView for an NSStatusItem?

I am having some troubles understanding how to wire a custom NSView for an NSMenuItem to support both animation and dragging and dropping. I have the following subclass of NSView handling the bulk of the job. It draws my icon when the application launches correctly, but I have been unable to correctly setup the subview to change when I invoke the setIcon function from another caller. Is there some element of the design that I am missing?
TrayIconView.m
#import "TrayIconView.h"
#implementation TrayIconView
#synthesize statusItem;
static NSImageView *_imageView;
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
statusItem = nil;
isMenuVisible = NO;
_imageView = [[NSImageView alloc] initWithFrame:[self bounds]];
[self addSubview:_imageView];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Draw status bar background, highlighted if menu is showing
[statusItem drawStatusBarBackgroundInRect:[self bounds]
withHighlight:isMenuVisible];
}
- (void)mouseDown:(NSEvent *)event {
[[self menu] setDelegate:self];
[statusItem popUpStatusItemMenu:[self menu]];
[self setNeedsDisplay:YES];
}
- (void)rightMouseDown:(NSEvent *)event {
// Treat right-click just like left-click
[self mouseDown:event];
}
- (void)menuWillOpen:(NSMenu *)menu {
isMenuVisible = YES;
[self setNeedsDisplay:YES];
}
- (void)menuDidClose:(NSMenu *)menu {
isMenuVisible = NO;
[menu setDelegate:nil];
[self setNeedsDisplay:YES];
}
- (void)setIcon:(NSImage *)icon {
[_imageView setImage:icon];
}
TrayIconView.h
#import <Cocoa/Cocoa.h>
#interface TrayIconView : NSView
{
BOOL isMenuVisible;
}
#property (retain, nonatomic) NSStatusItem *statusItem;
- (void)setIcon:(NSImage *)icon;
#end
The solution to this problem was actually outside of the view detailed here. The caller of the interface was being double instantiated on accident, thus nulling out the reference to the previously created NSView. After correcting that concern the app draws and works just fine.
With regard to dragging, I just implemented a subclass of NSView that implemented the Cocoa draggable protocol and added it as a subview to this parent class. That allows dragging onto the currently established NSRect that contains the menubar icon.