Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(65)

Side by Side Diff: chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm

Issue 8141003: [Mac] Restore the old bookmark menus now that the experiment is over. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 9 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" 5 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
6 6
7 #include "base/mac/mac_util.h" 7 #include "base/mac/mac_util.h"
8 #include "base/sys_string_conversions.h"
8 #include "chrome/browser/bookmarks/bookmark_model.h" 9 #include "chrome/browser/bookmarks/bookmark_model.h"
10 #include "chrome/browser/bookmarks/bookmark_utils.h"
11 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
9 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" 12 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
10 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" 13 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
11 #include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h" 14 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
12 15 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
13 // Forward-declare symbols that are part of the 10.6 SDK. 16 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
14 #if !defined(MAC_OS_X_VERSION_10_6) || \ 17 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
15 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_6 18 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
16 19 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
17 @interface NSMenu (SnowLeopardSDK) 20 #import "chrome/browser/ui/cocoa/event_utils.h"
18 - (BOOL)popUpMenuPositioningItem:(NSMenuItem*)item 21 #include "ui/base/theme_provider.h"
19 atLocation:(NSPoint)location 22
20 inView:(NSView*)view; 23 using bookmarks::kBookmarkBarMenuCornerRadius;
24
25 namespace {
26
27 // Frequency of the scrolling timer in seconds.
28 const NSTimeInterval kBookmarkBarFolderScrollInterval = 0.1;
29
30 // Amount to scroll by per timer fire. We scroll rather slowly; to
31 // accomodate we do several at a time.
32 const CGFloat kBookmarkBarFolderScrollAmount =
33 3 * bookmarks::kBookmarkFolderButtonHeight;
34
35 // Amount to scroll for each scroll wheel roll.
36 const CGFloat kBookmarkBarFolderScrollWheelAmount =
37 1 * bookmarks::kBookmarkFolderButtonHeight;
38
39 // Determining adjustments to the layout of the folder menu window in response
40 // to resizing and scrolling relies on many visual factors. The following
41 // struct is used to pass around these factors to the several support
42 // functions involved in the adjustment calculations and application.
43 struct LayoutMetrics {
44 // Metrics applied during the final layout adjustments to the window,
45 // the main visible content view, and the menu content view (i.e. the
46 // scroll view).
47 CGFloat windowLeft;
48 NSSize windowSize;
49 // The proposed and then final scrolling adjustment made to the scrollable
50 // area of the folder menu. This may be modified during the window layout
51 // primarily as a result of hiding or showing the scroll arrows.
52 CGFloat scrollDelta;
53 NSRect windowFrame;
54 NSRect visibleFrame;
55 NSRect scrollerFrame;
56 NSPoint scrollPoint;
57 // The difference between 'could' and 'can' in these next four data members
58 // is this: 'could' represents the previous condition for scrollability
59 // while 'can' represents what the new condition will be for scrollability.
60 BOOL couldScrollUp;
61 BOOL canScrollUp;
62 BOOL couldScrollDown;
63 BOOL canScrollDown;
64 // Determines the optimal time during folder menu layout when the contents
65 // of the button scroll area should be scrolled in order to prevent
66 // flickering.
67 BOOL preScroll;
68
69 // Intermediate metrics used in determining window vertical layout changes.
70 CGFloat deltaWindowHeight;
71 CGFloat deltaWindowY;
72 CGFloat deltaVisibleHeight;
73 CGFloat deltaVisibleY;
74 CGFloat deltaScrollerHeight;
75 CGFloat deltaScrollerY;
76
77 // Convenience metrics used in multiple functions (carried along here in
78 // order to eliminate the need to calculate in multiple places and
79 // reduce the possibility of bugs).
80 CGFloat minimumY;
81 CGFloat oldWindowY;
82 CGFloat folderY;
83 CGFloat folderTop;
84
85 LayoutMetrics(CGFloat windowLeft, NSSize windowSize, CGFloat scrollDelta) :
86 windowLeft(windowLeft),
87 windowSize(windowSize),
88 scrollDelta(scrollDelta),
89 couldScrollUp(NO),
90 canScrollUp(NO),
91 couldScrollDown(NO),
92 canScrollDown(NO),
93 preScroll(NO),
94 deltaWindowHeight(0.0),
95 deltaWindowY(0.0),
96 deltaVisibleHeight(0.0),
97 deltaVisibleY(0.0),
98 deltaScrollerHeight(0.0),
99 deltaScrollerY(0.0),
100 oldWindowY(0.0),
101 folderY(0.0),
102 folderTop(0.0) {}
103 };
104
105 } // namespace
106
107
108 // Required to set the right tracking bounds for our fake menus.
109 @interface NSView(Private)
110 - (void)_updateTrackingAreas;
21 @end 111 @end
22 112
23 #endif // MAC_OS_X_VERSION_10_6 113 @interface BookmarkBarFolderController(Private)
114 - (void)configureWindow;
115 - (void)addOrUpdateScrollTracking;
116 - (void)removeScrollTracking;
117 - (void)endScroll;
118 - (void)addScrollTimerWithDelta:(CGFloat)delta;
119
120 // Helper function to configureWindow which performs a basic layout of
121 // the window subviews, in particular the menu buttons and the window width.
122 - (void)layOutWindowWithHeight:(CGFloat)height;
123
124 // Determine the best button width (which will be the widest button or the
125 // maximum allowable button width, whichever is less) and resize all buttons.
126 // Return the new width so that the window can be adjusted.
127 - (CGFloat)adjustButtonWidths;
128
129 // Returns the total menu height needed to display |buttonCount| buttons.
130 // Does not do any fancy tricks like trimming the height to fit on the screen.
131 - (int)menuHeightForButtonCount:(int)buttonCount;
132
133 // Adjust layout of the folder menu window components, showing/hiding the
134 // scroll up/down arrows, and resizing as necessary for a proper disaplay.
135 // In order to reduce window flicker, all layout changes are deferred until
136 // the final step of the adjustment. To accommodate this deferral, window
137 // height and width changes needed by callers to this function pass their
138 // desired window changes in |size|. When scrolling is to be performed
139 // any scrolling change is given by |scrollDelta|. The ultimate amount of
140 // scrolling may be different from |scrollDelta| in order to accommodate
141 // changes in the scroller view layout. These proposed window adjustments
142 // are passed to helper functions using a LayoutMetrics structure.
143 //
144 // This function should be called when: 1) initially setting up a folder menu
145 // window, 2) responding to scrolling of the contents (which may affect the
146 // height of the window), 3) addition or removal of bookmark items (such as
147 // during cut/paste/delete/drag/drop operations).
148 - (void)adjustWindowLeft:(CGFloat)windowLeft
149 size:(NSSize)windowSize
150 scrollingBy:(CGFloat)scrollDelta;
151
152 // Support function for adjustWindowLeft:size:scrollingBy: which initializes
153 // the layout adjustments by gathering current folder menu window and subviews
154 // positions and sizes. This information is set in the |layoutMetrics|
155 // structure.
156 - (void)gatherMetrics:(LayoutMetrics*)layoutMetrics;
157
158 // Support function for adjustWindowLeft:size:scrollingBy: which calculates
159 // the changes which must be applied to the folder menu window and subviews
160 // positions and sizes. |layoutMetrics| contains the proposed window size
161 // and scrolling along with the other current window and subview layout
162 // information. The values in |layoutMetrics| are then adjusted to
163 // accommodate scroll arrow presentation and window growth.
164 - (void)adjustMetrics:(LayoutMetrics*)layoutMetrics;
165
166 // Support function for adjustMetrics: which calculates the layout changes
167 // required to accommodate changes in the position and scrollability
168 // of the top of the folder menu window.
169 - (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics;
170
171 // Support function for adjustMetrics: which calculates the layout changes
172 // required to accommodate changes in the position and scrollability
173 // of the bottom of the folder menu window.
174 - (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics;
175
176 // Support function for adjustWindowLeft:size:scrollingBy: which applies
177 // the layout adjustments to the folder menu window and subviews.
178 - (void)applyMetrics:(LayoutMetrics*)layoutMetrics;
179
180 // This function is called when buttons are added or removed from the folder
181 // menu, and which may require a change in the layout of the folder menu
182 // window. Such layout changes may include horizontal placement, width,
183 // height, and scroller visibility changes. (This function calls through
184 // to -[adjustWindowLeft:size:scrollingBy:].)
185 // |buttonCount| should contain the updated count of menu buttons.
186 - (void)adjustWindowForButtonCount:(NSUInteger)buttonCount;
187
188 // A helper function which takes the desired amount to scroll, given by
189 // |scrollDelta|, and calculates the actual scrolling change to be applied
190 // taking into account the layout of the folder menu window and any
191 // changes in it's scrollability. (For example, when scrolling down and the
192 // top-most menu item is coming into view we will only scroll enough for
193 // that item to be completely presented, which may be less than the
194 // scroll amount requested.)
195 - (CGFloat)determineFinalScrollDelta:(CGFloat)scrollDelta;
196
197 // |point| is in the base coordinate system of the destination window;
198 // it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
199 // made and inserted into the new location while leaving the bookmark in
200 // the old location, otherwise move the bookmark by removing from its old
201 // location and inserting into the new location.
202 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
203 to:(NSPoint)point
204 copy:(BOOL)copy;
205
206 @end
207
208 @interface BookmarkButton (BookmarkBarFolderMenuHighlighting)
209
210 // Make the button's border frame always appear when |forceOn| is YES,
211 // otherwise only border the button when the mouse is inside the button.
212 - (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn;
213
214 @end
215
216 @implementation BookmarkButton (BookmarkBarFolderMenuHighlighting)
217
218 - (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn {
219 [self setShowsBorderOnlyWhileMouseInside:!forceOn];
220 [self setNeedsDisplay];
221 }
222
223 @end
24 224
25 @implementation BookmarkBarFolderController 225 @implementation BookmarkBarFolderController
26 226
227 @synthesize subFolderGrowthToRight = subFolderGrowthToRight_;
228
27 - (id)initWithParentButton:(BookmarkButton*)button 229 - (id)initWithParentButton:(BookmarkButton*)button
28 bookmarkModel:(BookmarkModel*)model 230 parentController:(BookmarkBarFolderController*)parentController
29 barController:(BookmarkBarController*)barController { 231 barController:(BookmarkBarController*)barController {
30 if ((self = [super init])) { 232 NSString* nibPath =
233 [base::mac::MainAppBundle() pathForResource:@"BookmarkBarFolderWindow"
234 ofType:@"nib"];
235 if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
31 parentButton_.reset([button retain]); 236 parentButton_.reset([button retain]);
32 barController_ = barController; 237 selectedIndex_ = -1;
33 menu_.reset([[NSMenu alloc] initWithTitle:@""]); 238
34 menuBridge_.reset(new BookmarkMenuBridge([parentButton_ bookmarkNode], 239 // We want the button to remain bordered as part of the menu path.
35 model->profile(), menu_)); 240 [button forceButtonBorderToStayOnAlways:YES];
36 [menuBridge_->controller() setDelegate:self]; 241
242 parentController_.reset([parentController retain]);
243 if (!parentController_)
244 [self setSubFolderGrowthToRight:YES];
245 else
246 [self setSubFolderGrowthToRight:[parentController
247 subFolderGrowthToRight]];
248 barController_ = barController; // WEAK
249 buttons_.reset([[NSMutableArray alloc] init]);
250 folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
251 [self configureWindow];
252 hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]);
37 } 253 }
38 return self; 254 return self;
39 } 255 }
40 256
257 - (void)dealloc {
258 [self clearInputText];
259
260 // The button is no longer part of the menu path.
261 [parentButton_ forceButtonBorderToStayOnAlways:NO];
262 [parentButton_ setNeedsDisplay];
263
264 [self removeScrollTracking];
265 [self endScroll];
266 [hoverState_ draggingExited];
267
268 // Delegate pattern does not retain; make sure pointers to us are removed.
269 for (BookmarkButton* button in buttons_.get()) {
270 [button setDelegate:nil];
271 [button setTarget:nil];
272 [button setAction:nil];
273 }
274
275 // Note: we don't need to
276 // [NSObject cancelPreviousPerformRequestsWithTarget:self];
277 // Because all of our performSelector: calls use withDelay: which
278 // retains us.
279 [super dealloc];
280 }
281
282 - (void)awakeFromNib {
283 NSRect windowFrame = [[self window] frame];
284 NSRect scrollViewFrame = [scrollView_ frame];
285 padding_ = NSWidth(windowFrame) - NSWidth(scrollViewFrame);
286 verticalScrollArrowHeight_ = NSHeight([scrollUpArrowView_ frame]);
287 }
288
289 // Overriden from NSWindowController to call childFolderWillShow: before showing
290 // the window.
291 - (void)showWindow:(id)sender {
292 [barController_ childFolderWillShow:self];
293 [super showWindow:sender];
294 }
295
296 - (int)buttonCount {
297 return [[self buttons] count];
298 }
299
41 - (BookmarkButton*)parentButton { 300 - (BookmarkButton*)parentButton {
42 return parentButton_.get(); 301 return parentButton_.get();
43 } 302 }
44 303
45 - (void)openMenu { 304 - (void)offsetFolderMenuWindow:(NSSize)offset {
46 // Retain self so that whatever created this can forefit ownership if it 305 NSWindow* window = [self window];
47 // wants. This call is balanced in |-bookmarkMenuDidClose:|. 306 NSRect windowFrame = [window frame];
307 windowFrame.origin.x -= offset.width;
308 windowFrame.origin.y += offset.height; // Yes, in the opposite direction!
309 [window setFrame:windowFrame display:YES];
310 [folderController_ offsetFolderMenuWindow:offset];
311 }
312
313 - (void)reconfigureMenu {
314 [NSObject cancelPreviousPerformRequestsWithTarget:self];
315 for (BookmarkButton* button in buttons_.get()) {
316 [button setDelegate:nil];
317 [button removeFromSuperview];
318 }
319 [buttons_ removeAllObjects];
320 [self configureWindow];
321 }
322
323 #pragma mark Private Methods
324
325 - (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)child {
326 NSImage* image = child ? [barController_ faviconForNode:child] : nil;
327 NSMenu* menu = child ? child->is_folder() ? folderMenu_ : buttonMenu_ : nil;
328 BookmarkBarFolderButtonCell* cell =
329 [BookmarkBarFolderButtonCell buttonCellForNode:child
330 contextMenu:menu
331 cellText:nil
332 cellImage:image];
333 [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
334 return cell;
335 }
336
337 // Redirect to our logic shared with BookmarkBarController.
338 - (IBAction)openBookmarkFolderFromButton:(id)sender {
339 [folderTarget_ openBookmarkFolderFromButton:sender];
340 }
341
342 // Create a bookmark button for the given node using frame.
343 //
344 // If |node| is NULL this is an "(empty)" button.
345 // Does NOT add this button to our button list.
346 // Returns an autoreleased button.
347 // Adjusts the input frame width as appropriate.
348 //
349 // TODO(jrg): combine with addNodesToButtonList: code from
350 // bookmark_bar_controller.mm, and generalize that to use both x and y
351 // offsets.
352 // http://crbug.com/35966
353 - (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node
354 frame:(NSRect)frame {
355 BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
356 DCHECK(cell);
357
358 // We must decide if we draw the folder arrow before we ask the cell
359 // how big it needs to be.
360 if (node && node->is_folder()) {
361 // Warning when combining code with bookmark_bar_controller.mm:
362 // this call should NOT be made for the bar buttons; only for the
363 // subfolder buttons.
364 [cell setDrawFolderArrow:YES];
365 }
366
367 // The "+2" is needed because, sometimes, Cocoa is off by a tad when
368 // returning the value it thinks it needs.
369 CGFloat desired = [cell cellSize].width + 2;
370 // The width is determined from the maximum of the proposed width
371 // (provided in |frame|) or the natural width of the title, then
372 // limited by the abolute minimum and maximum allowable widths.
373 frame.size.width =
374 std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth,
375 std::max(frame.size.width, desired)),
376 bookmarks::kBookmarkMenuButtonMaximumWidth);
377
378 BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame]
379 autorelease];
380 DCHECK(button);
381
382 [button setCell:cell];
383 [button setDelegate:self];
384 if (node) {
385 if (node->is_folder()) {
386 [button setTarget:self];
387 [button setAction:@selector(openBookmarkFolderFromButton:)];
388 } else {
389 // Make the button do something.
390 [button setTarget:self];
391 [button setAction:@selector(openBookmark:)];
392 // Add a tooltip.
393 [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
394 [button setAcceptsTrackIn:YES];
395 }
396 } else {
397 [button setEnabled:NO];
398 [button setBordered:NO];
399 }
400 return button;
401 }
402
403 - (id)folderTarget {
404 return folderTarget_.get();
405 }
406
407
408 // Our parent controller is another BookmarkBarFolderController, so
409 // our window is to the right or left of it. We use a little overlap
410 // since it looks much more menu-like than with none. If we would
411 // grow off the screen, switch growth to the other direction. Growth
412 // direction sticks for folder windows which are descendents of us.
413 // If we have tried both directions and neither fits, degrade to a
414 // default.
415 - (CGFloat)childFolderWindowLeftForWidth:(int)windowWidth {
416 // We may legitimately need to try two times (growth to right and
417 // left but not in that order). Limit us to three tries in case
418 // the folder window can't fit on either side of the screen; we
419 // don't want to loop forever.
420 CGFloat x;
421 int tries = 0;
422 while (tries < 2) {
423 // Try to grow right.
424 if ([self subFolderGrowthToRight]) {
425 tries++;
426 x = NSMaxX([[parentButton_ window] frame]) -
427 bookmarks::kBookmarkMenuOverlap;
428 // If off the screen, switch direction.
429 if ((x + windowWidth +
430 bookmarks::kBookmarkHorizontalScreenPadding) >
431 NSMaxX([[[self window] screen] visibleFrame])) {
432 [self setSubFolderGrowthToRight:NO];
433 } else {
434 return x;
435 }
436 }
437 // Try to grow left.
438 if (![self subFolderGrowthToRight]) {
439 tries++;
440 x = NSMinX([[parentButton_ window] frame]) +
441 bookmarks::kBookmarkMenuOverlap -
442 windowWidth;
443 // If off the screen, switch direction.
444 if (x < NSMinX([[[self window] screen] visibleFrame])) {
445 [self setSubFolderGrowthToRight:YES];
446 } else {
447 return x;
448 }
449 }
450 }
451 // Unhappy; do the best we can.
452 return NSMaxX([[[self window] screen] visibleFrame]) - windowWidth;
453 }
454
455
456 // Compute and return the top left point of our window (screen
457 // coordinates). The top left is positioned in a manner similar to
458 // cascading menus. Windows may grow to either the right or left of
459 // their parent (if a sub-folder) so we need to know |windowWidth|.
460 - (NSPoint)windowTopLeftForWidth:(int)windowWidth height:(int)windowHeight {
461 CGFloat kMinSqueezedMenuHeight = bookmarks::kBookmarkFolderButtonHeight * 2.0;
462 NSPoint newWindowTopLeft;
463 if (![parentController_ isKindOfClass:[self class]]) {
464 // If we're not popping up from one of ourselves, we must be
465 // popping up from the bookmark bar itself. In this case, start
466 // BELOW the parent button. Our left is the button left; our top
467 // is bottom of button's parent view.
468 NSPoint buttonBottomLeftInScreen =
469 [[parentButton_ window]
470 convertBaseToScreen:[parentButton_
471 convertPoint:NSZeroPoint toView:nil]];
472 NSPoint bookmarkBarBottomLeftInScreen =
473 [[parentButton_ window]
474 convertBaseToScreen:[[parentButton_ superview]
475 convertPoint:NSZeroPoint toView:nil]];
476 newWindowTopLeft = NSMakePoint(
477 buttonBottomLeftInScreen.x + bookmarks::kBookmarkBarButtonOffset,
478 bookmarkBarBottomLeftInScreen.y + bookmarks::kBookmarkBarMenuOffset);
479 // Make sure the window is on-screen; if not, push left. It is
480 // intentional that top level folders "push left" slightly
481 // different than subfolders.
482 NSRect screenFrame = [[[parentButton_ window] screen] visibleFrame];
483 CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame);
484 if (spillOff > 0.0) {
485 newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff,
486 NSMinX(screenFrame));
487 }
488 // The menu looks bad when it is squeezed up against the bottom of the
489 // screen and ends up being only a few pixels tall. If it meets the
490 // threshold for this case, instead show the menu above the button.
491 NSRect visFrame = [[[parentButton_ window] screen] visibleFrame];
492 CGFloat availableVerticalSpace = newWindowTopLeft.y -
493 (NSMinY(visFrame) + bookmarks::kScrollWindowVerticalMargin);
494 if ((availableVerticalSpace < kMinSqueezedMenuHeight) &&
495 (windowHeight > availableVerticalSpace)) {
496 newWindowTopLeft.y = std::min(
497 newWindowTopLeft.y + windowHeight + NSHeight([parentButton_ frame]),
498 NSMaxY(visFrame));
499 }
500 } else {
501 // Parent is a folder: expose as much as we can vertically; grow right/left.
502 newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth];
503 NSPoint topOfWindow = NSMakePoint(0,
504 NSMaxY([parentButton_ frame]) -
505 bookmarks::kBookmarkVerticalPadding);
506 topOfWindow = [[parentButton_ window]
507 convertBaseToScreen:[[parentButton_ superview]
508 convertPoint:topOfWindow toView:nil]];
509 newWindowTopLeft.y = topOfWindow.y;
510 }
511 return newWindowTopLeft;
512 }
513
514 // Set our window level to the right spot so we're above the menubar, dock, etc.
515 // Factored out so we can override/noop in a unit test.
516 - (void)configureWindowLevel {
517 [[self window] setLevel:NSPopUpMenuWindowLevel];
518 }
519
520 - (int)menuHeightForButtonCount:(int)buttonCount {
521 // This does not take into account any padding which may be required at the
522 // top and/or bottom of the window.
523 return (buttonCount * bookmarks::kBookmarkFolderButtonHeight) +
524 2 * bookmarks::kBookmarkVerticalPadding;
525 }
526
527 - (void)adjustWindowLeft:(CGFloat)windowLeft
528 size:(NSSize)windowSize
529 scrollingBy:(CGFloat)scrollDelta {
530 // Callers of this function should make adjustments to the vertical
531 // attributes of the folder view only (height, scroll position).
532 // This function will then make appropriate layout adjustments in order
533 // to accommodate screen/dock margins, scroll-up and scroll-down arrow
534 // presentation, etc.
535 // The 4 views whose vertical height and origins may be adjusted
536 // by this function are:
537 // 1) window, 2) visible content view, 3) scroller view, 4) folder view.
538
539 LayoutMetrics layoutMetrics(windowLeft, windowSize, scrollDelta);
540 [self gatherMetrics:&layoutMetrics];
541 [self adjustMetrics:&layoutMetrics];
542 [self applyMetrics:&layoutMetrics];
543 }
544
545 - (void)gatherMetrics:(LayoutMetrics*)layoutMetrics {
546 LayoutMetrics& metrics(*layoutMetrics);
547 NSWindow* window = [self window];
548 metrics.windowFrame = [window frame];
549 metrics.visibleFrame = [visibleView_ frame];
550 metrics.scrollerFrame = [scrollView_ frame];
551 metrics.scrollPoint = [scrollView_ documentVisibleRect].origin;
552 metrics.scrollPoint.y -= metrics.scrollDelta;
553 metrics.couldScrollUp = ![scrollUpArrowView_ isHidden];
554 metrics.couldScrollDown = ![scrollDownArrowView_ isHidden];
555
556 metrics.deltaWindowHeight = 0.0;
557 metrics.deltaWindowY = 0.0;
558 metrics.deltaVisibleHeight = 0.0;
559 metrics.deltaVisibleY = 0.0;
560 metrics.deltaScrollerHeight = 0.0;
561 metrics.deltaScrollerY = 0.0;
562
563 metrics.minimumY = NSMinY([[window screen] visibleFrame]) +
564 bookmarks::kScrollWindowVerticalMargin;
565 metrics.oldWindowY = NSMinY(metrics.windowFrame);
566 metrics.folderY =
567 metrics.scrollerFrame.origin.y + metrics.visibleFrame.origin.y +
568 metrics.oldWindowY - metrics.scrollPoint.y;
569 metrics.folderTop = metrics.folderY + NSHeight([folderView_ frame]);
570 }
571
572 - (void)adjustMetrics:(LayoutMetrics*)layoutMetrics {
573 LayoutMetrics& metrics(*layoutMetrics);
574 NSScreen* screen = [[self window] screen];
575 CGFloat effectiveFolderY = metrics.folderY;
576 if (!metrics.couldScrollUp && !metrics.couldScrollDown)
577 effectiveFolderY -= metrics.windowSize.height;
578 metrics.canScrollUp = effectiveFolderY < metrics.minimumY;
579 CGFloat maximumY =
580 NSMaxY([screen visibleFrame]) - bookmarks::kScrollWindowVerticalMargin;
581 metrics.canScrollDown = metrics.folderTop > maximumY;
582
583 // Accommodate changes in the bottom of the menu.
584 [self adjustMetricsForMenuBottomChanges:layoutMetrics];
585
586 // Accommodate changes in the top of the menu.
587 [self adjustMetricsForMenuTopChanges:layoutMetrics];
588
589 metrics.scrollerFrame.origin.y += metrics.deltaScrollerY;
590 metrics.scrollerFrame.size.height += metrics.deltaScrollerHeight;
591 metrics.visibleFrame.origin.y += metrics.deltaVisibleY;
592 metrics.visibleFrame.size.height += metrics.deltaVisibleHeight;
593 metrics.preScroll = metrics.canScrollUp && !metrics.couldScrollUp &&
594 metrics.scrollDelta == 0.0 && metrics.deltaWindowHeight >= 0.0;
595 metrics.windowFrame.origin.y += metrics.deltaWindowY;
596 metrics.windowFrame.origin.x = metrics.windowLeft;
597 metrics.windowFrame.size.height += metrics.deltaWindowHeight;
598 metrics.windowFrame.size.width = metrics.windowSize.width;
599 }
600
601 - (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics {
602 LayoutMetrics& metrics(*layoutMetrics);
603 if (metrics.canScrollUp) {
604 if (!metrics.couldScrollUp) {
605 // Couldn't -> Can
606 metrics.deltaWindowY = -metrics.oldWindowY;
607 metrics.deltaWindowHeight = -metrics.deltaWindowY;
608 metrics.deltaVisibleY = metrics.minimumY;
609 metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
610 metrics.deltaScrollerY = verticalScrollArrowHeight_;
611 metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
612 // Adjust the scroll delta if we've grown the window and it is
613 // now scroll-up-able, but don't adjust it if we've
614 // scrolled down and it wasn't scroll-up-able but now is.
615 if (metrics.canScrollDown == metrics.couldScrollDown) {
616 CGFloat deltaScroll = metrics.deltaWindowY + metrics.deltaScrollerY +
617 metrics.deltaVisibleY;
618 metrics.scrollPoint.y += deltaScroll + metrics.windowSize.height;
619 }
620 } else if (!metrics.canScrollDown && metrics.windowSize.height > 0.0) {
621 metrics.scrollPoint.y += metrics.windowSize.height;
622 }
623 } else {
624 if (metrics.couldScrollUp) {
625 // Could -> Can't
626 metrics.deltaWindowY = metrics.folderY - metrics.oldWindowY;
627 metrics.deltaWindowHeight = -metrics.deltaWindowY;
628 metrics.deltaVisibleY = -metrics.visibleFrame.origin.y;
629 metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
630 metrics.deltaScrollerY = -verticalScrollArrowHeight_;
631 metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
632 // We are no longer scroll-up-able so the scroll point drops to zero.
633 metrics.scrollPoint.y = 0.0;
634 } else {
635 // Couldn't -> Can't
636 // Check for menu height change by looking at the relative tops of the
637 // menu folder and the window folder, which previously would have been
638 // the same.
639 metrics.deltaWindowY = NSMaxY(metrics.windowFrame) - metrics.folderTop;
640 metrics.deltaWindowHeight = -metrics.deltaWindowY;
641 }
642 }
643 }
644
645 - (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics {
646 LayoutMetrics& metrics(*layoutMetrics);
647 if (metrics.canScrollDown == metrics.couldScrollDown) {
648 if (!metrics.canScrollDown) {
649 // Not scroll-down-able but the menu top has changed.
650 metrics.deltaWindowHeight += metrics.scrollDelta;
651 }
652 } else {
653 if (metrics.canScrollDown) {
654 // Couldn't -> Can
655 metrics.deltaWindowHeight += (NSMaxY([[[self window] screen]
656 visibleFrame]) -
657 NSMaxY(metrics.windowFrame));
658 metrics.deltaVisibleHeight -= bookmarks::kScrollWindowVerticalMargin;
659 metrics.deltaScrollerHeight -= verticalScrollArrowHeight_;
660 } else {
661 // Could -> Can't
662 metrics.deltaWindowHeight -= bookmarks::kScrollWindowVerticalMargin;
663 metrics.deltaVisibleHeight += bookmarks::kScrollWindowVerticalMargin;
664 metrics.deltaScrollerHeight += verticalScrollArrowHeight_;
665 }
666 }
667 }
668
669 - (void)applyMetrics:(LayoutMetrics*)layoutMetrics {
670 LayoutMetrics& metrics(*layoutMetrics);
671 // Hide or show the scroll arrows.
672 if (metrics.canScrollUp != metrics.couldScrollUp)
673 [scrollUpArrowView_ setHidden:metrics.couldScrollUp];
674 if (metrics.canScrollDown != metrics.couldScrollDown)
675 [scrollDownArrowView_ setHidden:metrics.couldScrollDown];
676
677 // Adjust the geometry. The order is important because of sizer dependencies.
678 [scrollView_ setFrame:metrics.scrollerFrame];
679 [visibleView_ setFrame:metrics.visibleFrame];
680 // This little bit of trickery handles the one special case where
681 // the window is now scroll-up-able _and_ going to be resized -- scroll
682 // first in order to prevent flashing.
683 if (metrics.preScroll)
684 [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
685
686 [[self window] setFrame:metrics.windowFrame display:YES];
687
688 // In all other cases we defer scrolling until the window has been resized
689 // in order to prevent flashing.
690 if (!metrics.preScroll)
691 [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
692
693 // TODO(maf) find a non-SPI way to do this.
694 // Hack. This is the only way I've found to get the tracking area cache
695 // to update properly during a mouse tracking loop.
696 // Without this, the item tracking-areas are wrong when using a scrollable
697 // menu with the mouse held down.
698 NSView *contentView = [[self window] contentView] ;
699 if ([contentView respondsToSelector:@selector(_updateTrackingAreas)])
700 [contentView _updateTrackingAreas];
701
702
703 if (metrics.canScrollUp != metrics.couldScrollUp ||
704 metrics.canScrollDown != metrics.couldScrollDown ||
705 metrics.scrollDelta != 0.0) {
706 if (metrics.canScrollUp || metrics.canScrollDown)
707 [self addOrUpdateScrollTracking];
708 else
709 [self removeScrollTracking];
710 }
711 }
712
713 - (void)adjustWindowForButtonCount:(NSUInteger)buttonCount {
714 NSRect folderFrame = [folderView_ frame];
715 CGFloat newMenuHeight =
716 (CGFloat)[self menuHeightForButtonCount:[buttons_ count]];
717 CGFloat deltaMenuHeight = newMenuHeight - NSHeight(folderFrame);
718 // If the height has changed then also change the origin, and adjust the
719 // scroll (if scrolling).
720 if ([self canScrollUp]) {
721 NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
722 scrollPoint.y += deltaMenuHeight;
723 [[scrollView_ documentView] scrollPoint:scrollPoint];
724 }
725 folderFrame.size.height += deltaMenuHeight;
726 [folderView_ setFrameSize:folderFrame.size];
727 CGFloat windowWidth = [self adjustButtonWidths] + padding_;
728 NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
729 height:deltaMenuHeight];
730 CGFloat left = newWindowTopLeft.x;
731 NSSize newSize = NSMakeSize(windowWidth, deltaMenuHeight);
732 [self adjustWindowLeft:left size:newSize scrollingBy:0.0];
733 }
734
735 // Determine window size and position.
736 // Create buttons for all our nodes.
737 // TODO(jrg): break up into more and smaller routines for easier unit testing.
738 - (void)configureWindow {
739 const BookmarkNode* node = [parentButton_ bookmarkNode];
740 DCHECK(node);
741 int startingIndex = [[parentButton_ cell] startingChildIndex];
742 DCHECK_LE(startingIndex, node->child_count());
743 // Must have at least 1 button (for "empty")
744 int buttons = std::max(node->child_count() - startingIndex, 1);
745
746 // Prelim height of the window. We'll trim later as needed.
747 int height = [self menuHeightForButtonCount:buttons];
748 // We'll need this soon...
749 [self window];
750
751 // TODO(jrg): combine with frame code in bookmark_bar_controller.mm
752 // http://crbug.com/35966
753 NSRect buttonsOuterFrame = NSMakeRect(
754 0,
755 height - bookmarks::kBookmarkFolderButtonHeight -
756 bookmarks::kBookmarkVerticalPadding,
757 bookmarks::kDefaultBookmarkWidth,
758 bookmarks::kBookmarkFolderButtonHeight);
759
760 // TODO(jrg): combine with addNodesToButtonList: code from
761 // bookmark_bar_controller.mm (but use y offset)
762 // http://crbug.com/35966
763 if (node->empty()) {
764 // If no children we are the empty button.
765 BookmarkButton* button = [self makeButtonForNode:nil
766 frame:buttonsOuterFrame];
767 [buttons_ addObject:button];
768 [folderView_ addSubview:button];
769 } else {
770 for (int i = startingIndex; i < node->child_count(); ++i) {
771 const BookmarkNode* child = node->GetChild(i);
772 BookmarkButton* button = [self makeButtonForNode:child
773 frame:buttonsOuterFrame];
774 [buttons_ addObject:button];
775 [folderView_ addSubview:button];
776 buttonsOuterFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
777 }
778 }
779 [self layOutWindowWithHeight:height];
780 }
781
782 - (void)layOutWindowWithHeight:(CGFloat)height {
783 // Lay out the window by adjusting all button widths to be consistent, then
784 // base the window width on this ideal button width.
785 CGFloat buttonWidth = [self adjustButtonWidths];
786 CGFloat windowWidth = buttonWidth + padding_;
787 NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
788 height:height];
789 // Make sure as much of a submenu is exposed (which otherwise would be a
790 // problem if the parent button is close to the bottom of the screen).
791 if ([parentController_ isKindOfClass:[self class]]) {
792 CGFloat minimumY = NSMinY([[[self window] screen] visibleFrame]) +
793 bookmarks::kScrollWindowVerticalMargin +
794 height;
795 newWindowTopLeft.y = MAX(newWindowTopLeft.y, minimumY);
796 }
797 NSWindow* window = [self window];
798 NSRect windowFrame = NSMakeRect(newWindowTopLeft.x,
799 newWindowTopLeft.y - height,
800 windowWidth, height);
801 [window setFrame:windowFrame display:NO];
802 NSRect folderFrame = NSMakeRect(0, 0, windowWidth, height);
803 [folderView_ setFrame:folderFrame];
804 NSSize newSize = NSMakeSize(windowWidth, 0.0);
805 [self adjustWindowLeft:newWindowTopLeft.x size:newSize scrollingBy:0.0];
806 [self configureWindowLevel];
807 [window display];
808 }
809
810 // TODO(mrossetti): See if the following can be moved into view's viewWillDraw:.
811 - (CGFloat)adjustButtonWidths {
812 CGFloat width = bookmarks::kBookmarkMenuButtonMinimumWidth;
813 // Use the cell's size as the base for determining the desired width of the
814 // button rather than the button's current width. -[cell cellSize] always
815 // returns the 'optimum' size of the cell based on the cell's contents even
816 // if it's less than the current button size. Relying on the button size
817 // would result in buttons that could only get wider but we want to handle
818 // the case where the widest button gets removed from a folder menu.
819 for (BookmarkButton* button in buttons_.get())
820 width = std::max(width, [[button cell] cellSize].width);
821 width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth);
822 // Things look and feel more menu-like if all the buttons are the
823 // full width of the window, especially if there are submenus.
824 for (BookmarkButton* button in buttons_.get()) {
825 NSRect buttonFrame = [button frame];
826 buttonFrame.size.width = width;
827 [button setFrame:buttonFrame];
828 }
829 return width;
830 }
831
832 // Start a "scroll up" timer.
833 - (void)beginScrollWindowUp {
834 [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount];
835 }
836
837 // Start a "scroll down" timer.
838 - (void)beginScrollWindowDown {
839 [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount];
840 }
841
842 // End a scrolling timer. Can be called excessively with no harm.
843 - (void)endScroll {
844 if (scrollTimer_) {
845 [scrollTimer_ invalidate];
846 scrollTimer_ = nil;
847 verticalScrollDelta_ = 0;
848 }
849 }
850
851 - (int)indexOfButton:(BookmarkButton*)button {
852 if (button == nil)
853 return -1;
854 int index = [buttons_ indexOfObject:button];
855 return (index == NSNotFound) ? -1 : index;
856 }
857
858 - (BookmarkButton*)buttonAtIndex:(int)which {
859 if (which < 0 || which >= [self buttonCount])
860 return nil;
861 return [buttons_ objectAtIndex:which];
862 }
863
864 // Private, called by performOneScroll only.
865 // If the button at index contains the mouse it will select it and return YES.
866 // Otherwise returns NO.
867 - (BOOL)selectButtonIfHoveredAtIndex:(int)index {
868 BookmarkButton *btn = [self buttonAtIndex:index];
869 if ([[btn cell] isMouseReallyInside]) {
870 buttonThatMouseIsIn_ = btn;
871 [self setSelectedButtonByIndex:index];
872 return YES;
873 }
874 return NO;
875 }
876
877 // Perform a single scroll of the specified amount.
878 - (void)performOneScroll:(CGFloat)delta {
879 if (delta == 0.0)
880 return;
881 CGFloat finalDelta = [self determineFinalScrollDelta:delta];
882 if (finalDelta == 0.0)
883 return;
884 int index = [self indexOfButton:buttonThatMouseIsIn_];
885 // Check for a current mouse-initiated selection.
886 BOOL maintainHoverSelection =
887 (buttonThatMouseIsIn_ &&
888 [[buttonThatMouseIsIn_ cell] isMouseReallyInside] &&
889 selectedIndex_ != -1 &&
890 index == selectedIndex_);
891 NSRect windowFrame = [[self window] frame];
892 NSSize newSize = NSMakeSize(NSWidth(windowFrame), 0.0);
893 [self adjustWindowLeft:windowFrame.origin.x
894 size:newSize
895 scrollingBy:finalDelta];
896 // We have now scrolled.
897 if (!maintainHoverSelection)
898 return;
899 // Is mouse still in the same hovered button?
900 if ([[buttonThatMouseIsIn_ cell] isMouseReallyInside])
901 return;
902 // The finalDelta scroll direction will tell us us whether to search up or
903 // down the buttons array for the newly hovered button.
904 if (finalDelta < 0.0) { // Scrolled up, so search backwards for new hover.
905 index--;
906 while (index >= 0) {
907 if ([self selectButtonIfHoveredAtIndex:index])
908 return;
909 index--;
910 }
911 } else { // Scrolled down, so search forward for new hovered button.
912 index++;
913 int btnMax = [self buttonCount];
914 while (index < btnMax) {
915 if ([self selectButtonIfHoveredAtIndex:index])
916 return;
917 index++;
918 }
919 }
920 }
921
922 - (CGFloat)determineFinalScrollDelta:(CGFloat)delta {
923 if ((delta > 0.0 && ![scrollUpArrowView_ isHidden]) ||
924 (delta < 0.0 && ![scrollDownArrowView_ isHidden])) {
925 NSWindow* window = [self window];
926 NSRect windowFrame = [window frame];
927 NSScreen* screen = [window screen];
928 NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin;
929 CGFloat scrollY = scrollPosition.y;
930 NSRect scrollerFrame = [scrollView_ frame];
931 CGFloat scrollerY = NSMinY(scrollerFrame);
932 NSRect visibleFrame = [visibleView_ frame];
933 CGFloat visibleY = NSMinY(visibleFrame);
934 CGFloat windowY = NSMinY(windowFrame);
935 CGFloat offset = scrollerY + visibleY + windowY;
936
937 if (delta > 0.0) {
938 // Scrolling up.
939 CGFloat minimumY = NSMinY([screen visibleFrame]) +
940 bookmarks::kScrollWindowVerticalMargin;
941 CGFloat maxUpDelta = scrollY - offset + minimumY;
942 delta = MIN(delta, maxUpDelta);
943 } else {
944 // Scrolling down.
945 NSRect screenFrame = [screen visibleFrame];
946 CGFloat topOfScreen = NSMaxY(screenFrame);
947 NSRect folderFrame = [folderView_ frame];
948 CGFloat folderHeight = NSHeight(folderFrame);
949 CGFloat folderTop = folderHeight - scrollY + offset;
950 CGFloat maxDownDelta =
951 topOfScreen - folderTop - bookmarks::kScrollWindowVerticalMargin;
952 delta = MAX(delta, maxDownDelta);
953 }
954 } else {
955 delta = 0.0;
956 }
957 return delta;
958 }
959
960 // Perform a scroll of the window on the screen.
961 // Called by a timer when scrolling.
962 - (void)performScroll:(NSTimer*)timer {
963 DCHECK(verticalScrollDelta_);
964 [self performOneScroll:verticalScrollDelta_];
965 }
966
967
968 // Add a timer to fire at a regular interval which scrolls the
969 // window vertically |delta|.
970 - (void)addScrollTimerWithDelta:(CGFloat)delta {
971 if (scrollTimer_ && verticalScrollDelta_ == delta)
972 return;
973 [self endScroll];
974 verticalScrollDelta_ = delta;
975 scrollTimer_ = [NSTimer timerWithTimeInterval:kBookmarkBarFolderScrollInterval
976 target:self
977 selector:@selector(performScroll:)
978 userInfo:nil
979 repeats:YES];
980
981 [[NSRunLoop mainRunLoop] addTimer:scrollTimer_ forMode:NSRunLoopCommonModes];
982 }
983
984
985 // Called as a result of our tracking area. Warning: on the main
986 // screen (of a single-screened machine), the minimum mouse y value is
987 // 1, not 0. Also, we do not get events when the mouse is above the
988 // menubar (to be fixed by setting the proper window level; see
989 // initializer).
990 // Note [theEvent window] may not be our window, as we also get these messages
991 // forwarded from BookmarkButton's mouse tracking loop.
992 - (void)mouseMovedOrDragged:(NSEvent*)theEvent {
993 NSPoint eventScreenLocation =
994 [[theEvent window] convertBaseToScreen:[theEvent locationInWindow]];
995
996 // Base hot spot calculations on the positions of the scroll arrow views.
997 NSRect testRect = [scrollDownArrowView_ frame];
998 NSPoint testPoint = [visibleView_ convertPoint:testRect.origin
999 toView:nil];
1000 testPoint = [[self window] convertBaseToScreen:testPoint];
1001 CGFloat closeToTopOfScreen = testPoint.y;
1002
1003 testRect = [scrollUpArrowView_ frame];
1004 testPoint = [visibleView_ convertPoint:testRect.origin toView:nil];
1005 testPoint = [[self window] convertBaseToScreen:testPoint];
1006 CGFloat closeToBottomOfScreen = testPoint.y + testRect.size.height;
1007 if (eventScreenLocation.y <= closeToBottomOfScreen &&
1008 ![scrollUpArrowView_ isHidden]) {
1009 [self beginScrollWindowUp];
1010 } else if (eventScreenLocation.y > closeToTopOfScreen &&
1011 ![scrollDownArrowView_ isHidden]) {
1012 [self beginScrollWindowDown];
1013 } else {
1014 [self endScroll];
1015 }
1016 }
1017
1018 - (void)mouseMoved:(NSEvent*)theEvent {
1019 [self mouseMovedOrDragged:theEvent];
1020 }
1021
1022 - (void)mouseDragged:(NSEvent*)theEvent {
1023 [self mouseMovedOrDragged:theEvent];
1024 }
1025
1026 - (void)mouseExited:(NSEvent*)theEvent {
1027 [self endScroll];
1028 }
1029
1030 // Add a tracking area so we know when the mouse is pinned to the top
1031 // or bottom of the screen. If that happens, and if the mouse
1032 // position overlaps the window, scroll it.
1033 - (void)addOrUpdateScrollTracking {
1034 [self removeScrollTracking];
1035 NSView* view = [[self window] contentView];
1036 scrollTrackingArea_.reset([[CrTrackingArea alloc]
1037 initWithRect:[view bounds]
1038 options:(NSTrackingMouseMoved |
1039 NSTrackingMouseEnteredAndExited |
1040 NSTrackingActiveAlways |
1041 NSTrackingEnabledDuringMouseDrag
1042 )
1043 proxiedOwner:self
1044 userInfo:nil]);
1045 [view addTrackingArea:scrollTrackingArea_.get()];
1046 }
1047
1048 // Remove the tracking area associated with scrolling.
1049 - (void)removeScrollTracking {
1050 if (scrollTrackingArea_.get()) {
1051 [[[self window] contentView] removeTrackingArea:scrollTrackingArea_.get()];
1052 [scrollTrackingArea_.get() clearOwner];
1053 }
1054 scrollTrackingArea_.reset();
1055 }
1056
1057 // Close the old hover-open bookmark folder, and open a new one. We
1058 // do both in one step to allow for a delay in closing the old one.
1059 // See comments above kDragHoverCloseDelay (bookmark_bar_controller.h)
1060 // for more details.
1061 - (void)openBookmarkFolderFromButtonAndCloseOldOne:(id)sender {
1062 // Ignore if sender button is in a window that's just been hidden - that
1063 // would leave us with an orphaned menu. BUG 69002
1064 if ([[sender window] isVisible] != YES)
1065 return;
1066 // If an old submenu exists, close it immediately.
1067 [self closeBookmarkFolder:sender];
1068
1069 // Open a new one if meaningful.
1070 if ([sender isFolder])
1071 [folderTarget_ openBookmarkFolderFromButton:sender];
1072 }
1073
1074 - (NSArray*)buttons {
1075 return buttons_.get();
1076 }
1077
1078 - (void)close {
1079 [folderController_ close];
1080 [super close];
1081 }
1082
1083 - (void)scrollWheel:(NSEvent *)theEvent {
1084 if (![scrollUpArrowView_ isHidden] || ![scrollDownArrowView_ isHidden]) {
1085 // We go negative since an NSScrollView has a flipped coordinate frame.
1086 CGFloat amt = kBookmarkBarFolderScrollWheelAmount * -[theEvent deltaY];
1087 [self performOneScroll:amt];
1088 }
1089 }
1090
1091 #pragma mark Actions Forwarded to Parent BookmarkBarController
1092
1093 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
1094 return [barController_ validateUserInterfaceItem:item];
1095 }
1096
1097 - (IBAction)openBookmark:(id)sender {
1098 [barController_ openBookmark:sender];
1099 }
1100
1101 - (IBAction)openBookmarkInNewForegroundTab:(id)sender {
1102 [barController_ openBookmarkInNewForegroundTab:sender];
1103 }
1104
1105 - (IBAction)openBookmarkInNewWindow:(id)sender {
1106 [barController_ openBookmarkInNewWindow:sender];
1107 }
1108
1109 - (IBAction)openBookmarkInIncognitoWindow:(id)sender {
1110 [barController_ openBookmarkInIncognitoWindow:sender];
1111 }
1112
1113 - (IBAction)editBookmark:(id)sender {
1114 [barController_ editBookmark:sender];
1115 }
1116
1117 - (IBAction)cutBookmark:(id)sender {
1118 [self closeBookmarkFolder:self];
1119 [barController_ cutBookmark:sender];
1120 }
1121
1122 - (IBAction)copyBookmark:(id)sender {
1123 [barController_ copyBookmark:sender];
1124 }
1125
1126 - (IBAction)pasteBookmark:(id)sender {
1127 [barController_ pasteBookmark:sender];
1128 }
1129
1130 - (IBAction)deleteBookmark:(id)sender {
1131 [self closeBookmarkFolder:self];
1132 [barController_ deleteBookmark:sender];
1133 }
1134
1135 - (IBAction)openAllBookmarks:(id)sender {
1136 [barController_ openAllBookmarks:sender];
1137 }
1138
1139 - (IBAction)openAllBookmarksNewWindow:(id)sender {
1140 [barController_ openAllBookmarksNewWindow:sender];
1141 }
1142
1143 - (IBAction)openAllBookmarksIncognitoWindow:(id)sender {
1144 [barController_ openAllBookmarksIncognitoWindow:sender];
1145 }
1146
1147 - (IBAction)addPage:(id)sender {
1148 [barController_ addPage:sender];
1149 }
1150
1151 - (IBAction)addFolder:(id)sender {
1152 [barController_ addFolder:sender];
1153 }
1154
1155 #pragma mark Drag & Drop
1156
1157 // Find something like std::is_between<T>? I can't believe one doesn't exist.
1158 // http://crbug.com/35966
1159 static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
1160 return ((value >= low) && (value <= high));
1161 }
1162
1163 // Return the proposed drop target for a hover open button, or nil if none.
1164 //
1165 // TODO(jrg): this is just like the version in
1166 // bookmark_bar_controller.mm, but vertical instead of horizontal.
1167 // Generalize to be axis independent then share code.
1168 // http://crbug.com/35966
1169 - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point {
1170 for (BookmarkButton* button in buttons_.get()) {
1171 // No early break -- makes no assumption about button ordering.
1172
1173 // Intentionally NOT using NSPointInRect() so that scrolling into
1174 // a submenu doesn't cause it to be closed.
1175 if (ValueInRangeInclusive(NSMinY([button frame]),
1176 point.y,
1177 NSMaxY([button frame]))) {
1178
1179 // Over a button but let's be a little more specific
1180 // (e.g. over the middle half).
1181 NSRect frame = [button frame];
1182 NSRect middleHalfOfButton = NSInsetRect(frame, 0, frame.size.height / 4);
1183 if (ValueInRangeInclusive(NSMinY(middleHalfOfButton),
1184 point.y,
1185 NSMaxY(middleHalfOfButton))) {
1186 // It makes no sense to drop on a non-folder; there is no hover.
1187 if (![button isFolder])
1188 return nil;
1189 // Got it!
1190 return button;
1191 } else {
1192 // Over a button but not over the middle half.
1193 return nil;
1194 }
1195 }
1196 }
1197 // Not hovering over a button.
1198 return nil;
1199 }
1200
1201 // TODO(jrg): again we have code dup, sort of, with
1202 // bookmark_bar_controller.mm, but the axis is changed. One minor
1203 // difference is accomodation for the "empty" button (which may not
1204 // exist in the future).
1205 // http://crbug.com/35966
1206 - (int)indexForDragToPoint:(NSPoint)point {
1207 // Identify which buttons we are between. For now, assume a button
1208 // location is at the center point of its view, and that an exact
1209 // match means "place before".
1210 // TODO(jrg): revisit position info based on UI team feedback.
1211 // dropLocation is in bar local coordinates.
1212 // http://crbug.com/36276
1213 NSPoint dropLocation =
1214 [folderView_ convertPoint:point
1215 fromView:[[self window] contentView]];
1216 BookmarkButton* buttonToTheTopOfDraggedButton = nil;
1217 // Buttons are laid out in this array from top to bottom (screen
1218 // wise), which means "biggest y" --> "smallest y".
1219 for (BookmarkButton* button in buttons_.get()) {
1220 CGFloat midpoint = NSMidY([button frame]);
1221 if (dropLocation.y > midpoint) {
1222 break;
1223 }
1224 buttonToTheTopOfDraggedButton = button;
1225 }
1226
1227 // TODO(jrg): On Windows, dropping onto (empty) highlights the
1228 // entire drop location and does not use an insertion point.
1229 // http://crbug.com/35967
1230 if (!buttonToTheTopOfDraggedButton) {
1231 // We are at the very top (we broke out of the loop on the first try).
1232 return 0;
1233 }
1234 if ([buttonToTheTopOfDraggedButton isEmpty]) {
1235 // There is a button but it's an empty placeholder.
1236 // Default to inserting on top of it.
1237 return 0;
1238 }
1239 const BookmarkNode* beforeNode = [buttonToTheTopOfDraggedButton
1240 bookmarkNode];
1241 DCHECK(beforeNode);
1242 // Be careful if the number of buttons != number of nodes.
1243 return ((beforeNode->parent()->GetIndexOf(beforeNode) + 1) -
1244 [[parentButton_ cell] startingChildIndex]);
1245 }
1246
1247 // TODO(jrg): Yet more code dup.
1248 // http://crbug.com/35966
1249 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
1250 to:(NSPoint)point
1251 copy:(BOOL)copy {
1252 DCHECK(sourceNode);
1253
1254 // Drop destination.
1255 const BookmarkNode* destParent = NULL;
1256 int destIndex = 0;
1257
1258 // First check if we're dropping on a button. If we have one, and
1259 // it's a folder, drop in it.
1260 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1261 if ([button isFolder]) {
1262 destParent = [button bookmarkNode];
1263 // Drop it at the end.
1264 destIndex = [button bookmarkNode]->child_count();
1265 } else {
1266 // Else we're dropping somewhere in the folder, so find the right spot.
1267 destParent = [parentButton_ bookmarkNode];
1268 destIndex = [self indexForDragToPoint:point];
1269 // Be careful if the number of buttons != number of nodes.
1270 destIndex += [[parentButton_ cell] startingChildIndex];
1271 }
1272
1273 // Prevent cycles.
1274 BOOL wasCopiedOrMoved = NO;
1275 if (!destParent->HasAncestor(sourceNode)) {
1276 if (copy)
1277 [self bookmarkModel]->Copy(sourceNode, destParent, destIndex);
1278 else
1279 [self bookmarkModel]->Move(sourceNode, destParent, destIndex);
1280 wasCopiedOrMoved = YES;
1281 // Movement of a node triggers observers (like us) to rebuild the
1282 // bar so we don't have to do so explicitly.
1283 }
1284
1285 return wasCopiedOrMoved;
1286 }
1287
1288 // TODO(maf): Implement live drag & drop animation using this hook.
1289 - (void)setDropInsertionPos:(CGFloat)where {
1290 }
1291
1292 // TODO(maf): Implement live drag & drop animation using this hook.
1293 - (void)clearDropInsertionPos {
1294 }
1295
1296 #pragma mark NSWindowDelegate Functions
1297
1298 - (void)windowWillClose:(NSNotification*)notification {
1299 // Also done by the dealloc method, but also doing it here is quicker and
1300 // more reliable.
1301 [parentButton_ forceButtonBorderToStayOnAlways:NO];
1302
1303 // If a "hover open" is pending when the bookmark bar folder is
1304 // closed, be sure it gets cancelled.
1305 [NSObject cancelPreviousPerformRequestsWithTarget:self];
1306
1307 [self endScroll]; // Just in case we were scrolling.
1308 [barController_ childFolderWillClose:self];
1309 [self closeBookmarkFolder:self];
1310 [self autorelease];
1311 }
1312
1313 #pragma mark BookmarkButtonDelegate Protocol
1314
1315 - (void)fillPasteboard:(NSPasteboard*)pboard
1316 forDragOfButton:(BookmarkButton*)button {
1317 [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
1318
1319 // Close our folder menu and submenus since we know we're going to be dragged.
1320 [self closeBookmarkFolder:self];
1321 }
1322
1323 // Called from BookmarkButton.
1324 // Unlike bookmark_bar_controller's version, we DO default to being enabled.
1325 - (void)mouseEnteredButton:(id)sender event:(NSEvent*)event {
1326 [[NSCursor arrowCursor] set];
1327
1328 buttonThatMouseIsIn_ = sender;
1329 [self setSelectedButtonByIndex:[self indexOfButton:sender]];
1330
1331 // Cancel a previous hover if needed.
1332 [NSObject cancelPreviousPerformRequestsWithTarget:self];
1333
1334 // If already opened, then we exited but re-entered the button
1335 // (without entering another button open), do nothing.
1336 if ([folderController_ parentButton] == sender)
1337 return;
1338
1339 [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:)
1340 withObject:sender
1341 afterDelay:bookmarks::kHoverOpenDelay
1342 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
1343 }
1344
1345 // Called from the BookmarkButton
1346 - (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
1347 if (buttonThatMouseIsIn_ == sender)
1348 buttonThatMouseIsIn_ = nil;
1349 [self setSelectedButtonByIndex:-1];
1350
1351 // Stop any timer about opening a new hover-open folder.
1352
1353 // Since a performSelector:withDelay: on self retains self, it is
1354 // possible that a cancelPreviousPerformRequestsWithTarget: reduces
1355 // the refcount to 0, releasing us. That's a bad thing to do while
1356 // this object (or others it may own) is in the event chain. Thus
1357 // we have a retain/autorelease.
48 [self retain]; 1358 [self retain];
49 1359 [NSObject cancelPreviousPerformRequestsWithTarget:self];
50 // If the system supports opening the menu at a specific point, do so. 1360 [self autorelease];
51 // Otherwise, it will be opened at the mouse event location. Eventually these 1361 }
52 // should be switched to NSPopUpButtonCells so that this is taken care of 1362
53 // automatically. 1363 - (NSWindow*)browserWindow {
54 if ([menu_ respondsToSelector: 1364 return [parentController_ browserWindow];
55 @selector(popUpMenuPositioningItem:atLocation:inView:)]) { 1365 }
56 NSPoint point = [parentButton_ frame].origin; 1366
57 point.y -= bookmarks::kBookmarkBarMenuOffset; 1367 - (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
58 [menu_ popUpMenuPositioningItem:nil 1368 return [barController_ canEditBookmarks] &&
59 atLocation:point 1369 [barController_ canEditBookmark:[button bookmarkNode]];
60 inView:[parentButton_ superview]]; 1370 }
1371
1372 - (void)didDragBookmarkToTrash:(BookmarkButton*)button {
1373 [barController_ didDragBookmarkToTrash:button];
1374 }
1375
1376 - (void)bookmarkDragDidEnd:(BookmarkButton*)button
1377 operation:(NSDragOperation)operation {
1378 [barController_ bookmarkDragDidEnd:button
1379 operation:operation];
1380 }
1381
1382
1383 #pragma mark BookmarkButtonControllerProtocol
1384
1385 // Recursively close all bookmark folders.
1386 - (void)closeAllBookmarkFolders {
1387 // Closing the top level implicitly closes all children.
1388 [barController_ closeAllBookmarkFolders];
1389 }
1390
1391 // Close our bookmark folder (a sub-controller) if we have one.
1392 - (void)closeBookmarkFolder:(id)sender {
1393 if (folderController_) {
1394 // Make this menu key, so key status doesn't go back to the browser
1395 // window when the submenu closes.
1396 [[self window] makeKeyWindow];
1397 [self setSubFolderGrowthToRight:YES];
1398 [[folderController_ window] close];
1399 folderController_ = nil;
1400 }
1401 }
1402
1403 - (BookmarkModel*)bookmarkModel {
1404 return [barController_ bookmarkModel];
1405 }
1406
1407 - (BOOL)draggingAllowed:(id<NSDraggingInfo>)info {
1408 return [barController_ draggingAllowed:info];
1409 }
1410
1411 // TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1412 // Most of the work (e.g. drop indicator) is taken care of in the
1413 // folder_view. Here we handle hover open issues for subfolders.
1414 // Caution: there are subtle differences between this one and
1415 // bookmark_bar_controller.mm's version.
1416 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
1417 NSPoint currentLocation = [info draggingLocation];
1418 BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation];
1419
1420 // Don't allow drops that would result in cycles.
1421 if (button) {
1422 NSData* data = [[info draggingPasteboard]
1423 dataForType:kBookmarkButtonDragType];
1424 if (data && [info draggingSource]) {
1425 BookmarkButton* sourceButton = nil;
1426 [data getBytes:&sourceButton length:sizeof(sourceButton)];
1427 const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1428 const BookmarkNode* destNode = [button bookmarkNode];
1429 if (destNode->HasAncestor(sourceNode))
1430 button = nil;
1431 }
1432 }
1433 // Delegate handling of dragging over a button to the |hoverState_| member.
1434 return [hoverState_ draggingEnteredButton:button];
1435 }
1436
1437 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info {
1438 return NSDragOperationMove;
1439 }
1440
1441 // Unlike bookmark_bar_controller, we need to keep track of dragging state.
1442 // We also need to make sure we cancel the delayed hover close.
1443 - (void)draggingExited:(id<NSDraggingInfo>)info {
1444 // NOT the same as a cancel --> we may have moved the mouse into the submenu.
1445 // Delegate handling of the hover button to the |hoverState_| member.
1446 [hoverState_ draggingExited];
1447 }
1448
1449 - (BOOL)dragShouldLockBarVisibility {
1450 return [parentController_ dragShouldLockBarVisibility];
1451 }
1452
1453 // TODO(jrg): ARGH more code dup.
1454 // http://crbug.com/35966
1455 - (BOOL)dragButton:(BookmarkButton*)sourceButton
1456 to:(NSPoint)point
1457 copy:(BOOL)copy {
1458 DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
1459 const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1460 return [self dragBookmark:sourceNode to:point copy:copy];
1461 }
1462
1463 // TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1464 // http://crbug.com/35966
1465 - (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
1466 BOOL dragged = NO;
1467 std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
1468 if (nodes.size()) {
1469 BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
1470 NSPoint dropPoint = [info draggingLocation];
1471 for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
1472 it != nodes.end(); ++it) {
1473 const BookmarkNode* sourceNode = *it;
1474 dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
1475 }
1476 }
1477 return dragged;
1478 }
1479
1480 // TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1481 // http://crbug.com/35966
1482 - (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
1483 std::vector<const BookmarkNode*> dragDataNodes;
1484 BookmarkNodeData dragData;
1485 if(dragData.ReadFromDragClipboard()) {
1486 BookmarkModel* bookmarkModel = [self bookmarkModel];
1487 Profile* profile = bookmarkModel->profile();
1488 std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile));
1489 dragDataNodes.assign(nodes.begin(), nodes.end());
1490 }
1491 return dragDataNodes;
1492 }
1493
1494 // Return YES if we should show the drop indicator, else NO.
1495 // TODO(jrg): ARGH code dup!
1496 // http://crbug.com/35966
1497 - (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
1498 return ![self buttonForDroppingOnAtPoint:point];
1499 }
1500
1501 // Button selection change code to support type to select and arrow key events.
1502 #pragma mark Keyboard Support
1503
1504 // Scroll the menu to show the selected button, if it's not already visible.
1505 - (void)showSelectedButton {
1506 int bMaxIndex = [self buttonCount] - 1; // Max array index in button array.
1507
1508 // Is there a valid selected button?
1509 if (bMaxIndex < 0 || selectedIndex_ < 0 || selectedIndex_ > bMaxIndex)
1510 return;
1511
1512 // Is the menu scrollable anyway?
1513 if (![self canScrollUp] && ![self canScrollDown])
1514 return;
1515
1516 // Now check to see if we need to scroll, which way, and how far.
1517 CGFloat delta = 0.0;
1518 NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
1519 CGFloat itemBottom = (bMaxIndex - selectedIndex_) *
1520 bookmarks::kBookmarkFolderButtonHeight;
1521 CGFloat itemTop = itemBottom + bookmarks::kBookmarkFolderButtonHeight;
1522 CGFloat viewHeight = NSHeight([scrollView_ frame]);
1523
1524 if (scrollPoint.y > itemBottom) { // Need to scroll down.
1525 delta = scrollPoint.y - itemBottom;
1526 } else if ((scrollPoint.y + viewHeight) < itemTop) { // Need to scroll up.
1527 delta = -(itemTop - (scrollPoint.y + viewHeight));
1528 } else { // No need to scroll.
1529 return;
1530 }
1531
1532 [self performOneScroll:delta];
1533 }
1534
1535 // All changes to selectedness of buttons (aka fake menu items) ends up
1536 // calling this method to actually flip the state of items.
1537 // Needs to handle -1 as the invalid index (when nothing is selected) and
1538 // greater than range values too.
1539 - (void)setStateOfButtonByIndex:(int)index
1540 state:(bool)state {
1541 if (index >= 0 && index < [self buttonCount])
1542 [[buttons_ objectAtIndex:index] highlight:state];
1543 }
1544
1545 // Selects the required button and deselects the previously selected one.
1546 // An index of -1 means no selection.
1547 - (void)setSelectedButtonByIndex:(int)index {
1548 if (index == selectedIndex_)
1549 return;
1550
1551 [self setStateOfButtonByIndex:selectedIndex_ state:NO];
1552 [self setStateOfButtonByIndex:index state:YES];
1553 selectedIndex_ = index;
1554
1555 [self showSelectedButton];
1556 }
1557
1558 - (void)clearInputText {
1559 [typedPrefix_ release];
1560 typedPrefix_ = nil;
1561 }
1562
1563 // Find the earliest item in the folder which has the target prefix.
1564 // Returns nil if there is no prefix or there are no matches.
1565 // These are in no particular order, and not particularly numerous, so linear
1566 // search should be OK.
1567 // -1 means no match.
1568 - (int)earliestBookmarkIndexWithPrefix:(NSString*)prefix {
1569 if ([prefix length] == 0) // Also handles nil.
1570 return -1;
1571 int maxButtons = [buttons_ count];
1572 NSString *lowercasePrefix = [prefix lowercaseString];
1573 for (int i = 0 ; i < maxButtons ; ++i) {
1574 BookmarkButton* button = [buttons_ objectAtIndex:i];
1575 if ([[[button title] lowercaseString] hasPrefix:lowercasePrefix])
1576 return i;
1577 }
1578 return -1;
1579 }
1580
1581 - (void)setSelectedButtonByPrefix:(NSString*)prefix {
1582 [self setSelectedButtonByIndex:[self earliestBookmarkIndexWithPrefix:prefix]];
1583 }
1584
1585 - (void)selectPrevious {
1586 int newIndex;
1587 if (selectedIndex_ == 0)
1588 return;
1589 if (selectedIndex_ < 0)
1590 newIndex = [self buttonCount] -1;
1591 else
1592 newIndex = std::max(selectedIndex_ - 1, 0);
1593 [self setSelectedButtonByIndex:newIndex];
1594 }
1595
1596 - (void) selectNext {
1597 if (selectedIndex_ + 1 < [self buttonCount])
1598 [self setSelectedButtonByIndex:selectedIndex_ + 1];
1599 }
1600
1601 - (BOOL)handleInputText:(NSString*)newText {
1602 const unichar kUnicodeEscape = 0x001B;
1603 const unichar kUnicodeSpace = 0x0020;
1604
1605 // Event goes to the deepest nested open submenu.
1606 if (folderController_)
1607 return [folderController_ handleInputText:newText];
1608
1609 // Look for arrow keys or other function keys.
1610 if ([newText length] == 1) {
1611 // Get the 16-bit unicode char.
1612 unichar theChar = [newText characterAtIndex:0];
1613 switch (theChar) {
1614
1615 // Keys that trigger opening of the selection.
1616 case kUnicodeSpace: // Space.
1617 case NSNewlineCharacter:
1618 case NSCarriageReturnCharacter:
1619 case NSEnterCharacter:
1620 if (selectedIndex_ >= 0 && selectedIndex_ < [self buttonCount]) {
1621 [self openBookmark:[buttons_ objectAtIndex:selectedIndex_]];
1622 return NO; // NO because the selection-handling code will close later.
1623 } else {
1624 return YES; // Triggering with no selection closes the menu.
1625 }
1626 // Keys that cancel and close the menu.
1627 case kUnicodeEscape:
1628 case NSDeleteCharacter:
1629 case NSBackspaceCharacter:
1630 [self clearInputText];
1631 return YES;
1632 // Keys that change selection directionally.
1633 case NSUpArrowFunctionKey:
1634 [self clearInputText];
1635 [self selectPrevious];
1636 return NO;
1637 case NSDownArrowFunctionKey:
1638 [self clearInputText];
1639 [self selectNext];
1640 return NO;
1641 // Keys that open and close submenus.
1642 case NSRightArrowFunctionKey: {
1643 BookmarkButton* btn = [self buttonAtIndex:selectedIndex_];
1644 if (btn && [btn isFolder]) {
1645 [self openBookmarkFolderFromButtonAndCloseOldOne:btn];
1646 [folderController_ selectNext];
1647 }
1648 [self clearInputText];
1649 return NO;
1650 }
1651 case NSLeftArrowFunctionKey:
1652 [self clearInputText];
1653 [parentController_ closeBookmarkFolder:self];
1654 return NO;
1655
1656 // Check for other keys that should close the menu.
1657 default: {
1658 if (theChar > NSUpArrowFunctionKey &&
1659 theChar <= NSModeSwitchFunctionKey) {
1660 [self clearInputText];
1661 return YES;
1662 }
1663 break;
1664 }
1665 }
1666 }
1667
1668 // It is a char or string worth adding to the type-select buffer.
1669 NSString *newString = (!typedPrefix_) ?
1670 newText : [typedPrefix_ stringByAppendingString:newText];
1671 [typedPrefix_ release];
1672 typedPrefix_ = [newString retain];
1673 [self setSelectedButtonByPrefix:typedPrefix_];
1674 return NO;
1675 }
1676
1677 // Return the y position for a drop indicator.
1678 //
1679 // TODO(jrg): again we have code dup, sort of, with
1680 // bookmark_bar_controller.mm, but the axis is changed.
1681 // http://crbug.com/35966
1682 - (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
1683 CGFloat y = 0;
1684 int destIndex = [self indexForDragToPoint:point];
1685 int numButtons = static_cast<int>([buttons_ count]);
1686
1687 // If it's a drop strictly between existing buttons or at the very beginning
1688 if (destIndex >= 0 && destIndex < numButtons) {
1689 // ... put the indicator right between the buttons.
1690 BookmarkButton* button =
1691 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)];
1692 DCHECK(button);
1693 NSRect buttonFrame = [button frame];
1694 y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding;
1695
1696 // If it's a drop at the end (past the last button, if there are any) ...
1697 } else if (destIndex == numButtons) {
1698 // and if it's past the last button ...
1699 if (numButtons > 0) {
1700 // ... find the last button, and put the indicator below it.
1701 BookmarkButton* button =
1702 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
1703 DCHECK(button);
1704 NSRect buttonFrame = [button frame];
1705 y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding;
1706
1707 }
61 } else { 1708 } else {
62 [NSMenu popUpContextMenu:menu_ 1709 NOTREACHED();
63 withEvent:[NSApp currentEvent] 1710 }
64 forView:parentButton_]; 1711
65 } 1712 return y;
66 } 1713 }
67 1714
68 - (void)closeMenu { 1715 - (ui::ThemeProvider*)themeProvider {
69 NSArray* modes = [NSArray arrayWithObject:NSRunLoopCommonModes]; 1716 return [parentController_ themeProvider];
70 [menu_ performSelector:@selector(cancelTracking) 1717 }
71 withObject:nil 1718
72 afterDelay:0.0 1719 - (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
73 inModes:modes]; 1720 // Do nothing.
74 } 1721 }
75 1722
76 - (void)setOffTheSideNodeStartIndex:(size_t)index { 1723 - (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
77 menuBridge_->set_off_the_side_node_start_index(index); 1724 // Do nothing.
78 } 1725 }
79 1726
80 - (void)bookmarkMenuDidClose:(BookmarkMenuCocoaController*)controller { 1727 - (BookmarkBarFolderController*)folderController {
81 // Inform the bookmark bar that the folder has closed on the next iteration 1728 return folderController_;
82 // of the event loop. If the menu was closed via a click event on a folder 1729 }
83 // button, this message will be received before dispatching the click event 1730
84 // to the button. If the button is the same folder button that ran the menu 1731 - (void)faviconLoadedForNode:(const BookmarkNode*)node {
85 // in the first place, this will recursively pop open the menu because the 1732 for (BookmarkButton* button in buttons_.get()) {
86 // active folder will be nil-ed by |-closeBookmarkFolder:|. To prevent that, 1733 if ([button bookmarkNode] == node) {
87 // perform the selector on the next iteration of the loop. 1734 [button setImage:[barController_ faviconForNode:node]];
88 [barController_ performSelector:@selector(closeBookmarkFolder:) 1735 [button setNeedsDisplay:YES];
89 withObject:self 1736 return;
90 afterDelay:0.0]; 1737 }
91 1738 }
92 // This controller is created on-demand and should be released when the menu 1739
93 // closes because a new one will be created when it is opened again. 1740 // Node was not in this menu, try submenu.
94 [self autorelease]; 1741 if (folderController_)
95 } 1742 [folderController_ faviconLoadedForNode:node];
96 1743 }
97 @end 1744
98 1745 // Add a new folder controller as triggered by the given folder button.
99 //////////////////////////////////////////////////////////////////////////////// 1746 - (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
100 1747 if (folderController_)
101 @implementation BookmarkBarFolderController (ExposedForTesting) 1748 [self closeBookmarkFolder:self];
102 1749
103 - (BookmarkMenuBridge*)menuBridge { 1750 // Folder controller, like many window controllers, owns itself.
104 return menuBridge_.get(); 1751 folderController_ =
105 } 1752 [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
106 1753 parentController:self
107 @end 1754 barController:barController_];
1755 [folderController_ showWindow:self];
1756 }
1757
1758 - (void)openAll:(const BookmarkNode*)node
1759 disposition:(WindowOpenDisposition)disposition {
1760 [barController_ openAll:node disposition:disposition];
1761 }
1762
1763 - (void)addButtonForNode:(const BookmarkNode*)node
1764 atIndex:(NSInteger)buttonIndex {
1765 // Propose the frame for the new button. By default, this will be set to the
1766 // topmost button's frame (and there will always be one) offset upward in
1767 // anticipation of insertion.
1768 NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame];
1769 newButtonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1770 // When adding a button to an empty folder we must remove the 'empty'
1771 // placeholder button. This can be detected by checking for a parent
1772 // child count of 1.
1773 const BookmarkNode* parentNode = node->parent();
1774 if (parentNode->child_count() == 1) {
1775 BookmarkButton* emptyButton = [buttons_ lastObject];
1776 newButtonFrame = [emptyButton frame];
1777 [emptyButton setDelegate:nil];
1778 [emptyButton removeFromSuperview];
1779 [buttons_ removeLastObject];
1780 }
1781
1782 if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count])
1783 buttonIndex = [buttons_ count];
1784
1785 // Offset upward by one button height all buttons above insertion location.
1786 BookmarkButton* button = nil; // Remember so it can be de-highlighted.
1787 for (NSInteger i = 0; i < buttonIndex; ++i) {
1788 button = [buttons_ objectAtIndex:i];
1789 // Remember this location in case it's the last button being moved
1790 // which is where the new button will be located.
1791 newButtonFrame = [button frame];
1792 NSRect buttonFrame = [button frame];
1793 buttonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1794 [button setFrame:buttonFrame];
1795 }
1796 [[button cell] mouseExited:nil]; // De-highlight.
1797 BookmarkButton* newButton = [self makeButtonForNode:node
1798 frame:newButtonFrame];
1799 [buttons_ insertObject:newButton atIndex:buttonIndex];
1800 [folderView_ addSubview:newButton];
1801
1802 // Close any child folder(s) which may still be open.
1803 [self closeBookmarkFolder:self];
1804
1805 [self adjustWindowForButtonCount:[buttons_ count]];
1806 }
1807
1808 // More code which essentially duplicates that of BookmarkBarController.
1809 // TODO(mrossetti,jrg): http://crbug.com/35966
1810 - (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
1811 DCHECK([urls count] == [titles count]);
1812 BOOL nodesWereAdded = NO;
1813 // Figure out where these new bookmarks nodes are to be added.
1814 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1815 BookmarkModel* bookmarkModel = [self bookmarkModel];
1816 const BookmarkNode* destParent = NULL;
1817 int destIndex = 0;
1818 if ([button isFolder]) {
1819 destParent = [button bookmarkNode];
1820 // Drop it at the end.
1821 destIndex = [button bookmarkNode]->child_count();
1822 } else {
1823 // Else we're dropping somewhere in the folder, so find the right spot.
1824 destParent = [parentButton_ bookmarkNode];
1825 destIndex = [self indexForDragToPoint:point];
1826 // Be careful if the number of buttons != number of nodes.
1827 destIndex += [[parentButton_ cell] startingChildIndex];
1828 }
1829
1830 // Create and add the new bookmark nodes.
1831 size_t urlCount = [urls count];
1832 for (size_t i = 0; i < urlCount; ++i) {
1833 GURL gurl;
1834 const char* string = [[urls objectAtIndex:i] UTF8String];
1835 if (string)
1836 gurl = GURL(string);
1837 // We only expect to receive valid URLs.
1838 DCHECK(gurl.is_valid());
1839 if (gurl.is_valid()) {
1840 bookmarkModel->AddURL(destParent,
1841 destIndex++,
1842 base::SysNSStringToUTF16([titles objectAtIndex:i]),
1843 gurl);
1844 nodesWereAdded = YES;
1845 }
1846 }
1847 return nodesWereAdded;
1848 }
1849
1850 - (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
1851 if (fromIndex != toIndex) {
1852 if (toIndex == -1)
1853 toIndex = [buttons_ count];
1854 BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
1855 if (movedButton == buttonThatMouseIsIn_)
1856 buttonThatMouseIsIn_ = nil;
1857 [buttons_ removeObjectAtIndex:fromIndex];
1858 NSRect movedFrame = [movedButton frame];
1859 NSPoint toOrigin = movedFrame.origin;
1860 [movedButton setHidden:YES];
1861 if (fromIndex < toIndex) {
1862 BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
1863 toOrigin = [targetButton frame].origin;
1864 for (NSInteger i = fromIndex; i < toIndex; ++i) {
1865 BookmarkButton* button = [buttons_ objectAtIndex:i];
1866 NSRect frame = [button frame];
1867 frame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1868 [button setFrameOrigin:frame.origin];
1869 }
1870 } else {
1871 BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex];
1872 toOrigin = [targetButton frame].origin;
1873 for (NSInteger i = fromIndex - 1; i >= toIndex; --i) {
1874 BookmarkButton* button = [buttons_ objectAtIndex:i];
1875 NSRect buttonFrame = [button frame];
1876 buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1877 [button setFrameOrigin:buttonFrame.origin];
1878 }
1879 }
1880 [buttons_ insertObject:movedButton atIndex:toIndex];
1881 [movedButton setFrameOrigin:toOrigin];
1882 [movedButton setHidden:NO];
1883 }
1884 }
1885
1886 // TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1887 - (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
1888 // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360
1889 BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
1890 NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
1891
1892 // If a hover-open is pending, cancel it.
1893 if (oldButton == buttonThatMouseIsIn_) {
1894 [NSObject cancelPreviousPerformRequestsWithTarget:self];
1895 buttonThatMouseIsIn_ = nil;
1896 }
1897
1898 // Deleting a button causes rearrangement that enables us to lose a
1899 // mouse-exited event. This problem doesn't appear to exist with
1900 // other keep-menu-open options (e.g. add folder). Since the
1901 // showsBorderOnlyWhileMouseInside uses a tracking area, simple
1902 // tricks (e.g. sending an extra mouseExited: to the button) don't
1903 // fix the problem.
1904 // http://crbug.com/54324
1905 for (NSButton* button in buttons_.get()) {
1906 if ([button showsBorderOnlyWhileMouseInside]) {
1907 [button setShowsBorderOnlyWhileMouseInside:NO];
1908 [button setShowsBorderOnlyWhileMouseInside:YES];
1909 }
1910 }
1911
1912 [oldButton setDelegate:nil];
1913 [oldButton removeFromSuperview];
1914 [buttons_ removeObjectAtIndex:buttonIndex];
1915 for (NSInteger i = 0; i < buttonIndex; ++i) {
1916 BookmarkButton* button = [buttons_ objectAtIndex:i];
1917 NSRect buttonFrame = [button frame];
1918 buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1919 [button setFrame:buttonFrame];
1920 }
1921 // Search for and adjust submenus, if necessary.
1922 NSInteger buttonCount = [buttons_ count];
1923 if (buttonCount) {
1924 BookmarkButton* subButton = [folderController_ parentButton];
1925 for (NSButton* aButton in buttons_.get()) {
1926 // If this button is showing its menu then we need to move the menu, too.
1927 if (aButton == subButton)
1928 [folderController_ offsetFolderMenuWindow:NSMakeSize(0.0,
1929 bookmarks::kBookmarkBarHeight)];
1930 }
1931 } else {
1932 // If all nodes have been removed from this folder then add in the
1933 // 'empty' placeholder button.
1934 NSRect buttonFrame =
1935 NSMakeRect(0.0, 0.0, bookmarks::kDefaultBookmarkWidth,
1936 bookmarks::kBookmarkFolderButtonHeight);
1937 BookmarkButton* button = [self makeButtonForNode:nil
1938 frame:buttonFrame];
1939 [buttons_ addObject:button];
1940 [folderView_ addSubview:button];
1941 buttonCount = 1;
1942 }
1943
1944 [self adjustWindowForButtonCount:buttonCount];
1945
1946 if (animate && !ignoreAnimations_)
1947 NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
1948 NSZeroSize, nil, nil, nil);
1949 }
1950
1951 - (id<BookmarkButtonControllerProtocol>)controllerForNode:
1952 (const BookmarkNode*)node {
1953 // See if we are holding this node, otherwise see if it is in our
1954 // hierarchy of visible folder menus.
1955 if ([parentButton_ bookmarkNode] == node)
1956 return self;
1957 return [folderController_ controllerForNode:node];
1958 }
1959
1960 #pragma mark TestingAPI Only
1961
1962 - (BOOL)canScrollUp {
1963 return ![scrollUpArrowView_ isHidden];
1964 }
1965
1966 - (BOOL)canScrollDown {
1967 return ![scrollDownArrowView_ isHidden];
1968 }
1969
1970 - (CGFloat)verticalScrollArrowHeight {
1971 return verticalScrollArrowHeight_;
1972 }
1973
1974 - (NSView*)visibleView {
1975 return visibleView_;
1976 }
1977
1978 - (NSScrollView*)scrollView {
1979 return scrollView_;
1980 }
1981
1982 - (NSView*)folderView {
1983 return folderView_;
1984 }
1985
1986 - (void)setIgnoreAnimations:(BOOL)ignore {
1987 ignoreAnimations_ = ignore;
1988 }
1989
1990 - (BookmarkButton*)buttonThatMouseIsIn {
1991 return buttonThatMouseIsIn_;
1992 }
1993
1994 @end // BookmarkBarFolderController
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698