summaryrefslogtreecommitdiffstats
path: root/chrome/browser/cocoa/disclosure_view_controller.mm
blob: 5b735722b8113ca8d07df6a7c7c640fe67805cff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// Copyright (c) 2010 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/cocoa/disclosure_view_controller.h"
#include "base/logging.h"

namespace {
const NSInteger kClosedBoxHeight = 20;
const CGFloat kDisclosureAnimationDurationSeconds = .2;
NSString* const kKVODisclosedKey = @"disclosed";
}

// This class externalizes the state of the disclosure control.  When the
// disclosure control is pressed it changes the state of this object.  In turn
// the KVO machinery detects the change to |disclosed| and signals the
// |observeValueForKeyPath| call in the |DisclosureViewController|.
@interface DisclosureViewState : NSObject {
 @private
  NSCellStateValue disclosed;
}
@end

@implementation DisclosureViewState
@end

@interface DisclosureViewController(PrivateMethods)

- (void)initFrameSize:(NSCellStateValue)state;
- (NSRect)openStateFrameSize:(NSRect)startFrame;
- (NSRect)closedStateFrameSize:(NSRect)startFrame;
- (void)startAnimations:(NSView*)view
                  start:(NSRect)startFrame
                    end:(NSRect)endFrame;
- (void)discloseDetails:(NSCellStateValue)state;
- (void)setContentViewVisibility;
- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context;

@end

@implementation DisclosureViewController

@synthesize disclosureState = disclosureState_;

- (void)awakeFromNib {
  // Create the disclosure state.
  [self setDisclosureState:[[[DisclosureViewState alloc] init] autorelease]];

  // Set up the initial disclosure state before we install the observer.
  // We don't want our animations firing before we're done initializing.
  [disclosureState_ setValue:[NSNumber numberWithInt:initialDisclosureState_]
      forKey:kKVODisclosedKey];

  // Pick up "open" height from the initial state of the view in the nib.
  openHeight_ = [[self view] frame].size.height;

  // Set frame size according to initial disclosure state.
  [self initFrameSize:initialDisclosureState_];

  // Set content visibility according to initial disclosure state.
  [self setContentViewVisibility];

  // Setup observers so that when disclosure state changes we resize frame
  // accordingly.
  [disclosureState_ addObserver:self forKeyPath:kKVODisclosedKey
      options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
      context:nil];
}

- (id)initWithNibName:(NSString *)nibNameOrNil
               bundle:(NSBundle *)nibBundleOrNil
           disclosure:(NSCellStateValue)disclosureState {
  if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) {
    initialDisclosureState_ = disclosureState;
  }
  return self;
}

- (void)dealloc {
  [disclosureState_ removeObserver:self forKeyPath:kKVODisclosedKey];
  [animation_ stopAnimation];
  [disclosureState_ release];
  [super dealloc];
}

@end

@implementation DisclosureViewController(PrivateMethods)

// Initializes the view's frame geometry based on the input |state|.
// If the |state| is NSOnState then the frame size corresponds to "open".
// If the |state| is NSOffState then the frame size corresponds to "closed".
// The |origin.x| and |size.width| remain unchanged, but the |origin.y| and
// |size.height| may vary.
- (void)initFrameSize:(NSCellStateValue)state {
  if (state == NSOnState) {
    [[self view] setFrame:[self openStateFrameSize:[[self view] frame]]];
  }
  else if (state == NSOffState) {
    [[self view] setFrame:[self closedStateFrameSize:[[self view] frame]]];
  }
  else {
    NOTREACHED();
  }
}

// Computes the frame geometry during the "open" state of the disclosure view.
- (NSRect)openStateFrameSize:(NSRect)startFrame {
  return NSMakeRect(startFrame.origin.x,
                    startFrame.size.height - openHeight_ +
                        startFrame.origin.y,
                    startFrame.size.width,
                    openHeight_);
}

