Skip to content

Instantly share code, notes, and snippets.

@millenomi
Created August 2, 2011 17:48
Show Gist options
  • Save millenomi/1120762 to your computer and use it in GitHub Desktop.
Save millenomi/1120762 to your computer and use it in GitHub Desktop.
//
// ILDeleteGesture.h
// Gestures
//
// Created by ∞ on 02/08/11.
// Copyright 2011 Emanuele Vulcano. All rights reserved.
//
// Use this under the provisions of the MIT license: <http://www.opensource.org/licenses/mit-license.php>
#import <Cocoa/Cocoa.h>
@protocol ILDeleteGestureDelegate;
// A deletion gesture is performed by swiping left-to-right on an onscreen item (a view or part of it) with two fingers, simulating the same gesture as performed on iOS table views.
@interface ILDeleteGesture : NSResponder
- (id) init; // designated. you have to feed scroll events using -processEvent: if you use this initializer.
- (id) initWithView:(NSView*) view; // adds itself to the responder chain between that view and its next responder. This automatically provides the gesture with events and is the recommended way to use it.
@property(assign, nonatomic) id <ILDeleteGestureDelegate> delegate;
- (BOOL) shouldProcessEvent:(NSEvent*) event;
- (BOOL) processEvent:(NSEvent*) event; // YES if handled here, NO if not. You should handle events, or pass to next responder, only if NO is returned.
@end
// ------------
typedef enum {
// The gesture occurred partially, but was not recognized as a valid user action. It is recommended that the app do nothing.
kILDeleteGestureResultNone = 0,
// The gesture occurred, but it might or might not have been an error (because it was very fast, for instance). It is recommended that the app shows a deletion confirmation UI (for example, a delete button on the row, just like in iOS), but otherwise do nothing.
kILDeleteGestureResultConfirmDeletion,
// The gesture occurred and it was clear that the user performed the gesture willfully. It is recommended that you perform the deletion operation immediately.
kILDeleteGestureResultDelete,
} ILDeleteGestureResult;
@protocol ILDeleteGestureDelegate <NSObject>
// This tells the gesture how many pixels the item to delete onscreen is long. The gesture will not succeed unless the gesture is performed for a reasonable portion of this amount.
- (CGFloat) maximumAmountOfPixelsForPerformingDeleteGesture:(ILDeleteGesture*) gesture; // typically the view's width.
// The gesture has started recognizing.
- (void) deleteGestureDidStart:(ILDeleteGesture *)gesture;
// The gesture stopped recognizing a movement because it wasn't a deletion gesture.
// If the gesture stops recognizing, you will not get a deleteGesture:didEndWithResult: call.
- (void) deleteGestureDidCancel:(ILDeleteGesture *)gesture;
// The deletion gesture progressed. You should show feedback to the user that the gesture is being performed.
// Since unlike on iOS there is no finger, and there is no pointer movement during a deletion gesture, it's recommended that this feedback shows some kind of indicator on the item to be deleted, such as a strike-through, that fills the item left-to-right proportionally to the progress of the gesture.
// Note that the progress of the gesture does not increase monotonically; the user may cancel the gesture by "taking back" the gesture (swiping back to the left). As such, the progress may go up or down in successive calls.
// The provisional result is the result the gesture would have were it to end right now. You may use it to provide some kind of UI feedback to the user. You may get multiple calls to this method for the same progress if the result has changed in the meanwhile.
- (void) deleteGesture:(ILDeleteGesture*) gesture didProgress:(CGFloat) progress provisionalResult:(ILDeleteGestureResult) result;
// The gesture was performed and ended. You should perform an action that's appropriate for the passed-in result.
- (void) deleteGesture:(ILDeleteGesture *)gesture didEndWithResult:(ILDeleteGestureResult) result;
@end
//
// ILDeleteGesture.m
// Gestures
//
// Created by ∞ on 02/08/11.
// Copyright 2011 Emanuele Vulcano. All rights reserved.
//
// Use this under the provisions of the MIT license: <http://www.opensource.org/licenses/mit-license.php>
#import "ILDeleteGesture.h"
typedef enum {
kILDeleteGestureStateIdle = 0,
kILDeleteGestureStateRecognizing,
kILDeleteGestureStateWaitingForIdle,
} ILDeleteGestureState;
#define kILDeleteGestureTooQuickDurationThreshold 0.1
static ILDeleteGestureResult ILDeleteGestureResultForValues(CGFloat pixelsTravelled, CGFloat maxPixelsToTravel, CFTimeInterval absoluteTimeOfLastPositiveIncrementStart) {
if (pixelsTravelled >= 0.9 * maxPixelsToTravel) {
if (CFAbsoluteTimeGetCurrent() - absoluteTimeOfLastPositiveIncrementStart <= kILDeleteGestureTooQuickDurationThreshold)
return kILDeleteGestureResultConfirmDeletion;
else
return kILDeleteGestureResultDelete;
}
return kILDeleteGestureResultNone;
}
@implementation ILDeleteGesture {
ILDeleteGestureState state;
CGFloat maxPixelsToTravel, pixelsTravelled, verticalMovement;
CFTimeInterval absoluteTimeOfLastPositiveIncrementStart;
}
- (id)init;
{
return [super init];
}
- (id) initWithView:(NSView*) view;
{
if ((self = [super init])) {
[self setNextResponder:[view nextResponder]];
[view setNextResponder:self];
}
return self;
}
- (void)scrollWheel:(NSEvent *)theEvent;
{
if (![self processEvent:theEvent])
[[self nextResponder] scrollWheel:theEvent];
}
@synthesize delegate;
- (BOOL) shouldProcessEvent:(NSEvent*) event;
{
return ([event type] == NSScrollWheel) && (
(state != kILDeleteGestureStateWaitingForIdle) ||
([event phase] == NSEventPhaseEnded || [event phase] == NSEventPhaseCancelled)
);
}
- (BOOL) processEvent:(NSEvent*) event;
{
if (![self shouldProcessEvent:event])
return NO;
switch (state) {
case kILDeleteGestureStateIdle:
if ([event phase] == NSEventPhaseBegan) {
maxPixelsToTravel = [self.delegate maximumAmountOfPixelsForPerformingDeleteGesture:self];
if (maxPixelsToTravel == 0)
return NO;
pixelsTravelled = 0;
verticalMovement = 0;
absoluteTimeOfLastPositiveIncrementStart = CFAbsoluteTimeGetCurrent();
state = kILDeleteGestureStateRecognizing;
[self.delegate deleteGestureDidStart:self];
return YES;
}
break;
case kILDeleteGestureStateRecognizing:
if ([event phase] == NSEventPhaseBegan ||
[event phase] == NSEventPhaseChanged) {
verticalMovement += [event scrollingDeltaY];
#define kILDeleteGestureMaximumVerticalMovementBeforeCancelling 40.0
if (ABS(verticalMovement) > kILDeleteGestureMaximumVerticalMovementBeforeCancelling) {
state = kILDeleteGestureStateWaitingForIdle;
[self.delegate deleteGestureDidCancel:self];
return NO;
}
CGFloat delta = [event scrollingDeltaX];
if (delta > 0) {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tellDelegateAboutPossibleChangeInResult) object:nil];
absoluteTimeOfLastPositiveIncrementStart = CFAbsoluteTimeGetCurrent();
[self performSelector:@selector(tellDelegateAboutPossibleChangeInResult) withObject:nil afterDelay:kILDeleteGestureTooQuickDurationThreshold + 0.05];
}
pixelsTravelled += delta;
if (pixelsTravelled > maxPixelsToTravel)
pixelsTravelled = maxPixelsToTravel;
if (pixelsTravelled < 0)
pixelsTravelled = 0;
[self.delegate deleteGesture:self didProgress:(pixelsTravelled / maxPixelsToTravel) provisionalResult:ILDeleteGestureResultForValues(pixelsTravelled, maxPixelsToTravel, absoluteTimeOfLastPositiveIncrementStart)];
return YES;
} else if ([event phase] == NSEventPhaseEnded) {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tellDelegateAboutPossibleChangeInResult) object:nil];
state = kILDeleteGestureStateIdle;
[self.delegate deleteGesture:self didEndWithResult:ILDeleteGestureResultForValues(pixelsTravelled, maxPixelsToTravel, absoluteTimeOfLastPositiveIncrementStart)];
return YES;
}
break;
case kILDeleteGestureStateWaitingForIdle:
if ([event phase] == NSEventPhaseEnded || [event phase] == NSEventPhaseCancelled)
state = kILDeleteGestureStateIdle;
break;
}
return NO;
}
- (void) tellDelegateAboutPossibleChangeInResult;
{
if (state == kILDeleteGestureStateRecognizing) {
[self.delegate deleteGesture:self didProgress:(pixelsTravelled / maxPixelsToTravel) provisionalResult:ILDeleteGestureResultForValues(pixelsTravelled, maxPixelsToTravel, absoluteTimeOfLastPositiveIncrementStart)];
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment