// // SRRecorderCell.m // ShortcutRecorder // // Copyright 2006-2007 Contributors. All rights reserved. // // License: BSD // // Contributors: // David Dauer // Jesper // Jamie Kirkpatrick #import "SRRecorderCell.h" #import "SRRecorderControl.h" #import "SRKeyCodeTransformer.h" #import "SRValidator.h" #import "DBUserDefaultsController.h" @interface SRRecorderCell (Private) - (void)_privateInit; - (void)_createGradient; - (void)_setJustChanged; - (void)_startRecordingTransition; - (void)_endRecordingTransition; - (void)_transitionTick; - (void)_startRecording; - (void)_endRecording; - (BOOL)_effectiveIsAnimating; - (BOOL)_supportsAnimation; - (NSString *)_defaultsKeyForAutosaveName:(NSString *)name; - (void)_saveKeyCombo; - (void)_loadKeyCombo; - (NSRect)_removeButtonRectForFrame:(NSRect)cellFrame; - (NSRect)_snapbackRectForFrame:(NSRect)cellFrame; - (NSUInteger)_filteredCocoaFlags:(NSUInteger)flags; - (NSUInteger)_filteredCocoaToCarbonFlags:(NSUInteger)cocoaFlags; - (BOOL)_validModifierFlags:(NSUInteger)flags; - (BOOL)_isEmpty; @end #pragma mark - @implementation SRRecorderCell - (id)init { self = [super init]; [self _privateInit]; return self; } - (void)dealloc { [validator release]; [keyCharsIgnoringModifiers release]; [keyChars release]; [recordingGradient release]; [autosaveName release]; [cancelCharacterSet release]; [super dealloc]; } #pragma mark *** Coding Support *** - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder: aDecoder]; [self _privateInit]; if ([aDecoder allowsKeyedCoding]) { autosaveName = [[aDecoder decodeObjectForKey: @"autosaveName"] retain]; keyCombo.code = [[aDecoder decodeObjectForKey: @"keyComboCode"] shortValue]; keyCombo.flags = [[aDecoder decodeObjectForKey: @"keyComboFlags"] unsignedIntegerValue]; if ([aDecoder containsValueForKey:@"keyChars"]) { hasKeyChars = YES; keyChars = (NSString *)[aDecoder decodeObjectForKey: @"keyChars"]; keyCharsIgnoringModifiers = (NSString *)[aDecoder decodeObjectForKey: @"keyCharsIgnoringModifiers"]; } allowedFlags = [[aDecoder decodeObjectForKey: @"allowedFlags"] unsignedIntegerValue]; requiredFlags = [[aDecoder decodeObjectForKey: @"requiredFlags"] unsignedIntegerValue]; allowsKeyOnly = [[aDecoder decodeObjectForKey:@"allowsKeyOnly"] boolValue]; escapeKeysRecord = [[aDecoder decodeObjectForKey:@"escapeKeysRecord"] boolValue]; isAnimating = [[aDecoder decodeObjectForKey:@"isAnimating"] boolValue]; style = [[aDecoder decodeObjectForKey:@"style"] shortValue]; } else { autosaveName = [[aDecoder decodeObject] retain]; keyCombo.code = [[aDecoder decodeObject] shortValue]; keyCombo.flags = [[aDecoder decodeObject] unsignedIntegerValue]; allowedFlags = [[aDecoder decodeObject] unsignedIntegerValue]; requiredFlags = [[aDecoder decodeObject] unsignedIntegerValue]; } allowedFlags |= NSFunctionKeyMask; [self _loadKeyCombo]; return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder: aCoder]; if ([aCoder allowsKeyedCoding]) { [aCoder encodeObject:[self autosaveName] forKey:@"autosaveName"]; [aCoder encodeObject:[NSNumber numberWithShort: keyCombo.code] forKey:@"keyComboCode"]; [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:keyCombo.flags] forKey:@"keyComboFlags"]; [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:allowedFlags] forKey:@"allowedFlags"]; [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:requiredFlags] forKey:@"requiredFlags"]; if (hasKeyChars) { [aCoder encodeObject:keyChars forKey:@"keyChars"]; [aCoder encodeObject:keyCharsIgnoringModifiers forKey:@"keyCharsIgnoringModifiers"]; } [aCoder encodeObject:[NSNumber numberWithBool: allowsKeyOnly] forKey:@"allowsKeyOnly"]; [aCoder encodeObject:[NSNumber numberWithBool: escapeKeysRecord] forKey:@"escapeKeysRecord"]; [aCoder encodeObject:[NSNumber numberWithBool: isAnimating] forKey:@"isAnimating"]; [aCoder encodeObject:[NSNumber numberWithShort:style] forKey:@"style"]; } else { // Unkeyed archiving and encoding is deprecated and unsupported. Use keyed archiving and encoding. [aCoder encodeObject: [self autosaveName]]; [aCoder encodeObject: [NSNumber numberWithShort: keyCombo.code]]; [aCoder encodeObject: [NSNumber numberWithUnsignedInteger: keyCombo.flags]]; [aCoder encodeObject: [NSNumber numberWithUnsignedInteger:allowedFlags]]; [aCoder encodeObject: [NSNumber numberWithUnsignedInteger:requiredFlags]]; } } - (id)copyWithZone:(NSZone *)zone { SRRecorderCell *cell; cell = (SRRecorderCell *)[super copyWithZone: zone]; cell->recordingGradient = [recordingGradient retain]; cell->autosaveName = [autosaveName retain]; cell->isRecording = isRecording; cell->mouseInsideTrackingArea = mouseInsideTrackingArea; cell->mouseDown = mouseDown; cell->removeTrackingRectTag = removeTrackingRectTag; cell->snapbackTrackingRectTag = snapbackTrackingRectTag; cell->keyCombo = keyCombo; cell->allowedFlags = allowedFlags; cell->requiredFlags = requiredFlags; cell->recordingFlags = recordingFlags; cell->allowsKeyOnly = allowsKeyOnly; cell->escapeKeysRecord = escapeKeysRecord; cell->isAnimating = isAnimating; cell->style = style; cell->cancelCharacterSet = [cancelCharacterSet retain]; cell->delegate = delegate; return cell; } #pragma mark *** Drawing *** + (BOOL)styleSupportsAnimation:(SRRecorderStyle)style { return (style == SRGreyStyle); } - (BOOL)animates { return isAnimating; } - (void)setAnimates:(BOOL)an { isAnimating = an; } - (SRRecorderStyle)style { return style; } - (void)setStyle:(SRRecorderStyle)nStyle { switch (nStyle) { case SRGreyStyle: style = SRGreyStyle; break; case SRGradientBorderStyle: default: style = SRGradientBorderStyle; break; } } - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { CGFloat radius = 0; if (style == SRGradientBorderStyle) { NSRect whiteRect = cellFrame; NSBezierPath *roundedRect; // Draw gradient when in recording mode if (isRecording) { radius = NSHeight(cellFrame) / 2.0f; roundedRect = [NSBezierPath bezierPathWithRoundedRect:cellFrame xRadius:radius yRadius:radius]; // Fill background with gradient [[NSGraphicsContext currentContext] saveGraphicsState]; [roundedRect addClip]; [recordingGradient drawInRect:cellFrame angle:90.0f]; [[NSGraphicsContext currentContext] restoreGraphicsState]; // Highlight if inside or down if (mouseInsideTrackingArea) { [[[NSColor blackColor] colorWithAlphaComponent: (mouseDown ? 0.4f : 0.2f)] set]; [roundedRect fill]; } // Draw snapback image NSImage *snapBackArrow = SRResIndImage(@"SRSnapback"); [snapBackArrow dissolveToPoint:[self _snapbackRectForFrame: cellFrame].origin fraction:1.0f]; // Because of the gradient and snapback image, the white rounded rect will be smaller whiteRect = NSInsetRect(cellFrame, 9.5f, 2.0f); whiteRect.origin.x -= 7.5f; } // Draw white rounded box radius = NSHeight(whiteRect) / 2.0f; roundedRect = [NSBezierPath bezierPathWithRoundedRect:whiteRect xRadius:radius yRadius:radius]; [[NSGraphicsContext currentContext] saveGraphicsState]; [roundedRect addClip]; [[NSColor whiteColor] set]; [NSBezierPath fillRect: whiteRect]; // Draw border and remove badge if needed if (!isRecording) { [[NSColor windowFrameColor] set]; [roundedRect stroke]; // If key combination is set and valid, draw remove image if (![self _isEmpty] && [self isEnabled]) { NSString *removeImageName = [NSString stringWithFormat: @"SRRemoveShortcut%@", (mouseInsideTrackingArea ? (mouseDown ? @"Pressed" : @"Rollover") : (mouseDown ? @"Rollover" : @""))]; NSImage *removeImage = SRResIndImage(removeImageName); [removeImage dissolveToPoint:[self _removeButtonRectForFrame: cellFrame].origin fraction:1.0f]; } } [[NSGraphicsContext currentContext] restoreGraphicsState]; // Draw text NSMutableParagraphStyle *mpstyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease]; [mpstyle setLineBreakMode: NSLineBreakByTruncatingTail]; [mpstyle setAlignment: NSCenterTextAlignment]; // Only the KeyCombo should be black and in a bigger font size BOOL recordingOrEmpty = (isRecording || [self _isEmpty]); NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys: mpstyle, NSParagraphStyleAttributeName, [NSFont systemFontOfSize: (recordingOrEmpty ? [NSFont labelFontSize] : [NSFont smallSystemFontSize])], NSFontAttributeName, (recordingOrEmpty ? [NSColor disabledControlTextColor] : [NSColor blackColor]), NSForegroundColorAttributeName, nil]; NSString *displayString; if (isRecording) { // Recording, but no modifier keys down if (![self _validModifierFlags: recordingFlags]) { if (mouseInsideTrackingArea) { // Mouse over snapback displayString = SRLoc(@"Use old shortcut"); } else { // Mouse elsewhere displayString = SRLoc(@"Type shortcut"); } } else { // Display currently pressed modifier keys displayString = SRStringForCocoaModifierFlags( recordingFlags ); // Fall back on 'Type shortcut' if we don't have modifier flags to display; this will happen for the fn key depressed if (![displayString length]) { displayString = SRLoc(@"Type shortcut"); } } } else { // Not recording... if ([self _isEmpty]) { displayString = SRLoc(@"Click to record shortcut"); } else { // Display current key combination displayString = [self keyComboString]; } } // Calculate rect in which to draw the text in... NSRect textRect = cellFrame; textRect.size.width -= 6; textRect.size.width -= ((!isRecording && [self _isEmpty]) ? 6 : (isRecording ? [self _snapbackRectForFrame: cellFrame].size.width : [self _removeButtonRectForFrame: cellFrame].size.width) + 6); textRect.origin.x += 6; textRect.origin.y = -(NSMidY(cellFrame) - [displayString sizeWithAttributes: attributes].height/2); // Finally draw it [displayString drawInRect:textRect withAttributes:attributes]; // draw a focus ring...? if ( [self showsFirstResponder] ) { [NSGraphicsContext saveGraphicsState]; NSSetFocusRingStyle(NSFocusRingOnly); radius = NSHeight(cellFrame) / 2.0f; [[NSBezierPath bezierPathWithRoundedRect:cellFrame xRadius:radius yRadius:radius] fill]; [NSGraphicsContext restoreGraphicsState]; } } else { // NSRect rawCellFrame = cellFrame; cellFrame = NSInsetRect(cellFrame,0.5f,0.5f); NSRect whiteRect = cellFrame; NSBezierPath *roundedRect; BOOL isVaguelyRecording = isRecording; CGFloat xanim = 0.0f; if (isAnimatingNow) { // NSLog(@"tp: %f; xanim: %f", transitionProgress, xanim); xanim = (SRAnimationEaseInOut(transitionProgress)); // NSLog(@"tp: %f; xanim: %f", transitionProgress, xanim); } CGFloat alphaRecording = 1.0f; CGFloat alphaView = 1.0f; if (isAnimatingNow && !isAnimatingTowardsRecording) { alphaRecording = 1.0f - xanim; alphaView = xanim; } if (isAnimatingNow && isAnimatingTowardsRecording) { alphaView = 1.0f - xanim; alphaRecording = xanim; } if (isAnimatingNow) { //NSLog(@"animation step: %f, effective: %f, alpha recording: %f, view: %f", transitionProgress, xanim, alphaRecording, alphaView); } if (isAnimatingNow && isAnimatingTowardsRecording) { isVaguelyRecording = YES; } // NSAffineTransform *transitionMovement = [NSAffineTransform transform]; NSAffineTransform *viewportMovement = [NSAffineTransform transform]; // Draw gradient when in recording mode if (isVaguelyRecording) { if (isAnimatingNow) { // [transitionMovement translateXBy:(isAnimatingTowardsRecording ? -(NSWidth(cellFrame)*(1.0-xanim)) : +(NSWidth(cellFrame)*xanim)) yBy:0.0]; if (SRAnimationAxisIsY) { // [viewportMovement translateXBy:0.0 yBy:(isAnimatingTowardsRecording ? -(NSHeight(cellFrame)*(xanim)) : -(NSHeight(cellFrame)*(1.0-xanim)))]; [viewportMovement translateXBy:0.0f yBy:(isAnimatingTowardsRecording ? NSHeight(cellFrame)*(xanim) : NSHeight(cellFrame)*(1.0f-xanim))]; } else { [viewportMovement translateXBy:(isAnimatingTowardsRecording ? -(NSWidth(cellFrame)*(xanim)) : -(NSWidth(cellFrame)*(1.0f-xanim))) yBy:0.0f]; } } else { if (SRAnimationAxisIsY) { [viewportMovement translateXBy:0.0f yBy:NSHeight(cellFrame)]; } else { [viewportMovement translateXBy:-(NSWidth(cellFrame)) yBy:0.0f]; } } } // Draw white rounded box radius = NSHeight(whiteRect) / 2.0f; roundedRect = [NSBezierPath bezierPathWithRoundedRect:whiteRect xRadius:radius yRadius:radius]; [[NSColor whiteColor] set]; [[NSGraphicsContext currentContext] saveGraphicsState]; [roundedRect fill]; [[NSColor windowFrameColor] set]; [roundedRect stroke]; [roundedRect addClip]; // if (isVaguelyRecording) { NSRect snapBackRect = SRAnimationOffsetRect([self _snapbackRectForFrame: cellFrame],cellFrame); // NSLog(@"snapbackrect: %@; offset: %@", NSStringFromRect([self _snapbackRectForFrame: cellFrame]), NSStringFromRect(snapBackRect)); NSPoint correctedSnapBackOrigin = [viewportMovement transformPoint:snapBackRect.origin]; NSRect correctedSnapBackRect = snapBackRect; // correctedSnapBackRect.origin.y = NSMinY(whiteRect); correctedSnapBackRect.size.height = NSHeight(whiteRect); correctedSnapBackRect.size.width *= 1.3f; correctedSnapBackRect.origin.y -= 5.0f; correctedSnapBackRect.origin.x -= 1.5f; correctedSnapBackOrigin.x -= 0.5f; correctedSnapBackRect.origin = [viewportMovement transformPoint:correctedSnapBackRect.origin]; NSBezierPath *snapBackButton = [NSBezierPath bezierPathWithRect:correctedSnapBackRect]; [[[[NSColor windowFrameColor] shadowWithLevel:0.2f] colorWithAlphaComponent:alphaRecording] set]; [snapBackButton stroke]; // NSLog(@"stroked along path of %@", NSStringFromRect(correctedSnapBackRect)); NSGradient *gradient = nil; if (mouseDown && mouseInsideTrackingArea) { gradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithCalibratedWhite:0.60f alpha:alphaRecording] endingColor:[NSColor colorWithCalibratedWhite:0.75f alpha:alphaRecording]]; } else { gradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithCalibratedWhite:0.75f alpha:alphaRecording] endingColor:[NSColor colorWithCalibratedWhite:0.90f alpha:alphaRecording]]; } CGFloat insetAmount = -([snapBackButton lineWidth]/2.0f); [gradient drawInRect:NSInsetRect(correctedSnapBackRect, insetAmount, insetAmount) angle:90.0f]; [gradient release]; /* // Highlight if inside or down if (mouseInsideTrackingArea) { [[[NSColor blackColor] colorWithAlphaComponent: alphaRecording*(mouseDown ? 0.15 : 0.1)] set]; [snapBackButton fill]; }*/ // Draw snapback image NSImage *snapBackArrow = SRResIndImage(@"SRSnapback"); [snapBackArrow dissolveToPoint:correctedSnapBackOrigin fraction:1.0f*alphaRecording]; } // Draw border and remove badge if needed /* if (!isVaguelyRecording) { */ // If key combination is set and valid, draw remove image if (![self _isEmpty] && [self isEnabled]) { NSString *removeImageName = [NSString stringWithFormat: @"SRRemoveShortcut%@", (mouseInsideTrackingArea ? (mouseDown ? @"Pressed" : @"Rollover") : (mouseDown ? @"Rollover" : @""))]; NSImage *removeImage = SRResIndImage(removeImageName); [removeImage dissolveToPoint:[viewportMovement transformPoint:([self _removeButtonRectForFrame: cellFrame].origin)] fraction:alphaView]; //NSLog(@"drew removeImage with alpha %f", alphaView); } // } // Draw text NSMutableParagraphStyle *mpstyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease]; [mpstyle setLineBreakMode: NSLineBreakByTruncatingTail]; [mpstyle setAlignment: NSCenterTextAlignment]; CGFloat alphaCombo = alphaView; CGFloat alphaRecordingText = alphaRecording; if (comboJustChanged) { alphaCombo = 1.0f; alphaRecordingText = 0.0f;//(alphaRecordingText/2.0); } NSString *displayString; { // Only the KeyCombo should be black and in a bigger font size BOOL recordingOrEmpty = (isVaguelyRecording || [self _isEmpty]); NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys: mpstyle, NSParagraphStyleAttributeName, [NSFont systemFontOfSize: (recordingOrEmpty ? [NSFont labelFontSize] : [NSFont smallSystemFontSize])], NSFontAttributeName, [(recordingOrEmpty ? [NSColor disabledControlTextColor] : [NSColor blackColor]) colorWithAlphaComponent:alphaRecordingText], NSForegroundColorAttributeName, nil]; // Recording, but no modifier keys down if (![self _validModifierFlags: recordingFlags]) { if (mouseInsideTrackingArea) { // Mouse over snapback displayString = SRLoc(@"Use old shortcut"); } else { // Mouse elsewhere displayString = SRLoc(@"Type shortcut"); } } else { // Display currently pressed modifier keys displayString = SRStringForCocoaModifierFlags( recordingFlags ); // Fall back on 'Type shortcut' if we don't have modifier flags to display; this will happen for the fn key depressed if (![displayString length]) { displayString = SRLoc(@"Type shortcut"); } } // Calculate rect in which to draw the text in... NSRect textRect = SRAnimationOffsetRect(cellFrame,cellFrame); //NSLog(@"draw record text in rect (preadjusted): %@", NSStringFromRect(textRect)); textRect.origin.y -= 3.0f; textRect.origin = [viewportMovement transformPoint:textRect.origin]; //NSLog(@"draw record text in rect: %@", NSStringFromRect(textRect)); // Finally draw it [displayString drawInRect:textRect withAttributes:attributes]; } { // Only the KeyCombo should be black and in a bigger font size NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys: mpstyle, NSParagraphStyleAttributeName, [NSFont systemFontOfSize: ([self _isEmpty] ? [NSFont labelFontSize] : [NSFont smallSystemFontSize])], NSFontAttributeName, [([self _isEmpty] ? [NSColor disabledControlTextColor] : [NSColor blackColor]) colorWithAlphaComponent:alphaCombo], NSForegroundColorAttributeName, nil]; // Not recording... if ([self _isEmpty]) { displayString = SRLoc(@"Click to record shortcut"); } else { // Display current key combination displayString = [self keyComboString]; } // Calculate rect in which to draw the text in... NSRect textRect = cellFrame; /* textRect.size.width -= 6; textRect.size.width -= (([self _removeButtonRectForFrame: cellFrame].size.width) + 6); // textRect.origin.x += 6;*/ //NSFont *f = [attributes objectForKey:NSFontAttributeName]; //double lineHeight = [[[NSLayoutManager alloc] init] defaultLineHeightForFont:f]; // textRect.size.height = lineHeight; if (!comboJustChanged) { //NSLog(@"draw view text in rect (pre-adjusted): %@", NSStringFromRect(textRect)); textRect.origin = [viewportMovement transformPoint:textRect.origin]; } textRect.origin.y = NSMinY(textRect)-3.0f;// - ((lineHeight/2.0)+([f descender]/2.0)); //NSLog(@"draw view text in rect: %@", NSStringFromRect(textRect)); // Finally draw it [displayString drawInRect:textRect withAttributes:attributes]; } [[NSGraphicsContext currentContext] restoreGraphicsState]; // draw a focus ring...? if ( [self showsFirstResponder] ) { [NSGraphicsContext saveGraphicsState]; NSSetFocusRingStyle(NSFocusRingOnly); radius = NSHeight(cellFrame) / 2.0f; [[NSBezierPath bezierPathWithRoundedRect:cellFrame xRadius:radius yRadius:radius] fill]; [NSGraphicsContext restoreGraphicsState]; } } } #pragma mark *** Mouse Tracking *** - (void)resetTrackingRects { SRRecorderControl *controlView = (SRRecorderControl *)[self controlView]; NSRect cellFrame = [controlView bounds]; NSPoint mouseLocation = [controlView convertPoint:[[NSApp currentEvent] locationInWindow] fromView:nil]; // We're not to be tracked if we're not enabled if (![self isEnabled]) { if (removeTrackingRectTag != 0) [controlView removeTrackingRect: removeTrackingRectTag]; if (snapbackTrackingRectTag != 0) [controlView removeTrackingRect: snapbackTrackingRectTag]; return; } // We're either in recording or normal display mode if (!isRecording) { // Create and register tracking rect for the remove badge if shortcut is not empty NSRect removeButtonRect = [self _removeButtonRectForFrame: cellFrame]; BOOL mouseInside = [controlView mouse:mouseLocation inRect:removeButtonRect]; if (removeTrackingRectTag != 0) [controlView removeTrackingRect: removeTrackingRectTag]; removeTrackingRectTag = [controlView addTrackingRect:removeButtonRect owner:self userData:nil assumeInside:mouseInside]; if (mouseInsideTrackingArea != mouseInside) mouseInsideTrackingArea = mouseInside; } else { // Create and register tracking rect for the snapback badge if we're in recording mode NSRect snapbackRect = [self _snapbackRectForFrame: cellFrame]; BOOL mouseInside = [controlView mouse:mouseLocation inRect:snapbackRect]; if (snapbackTrackingRectTag != 0) [controlView removeTrackingRect: snapbackTrackingRectTag]; snapbackTrackingRectTag = [controlView addTrackingRect:snapbackRect owner:self userData:nil assumeInside:mouseInside]; if (mouseInsideTrackingArea != mouseInside) mouseInsideTrackingArea = mouseInside; } } - (void)mouseEntered:(NSEvent *)theEvent { NSView *view = [self controlView]; if ([[view window] isKeyWindow] || [view acceptsFirstMouse: theEvent]) { mouseInsideTrackingArea = YES; [view display]; } } - (void)mouseExited:(NSEvent*)theEvent { NSView *view = [self controlView]; if ([[view window] isKeyWindow] || [view acceptsFirstMouse: theEvent]) { mouseInsideTrackingArea = NO; [view display]; } } - (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(SRRecorderControl *)controlView untilMouseUp:(BOOL)flag { NSEvent *currentEvent = theEvent; NSPoint mouseLocation; NSRect trackingRect = (isRecording ? [self _snapbackRectForFrame: cellFrame] : [self _removeButtonRectForFrame: cellFrame]); NSRect leftRect = cellFrame; // Determine the area without any badge if (!NSEqualRects(trackingRect,NSZeroRect)) leftRect.size.width -= NSWidth(trackingRect) + 4; do { mouseLocation = [controlView convertPoint: [currentEvent locationInWindow] fromView:nil]; switch ([currentEvent type]) { case NSLeftMouseDown: { // Check if mouse is over remove/snapback image if ([controlView mouse:mouseLocation inRect:trackingRect]) { mouseDown = YES; [controlView setNeedsDisplayInRect: cellFrame]; } break; } case NSLeftMouseDragged: { // Recheck if mouse is still over the image while dragging mouseInsideTrackingArea = [controlView mouse:mouseLocation inRect:trackingRect]; [controlView setNeedsDisplayInRect: cellFrame]; break; } default: // NSLeftMouseUp { mouseDown = NO; mouseInsideTrackingArea = [controlView mouse:mouseLocation inRect:trackingRect]; if (mouseInsideTrackingArea) { if (isRecording) { // Mouse was over snapback, just redraw [self _endRecordingTransition]; } else { // Mouse was over the remove image, reset all [self setKeyCombo: SRMakeKeyCombo(ShortcutRecorderEmptyCode, ShortcutRecorderEmptyFlags)]; } } else if ([controlView mouse:mouseLocation inRect:leftRect] && !isRecording) { if ([self isEnabled]) { [self _startRecordingTransition]; } /* maybe beep if not editable? else { NSBeep(); } */ } // Any click inside will make us firstResponder if ([self isEnabled]) [[controlView window] makeFirstResponder: controlView]; // Reset tracking rects and redisplay [self resetTrackingRects]; [controlView setNeedsDisplayInRect: cellFrame]; return YES; } } } while ((currentEvent = [[controlView window] nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask) untilDate:[NSDate distantFuture] inMode:NSEventTrackingRunLoopMode dequeue:YES])); return YES; } #pragma mark *** Delegate *** - (id)delegate { return delegate; } - (void)setDelegate:(id)aDelegate { delegate = aDelegate; } #pragma mark *** Responder Control *** - (BOOL) becomeFirstResponder; { // reset tracking rects and redisplay [self resetTrackingRects]; [[self controlView] display]; return YES; } - (BOOL)resignFirstResponder; { if (isRecording) { [self _endRecordingTransition]; } [self resetTrackingRects]; [[self controlView] display]; return YES; } #pragma mark *** Key Combination Control *** - (BOOL) performKeyEquivalent:(NSEvent *)theEvent { NSUInteger flags = [self _filteredCocoaFlags: [theEvent modifierFlags]]; NSNumber *keyCodeNumber = [NSNumber numberWithUnsignedShort: [theEvent keyCode]]; BOOL snapback = [cancelCharacterSet containsObject: keyCodeNumber]; BOOL validModifiers = [self _validModifierFlags: (snapback) ? [theEvent modifierFlags] : flags]; // Snapback key shouldn't interfer with required flags! // Special case for the space key when we aren't recording... if (!isRecording && [[theEvent characters] isEqualToString:@" "]) { [self _startRecordingTransition]; return YES; } // Do something as long as we're in recording mode and a modifier key or cancel key is pressed if (isRecording && (validModifiers || snapback)) { if (!snapback || validModifiers) { BOOL goAhead = YES; // Special case: if a snapback key has been entered AND modifiers are deemed valid... if (snapback && validModifiers) { // ...AND we're set to allow plain keys if (allowsKeyOnly) { // ...AND modifiers are empty, or empty save for the Function key // (needed, since forward delete is fn+delete on laptops) if (flags == ShortcutRecorderEmptyFlags || flags == (ShortcutRecorderEmptyFlags | NSFunctionKeyMask)) { // ...check for behavior in escapeKeysRecord. if (!escapeKeysRecord) { goAhead = NO; } } } } if (goAhead) { NSString *character = [[theEvent charactersIgnoringModifiers] uppercaseString]; // accents like "´" or "`" will be ignored since we don't get a keycode if ([character length]) { NSError *error = nil; // Check if key combination is already used or not allowed by the delegate if ( [validator isKeyCode:[theEvent keyCode] andFlagsTaken:[self _filteredCocoaToCarbonFlags:flags] error:&error] ) { // display the error... NSAlert *alert = [NSAlert alertWithNonRecoverableError:error]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert runModal]; // Recheck pressed modifier keys [self flagsChanged: [NSApp currentEvent]]; return YES; } else { // All ok, set new combination keyCombo.flags = flags; keyCombo.code = [theEvent keyCode]; hasKeyChars = YES; keyChars = [[theEvent characters] retain]; keyCharsIgnoringModifiers = [[theEvent charactersIgnoringModifiers] retain]; // NSLog(@"keychars: %@, ignoringmods: %@", keyChars, keyCharsIgnoringModifiers); // NSLog(@"calculated keychars: %@, ignoring: %@", SRStringForKeyCode(keyCombo.code), SRCharacterForKeyCodeAndCocoaFlags(keyCombo.code,keyCombo.flags)); // Notify delegate if (delegate != nil && [delegate respondsToSelector: @selector(shortcutRecorderCell:keyComboDidChange:)]) [delegate shortcutRecorderCell:self keyComboDidChange:keyCombo]; // Save if needed [self _saveKeyCombo]; [self _setJustChanged]; } } else { // invalid character NSBeep(); } } } // reset values and redisplay recordingFlags = ShortcutRecorderEmptyFlags; [self _endRecordingTransition]; [self resetTrackingRects]; [[self controlView] display]; return YES; } else { //Start recording when the spacebar is pressed while the control is first responder if (([[[self controlView] window] firstResponder] == [self controlView]) && ([[theEvent characters] length] && [[theEvent characters] characterAtIndex:0] == 32) && ([self isEnabled])) { [self _startRecordingTransition]; } } return NO; } - (void)flagsChanged:(NSEvent *)theEvent { if (isRecording) { recordingFlags = [self _filteredCocoaFlags: [theEvent modifierFlags]]; [[self controlView] display]; } } #pragma mark - - (NSUInteger)allowedFlags { return allowedFlags; } - (void)setAllowedFlags:(NSUInteger)flags { allowedFlags = flags; // filter new flags and change keycombo if not recording if (isRecording) { recordingFlags = [self _filteredCocoaFlags: [[NSApp currentEvent] modifierFlags]];; } else { NSUInteger originalFlags = keyCombo.flags; keyCombo.flags = [self _filteredCocoaFlags: keyCombo.flags]; if (keyCombo.flags != originalFlags && keyCombo.code > ShortcutRecorderEmptyCode) { // Notify delegate if keyCombo changed if (delegate != nil && [delegate respondsToSelector: @selector(shortcutRecorderCell:keyComboDidChange:)]) [delegate shortcutRecorderCell:self keyComboDidChange:keyCombo]; // Save if needed [self _saveKeyCombo]; } } [[self controlView] display]; } - (BOOL)allowsKeyOnly { return allowsKeyOnly; } - (BOOL)escapeKeysRecord { return escapeKeysRecord; } - (void)setAllowsKeyOnly:(BOOL)nAllowsKeyOnly escapeKeysRecord:(BOOL)nEscapeKeysRecord { allowsKeyOnly = nAllowsKeyOnly; escapeKeysRecord = nEscapeKeysRecord; } - (NSUInteger)requiredFlags { return requiredFlags; } - (void)setRequiredFlags:(NSUInteger)flags { requiredFlags = flags; // filter new flags and change keycombo if not recording if (isRecording) { recordingFlags = [self _filteredCocoaFlags: [[NSApp currentEvent] modifierFlags]]; } else { NSUInteger originalFlags = keyCombo.flags; keyCombo.flags = [self _filteredCocoaFlags: keyCombo.flags]; if (keyCombo.flags != originalFlags && keyCombo.code > ShortcutRecorderEmptyCode) { // Notify delegate if keyCombo changed if (delegate != nil && [delegate respondsToSelector: @selector(shortcutRecorderCell:keyComboDidChange:)]) [delegate shortcutRecorderCell:self keyComboDidChange:keyCombo]; // Save if needed [self _saveKeyCombo]; } } [[self controlView] display]; } - (KeyCombo)keyCombo { return keyCombo; } - (void)setKeyCombo:(KeyCombo)aKeyCombo { keyCombo = aKeyCombo; keyCombo.flags = [self _filteredCocoaFlags: aKeyCombo.flags]; hasKeyChars = NO; // Notify delegate if (delegate != nil && [delegate respondsToSelector: @selector(shortcutRecorderCell:keyComboDidChange:)]) [delegate shortcutRecorderCell:self keyComboDidChange:keyCombo]; // Save if needed [self _saveKeyCombo]; [[self controlView] display]; } - (BOOL)canCaptureGlobalHotKeys { return globalHotKeys; } - (void)setCanCaptureGlobalHotKeys:(BOOL)inState { globalHotKeys = inState; } #pragma mark *** Autosave Control *** - (NSString *)autosaveName { return autosaveName; } - (void)setAutosaveName:(NSString *)aName { if (aName != autosaveName) { [autosaveName release]; autosaveName = [aName copy]; } } #pragma mark - - (NSString *)keyComboString { if ([self _isEmpty]) return nil; return [NSString stringWithFormat: @"%@%@", SRStringForCocoaModifierFlags( keyCombo.flags ), SRStringForKeyCode( keyCombo.code )]; } - (NSString *)keyChars { if (!hasKeyChars) return SRStringForKeyCode(keyCombo.code); return keyChars; } - (NSString *)keyCharsIgnoringModifiers { if (!hasKeyChars) return SRCharacterForKeyCodeAndCocoaFlags(keyCombo.code,keyCombo.flags); return keyCharsIgnoringModifiers; } @end #pragma mark - @implementation SRRecorderCell (Private) - (void)_privateInit { // init the validator object... validator = [[SRValidator alloc] initWithDelegate:self]; // Allow all modifier keys by default, nothing is required allowedFlags = ShortcutRecorderAllFlags; requiredFlags = ShortcutRecorderEmptyFlags; recordingFlags = ShortcutRecorderEmptyFlags; // Create clean KeyCombo keyCombo.flags = ShortcutRecorderEmptyFlags; keyCombo.code = ShortcutRecorderEmptyCode; keyChars = nil; keyCharsIgnoringModifiers = nil; hasKeyChars = NO; // These keys will cancel the recoding mode if not pressed with any modifier cancelCharacterSet = [[NSSet alloc] initWithObjects: [NSNumber numberWithInteger:ShortcutRecorderEscapeKey], [NSNumber numberWithInteger:ShortcutRecorderBackspaceKey], [NSNumber numberWithInteger:ShortcutRecorderDeleteKey], nil]; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(_createGradient) name:NSSystemColorsDidChangeNotification object:nil]; // recreate gradient if needed [self _createGradient]; [self _loadKeyCombo]; } - (void)_createGradient { NSColor *gradientStartColor = [[[NSColor alternateSelectedControlColor] shadowWithLevel: 0.2f] colorWithAlphaComponent: 0.9f]; NSColor *gradientEndColor = [[[NSColor alternateSelectedControlColor] highlightWithLevel: 0.2f] colorWithAlphaComponent: 0.9f]; recordingGradient = [[NSGradient alloc] initWithStartingColor:gradientStartColor endingColor:gradientEndColor]; } - (void)_setJustChanged { comboJustChanged = YES; } - (BOOL)_effectiveIsAnimating { return (isAnimating && [self _supportsAnimation]); } - (BOOL)_supportsAnimation { return [[self class] styleSupportsAnimation:style]; } - (void)_startRecordingTransition { if ([self _effectiveIsAnimating]) { isAnimatingTowardsRecording = YES; isAnimatingNow = YES; transitionProgress = 0.0f; [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(_transitionTick) object:nil]; [self performSelector:@selector(_transitionTick) withObject:nil afterDelay:(SRTransitionDuration/SRTransitionFrames)]; // NSLog(@"start recording-transition"); } else { [self _startRecording]; } } - (void)_endRecordingTransition { if ([self _effectiveIsAnimating]) { isAnimatingTowardsRecording = NO; isAnimatingNow = YES; transitionProgress = 0.0f; [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(_transitionTick) object:nil]; [self performSelector:@selector(_transitionTick) withObject:nil afterDelay:(SRTransitionDuration/SRTransitionFrames)]; // NSLog(@"end recording-transition"); } else { [self _endRecording]; } } - (void)_transitionTick { transitionProgress += (1.0f/SRTransitionFrames); // NSLog(@"transition tick: %f", transitionProgress); if (transitionProgress >= 0.998f) { // NSLog(@"transition deemed complete"); isAnimatingNow = NO; transitionProgress = 0.0f; if (isAnimatingTowardsRecording) { [self _startRecording]; } else { [self _endRecording]; } } else { // NSLog(@"more to do"); [[self controlView] setNeedsDisplay:YES]; [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(_transitionTick) object:nil]; [self performSelector:@selector(_transitionTick) withObject:nil afterDelay:(SRTransitionDuration/SRTransitionFrames)]; } } - (void)_startRecording; { // Jump into recording mode if mouse was inside the control but not over any image isRecording = YES; // Reset recording flags and determine which are required recordingFlags = [self _filteredCocoaFlags: ShortcutRecorderEmptyFlags]; /* [self setFocusRingType:NSFocusRingTypeNone]; [[self controlView] setFocusRingType:NSFocusRingTypeNone];*/ [[self controlView] setNeedsDisplay:YES]; // invalidate the focus ring rect... NSView *controlView = [self controlView]; [controlView setKeyboardFocusRingNeedsDisplayInRect:[controlView bounds]]; if (globalHotKeys) hotKeyModeToken = PushSymbolicHotKeyMode(kHIHotKeyModeAllDisabled); } - (void)_endRecording; { isRecording = NO; comboJustChanged = NO; /* [self setFocusRingType:NSFocusRingTypeNone]; [[self controlView] setFocusRingType:NSFocusRingTypeNone];*/ [[self controlView] setNeedsDisplay:YES]; // invalidate the focus ring rect... NSView *controlView = [self controlView]; [controlView setKeyboardFocusRingNeedsDisplayInRect:[controlView bounds]]; if (globalHotKeys) PopSymbolicHotKeyMode(hotKeyModeToken); } #pragma mark *** Autosave *** - (NSString *)_defaultsKeyForAutosaveName:(NSString *)name { return [NSString stringWithFormat: @"ShortcutRecorder %@", name]; } - (void)_saveKeyCombo { NSString *defaultsKey = [self autosaveName]; if (defaultsKey != nil && [defaultsKey length]) { id values = [[DBUserDefaultsController sharedUserDefaultsController] values]; NSDictionary *defaultsValue = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithShort: keyCombo.code], @"keyCode", [NSNumber numberWithUnsignedInteger: keyCombo.flags], @"modifierFlags", // cocoa [NSNumber numberWithUnsignedInteger:SRCocoaToCarbonFlags(keyCombo.flags)], @"modifiers", // carbon, for compatibility with PTKeyCombo nil]; if (hasKeyChars) { NSMutableDictionary *mutableDefaultsValue = [[defaultsValue mutableCopy] autorelease]; [mutableDefaultsValue setObject:keyChars forKey:@"keyChars"]; [mutableDefaultsValue setObject:keyCharsIgnoringModifiers forKey:@"keyCharsIgnoringModifiers"]; defaultsValue = mutableDefaultsValue; } [values setValue:defaultsValue forKey:[self _defaultsKeyForAutosaveName: defaultsKey]]; } } - (void)_loadKeyCombo { NSString *defaultsKey = [self autosaveName]; if (defaultsKey != nil && [defaultsKey length]) { id values = [[DBUserDefaultsController sharedUserDefaultsController] values]; NSDictionary *savedCombo = [values valueForKey: [self _defaultsKeyForAutosaveName: defaultsKey]]; NSInteger keyCode = [[savedCombo valueForKey: @"keyCode"] shortValue]; NSUInteger flags; if ((nil == [savedCombo valueForKey:@"modifierFlags"]) && (nil != [savedCombo valueForKey:@"modifiers"])) { // carbon, for compatibility with PTKeyCombo flags = SRCarbonToCocoaFlags([[savedCombo valueForKey: @"modifiers"] unsignedIntegerValue]); } else { // cocoa flags = [[savedCombo valueForKey: @"modifierFlags"] unsignedIntegerValue]; } keyCombo.flags = [self _filteredCocoaFlags: flags]; keyCombo.code = keyCode; NSString *kc = [savedCombo valueForKey: @"keyChars"]; hasKeyChars = (nil != kc); if (kc) { keyCharsIgnoringModifiers = [[savedCombo valueForKey: @"keyCharsIgnoringModifiers"] retain]; keyChars = [kc retain]; } // Notify delegate if (delegate != nil && [delegate respondsToSelector: @selector(shortcutRecorderCell:keyComboDidChange:)]) [delegate shortcutRecorderCell:self keyComboDidChange:keyCombo]; [[self controlView] display]; } } #pragma mark *** Drawing Helpers *** - (NSRect)_removeButtonRectForFrame:(NSRect)cellFrame { if ([self _isEmpty] || ![self isEnabled]) return NSZeroRect; NSRect removeButtonRect; NSImage *removeImage = SRResIndImage(@"SRRemoveShortcut"); removeButtonRect.origin = NSMakePoint(NSMaxX(cellFrame) - [removeImage size].width - 4, (NSMaxY(cellFrame) - [removeImage size].height)/2); removeButtonRect.size = [removeImage size]; return removeButtonRect; } - (NSRect)_snapbackRectForFrame:(NSRect)cellFrame { // if (!isRecording) return NSZeroRect; NSRect snapbackRect; NSImage *snapbackImage = SRResIndImage(@"SRSnapback"); snapbackRect.origin = NSMakePoint(NSMaxX(cellFrame) - [snapbackImage size].width - 2, (NSMaxY(cellFrame) - [snapbackImage size].height)/2 + 1); snapbackRect.size = [snapbackImage size]; return snapbackRect; } #pragma mark *** Filters *** - (NSUInteger)_filteredCocoaFlags:(NSUInteger)flags { NSUInteger filteredFlags = ShortcutRecorderEmptyFlags; NSUInteger a = allowedFlags; NSUInteger m = requiredFlags; if (m & NSCommandKeyMask) filteredFlags |= NSCommandKeyMask; else if ((flags & NSCommandKeyMask) && (a & NSCommandKeyMask)) filteredFlags |= NSCommandKeyMask; if (m & NSAlternateKeyMask) filteredFlags |= NSAlternateKeyMask; else if ((flags & NSAlternateKeyMask) && (a & NSAlternateKeyMask)) filteredFlags |= NSAlternateKeyMask; if ((m & NSControlKeyMask)) filteredFlags |= NSControlKeyMask; else if ((flags & NSControlKeyMask) && (a & NSControlKeyMask)) filteredFlags |= NSControlKeyMask; if ((m & NSShiftKeyMask)) filteredFlags |= NSShiftKeyMask; else if ((flags & NSShiftKeyMask) && (a & NSShiftKeyMask)) filteredFlags |= NSShiftKeyMask; if ((m & NSFunctionKeyMask)) filteredFlags |= NSFunctionKeyMask; else if ((flags & NSFunctionKeyMask) && (a & NSFunctionKeyMask)) filteredFlags |= NSFunctionKeyMask; return filteredFlags; } - (BOOL)_validModifierFlags:(NSUInteger)flags { return (allowsKeyOnly ? YES : (((flags & NSCommandKeyMask) || (flags & NSAlternateKeyMask) || (flags & NSControlKeyMask) || (flags & NSShiftKeyMask) || (flags & NSFunctionKeyMask)) ? YES : NO)); } #pragma mark - - (NSUInteger)_filteredCocoaToCarbonFlags:(NSUInteger)cocoaFlags { NSUInteger carbonFlags = ShortcutRecorderEmptyFlags; NSUInteger filteredFlags = [self _filteredCocoaFlags: cocoaFlags]; if (filteredFlags & NSCommandKeyMask) carbonFlags |= cmdKey; if (filteredFlags & NSAlternateKeyMask) carbonFlags |= optionKey; if (filteredFlags & NSControlKeyMask) carbonFlags |= controlKey; if (filteredFlags & NSShiftKeyMask) carbonFlags |= shiftKey; // I couldn't find out the equivalent constant in Carbon, but apparently it must use the same one as Cocoa. -AK if (filteredFlags & NSFunctionKeyMask) carbonFlags |= NSFunctionKeyMask; return carbonFlags; } #pragma mark *** Internal Check *** - (BOOL)_isEmpty { return ( ![self _validModifierFlags: keyCombo.flags] || !SRStringForKeyCode( keyCombo.code ) ); } #pragma mark *** Delegate pass-through *** - (BOOL) shortcutValidator:(SRValidator *)validator isKeyCode:(NSInteger)keyCode andFlagsTaken:(NSUInteger)flags reason:(NSString **)aReason; { SEL selector = @selector( shortcutRecorderCell:isKeyCode:andFlagsTaken:reason: ); if ( ( delegate ) && ( [delegate respondsToSelector:selector] ) ) { return [delegate shortcutRecorderCell:self isKeyCode:keyCode andFlagsTaken:flags reason:aReason]; } return NO; } @end