// Computes the frame geometry during the "closed" state of the disclosure view.
- (NSRect)closedStateFrameSize:(NSRect)startFrame {
  return NSMakeRect(startFrame.origin.x,
                    startFrame.size.height - kClosedBoxHeight +
                        startFrame.origin.y,
                    startFrame.size.width,
                    kClosedBoxHeight);
}

// Animates the opening or closing of the disclosure view.  The |startFrame|
// specifies the frame geometry at the beginning of the animation and the
// |endFrame| specifies the geometry at the end of the animation.  The input
// |view| is view managed by this controller.
- (void)startAnimations:(NSView*)view
                  start:(NSRect)startFrame
                    end:(NSRect)endFrame
{
  // Setup dictionary describing animation.
  // Create the attributes dictionary for the first view.
  NSMutableDictionary* dictionary;
  dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
      // Specify which view to modify.
      view, NSViewAnimationTargetKey,
      // Specify the starting position of the view.
      [NSValue valueWithRect:startFrame], NSViewAnimationStartFrameKey,
      // Change the ending position of the view.
      [NSValue valueWithRect:endFrame], NSViewAnimationEndFrameKey,
      nil];

  // Stop any existing animation.
  [animation_ stopAnimation];

  // Create the view animation object.
  animation_.reset([[NSViewAnimation alloc] initWithViewAnimations:
               [NSArray arrayWithObject:dictionary]]);

  // Set some additional attributes for the animation.
  [animation_ setDuration:kDisclosureAnimationDurationSeconds];
  [animation_ setAnimationCurve:NSAnimationEaseIn];

  // Set self as delegate so we can toggle visibility at end of animation.
  [animation_ setDelegate:self];

  // Run the animation.
  [animation_ startAnimation];
}

// NSAnimationDelegate method.  Before starting the animation we show the
// |detailedView_|.
- (BOOL)animationShouldStart:(NSAnimation*)animation {
  [detailedView_ setHidden:NO];
  return YES;
}

// NSAnimationDelegate method.  If animation stops before ending we release
// our animation object.
- (void)animationDidStop:(NSAnimation*)animation {
  animation_.reset();
}

// NSAnimationDelegate method.  Once the disclosure animation is over we set
// content view visibility to match disclosure state.
// |animation_| reference is relinquished at end of animation.
- (void)animationDidEnd:(NSAnimation*)animation {
  [self setContentViewVisibility];
  animation_.reset();
}

// This method is invoked when the disclosure state changes.  It computes
// the appropriate view frame geometry and then initiates the animation to
// change that geometry.
- (void)discloseDetails:(NSCellStateValue)state {
  NSRect startFrame = [[self view] frame];
  NSRect endFrame = startFrame;

  if (state == NSOnState) {
    endFrame = [self openStateFrameSize:startFrame];
  } else if (state == NSOffState) {
    endFrame = [self closedStateFrameSize:startFrame];
  } else {
    NOTREACHED();
    return;
  }

  [self startAnimations:[self view] start:startFrame end:endFrame];
}

// Sets the "hidden" state of the content view according to the current
// disclosure state.  We do this so that the view hierarchy knows to remove
// undisclosed content from the first responder chain.
- (void)setContentViewVisibility {
  NSCellStateValue disclosed = [[disclosureState_ valueForKey:kKVODisclosedKey]
      intValue];

  if (disclosed == NSOnState) {
    [detailedView_ setHidden:NO];
  } else if (disclosed == NSOffState) {
    [detailedView_ setHidden:YES];
  } else {
    NOTREACHED();
    return;
  }
}

// The |DisclosureViewController| is an observer of an instance of a
// |DisclosureViewState| object.  This object lives within the controller's
// nib file.  When the KVO machinery detects a change to the state
// it triggers this call and we initiate the change in frame geometry of the
// view.
- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  if ([keyPath isEqualToString:kKVODisclosedKey]) {
    NSCellStateValue newValue =
        [[change objectForKey:NSKeyValueChangeNewKey] intValue];
    NSCellStateValue oldValue =
        [[change objectForKey:NSKeyValueChangeOldKey] intValue];

    if (newValue != oldValue) {
      [self discloseDetails:newValue];
    }
  }
}

@end