How do I make a button in a custom NSView change color on mouseover? - cocoa

This is for a Mac app written with Cocoa and Objective-C.
I have a custom NSView class that essentially works as a collection of buttons and stores the value of the selected button. Sort of like an NSSlider that snaps to the tick marks but with buttons instead of a slider. The image below on the left is what it looks like.
Now what I want to do is make it so that when the mouse moves over each button, it covers that button with a semi-transparent blue color that then stays there when it is clicked. I've made a few attempts and you can see the latest result in the image on the right:
This is what happens after mousing over all the buttons. For some reason it draws using the window's origin instead of drawing inside the MyButtonView. Also, it is not semi-transparent. I haven't yet worried about redrawing the normal button when the mouse leaves the rectangle since this part isn't working yet anyway.
Now here's the pertinent code.
Inside the initWithFrame method of the MyButtonView class:
for (int i = 0; i < 12; i++) {
yOrigin = kBorderSize + (buttonHeight * i) + (kSeparatorSize * i);
NSRect newRect = { {xOrigin, yOrigin}, {buttonWidth, buttonHeight} };
[buttonRectangles addObject:NSStringFromRect(newRect)];
[self addTrackingRect:newRect owner:self userData:NULL assumeInside:NO];
}
The methods that draw the blue rectangles:
- (void)mouseEntered:(NSEvent *)theEvent {
NSRect rect = [[theEvent trackingArea] rect];
[self drawHoverRect:rect withColor:hoverBlue];
}
- (void)drawHoverRect:(NSRect)rect withColor:(NSColor *)color {
[color set];
NSRectFill(rect);
[self displayRect:rect];
}
I have no idea how to do this. I've been poring over Apple's documentation for a few hours and can't figure it out. Obviously though, I'm no veteran to Cocoa or Objective-C so I would love some help.

One fundamental problem you have is that you are bypassing the normal drawing mechanisms and trying to force the drawing yourself. This is a common mistake for first timers. Before you go any further, you should read Apple's View Programming Guide:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaViewsGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40002978
If you have trouble with that, then you might need to back up and start with some of the more fundamental Objective-C/Cocoa guides and documentation.
Back to your actual view, one thing that you are going to have to do in this view is do all your drawing in the drawRect: method. You should be tracking the state of your mouse movements via some kind of data structure and then drawing according to that data structure in your drawRect: method. You will call
[self setNeedsDisplay:YES];
in your mouse tracking method(s), after you've recorded whatever change has occurred in your data structure. If you only want to ever draw one button highlighted at a time, then your data structure could be as simple as an NSInteger whose value you set to the index of your selected button (or -1 or whatever to indicate no selection).
For the sake of learning, the reason your blue boxes are currently drawing from the window's origin is that you are calling drawing code outside of the "context" that's normally setup for your view when drawRect: is called by the system. That "context" would include a translation to move the current origin to the origin of your view, rather than the origin of the window.

Related

Problems with Background: NSButton on NSView

