// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"

#include "base/logging.h"

// Minimum and maximum click-hold timeout.
static const NSTimeInterval kMinTimeout = 0.0;
static const NSTimeInterval kMaxTimeout = 5.0;

// Drag distance threshold to activate click-hold; should be >= 0.
static const CGFloat kDragDistThreshold = 2.5;

// See |-resetToDefaults| (and header file) for other default values.

@interface ClickHoldButtonCell (Private)
- (void)resetToDefaults;
@end  // @interface ClickHoldButtonCell (Private)

@implementation ClickHoldButtonCell

// Overrides:

+ (BOOL)prefersTrackingUntilMouseUp {
  return NO;
}

- (id)init {
  if ((self = [super init]))
    [self resetToDefaults];
  return self;
}

- (id)initWithCoder:(NSCoder*)decoder {
  if ((self = [super initWithCoder:decoder]))
    [self resetToDefaults];
  return self;
}

- (id)initImageCell:(NSImage*)image {
  if ((self = [super initImageCell:image]))
    [self resetToDefaults];
  return self;
}

- (id)initTextCell:(NSString*)string {
  if ((self = [super initTextCell:string]))
    [self resetToDefaults];
  return self;
}

- (BOOL)startTrackingAt:(NSPoint)startPoint
                 inView:(NSView*)controlView {
  return enableClickHold_ ? YES :
                            [super startTrackingAt:startPoint
                                            inView:controlView];
}

- (BOOL)continueTracking:(NSPoint)lastPoint
                      at:(NSPoint)currentPoint
                  inView:(NSView*)controlView {
  return enableClickHold_ ? YES :
                            [super continueTracking:lastPoint
                                                 at:currentPoint
                                             inView:controlView];
}

- (BOOL)trackMouse:(NSEvent*)originalEvent
            inRect:(NSRect)cellFrame
            ofView:(NSView*)controlView
      untilMouseUp:(BOOL)untilMouseUp {
  if (!enableClickHold_) {
    return [super trackMouse:originalEvent
                      inRect:cellFrame
                      ofView:controlView
                untilMouseUp:untilMouseUp];
  }

  // If doing click-hold, track the mouse ourselves.
  NSPoint currPoint = [controlView convertPoint:[originalEvent locationInWindow]
                                       fromView:nil];
  NSPoint lastPoint = currPoint;
  NSPoint firstPoint = currPoint;
  NSTimeInterval timeout =
      MAX(MIN(clickHoldTimeout_, kMaxTimeout), kMinTimeout);
  NSDate* clickHoldBailTime = [NSDate dateWithTimeIntervalSinceNow:timeout];

  if (![self startTrackingAt:currPoint inView:controlView])
    return NO;

  enum {
    kContinueTrack, kStopClickHold, kStopMouseUp, kStopLeftRect, kStopNoContinue
  } state = kContinueTrack;
  do {
    NSEvent* event = [NSApp nextEventMatchingMask:(NSLeftMouseDraggedMask |
                                                   NSLeftMouseUpMask)
                                        untilDate:clickHoldBailTime
                                           inMode:NSEventTrackingRunLoopMode
                                          dequeue:YES];
    currPoint = [controlView convertPoint:[event locationInWindow]
                                 fromView:nil];

    // Time-out.
    if (!event) {
      state = kStopClickHold;

    // Drag? (If distance meets threshold.)
    } else if (activateOnDrag_ && ([event type] == NSLeftMouseDragged)) {
      CGFloat dx = currPoint.x - firstPoint.x;
      CGFloat dy = currPoint.y - firstPoint.y;
      if ((dx*dx + dy*dy) >= (kDragDistThreshold*kDragDistThreshold))
        state = kStopClickHold;

    // Mouse up.
    } else if ([event type] == NSLeftMouseUp) {
      state = kStopMouseUp;

    // Stop tracking if mouse left frame rectangle (if requested to do so).
    } else if (trackOnlyInRect_ && ![controlView mouse:currPoint
                                                inRect:cellFrame]) {
      state = kStopLeftRect;

    // Stop tracking if instructed to.
    } else if (![self continueTracking:lastPoint
                                    at:currPoint
                                inView:controlView]) {
      state = kStopNoContinue;
    }

    lastPoint = currPoint;
  } while (state == kContinueTrack);

  [self stopTracking:lastPoint
                  at:lastPoint
              inView:controlView
           mouseIsUp:NO];

  switch (state) {
    case kStopClickHold:
      if (clickHoldAction_) {
        [static_cast<NSControl*>(controlView) sendAction:clickHoldAction_
                                                      to:clickHoldTarget_];
      }
      return YES;

    case kStopMouseUp:
      if ([self action]) {
        [static_cast<NSControl*>(controlView) sendAction:[self action]
                                                      to:[self target]];
      }
      return YES;

    case kStopLeftRect:
    case kStopNoContinue:
      return NO;

    default:
      NOTREACHED() << "Unknown terminating state!";
  }

  return NO;
}

// Accessors and mutators:

@synthesize enableClickHold = enableClickHold_;
@synthesize clickHoldTimeout = clickHoldTimeout_;
@synthesize trackOnlyInRect = trackOnlyInRect_;
@synthesize activateOnDrag = activateOnDrag_;
@synthesize clickHoldTarget = clickHoldTarget_;
@synthesize clickHoldAction = clickHoldAction_;

@end  // @implementation ClickHoldButtonCell

@implementation ClickHoldButtonCell (Private)

// Resets various members to defaults indicated in the header file. (Those
// without indicated defaults are *not* touched.) Please keep the values below
// in sync with the header file, and please be aware of side-effects on code
// which relies on the "published" defaults.
- (void)resetToDefaults {
  [self setEnableClickHold:NO];
  [self setClickHoldTimeout:0.25];
  [self setTrackOnlyInRect:NO];
  [self setActivateOnDrag:YES];
}

@end  // @implementation ClickHoldButtonCell (Private)