NSColorPanel blocking mouse up events - macos

I am using a NSColorWell which is set to continuously update. I need to know when the user is done editing the control (mouse up) from the color picker in the color panel.
I installed an event monitor and am successfully receiving mouse down and mouse moved messages, however NSColorPanel appears to block mouse up.
The bottom line is that I want to add the final selected color to my undo stack without all the intermediate colors generated while the user is choosing their selection.
Is there a way of creating a custom NSColorPanel and replacing the shared panel with the thought of overriding its mouseUp and sending a message?
In my research this issue has been broached on a few occasions, however I have not read a successful resolution.
Regards,
- George Lawrence Storm, Keencoyote Invention Services

I discovered that if we observe color keypath of NSColorPanel we get called one extra time on mouse up events. This allows us to ignore action messages from NSColorWell when the left mouse button is down and to get the final color from keypath observer.
In this application delegate example code colorChanged: is a NSColorWell action method.
void* const ColorPanelColorContext = (void*)1001;
#interface AppDelegate()
#property (weak) NSColorWell *updatingColorWell;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSColorPanel *colorPanel = [NSColorPanel sharedColorPanel];
[colorPanel addObserver:self forKeyPath:#"color"
options:0 context:ColorPanelColorContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (context == ColorPanelColorContext) {
if (![self isLeftMouseButtonDown]) {
if (self.updatingColorWell) {
NSColorWell *colorWell = self.updatingColorWell;
[colorWell sendAction:[colorWell action] to:[colorWell target]];
}
self.updatingColorWell = nil;
}
}
}
- (IBAction)colorChanged:(id)sender {
if ([self isLeftMouseButtonDown]) {
self.updatingColorWell = sender;
} else {
NSColorWell *colorWell = sender;
[self updateFinalColor:[colorWell color]];
self.updatingColorWell = nil;
}
}
- (void)updateFinalColor:(NSColor*)color {
// Do something with the final color...
}
- (BOOL)isLeftMouseButtonDown {
return ([NSEvent pressedMouseButtons] & 1) == 1;
}
#end

In the Interface Builder, select your color well, and then uncheck the Continuous checkbox in the Attributes Inspector. Additionally, add the following line of code somewhere appropriate like in the applicationDidFinishLaunching: method or awakeFromNib method:
[[NSColorPanel sharedColorPanel] setContinuous:NO];
In other words, both the shared color panel and your color well need to have continuous set to NO in order for this to work properly.

The proper way to do what you want is to use NSUndoManager's -begin/endUndoGrouping. So you'd do something like this
[undoManager beginUndoGrouping];
// ... whatever code you need to show the color picker
// ...then when the color has been chosen
[undoManager endUndoGrouping];
The purpose of undo groups is exactly what you're trying to accomplish - to make all changes into a single undo.

Related

Prevent QLPreviewView from grabbing focus

