NSImageView double click action - xcode

I have some NSImageView in my Mac App where the user can drag'n drop objects like .png or .pdf, to store them into User Shared Defaults, that works fine.
I would now like to set an action for when user double click on these NSImageView, but it seems to be a little bit difficult (I had no trouble for NSTableView, but 'setDoubleAction' is not available for NSImage, and tons of answers (here or with google) concerning NSImageView's actions point to making a NSButton instead of NSImageView, so that doesn't help)
Here is part of my AppDelegate.h:
#interface AppDelegate : NSObject <NSApplicationDelegate>{
(...)
#property (assign) IBOutlet NSImageView *iconeStatus;
(...)
#end
and here is part of my AppDelegate.m:
#import "AppDelegate.h"
#implementation AppDelegate
(...)
#synthesize iconeStatus = _iconeStatus;
(...)
- (void)awakeFromNib {
(...)
[_iconeStatus setTarget:self];
[_iconeStatus setAction:#selector(doubleClick:)];
(...)
}
(...)
- (void)doubleClick:(id)object {
//make sound if that works ...
[[NSSound soundNamed:#"Basso"] play];
}
But that doesn't work.
Can anyone tell me what's the easiest way to do this ?

You need to subclass NSImageView and add the following method to your subclass's implementation:
- (void)mouseDown:(NSEvent *)theEvent
{
NSInteger clickCount = [theEvent clickCount];
if (clickCount > 1) {
// User at least double clicked in image view
}
}

Code for Swift 4. Again the NSImageView is subclassed and the mouseDown function is overridden.
class MyImageView: NSImageView {
override func mouseDown(with event: NSEvent) {
let clickCount: Int = event.clickCount
if clickCount > 1 {
// User at least double clicked in image view
}
}
}

Another solution using extension:
extension NSImageView {
override open func mouseDown(with event: NSEvent) {
// your code here
}
}
Although this will add that functionality to every NSImageView, so perhaps that's not what you're looking for.

Related

How do I validate an NSButton in an NSToolbar?

I have a document-based app with a tool bar containing several NSButton which I need to validate. Base on other code here, I have subclassed NSToolbar:
#interface CustomToolbar : NSToolbar
#end
#implementation CustomToolbar
-(void)validateVisibleItems
{
for (NSToolbarItem *toolbarItem in self.visibleItems)
{
NSResponder *responder = toolbarItem.view;
while ((responder = [responder nextResponder]))
{
if ([responder respondsToSelector:toolbarItem.action])
{
[responder performSelector:#selector(validateToolbarItem:) withObject:toolbarItem];
}
}
}
}
#end
MyDocument (the File's owner) is set as the delegate of the toolbar. However
-(BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem
is never called. The buttons have an action set on them, so not sure why [responder respondsToSelector:toolbarItem.action] is always false.
I have tried subclassing the NSButton items:
#interface DocumentToolbarActionItem : NSToolbarItem
#implementation DocumentToolbarActionItem
-(void)validate
{
Document* document = [[self toolbar] delegate];
[self setEnabled:[document validateUserInterfaceItem:self]];
}
#end
But this results in an endless loop.
The document's validateUserInterfaceItem: method works for all other items in the app and I need to have my toolbar button call it to determine if they should be enabled or not.
My guess is that you're not calling through [super validateVisibleItems] and, so, losing the superclass behaviour of validation through the responder chain.

Detecting when a button is touched in SKScene?

I created a storyboard and added a scene with two buttons. I cannot figure out how to know when a button is pressed on my GameScene.swift class.
How can this be done?
You can you touchesBegan for that.
Here is example code for you:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches{
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self.playButton{
//your code
}
}
}
You appear to be mixing UIKit and SpriteKit here. I would personally advise against using UIButtons in conjunction with Sprite Kit. Is there a specific reason for doing so?
There are two ways you can implement button behavior within a Sprite Kit scene:
have the SKScene object handle the touches
have the button itself handle the touches
Dharmesh's answer uses method (1), where he implements the -touchesBegan method.
In my current project, I am using an SKNode subclass as a button (2). I am unfamiliar with Swift syntax so I have posted Objective-C code from my project instead. The method calls are similar though and should help illustrate the point.
If you want an SKNode to receive touches, set userInteractionEnabled to YES. Otherwise, the closest ancestor with userInteractionEnabled = YES (which typically is the containing SKScene) will receive a -touchesBegan/-touchesMoved/-touchesEnded message.
#interface VTObject : SKNode
#end
...
#implementation VTObject
- (instancetype)init {
if (self = [super init]) {
self.userInteractionEnabled = YES;
}
return self;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"button touched!");
}
#end
You should add the UIButton programatically, instead of in IB, to the SKScene's SKView (in didMoveToView for example). You can then set the target for the button with button.addTarget:action:forControlEvents:. Just remember to call button.removeFromSuperview() in willMoveFromView otherwise you'll see the buttons in your next scene.

Double Click in NSCollectionView

I'm trying to get my program to recognize a double click with an NSCollectionView. I've tried following this guide: http://www.springenwerk.com/2009/12/double-click-and-nscollectionview.html but when I do it, nothing happens because the delegate in IconViewBox is null:
The h file:
#interface IconViewBox : NSBox
{
IBOutlet id delegate;
}
#end
The m file:
#implementation IconViewBox
-(void)mouseDown:(NSEvent *)theEvent {
[super mouseDown:theEvent];
// check for click count above one, which we assume means it's a double click
if([theEvent clickCount] > 1) {
NSLog(#"double click!");
if(delegate && [delegate respondsToSelector:#selector(doubleClick:)]) {
NSLog(#"Runs through here");
[delegate performSelector:#selector(doubleClick:) withObject:self];
}
}
}
The second NSLog never gets printed because delegate is null. I've connected everything in my nib files and followed the instructions. Does anyone know why or an alternate why to do this?
You can capture multiple-clicks within your collection view item by subclassing the collection item's view.
Subclass NSView and add a mouseDown: method to detect multiple-clicks
Change the NSCollectionItem's view in the nib from NSView to MyCollectionView
Implement collectionItemViewDoubleClick: in the associated NSWindowController
This works by having the NSView subclass detect the double-click and it pass up the responder chain. The first object in the responder chain to implement collectionItemViewDoubleClick: is called.
Typically, you should implement collectionItemViewDoubleClick: in the associated NSWindowController, but it can be in any object within the responder chain.
#interface MyCollectionView : NSView
/** Capture double-clicks and pass up responder chain */
-(void)mouseDown:(NSEvent *)theEvent;
#end
#implementation MyCollectionView
-(void)mouseDown:(NSEvent *)theEvent
{
[super mouseDown:theEvent];
if (theEvent.clickCount > 1)
{
[NSApplication.sharedApplication sendAction:#selector(collectionItemViewDoubleClick:) to:nil from:self];
}
}
#end
Another option is to override the NSCollectionViewItem and add an NSClickGestureRecognizer like such:
- (void)viewDidLoad
{
NSClickGestureRecognizer *doubleClickGesture =
[[NSClickGestureRecognizer alloc] initWithTarget:self
action:#selector(onDoubleClick:)];
[doubleClickGesture setNumberOfClicksRequired:2];
// this should be the default, but without setting it, single clicks were delayed until the double click timed-out
[doubleClickGesture setDelaysPrimaryMouseButtonEvents:FALSE];
[self.view addGestureRecognizer:doubleClickGesture];
}
- (void)onDoubleClick:(NSGestureRecognizer *)sender
{
// by sending the action to nil, it is passed through the first responder chain
// to the first object that implements collectionItemViewDoubleClick:
[NSApp sendAction:#selector(collectionItemViewDoubleClick:) to:nil from:self];
}
What you said notwithstanding, you need to be sure you followed step four in the tutorial:
4. Open IconViewPrototype.xib in IB and connect the View's delegate outlet with "File's Owner":
That should do ya, provided you did follow the rest of the steps.

NSTextView value changed

I'm pretty new to mac development (coming from a web and iOS background) and I can't work out how I could get a notification every time the value of an NSTextView changes. Any ideas?
Ups I just saw that you want a callback from NSTextView and not NSTextField
Just add in the header of the object which should be the delegate the protocol
#interface delegateAppDelegate : NSObject <NSApplicationDelegate, NSTextViewDelegate> {
NSWindow *window;
}
After that you add a method like
-(void)textDidChange:(NSNotification *)notification {
NSLog(#"Ok");
}
Make sure you connected the delegate property of the NSTextView (not NSScrollView) with the object which should receive the delegate
Here's the solution:
NSTextView *textView = ...;
#interface MyClass : NSObject<NSTextStorageDelegate>
#property NSTextView *textView;
#end
MyClass *myClass = [[MyClass alloc] init];
myClass.textView = textView;
textView.textStorage.delegate = myClass;
#implementation MyClass
- (void)textStorageDidProcessEditing:(NSNotification *)aNotification
{
// self.textView.string will be the current value of the NSTextView
// and this will get invoked whenever the textView's value changes,
// BOTH from user changes (like typing) or programmatic changes,
// like textView.string = #"Foo";
}
#end
set the nstextfield's delegate. in the .h file of the delegate you add the delegate protocol
In the .m file you add a method like -(void)controlTextDidChange:(NSNotification *)obj {
NSLog(#"ok");
}
I hope that helps
Set the delegate and then use
- (void) controlTextDidChange: (NSNotification *) notification
{
}

iOS - forward all touches through a view

I have a view overlayed on top of many other views. I am only using the overaly to detect some number of touches on the screen, but other than that I don't want the view to stop the behavior of other views underneath, which are scrollviews, etc. How can I forward all the touches through this overlay view? It is a subclass of UIView.
Disabling user interaction was all I needed!
Objective-C:
myWebView.userInteractionEnabled = NO;
Swift:
myWebView.isUserInteractionEnabled = false
For passing touches from an overlay view to the views underneath, implement the following method in the UIView:
Objective-C:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(#"Passing all touches to the next view (if any), in the view stack.");
return NO;
}
Swift 5:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("Passing all touches to the next view (if any), in the view stack.")
return false
}
This is an old thread, but it came up on a search, so I thought I'd add my 2c. I have a covering UIView with subviews, and only want to intercept the touches that hit one of the subviews, so I modified PixelCloudSt's answer to:
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView* subview in self.subviews ) {
if ( [subview hitTest:[self convertPoint:point toView:subview] withEvent:event] != nil ) {
return YES;
}
}
return NO;
}
Improved version of #fresidue answer. You can use this UIView subclass as transparent view passing touches outside its subview. Implementation in Objective-C:
#interface PassthroughView : UIView
#end
#implementation PassthroughView
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *view in self.subviews) {
if (!view.hidden && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
}
#end
.
and in Swift:
class PassthroughView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
}
TIP:
Say then you have a large "holder" panel, perhaps with a table view behind. You make the "holder" panel PassthroughView. It will now work, you can scroll the table "through" the "holder".
But!
On top of the "holder" panel you have some labels or icons. Don't forget, of course those must simply be marked user interaction enabled OFF!
On top of the "holder" panel you have some buttons. Don't forget, of course those must simply be marked user interaction enabled ON!
Note that somewhat confusingly, the "holder" itself - the view you use PassthroughView on - must be marked user interaction enabled ON! That's ON!! (Otherwise, the code in PassthroughView simply will never be called.)
I needed to pass touches through a UIStackView. A UIView inside was transparent, but the UIStackView consumed all touches. This worked for me:
class PassThrouStackView: UIStackView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self {
return nil
}
return view
}
}
All arrangedSubviews still receive touches, but touches on the UIStackView itself went through to the view below (for me a mapView).
I had a similar issue with a UIStackView (but could be any other view).
My configuration was the following:
It's a classical case where I have a container that needed to be placed in the background, with buttons on the side. For layout purposes, I included the buttons in a UIStackView, but now the middle (empty) part of the stackView intercepts touches :-(
What I did is create a subclass of UIStackView with a property defining the subView that should be touchable.
Now, any touch on the side buttons (included in the * viewsWithActiveTouch* array) will be given to the buttons, while any touch on the stackview anywhere else than these views won't be intercepted, and therefore passed to whatever is below the stack view.
/** Subclass of UIStackView that does not accept touches, except for specific subviews given in the viewsWithActiveTouch array */
class NoTouchStackView: UIStackView {
var viewsWithActiveTouch: [UIView]?
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if let activeViews = viewsWithActiveTouch {
for view in activeViews {
if CGRectContainsPoint(view.frame, point) {
return view
}
}
}
return nil
}
}
If the view you want to forward the touches to doesn't happen to be a subview / superview, you can set up a custom property in your UIView subclass like so:
#interface SomeViewSubclass : UIView {
id forwardableTouchee;
}
#property (retain) id forwardableTouchee;
Make sure to synthesize it in your .m:
#synthesize forwardableTouchee;
And then include the following in any of your UIResponder methods such as:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.forwardableTouchee touchesBegan:touches withEvent:event];
}
Wherever you instantiate your UIView, set the forwardableTouchee property to whatever view you'd like the events to be forwarded to:
SomeViewSubclass *view = [[[SomeViewSubclass alloc] initWithFrame:someRect] autorelease];
view.forwardableTouchee = someOtherView;
In Swift 5
class ThroughView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let slideView = subviews.first else {
return false
}
return slideView.hitTest(convert(point, to: slideView), with: event) != nil
}
}
Looks like even thou its quite a lot of answers here, there is no one clean in swift that I needed.
So I took answer from #fresidue here and converted it to swift as it's what now mostly developers want to use here.
It solved my problem where I have some transparent toolbar with button but I want toolbar to be invisible to user and touch events should go through.
isUserInteractionEnabled = false as some stated is not an option based on my testing.
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for subview in subviews {
if subview.hitTest(convert(point, to: subview), with: event) != nil {
return true
}
}
return false
}
I had couple of labels inside StackView and I didn't have much success with the solutions above, instead I solved my problem using below code:
let item = ResponsiveLabel()
// Configure label
stackView.addArrangedSubview(item)
Subclassing UIStackView:
class PassThrouStackView:UIStackView{
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.arrangedSubviews {
let convertedPoint = convert(point, to: subview)
let labelPoint = subview.point(inside: convertedPoint, with: event)
if (labelPoint){
return subview
}
}
return nil
}
}
Then you could do something like:
class ResponsiveLabel:UILabel{
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Respond to touch
}
}
Try something like this...
for (UIView *view in subviews)
[view touchesBegan:touches withEvent:event];
The code above, in your touchesBegan method for example would pass the touches to all of the subviews of view.
The situation I was trying to do was build a control panel using controls inside nested UIStackView’s. Some of the controls had UITextField’s others with UIButton’s. Also, there were labels to identify the controls. What I wanted to do was put a big “invisible” button behind the control panel so that if a user tapped on an area outside a button or text field, that I could then catch that and take action - primarily dismiss any keyboard if a text field was active (resignFirstResponder). However, tapping on a label or other blank area in the control panel would not pass things through. The above discussions were helpful in coming up with my answer below.
Basically, I sub-classed UIStackView and overwrote the “point(inside:with) routine to look for the type of controls that needed the touch and “ignore” things like labels that I wanted to ignore. It also checks for inside UIStackView’s so that things can recurse into the control panel structure.
The code is a perhaps a little more verbose than it should be. But it was helpful in debugging and hopefully provides more clarity in what the routine is doing. Just be sure in Interface Builder to change the class of the UIStackView's to PassThruStack.
class PassThruStack: UIStackView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for view in self.subviews {
if !view.isHidden {
let isStack = view is UIStackView
let isButton = view is UIButton
let isText = view is UITextField
if isStack || isButton || isText {
let pointInside = view.point(inside: self.convert(point, to: view), with: event)
if pointInside {
return true
}
}
}
}
return false
}
}
As suggested by #PixelCloudStv if you want to throw touched from one view to another but with some additional control over this process - subclass UIView
//header
#interface TouchView : UIView
#property (assign, nonatomic) CGRect activeRect;
#end
//implementation
#import "TouchView.h"
#implementation TouchView
#pragma mark - Ovverride
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL moveTouch = YES;
if (CGRectContainsPoint(self.activeRect, point)) {
moveTouch = NO;
}
return moveTouch;
}
#end
After in interfaceBuilder just set class of View to TouchView and set active rect with your rect. Also u can change and implement other logic.

Resources