I have a window with a custom view in it. This view get's an image drawn as a background in it. Let's assume for now that it's a structured grey background image.
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
[self.titlebarBGImage drawInRect:dirtyRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
}
In this custom view I added a square textured button and configured it in IB to have no border. In my AppDelegate I then give this button an image in the applicationDidFinishLaunching method:
// ...
// [[self.minimizeButton cell] setBackgroundColor:[NSColor clearColor]];
[self.minimizeButton setImage:minimizeImage];
// ...
Somehow the same background (titlebarBGImage) is also drawn as the buttons background, so it has a structured grey colored image, but scaled to fit the button size. This looks silly and is not what I want obviously. If I then uncomment the line with setBackgroundColor: the background is cleared, but the custom views background also doesn't shine through where the buttons image is transparent (the button image is a circle) so I get a rectangle where the windows background shines through (i.e. green in the example image), not the background of the view the button is in.
Here's an image to make it clear what happens:
From top to bottom:
What I want to achieve, i.e. views grey structured bg shines through buttons transparent area
What I get with upper code, i.e. without the uncommented line: The custom views background is drawn squeezed into the buttons background
what I get with the clearColor line, i.e. window color (green) shines through, not the bg of the view.
How can I get the top most result?
Edit: I pushed an example project to github so that you can see what I mean.
One way to solve this is using Core Animation Layers. I found out watching this tutorial. For that you got to your MainMenu.xib file, select the Button object and click on the layers tab in the Utilities (see screenshot (1)). Then activate Core Animation Layer for the top most View. With that all subelements also have Core Animation Layer activated (There might be performance issues with that if the project is bigger, so take care).
Next I subclassed the NSButton (calling it BPButtonView in my Github project which I updated now). Select the Button in the xib-file and change the Custom Class to BPButtonView, or whatever you called it.
The code for the new NSButton subclass looks like this:
#implementation BPButtonView
- (BOOL)wantsUpdateLayer {
return YES;
}
- (void)updateLayer {
if ( [self.cell isHighlighted] ) {
self.layer.contents = [NSImage imageNamed:#"buttonbg.png"];
} else {
self.layer.contents = [NSImage imageNamed:#"buttonbg.png"];
}
}
#end
I used the if-statement in the updateLayer method just to show, that depending of the buttons state (pressed vs. unpressed) you can provide a different picture. In the Video tutorial there's also a nice trick how to achieve a clean resize of a buttons picture.
And here's a screenshot of the resulting app. The red circle in the middle is the button and the green textured thing is the views background.

Custom-drawn NSView Redraws Whole View Inside Edited Subviews

I have an NSView that is drawing a custom background with an image, but whenever I press a button or programmatically edit a label, it seems to draw the background image again inside of the edited subview. I've found that making the view layer-backed in IB solves the issue, but in the larger app I'm creating, making the view layer-backed causes a ton of other issues.
I made this example app to show as clearly as possible what is happening. The second image happens after pressing the button, which programmatically edits the label text. It seems as though the background image is drawing around both the button and label together, starting at the top of the button.
the view is being drawn like this:
- (void) drawRect:(NSRect)dirtyRect {
[[NSImage imageNamed:#"redGreenGradientBG.png"] drawInRect:dirtyRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
}
Before edit:
After edit:
Is there any way I can fix this without making the view layer-backed?
(Sorry about the gross gradient -- I thought it would illustrate my point clearest)
The dirtyRect parameter that's being passed in to -drawRect: is not the entire bounding rectangle of your view, but rather the rectangle that has been marked as needing an update (ie. the "dirty" rectangle, as the name suggests).
When you press the button or edit the label, its invalidating the display state for only that subview's bounding rectangle and therefore only that rectangle is being passed as dirtyRect. So what you're seeing in those screenshots is the image being drawn into a smaller rectangle inside your view's bounding rectangle.
In your case you should just redraw the entire background in -drawRect: like this (by using self.bounds as the drawing rectangle rather than dirtyRect):
- (void)drawRect:(NSRect)dirtyRect {
[[NSImage imageNamed:#"redGreenGradientBG.png"] drawInRect:self.bounds fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
}

NSButtons leave artefacts when used as subviews of a custom toolbar view

I'm placing a few buttons in a simple rectangular NSview which acts as a custom toolbar. On first render the buttons/views come out as expected, but every time a button is pressed (and sometimes with no mouse interaction at all) artefacts start appearing.
Before
After
I can eliminate the artefacts by calling a [self.toolbarView setNeedsDisplay:YES] in all the action and focus methods but this seems like a hack, is there any clean way to deal with this?
It was a beginner's problem. In the drawRect method
- (void)drawRect:(NSRect)dirtyRect
I was using the param dirtyRect for drawing an outline of my view, assuming it was the view's bounds, where in fact it was only the area around the buttons that became dirty when they were pressed. The 'artefacts' were actually my outline being drawn in the wrong place.
By correctly using the bounds of the view
NSRect drawingRect = [self bounds];
the 'artefacts' no longer appeared.
You just try to set focus ring for a buttons to 'none' in IB.

Are layer-hosting NSViews allowed to have subviews?

Layer-hosting NSViews (so NSViews that you supply a CALayer instance for and set it with setLayer:) can obviously contain subviews. Why obviously? Because in Apple's own Cocoa Slides sample code project, you can check a checkbox that switches the AssetCollectionView from being layer-backed to being layer-hosting:
- (void)setUsesQuartzCompositionBackground:(BOOL)flag {
if (usesQuartzCompositionBackground != flag) {
usesQuartzCompositionBackground = flag;
/* We can display a Quartz Composition in a layer-backed view tree by
substituting our own QCCompositionLayer in place of the default automanaged
layer that AppKit would otherwise create for the view. Eventually, hosting of
QCViews in a layer-backed view subtree may be made more automatic, rendering
this unnecessary. To minimize visual glitches during the transition,
temporarily suspend window updates during the switch, and toggle layer-backed
view rendering temporarily off and back on again while we prepare and set the
layer.
*/
[[self window] disableScreenUpdatesUntilFlush];
[self setWantsLayer:NO];
if (usesQuartzCompositionBackground) {
QCCompositionLayer *qcLayer = [QCCompositionLayer compositionLayerWithFile:[[NSBundle mainBundle] pathForResource:#"Cells" ofType:#"qtz"]];
[self setLayer:qcLayer];
} else {
[self setLayer:nil]; // Discard the QCCompositionLayer we were using, and let AppKit automatically create self's backing layer instead.
}
[self setWantsLayer:YES];
}
}
In the same AssetCollectionView class, subviews are added for each image that should be displayed:
- (AssetCollectionViewNode *)insertNodeForAssetAtIndex:(NSUInteger)index {
Asset *asset = [[[self assetCollection] assets] objectAtIndex:index];
AssetCollectionViewNode *node = [[AssetCollectionViewNode alloc] init];
[node setAsset:asset];
[[self animator] addSubview:[node rootView]];
[nodes addObject:node];
return [node autorelease];
}
When I build and run the app and play around with it, everything seems to be fine.
However, in Apple's NSView Class Reference for the setWantsLayer: method it reads:
When using a layer-hosting view you should not rely on the view for
drawing, nor should you add subviews to the layer-hosting view.
What is true? Is the sample code incorrect and it's just a coincidence that it works? Or is the documentation false (which I doubt)? Or is it OK because the subviews are added through the animator proxy?
When AppKit is "layer hosting" we assume you may (or may not) have a whole subtree of layers that AppKit doesn't know about.
If you add a subview to the layer hosted view, then it might not come out in the right sibling order that you want. Plus, we sometimes add and remove them, so it might change depending on when you call setLayer:, setWantsLayer: or when the view is added or removed from the superview. On Lion (and before) we remove the layers that we "own" (ie: layer backed) when the view is removed from the window (or superview).
It is okay to add subviews...their children-sibling-order in the sublayers array just might not be deterministic if you have sibling-layers that aren't NSViews.
I don't know what's the "right" answer to this. But I do think that the CocoaSlides example works within the boundaries of what the docs say you "shouldn't" do. In the example, look at where the insertNodeForAssetAtIndex: method is called, and you'll see that it only happens when the view is being populated, before it ever is assigned a layer or has setWantsLayer: called on it.
The docs don't say that a layer-hosted view can't contain any subviews, they just say that you can't add and subviews to one. At the point in time when those subviews are added, the main view hasn't yet become a layer-hosting view. After it has been turned into a layer-hosting view by having a manually created layer assigned to it, no more subviews are added.
So there's really no contradiction between the docs and this particular example. That being said, it could be interesting to explore this further, maybe by switching on the QC background layer right from the start, e.g. by sticking a [self setUsesQuartzCompositionBackground:YES]; right inside initWithFrame:.
SPOLIER ALERT:
It seems to work just fine. The creation of the display is a bit slower (not surprising with all that QC animation going on), but apart from that it's smooth sailing.
One comment about this code from Apple: it's busted.
When you first start the app up, note the nice gradient background. Turn QC on, then off.
Poof, no more gradient background.

How to make a transparent NSView subclass handle mouse events?

The problem
I have a transparent NSView on a transparent NSWindow. The view's drawRect: method draws some content (NSImages, NSBezierPaths and NSStrings) on the view but leaves parts of it transparent.
Clicking on the regions of the view that have been drawn on invokes the usual mouse event handling methods (mouseDown: and mouseUp:).
Clicking on the transparent areas gives focus to whatever window is behind my transparent window.
I would like to make parts of the transparent region clickable so that accidentally clicking between the elements drawn on my view does not cause the window to lose focus.
Solutions already attempted
Overriding the NSView's hitTest: method. Found that hitTest: was only called when clicking on a non-transparent area of the view.
Overriding the NSView's opaqueAncestor method. Found that this was not called when clicking on any part of the view.
Filling portions of the transparent area with [NSColor clearColor] in the drawRect: method, and with an almost-but-not-quite-transparent colour. This had no effect.
Experimented with the NSTrackingArea class. This appears to only add support for mouseEntered:, mouseExited:, mouseMoved:, and cursorUpdate: methods, not mouseUp: and mouseDown:.
I had the same problem. It looks like [window setIgnoresMouseEvents:NO] will do it.
(On Lion, at least. See http://www.cocoabuilder.com/archive/cocoa/306910-lion-breaks-the-ability-to-click-through-transparent-window-areas-when-the-window-is-resizable.html)
As far as I know, click events to transparent portions of windows aren't delivered to your application at all, so none of the normal event-chain overrides (i.e -hitTest:, -sendEvent:, etc) will work. The only way I can think of off the top of my head is to use Quartz Event Taps to capture all mouse clicks and then figure out if they're over a transparent area of your window manually. That, frankly, sounds like a huge PITA for not much gain.
George : you mentioned that you tried filling portions with an almost but not quite transparent color. In my testing, it only seems to work if the alpha value is above 0.05, so you might have some luck with something like this:
[[NSColor colorWithCalibratedRed:0.01 green:0.01 blue:0.01 alpha:0.05] set];
It's an ugly kludge, but it might work well enough to avoid using an event tap.
Did you try overriding
- (NSView *)hitTest:(NSPoint)aPoint
in your NSView sublcass?
You can use an event monitor to catch events outside a window/view.
https://developer.apple.com/library/mac/documentation/cocoa/conceptual/eventoverview/MonitoringEvents/MonitoringEvents.html
You can override the hitTest method in your NSView so that it always returns itself.
According to the NSView documentation, the hitTest method will either return the NSView that the user has clicked on, or nil if the point is not inside the NSView. In this case, I would invoke [super hitTest:], and then return the current view only if the result would otherwise be nil (just in case your custom view contains subviews).
- (NSView *)hitTest:(NSPoint)aPoint
{
NSView * clickedView = [super hitTest:aPoint];
if (clickedView == nil)
{
clickedView = self;
}
return clickedView;
}
You can do:
NSView* windowContent = [window contentView];
[windowContent setWantsLayer:YES]
Making sure that the background is transparent:
[[windowContent layer] setBackgroundColor:[[NSColor clearColor] CGColor]];
Another option would be to add a transparent background image that fills the contentView.

Resources