NSButton setAction selector - cocoa

I just want to add a NSButton with setAction Arguments.
NSRect frame = NSMakeRect(10, 40, 90, 40);
NSButton* pushButton = [[NSButton alloc] initWithFrame: frame];
[pushButton setTarget:self];
[pushButton setAction:#selector(myAction:)];
But I want to put an argument to the function myAction...
How ?
Thanks.

But I want to put an argument to the function myAction...
How ?
You can't.
… if there is more than one button that uses this method, we can not differentiate the sender (only with title)...
There are three ways to tell which button (or other control) is talking to you:
Assign each button (or other control) a tag, and compare the tags in your action method. When you create controls in a nib, this has the downside that you have to write the tag twice (once in the code, once in the nib). Since you're writing out the button by hand from scratch, you don't have that problem.
Have an outlet to every control that you expect to send you this message, and compare the sender to each outlet.
Have different action methods, with each control being the only one wired up to each action. Each action method then does not need to determine which control sent you that message, because you already know that by which method it is.
The problem with tags is the aforementioned repetitiveness. It's also very easy to neglect to name each tag, so you end up looking at code like if ([sender tag] == 42) and not knowing/having to look up which control is #42.
The problem with outlets is that your action method may get very long, and anyway is probably doing multiple different things that have no business being in the same method. (Which is also a problem with tags.)
So, I generally prefer the third solution. Create an action method for every button (or other control) that will have you as its target. You'll typically name the method and the button the same (like save: and “Save”) or something very similar (like terminate: and “Quit”), so you'll know just by reading each method which button it belongs to.

I never programatically created an NSButton, but I think that you just need to create a method like this:
- (void) myAction: (NSButton*)button{
//your code
}
And that's it !!

You can use associated Objects for passing arguments.
You can refer : http://labs.vectorform.com/2011/07/objective-c-associated-objects/
http://www.cocoanetics.com/2012/06/associated-objects/

.tag should be sufficient if your object have any integer uniqueID.
I use .identifier instead, since it support string based uniqueID.
Example:
...
for (index, app) in apps.enumerated() {
let appButton = NSButton(title: app.title, target: self, action: #selector(appButtonPressed))
appButton.identifier = NSUserInterfaceItemIdentifier(rawValue: app.guid)
}
...
#objc func appButtonPressed(sender: NSButton) {
print(sender.identifier?.rawValue)
}

Related

Can't change column width in response to outlineViewItemDidExpand

I want to update the column width to the width of the widest entry whenever a node in my NSOutlineView is expanded. Thus, my delegate listens to outlineViewItemDidExpand and calls
[column setWidth:maxColumnWidth];
in it. maxColumnWidth is the width of the widest entry.
However, for some reason, calling setWidth from inside outlineViewItemDidExpand doesn't seem to do anything. When I call it later, e.g. as a response to a button click, it works just fine, but from within outlineViewItemDidExpand it just does nothing.
So any idea what I could do to update the column width whenever the user clicks on an expander icon? Thanks a lot!
If autoresizesOutlineColumn is YES (default) then the outline view resizes the outline column after outlineViewItemDidExpand. Solution: set autoresizesOutlineColumn to NO.
I was able to solve this by calling setWidth() from a selector called by performSelector() from within outlineViewItemDidExpand, like so:
-(void) setColumnWidth
{
[theColumn setWidth:...];
}
-(void) outlineViewItemDidExpand:(NSNotification*)notification
{
// calling setWidth() here doesn't work
// [theColumn setWidth:...];
// but calling setWidth() from a selector works!
[self performSelector:#selector(setColumnWidth) withObject:nil afterDelay:0.0];
}
Don't ask me why calling setWidth() only works from within a selector but it actually does...

how to set text in second View Controller

I thought this would be a no brainer, but it turns out I have spent about 5 hours on this now.
I have two ViewControllers and I want to pass a pre formatted NSString to another VC, using a IBAction called putInfo. All this action is responsible for is putting a word into a label on another VC, .
so, in the first ViewController, I implemented the code like this :
- (IBAction)putInfo:(id)sender {
((secondViewController *)self.presentingViewController).ouputLabel.text = #"chicken";
}
I have tried other things like-grabbing a reference to the second VC, instantiating the second view controller, doing that thing where you initialize the second VC WithNibName--all that. ABove is just my latest failure.
This seems like it should be such a no brainer. any suggestions?
If you're using Storyboard, you'll need to use the method prepareForSegue.
But first you need to put a name to your segue: click on it, go to Attribute Inspector and write an Identifier name for the segue.
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([[segue identifier]isEqualToString:#"mySegue"])
{
secondViewController* second = segue.destinationViewController;
second.outputLabel = txtFieldSecondViewController;
}
}
You pass the value of a textField(or some string) to another NSString object of the second view.

Overriding "Edited" in window title for NSDocument

How do I prevent a window title from displaying "Edited" for an NSDocument which is dirty?
I'm managing saving and autosaving myself, using a web service, and just don't want the distraction in the title bar.
I've tried overriding:
NSDocument's -isDocumentEdited and -hasUnautosavedChanges always to return NO.
-[NSWindowController setDocumentEdited] to do nothing, or always to use NO regardless of the parameter's actual value.
-[NSWindowController synchronizeWindowTitleWithDocumentName] to do nothing.
-[NSWindow setDocumentEdited] to do nothing, or always to use NO regardless of the parameter's actual value.
In all cases, the title bar still changes to Edited when I make changes to a saved document.
If I override -[NSDocument updateChangeCount:] and -[NSDocument updateChangeCountWithToken:forSaveOperation:] to do nothing, I can prevent this from happening, but it affects saving, autosaving, and other document behaviors, too.
I also tried this:
[[self.window standardWindowButton: NSWindowDocumentVersionsButton] setTitle:nil];
That displayed a blank string instead of Edited, but the dash still appeared – the one which normally separates the document name and Edited.
Any idea how to pry apart this part of the window from the document?
Several options:
To get a pointer to the "dash", look for a TextField in [window.contentView.superview.subviews] with a stringValue equals to "-". You can set its text to an empty string as well.
#implementation NSWindow (DashRetrivalMethod)
- (NSTextField*)versionsDashTextField
{
NSTextField* res = nil;
NSView* themeFrame = [self.contentView superview];
for (NSView* tmp in [themeFrame subviews])
{
if ([tmp isKindOfClass:[NSTextField class]])
{
if ([[(NSTextField*)tmp stringValue] isEqualToString:#"—"])
{
res = (NSTextField*)tmp;
break;
}
}
}
return res;
}
#end
You can override NSWindow's -setRepresentedURL:. This would also affect the NSWindowDocumentIconButton and the popup menu, but you can manually create it if you want by: [NSWindow standardWindowButton: NSWindowDocumentIconButton].
Override one of these three NSDocument's undocumented methods:
// Always return here NO if you don't want the version button to appear.
// This seems to be the cleanest options, besides the fact that you are
/// overriding a private method.
- (BOOL)_shouldShowAutosaveButtonForWindow:(NSWindow*)window;
// Call super with NO
- (void)_setShowAutosaveButton:(BOOL)flag;
// Here the button and the dash are actually created
- (void)_endVersionsButtonUpdates;
// Here Cocoa hide or unhide the edited button
- (void)_updateDocumentEditedAndAnimate:(BOOL)flag
Have you tried overriding NSDocuments - (BOOL)hasUnautosavedChanges in addition to overriding - (BOOL) isDocumentEdited?
Although this is a late answer, you can easily determine what is going to be the title of your NSDocument window by overriding
- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName
in your NSWindowController and return the appropriate title.
You can do that also by overriding the property of your NSDocument:
- (NSString *)displayName
but this is not recommended by Apple, because that is normally used by the OS error handlers.
I added this answer, because none of the other answers really set me on the right path.

NSButton subclass that responds to right clicks

I have an NSButton subclass that I would like to make work with right mouse button clicks. Just overloading -rightMouseDown: won't cut it, as I would like the same kind of behaviour as for regular clicks (e.g. the button is pushed down, the user can cancel by leaving the button, the action is sent when the mouse is released, etc.).
What I have tried so far is overloading -rightMouse{Down,Up,Dragged}, changing the events to indicate the left mouse button clicks and then sending it to -mouse{Down,Up,Dragged}. Now this would clearly be a hack at best, and as it turns out Mac OS X did not like it all. I can click the button, but upon release, the button remains pushed in.
I could mimic the behaviour myself, which shouldn't be too complicated. However, I don't know how to make the button look pushed in.
Before you say "Don't! It's an unconventional Mac OS X behaviour and should be avoided": I have considered this and a right click could vastly improve the workflow. Basically the button cycles through 4 states, and I would like a right click to make it cycle in reverse. It's not an essential feature, but it would be nice. If you still feel like saying "Don't!", then let me know your thoughts. I appreciate it!
Thanks!
EDIT: This was my attempt of changing the event (you can't change the type, so I made a new one, copying all information across. I mean, I know this is the framework clearly telling me Don't Do This, but I gave it a go, as you do):
// I've contracted all three for brevity
- (void)rightMouse{Down,Up,Dragging}:(NSEvent *)theEvent {
NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouse{Down,Up,Dragging} location:[theEvent locationInWindow] modifierFlags:[theEvent modifierFlags] timestamp:[theEvent timestamp] windowNumber:[theEvent windowNumber] context:[theEvent context] eventNumber:[theEvent eventNumber] clickCount:[theEvent clickCount] pressure:[theEvent pressure]];
[self mouse{Down,Up,Dragging}:event];
}
UPDATE: I noticed that -mouseUp: was never sent to NSButton, and if I changed it to an NSControl, it was. I couldn't figure out why this was, until Francis McGrew pointed out that it contains its own event handling loop. Now, this also made sense to why before I could reroute the -rightMouseDown:, but the button wouldn't go up on release. This is because it was fetching new events on its own, that I couldn't intercept and convert from right to left mouse button events.
NSButton is entering a mouse tracking loop. To change this you will have to subclass NSButton and create your own custom tracking loop. Try this code:
- (void) rightMouseDown:(NSEvent *)theEvent
{
NSEvent *newEvent = theEvent;
BOOL mouseInBounds = NO;
while (YES)
{
mouseInBounds = NSPointInRect([newEvent locationInWindow], [self convertRect:[self frame] fromView:nil]);
[self highlight:mouseInBounds];
newEvent = [[self window] nextEventMatchingMask:NSRightMouseDraggedMask | NSRightMouseUpMask];
if (NSRightMouseUp == [newEvent type])
{
break;
}
}
if (mouseInBounds) [self performClick:nil];
}
This is how I do it; Hopefully it will work for you.
I've turned a left mouse click-and-hold into a fake right mouse down on a path control. I'm not sure this will solve all your problems, but I found that the key difference when I did this was changing the timestamp:
NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouseDown
location:[theEvent locationInWindow]
modifierFlags:[theEvent modifierFlags]
timestamp:CFAbsoluteGetTimeCurrent()
windowNumber:[theEvent windowNumber]
context:[theEvent context]
// I was surprised to find eventNumber didn't seem to need to be faked
eventNumber:[theEvent eventNumber]
clickCount:[theEvent clickCount]
pressure:[theEvent pressure]];
The other thing is that depending on your button type, its state may be the value that is making it appear pushed or not, so you might trying poking at that.
UPDATE: I think I've figured out why rightMouseUp: never gets called. Per the -[NSControl mouseDown:] docs, the button starts tracking the mouse when it gets a mouseDown event, and it doesn't stop tracking until it gets mouseUp. While it's tracking, it can't do anything else. I just tried, for example, at the end of a custom mouseDown::
[self performSelector:#selector(mouseUp:) withObject:myFakeMouseUpEvent afterDelay:1.0];
but this gets put off until a normal mouseUp: gets triggered some other way. So, if you've clicked the right mouse button, you can't (with the mouse) send a leftMouseUp, thus the button is still tracking, and won't accept a rightMouseUp event. I still don't know what the solution is, but I figured that would be useful information.
Not much to add to the answers above, but for those working in Swift, you may have trouble finding the constants for the event mask, buried deep in the documentation, and still more trouble finding a way to combine (OR) them in a way that the compiler accepts, so this may save you some time. Is there a neater way? This goes in your subclass -
var rightAction: Selector = nil
// add a new property, by analogy with action property
override func rightMouseDown(var theEvent: NSEvent!) {
var newEvent: NSEvent!
let maskUp = NSEventMask.RightMouseUpMask.rawValue
let maskDragged = NSEventMask.RightMouseDraggedMask.rawValue
let mask = Int( maskUp | maskDragged ) // cast from UInt
do {
newEvent = window!.nextEventMatchingMask(mask)
}
while newEvent.type == .RightMouseDragged
My loop has become a do..while, as it always has to execute at least once, and I never liked writing while true, and I don't need to do anything with the dragging events.
I had endless trouble getting meaningful results from convertRect(), perhaps because my controls were embedded in a table view. Thanks to Gustav Larsson above for my ending up with this for the last part -
let whereUp = newEvent.locationInWindow
let p = convertPoint(whereUp, fromView: nil)
let mouseInBounds = NSMouseInRect(p, bounds, flipped) // bounds, not frame
if mouseInBounds {
sendAction(rightAction, to: target)
// assuming rightAction and target have been allocated
}
}

Distinguishing a single click from a double click in Cocoa on the Mac

I have a custom NSView (it's one of many and they all live inside an NSCollectionView — I don't think that's relevant, but who knows). When I click the view, I want it to change its selection state (and redraw itself accordingly); when I double-click the view, I want it to pop up a larger preview window for the object that was just double-clicked.
My first looked like this:
- (void)mouseUp: (NSEvent *)theEvent {
if ([theEvent clickCount] == 1) [model setIsSelected: ![model isSelected]];
else if ([theEvent clickCount] == 2) if ([model hasBeenDownloaded]) [mainWindowController showPreviewWindowForPicture:model];
}
which mostly worked fine. Except, when I double-click the view, the selection state changes and the window pops up. This is not exactly what I want.
It seems like I have two options. I can either revert the selection state when responding to a double-click (undoing the errant single-click) or I can finagle some sort of NSTimer solution to build in a delay before responding to the single click. In other words, I can make sure that a second click is not forthcoming before changing the selection state.
This seemed more elegant, so it was the approach I took at first. The only real guidance I found from Google was on an unnamed site with a hyphen in its name. This approach mostly works with one big caveat.
The outstanding question is "How long should my NSTimer wait?". The unnamed site suggests using the Carbon function GetDblTime(). Aside from being unusable in 64-bit apps, the only documentation I can find for it says that it's returning clock-ticks. And I don't know how to convert those into seconds for NSTimer.
So what's the "correct" answer here? Fumble around with GetDblTime()? "Undo" the selection on a double-click? I can't figure out the Cocoa-idiomatic approach.
Delaying the changing of the selection state is (from what I've seen) the recommended way of doing this.
It's pretty simple to implement:
- (void)mouseUp:(NSEvent *)theEvent
{
if([theEvent clickCount] == 1) {
[model performSelector:#selector(toggleSelectedState) afterDelay:[NSEvent doubleClickInterval]];
}
else if([theEvent clickCount] == 2)
{
if([model hasBeenDownloaded])
{
[NSRunLoop cancelPreviousPerformRequestsWithTarget: model];
[mainWindowController showPreviewWindowForPicture:model];
}
}
}
(Notice that in 10.6, the double click interval is accessible as a class method on NSEvent)
If your single-click and double-click operations are really separate and unrelated, you need to use a timer on the first click and wait to see if a double-click is going to happen. That is true on any platform.
But that introduces an awkward delay in your single-click operation that users typically don't like. So you don't see that approach used very often.
A better approach is to have your single-click and double-click operations be related and complementary. For example, if you single-click an icon in Finder it is selected (immediately), and if you double-click an icon it is selected and opened (immediately). That is the behavior you should aim for.
In other words, the consequences of a single-click should be related to your double-click command. That way, you can deal with the effects of the single-click in your double-click handler without having to resort to using a timer.
Personally, I think you need to ask yourself why you want this non-standard behaviour.
Can you point to any other application which treats the first click in a double-click as being different from a single-click? I can't think of any...
Add two properties to your custom view.
// CustomView.h
#interface CustomView : NSView {
#protected
id m_target;
SEL m_doubleAction;
}
#property (readwrite) id target;
#property (readwrite) SEL doubleAction;
#end
Overwrite the mouseUp: method in your custom view.
// CustomView.m
#pragma mark - MouseEvents
- (void)mouseUp:(NSEvent*)event {
if (event.clickCount == 2) {
if (m_target && m_doubleAction && [m_target respondsToSelector:m_doubleAction]) {
[m_target performSelector:m_doubleAction];
}
}
}
Register your controller as the target with an doubleAction.
// CustomController.m
- (id)init {
self = [super init];
if (self) {
// Register self for double click events.
[(CustomView*)m_myView setTarget:self];
[(CustomView*)m_myView setDoubleAction:#selector(doubleClicked:)];
}
return self;
}
Implement what should be done when a double click happens.
// CustomController.m
- (void)doubleClicked:(id)sender {
// DO SOMETHING.
}
#Dave DeLong's solution in Swift 4.2 (Xcode 10, macOS 10.13), amended for use with event.location(in: view)
var singleClickPoint: CGPoint?
override func mouseDown(with event: NSEvent) {
singleClickPoint = event.location(in: self)
perform(#selector(GameScene.singleClickAction), with: nil, afterDelay: NSEvent.doubleClickInterval)
if event.clickCount == 2 {
RunLoop.cancelPreviousPerformRequests(withTarget: self)
singleClickPoint = nil
//do whatever you want on double-click
}
}
#objc func singleClickAction(){
guard let singleClickPoint = singleClickPoint else {return}
//do whatever you want on single-click
}
The reason I'm not using singleClickAction(at point: CGPoint) and calling it with: event.location(in: self) is that any point I pass in - including CGPoint.zero - ends up arriving in the singleClick Action as (0.0, 9.223372036854776e+18). I will be filing a radar for that, but for now, bypassing perform is the way to go. (Other objects seem to work just fine, but CGPoints do not.)

Resources