I have a list of files. Next to it I have a QLPreviewView which shows the currently selected file.
Unfortunately QLPreviewView loads a web view to preview bookmark files. Some web pages can grab keyboard focus. E.g. the Gmail login form places the insertion point into the user name field.
This breaks the flow of my application. I want to navigate my list using arrow keys. This is disrupted when keyboard focus is taken away from the table view.
So far the best I could come up with is to override - [NSWindow makeFirstResponder:] and not call super for instances of classes named with a QL prefix. Yuck.
Is there a more reasonable way to
Prevent unwanted changes of first responder?
or prevent user interaction on QLPreviewView and its subviews?
I ended up using a NSWindow subclass that allows QLPreviewViews and its private subviews to become first responder on user interaction, but prevents these views from simply stealing focus.
- (BOOL)makeFirstResponder:(NSResponder *)aResponder
{
NSString *classname = NSStringFromClass([aResponder class]);
// This is a hack to prevent Quick Look from stealing first responder
if ([classname hasPrefix:#"QL"]) {
BOOL shouldMakeFirstRespnder = NO;
NSEvent *currentEvent = [[NSApplication sharedApplication] currentEvent] ;
NSEventType eventType = currentEvent.type;
if ((eventType == NSLeftMouseDown) || (eventType == NSRightMouseDown) || (eventType == NSMouseEntered)) {
if ([aResponder isKindOfClass:[NSView class]]) {
NSView *view = (NSView *)aResponder;
NSPoint locationInWindow = currentEvent.locationInWindow;
NSPoint locationInView = [view convertPoint:locationInWindow fromView:nil];
BOOL pointInRect = NSPointInRect(locationInView, [view bounds]);
shouldMakeFirstRespnder = pointInRect;
}
}
if (!shouldMakeFirstRespnder) {
return NO;
}
}
return [super makeFirstResponder:aResponder];
}
Maybe you can subclass QLPreviewView and override its becomeFirstResponder so that you can either enable or disable it when your application should allow it to accept focus.
Header
#interface MyQLPreviewView : QLPreviewView
#end
Implementation
#implementation
- (BOOL)becomeFirstResponder
{
return NO;
}
#end

cursorUpdate called, but cursor not updated

I have been working on this for hours, have no idea what went wrong. I want a custom cursor for a button which is a subview of NSTextView, I add a tracking area and send the cursorUpdate message when mouse entered button.
The cursorUpdate method is indeed called every time the mouse entered the tracking area. But the cursor stays the IBeamCursor.
Any ideas?
Reference of the Apple Docs: managing cursor-update event
- (void)cursorUpdate:(NSEvent *)event {
[[NSCursor arrowCursor] set];
}
- (void)myAddTrackingArea {
[self myRemoveTrackingArea];
NSTrackingAreaOptions trackingOptions = NSTrackingCursorUpdate | NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow;
_trackingArea = [[NSTrackingArea alloc] initWithRect: [self bounds] options: trackingOptions owner: self userInfo: nil];
[self addTrackingArea: _trackingArea];
}
- (void)myRemoveTrackingArea {
if (_trackingArea)
{
[self removeTrackingArea: _trackingArea];
_trackingArea = nil;
}
}
I ran into the same problem.
The issue is, that NSTextView updates its cursor every time it receives a mouseMoved: event. The event is triggered by a self updating NSTrackingArea of the NSTextView, which always tracks the visible part of the NSTextView inside the NSScrollView. So there are maybe 2 solutions I can think of.
Override updateTrackingAreas remove the tracking area that is provided by Cocoa and make sure you always create a new one instead that excludes the button. (I would not do this!)
Override mouseMoved: and make sure it doesn't call super when the cursor is over the button.
- (void)mouseMoved:(NSEvent *)theEvent {
NSPoint windowPt = [theEvent locationInWindow];
NSPoint superViewPt = [[self superview]
convertPoint: windowPt fromView: nil];
if ([self hitTest: superViewPt] == self) {
[super mouseMoved:theEvent];
}
}
I had the same issue but using a simple NSView subclass that was a child of the window's contentView and did not reside within an NScrollView.
The documentation for the cursorUpdate flag of NSTrackingArea makes it sound like you only need to handle the mouse entering the tracking area rect. However, I had to manually check the mouse location as the cursorUpdate(event:) method is called both when the mouse enters the tracking area's rect and when it leaves the tracking rect. So if the cursorUpdate(event:) implementation only sets the cursor without checking whether it lies within the tracking area rect, it is set both when it enters and leaves the rect.
The documentation for cursorUpdate(event:) states:
Override this method to set the cursor image. The default
implementation uses cursor rectangles, if cursor rectangles are
currently valid. If they are not, it calls super to send the message
up the responder chain.
If the responder implements this method, but decides not to handle a
particular event, it should invoke the superclass implementation of
this method.
override func cursorUpdate(with event: NSEvent) {
// Convert mouse location to the view coordinates
let mouseLocation = convert(event.locationInWindow, from: nil)
// Check if the mouse location lies within the rect being tracked
if trackingRect.contains(mouseLocation) {
// Set the custom cursor
NSCursor.openHand.set()
} else {
// Reset the cursor
super.cursorUpdate(with: event)
}
}
I just ran across this through a Google search, so I thought I'd post my solution.
Subclass the NSTextView/NSTextField.
Follow the steps in the docs to create an NSTrackingArea. Should look something like the following. Put this code in the subclass's init method (also add the updateTrackingAreas method):
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:(NSTrackingMouseMoved | NSTrackingActiveInKeyWindow) owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
self.trackingArea = trackingArea;
Now you need to add the mouseMoved: method to the subclass:
- (void)mouseMoved:(NSEvent *)theEvent {
NSPoint point = [self convertPoint:theEvent.locationInWindow fromView:nil];
if (NSPointInRect(point, self.popUpButton.frame)) {
[[NSCursor arrowCursor] set];
} else {
[[NSCursor IBeamCursor] set];
}
}
Note: the self.popUpButton is the button that is a subview of the NSTextView/NSTextField.
That's it! Not too hard it ends up--just had to used mouseMoved: instead of cursorUpdate:. Took me a few hours to figure this out, hopefully someone can use it.

Button Crashes App with (id) sender

I am attempting to make a basic game which requires a serious of buttons to control player movement. Keep in mind I am using cocos-2d. My goal is to have the buttons be holdable and move a sprite when held down. The code i am using now looks like this.
CCMenuItemHoldable.h
#interface CCMenuItemSpriteHoldable : CCMenuItemSprite {
bool buttonHeld;
}
#property (readonly, nonatomic) bool buttonHeld;
CCMenuItemHoldable.m
#implementation CCMenuItemSpriteHoldable
#synthesize buttonHeld;
-(void) selected
{
[super selected];
buttonHeld = true;
[self setOpacity:128];
}
-(void) unselected
{
[super unselected];
buttonHeld = false;
[self setOpacity:64];
}
#end
and for the set up of the buttons
rightBtn = [CCMenuItemSpriteHoldable itemFromNormalSprite:[CCSprite spriteWithFile:#"art/hud/right.png"] selectedSprite:[CCSprite spriteWithFile:#"art/hud/right.png"] target:self selector:#selector(rightButtonPressed)];
CCMenu *directionalMenu = [CCMenu menuWithItems:leftBtn, rightBtn, nil];
[directionalMenu alignItemsHorizontallyWithPadding:0];
[directionalMenu setPosition:ccp(110,48)];
[self addChild:directionalMenu];
This all seems to work fine but when i do
-(void)rightButtonPressed:(id) sender
{
if([sender buttonHeld])
targetX = 10;
else{
targetX = 0;
}
}
The crash has been fixed but I am trying to get my sprite to move. In my game tick function I add the value of targetX to the position of the sprite on a timer, still no movement.
Please, always include a crash log when asking questions about crashes.
In your case, I can guess the problem. You are adding this selector:
#selector(rightButtonPressed)
Your method is called
rightButtonPressed:(id)sender
Which would be, as a selector, rightButtonPressed: - note the colon indicating that an argument is passed. Either change the method so it has no argument, or add a colon to the selector when you create the button.
The crash log would be telling you this - it would say "unrecognised selector sent to..." with the name of the receiving class, and the name of the selector.

CALayer only painting on input events

I have a Mac app that's using IKImageBrowserView. I've subclassed IKImageBrowserView and I'm returning a custom cell type from newCellForRepresentedItem.
In my cell, I'm creating and returning a layer from layerForType:
// When asked for a foreground layer, return a new layer that we'll render the icon decorations into
- (CALayer *)layerForType:(NSString *)type {
if ([type isEqualToString:IKImageBrowserCellForegroundLayer]) {
#synchronized(self) {
if (!self.foregroundLayer) {
self.foregroundLayer = [[CALayer alloc] init];
self.foregroundLayer.delegate = self;
self.foregroundLayer.needsDisplayOnBoundsChange = YES;
[self.foregroundLayer setNeedsDisplay];
}
}
return self.foregroundLayer;
} else {
return [super layerForType:type];
}
}
I have my cell observing an object, and calling setNeedsDisplay on my custom layer when it changes.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.percentDone = (float)self.bytesSoFar/self.bytesTotal;
NSLog(#"Update");
[self.foregroundLayer setNeedsDisplay];
}];
}
Here's the problem I'm having: The download proceeds, the object being observed fires the observer (so observeValueForKeyPath is called) and setNeedsDisplay is called. I verified this by logging with NSLog messages.
But the drawing method:
- (void)drawLayer:(CALayer *)theLayer
inContext:(CGContextRef)theContext
{
NSLog(#"Drawing");
// Drawing happens here
}
What I'm seeing is that the drawing starts out okay - printing "Update" and "Drawing" interleaved - but after a short time, the "Drawing" stops and just the "Update" messages continue.
If I click in the image browser, or tap a key on the keyboard, the "Drawing" resumes for a short while, and then stops, back to just "Update".
It's like I need to trigger a repaint using the keyboard or mouse - the setNeedsDisplay isn't doing it - but I don't understand why. It does work for a short time, stops working, then only works while I'm providing mouse input.
This has me baffled. I'd appreciate any suggestions.
I didn't find the problem here, but I did find a solution.
Invalidating IKImageBrowserView cells doesn't cause them to be repainted because IKImageBrowserView, probably for performance, doesn't redraw cells unless their version changes.
Instead of invalidating the cell, I now increment the imageVersion returned by my IKImageBrowserItem, and then invalidate the IKImageBrowserView. This reliably redraws the item.

NSButtonCell subclass' button type not working

I subclassed the NSButtonCell class to give a different look to my button. I have the drawing code working fine. It's a rounded button filled with CGGradients to look like the iTunes playback controls. They are not completely the same, but they resemble enough to me.
Now my problem is the button type. I set the button type in the -[PlayerButtonCell initImageCell:] but I'm not getting it working to only draw the pushed in look when the button is pressed. I present you a piece of my code:
//
// PlayerButtonCell.m
// DownTube
//
#import "PlayerButtonCell.h"
#implementation PlayerButtonCell
/* MARK: Init */
- (id)initImageCell:(NSImage *)image {
self = [super initImageCell:image];
if(self != nil) {
[self setImage:image];
[self setButtonType:NSMomentaryPushInButton];
}
return self;
}
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSImage *myImage;
NSBezierPath *myPath;
NSRect myFrame;
NSInteger myState;
myFrame = NSInsetRect(cellFrame , STROKE_WIDTH / 2.0 , STROKE_WIDTH / 2.0);
myImage = [self image];
myState = [self state];
NSLog(#"%d", [self buttonType]);
/* Create bezier path */
{
myPath = [NSBezierPath bezierPathWithOvalInRect:myFrame];
}
/* Fill with background color */
{
/* Fill code here */
}
/* Draw gradient if on */
if(myState == NSOffState) {
/* Code to draw the button when it's not pushed in */
} else {
/* Code to draw the button when it IS pressed */
}
/* Stroke */
{
/* Code to stroke the bezier path's edge. */
}
}
#end
As you can see, I check the pushed in status by the [NSButtonCell state] method. But the button cell acts like a check box, as in NSSwitchButton. This, I do not want. I want it to momentary light.
Hope someone can help me,
ief2
EDIT:
I create a button with this code of it is of any use:
- (NSButton *)makeRefreshButton {
NSButton *myButton;
NSRect myFrame;
NSImage *myIcon;
PlayerButtonCell *myCell;
myIcon = [NSImage imageNamed:kPlayerViewRefreshIconName];
myCell = [[PlayerButtonCell alloc] initImageCell:myIcon];
myFrame.origin = NSMakePoint(CONTENT_PADDING , CONTENT_PADDING);
myFrame.size = CONTROL_PLAYBACK_SIZE;
myButton = [[NSButton alloc] initWithFrame:myFrame];
[myButton setCell:myCell];
[myButton setButtonType:NSMomentaryPushInButton];
[myCell release];
return [myButton autorelease];
}
You're looking at the wrong property. -state is whether a button is checked or not. You want to look at isHighlighted, which tells you whether your button is pressed or not.
Look at a checkbox:
When you click and hold on it, the checkbox is still unchecked, but darkens slightly (702). Then when you release the mouse, it checks (704). Then when you click and hold again, the checked checkbox darkens, but is still checked (705), and only when you release inside, it actually unchecks (701).
So there are two different aspects, highlight and state, which combine to provide one of 4 appearances. Every NSButtonCell advances its state to whatever -nextState returns whenever it is clicked. But some button types show the state, while others don't. Look at any pushbutton (e.g. the "OK" button in a dialog window) and you'll find that behind the scenes its state is changed between on and off, even though it always draws the same way.
It is your drawing code that looks at the -showsStateBy property and only shows the state if that indicates it should.
have you tried [self setButtonType:NSMomentaryLightButton]; in the init?

Resources