diff options
author | jrg@chromium.org <jrg@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-08-20 21:24:29 +0000 |
---|---|---|
committer | jrg@chromium.org <jrg@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-08-20 21:24:29 +0000 |
commit | 7b2b99405d3f5c413cae047291a8c75176f5850d (patch) | |
tree | b41c5b940df10e7cc53676f25c3e6c7d2ffb7f69 /chrome/browser/cocoa | |
parent | 0b305d8786f0ed1c336cb4e78221ca564ba187f7 (diff) | |
download | chromium_src-7b2b99405d3f5c413cae047291a8c75176f5850d.zip chromium_src-7b2b99405d3f5c413cae047291a8c75176f5850d.tar.gz chromium_src-7b2b99405d3f5c413cae047291a8c75176f5850d.tar.bz2 |
Bookmark STAR bubble.
BUG=http://crbug.com/14929
Sample image attached to bug.
TEST=Click the STAR to add a bookmark.
Watch bubble come up. Title is "Bookmark added!"
Confirm fields are OK.
Switch tabs and see bubble go away.
Click STAR again.
Watch bubble come up. Title is "Bookmark"
Make sure all the buttons work (Edit, Close, Remove).
Make sure you can change the title and parent folder.
Make sure "Choose another folder..." opens edit window.
Review URL: http://codereview.chromium.org/171016
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@23886 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/cocoa')
21 files changed, 802 insertions, 12 deletions
diff --git a/chrome/browser/cocoa/bookmark_bar_controller.mm b/chrome/browser/cocoa/bookmark_bar_controller.mm index 1f64cf4..75e3441 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller.mm +++ b/chrome/browser/cocoa/bookmark_bar_controller.mm @@ -411,9 +411,9 @@ const CGFloat kBookmarkHorizontalPadding = 1.0; initWithParentWindow:[[self view] window] profile:profile_ node:node]; - [controller runModal]; + [controller runAsModalSheet]; - // runModal will run the window as a sheet. The + // runAsModalSheet will run the window as a sheet. The // BookmarkNameFolderController will release itself when the sheet // ends. } diff --git a/chrome/browser/cocoa/bookmark_bubble_controller.h b/chrome/browser/cocoa/bookmark_bubble_controller.h new file mode 100644 index 0000000..0760fa0 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_controller.h @@ -0,0 +1,98 @@ +// 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 <Cocoa/Cocoa.h> +#include "base/scoped_nsobject.h" + +class BookmarkModel; +class BookmarkNode; +@class BookmarkBubbleController; + +// Protocol for a BookmarkBubbleController's (BBC's) delegate. +@protocol BookmarkBubbleControllerDelegate + +// The bubble asks the delegate to perform an edit when needed. +- (void)editBookmarkNode:(const BookmarkNode*)node; + +// The bubble tells its delegate when it's done and can be deallocated. +- (void)doneWithBubbleController:(BookmarkBubbleController*)controller; + +@end + +// Controller for the bookmark bubble. The bookmark bubble is a +// bubble that pops up when clicking on the STAR next to the URL to +// add or remove it as a bookmark. This bubble allows for editing of +// the bookmark in various ways (name, folder, etc.) +// +// The bubble is stored in a nib as a view, not as a window, so we can +// make it an actual bubble. There is no nib-rific way to encode a +// NSBorderlessWindowMask NSWindow, and the style of an NSWindow can't +// be set other than init time. To deal, we create the NSWindow +// programatically, but encode the view in a nib. Thus, +// BookmarkBubbleController is an NSViewController, not an +// NSWindowController. +@interface BookmarkBubbleController : NSViewController { + @private + // Unexpected for this controller, perhaps, but our window does NOT + // come from a nib. + scoped_nsobject<NSWindow> window_; + + id<BookmarkBubbleControllerDelegate> delegate_; // weak like other delegates + NSWindow* parentWindow_; // weak + NSPoint topLeftForBubble_; + + // Both weak; owned by the current browser's profile + BookmarkModel* model_; + const BookmarkNode* node_; + + // A mapping from titles to nodes so we only have to walk this once. + scoped_nsobject<NSMutableArray> titleMapping_; + + BOOL alreadyBookmarked_; + scoped_nsobject<NSString> chooseAnotherFolder_; + + IBOutlet NSTextField* bigTitle_; // "Bookmark" or "Bookmark Added!" + IBOutlet NSTextField* nameTextField_; + IBOutlet NSComboBox* folderComboBox_; +} + +// |node| is the bookmark node we edit in this bubble. +// |alreadyBookmarked| tells us if the node was bookmarked before the +// user clicked on the star. (if NO, this is a brand new bookmark). +// The owner of this object is responsible for showing the bubble if +// it desires it to be visible on the screen. It is not shown by the +// init routine. Closing of the window happens implicitly on dealloc. +- (id)initWithDelegate:(id<BookmarkBubbleControllerDelegate>)delegate + parentWindow:(NSWindow*)parentWindow + topLeftForBubble:(NSPoint)topLeftForBubble + model:(BookmarkModel*)model + node:(const BookmarkNode*)node + alreadyBookmarked:(BOOL)alreadyBookmarked; + +- (void)showWindow; + +// Actions for buttons in the dialog. +- (IBAction)edit:(id)sender; +- (IBAction)close:(id)sender; +- (IBAction)remove:(id)sender; + +@end + + +// Exposed only for unit testing. +@interface BookmarkBubbleController(ExposedForUnitTesting) +- (NSWindow*)createBubbleWindow; +- (void)fillInFolderList; +- (BOOL)windowHasBeenClosed; +- (void)addFolderNodes:(const BookmarkNode*)parent toComboBox:(NSComboBox*)box; +- (void)updateBookmarkNode; +- (void)setTitle:(NSString *)title parentFolder:(NSString*)folder; +- (NSString*)chooseAnotherFolderString; +@end + +// Also private but I need to declare them specially for @synthesize to work. +@interface BookmarkBubbleController () +@property (readonly) id delegate; +@property (readonly) NSComboBox* folderComboBox; +@end diff --git a/chrome/browser/cocoa/bookmark_bubble_controller.mm b/chrome/browser/cocoa/bookmark_bubble_controller.mm new file mode 100644 index 0000000..7e9b1e2 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_controller.mm @@ -0,0 +1,215 @@ +// 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. + +#include "app/l10n_util_mac.h" +#include "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/cocoa/bookmark_bubble_controller.h" +#import "chrome/browser/cocoa/bookmark_bubble_window.h" +#include "grit/generated_resources.h" + + +@interface BookmarkBubbleController(PrivateAPI) +- (void)closeWindow; +@end + +@implementation BookmarkBubbleController + +@synthesize delegate = delegate_; +@synthesize folderComboBox = folderComboBox_; + +- (id)initWithDelegate:(id<BookmarkBubbleControllerDelegate>)delegate + parentWindow:(NSWindow*)parentWindow + topLeftForBubble:(NSPoint)topLeftForBubble + model:(BookmarkModel*)model + node:(const BookmarkNode*)node + alreadyBookmarked:(BOOL)alreadyBookmarked { + if ((self = [super initWithNibName:@"BookmarkBubble" + bundle:mac_util::MainAppBundle()])) { + // all these are weak... + delegate_ = delegate; + parentWindow_ = parentWindow; + topLeftForBubble_ = topLeftForBubble; + model_ = model; + node_ = node; + alreadyBookmarked_ = alreadyBookmarked; + // But this is strong. + titleMapping_.reset([[NSMutableDictionary alloc] init]); + } + return self; +} + +- (void)dealloc { + [self closeWindow]; + [super dealloc]; +} + +- (void)showWindow { + [self view]; // force nib load and window_ allocation + [window_ makeKeyAndOrderFront:self]; +} + +// Actually close the window. Do nothing else. +- (void)closeWindow { + [parentWindow_ removeChildWindow:window_]; + [window_ close]; +} + +- (void)awakeFromNib { + window_.reset([self createBubbleWindow]); + [parentWindow_ addChildWindow:window_ ordered:NSWindowAbove]; + + // Fill in inital values for text, controls, ... + + // Default is IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark". + // If adding for the 1st time the string becomes "Bookmark Added!" + if (!alreadyBookmarked_) { + NSString* title = + l10n_util::GetNSString(IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED); + [bigTitle_ setStringValue:title]; + } + + [self fillInFolderList]; +} + +- (IBAction)edit:(id)sender { + [self updateBookmarkNode]; + [self closeWindow]; + [delegate_ editBookmarkNode:node_]; + [delegate_ doneWithBubbleController:self]; +} + +- (IBAction)close:(id)sender { + if (node_) { + // no node_ if the bookmark was just removed + [self updateBookmarkNode]; + } + [self closeWindow]; + [delegate_ doneWithBubbleController:self]; +} + +// By implementing this, ESC causes the window to go away. +- (IBAction)cancel:(id)sender { + [self close:sender]; +} + +- (IBAction)remove:(id)sender { + model_->SetURLStarred(node_->GetURL(), node_->GetTitle(), false); + node_ = NULL; // no longer valid + [self close:self]; +} + +// We are the delegate of the combo box so we can tell when "choose +// another folder" was picked. +- (void)comboBoxSelectionDidChange:(NSNotification*)notification { + NSString* selected = [folderComboBox_ objectValueOfSelectedItem]; + if ([selected isEqual:chooseAnotherFolder_.get()]) { + [self edit:self]; + } +} + +// We are the delegate of our own window so we know when we lose key. +// When we lose key status we close, mirroring Windows behaivor. +- (void)windowDidResignKey:(NSNotification*)notification { + if ([window_ isVisible]) + [self close:self]; +} + +@end // BookmarkBubbleController + + +@implementation BookmarkBubbleController(ExposedForUnitTesting) + +// Create and return a retained NSWindow for this bubble. +- (NSWindow*)createBubbleWindow { + NSRect contentRect = [[self view] frame]; + NSPoint origin = topLeftForBubble_; + origin.y -= contentRect.size.height; // since it'll be our bottom-left + contentRect.origin = origin; + // Now convert to global coordinates since it'll be used for a window. + contentRect.origin = [parentWindow_ convertBaseToScreen:contentRect.origin]; + NSWindow* window = [[BookmarkBubbleWindow alloc] + initWithContentRect:contentRect]; + [window setDelegate:self]; + [window setContentView:[self view]]; + return window; +} + +// Fill in all information related to the folder combo box. +// +// TODO(jrg): make sure nested folders that have the same name are +// handled properly. +// http://crbug.com/19408 +- (void)fillInFolderList { + [nameTextField_ setStringValue:base::SysWideToNSString(node_->GetTitle())]; + [self addFolderNodes:model_->root_node() toComboBox:folderComboBox_]; + + // Add "Choose another folder...". Remember it for later to compare against. + chooseAnotherFolder_.reset( + [l10n_util::GetNSString(IDS_BOOMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER) + retain]); + [folderComboBox_ addItemWithObjectValue:chooseAnotherFolder_.get()]; + + // Finally, select the current parent. + NSString* parentTitle = base::SysWideToNSString( + node_->GetParent()->GetTitle()); + [folderComboBox_ selectItemWithObjectValue:parentTitle]; +} + +- (BOOL)windowHasBeenClosed { + return ![window_ isVisible]; +} + +// For the given folder node, walk the tree and add folder names to +// the given combo box. +// +// TODO(jrg): no distinction is made among folders with the same name. +- (void)addFolderNodes:(const BookmarkNode*)parent toComboBox:(NSComboBox*)box { + NSString* title = base::SysWideToNSString(parent->GetTitle()); + if ([title length]) { // no title if root + [box addItemWithObjectValue:title]; + [titleMapping_ setValue:[NSValue valueWithPointer:parent] forKey:title]; + } + for (int i = 0; i < parent->GetChildCount(); i++) { + const BookmarkNode* child = parent->GetChild(i); + if (child->is_folder()) + [self addFolderNodes:child toComboBox:box]; + } +} + +// Look at the dialog; if the user has changed anything, update the +// bookmark node to reflect this. +- (void)updateBookmarkNode { + // First the title... + NSString* oldTitle = base::SysWideToNSString(node_->GetTitle()); + NSString* newTitle = [nameTextField_ stringValue]; + if (![oldTitle isEqual:newTitle]) { + model_->SetTitle(node_, base::SysNSStringToWide(newTitle)); + } + // Then the parent folder. + NSString* oldParentTitle = base::SysWideToNSString( + node_->GetParent()->GetTitle()); + NSString* newParentTitle = [folderComboBox_ objectValueOfSelectedItem]; + if (![oldParentTitle isEqual:newParentTitle]) { + const BookmarkNode* newParent = static_cast<const BookmarkNode*>( + [[titleMapping_ objectForKey:newParentTitle] pointerValue]); + if (newParent) { + // newParent should only ever possibly be NULL in a unit test. + int index = newParent->GetChildCount(); + model_->Move(node_, newParent, index); + } + } +} + +- (void)setTitle:(NSString*)title parentFolder:(NSString*)folder { + [nameTextField_ setStringValue:title]; + [folderComboBox_ selectItemWithObjectValue:folder]; +} + +- (NSString*)chooseAnotherFolderString { + return chooseAnotherFolder_.get(); +} + +@end // implementation BookmarkBubbleController(ExposedForUnitTesting) diff --git a/chrome/browser/cocoa/bookmark_bubble_controller_unittest.mm b/chrome/browser/cocoa/bookmark_bubble_controller_unittest.mm new file mode 100644 index 0000000..2e741c0 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_controller_unittest.mm @@ -0,0 +1,213 @@ +// 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 <Cocoa/Cocoa.h> + +#include "base/basictypes.h" +#include "base/scoped_nsobject.h" +#import "chrome/browser/cocoa/bookmark_bubble_controller.h" +#include "chrome/browser/cocoa/browser_test_helper.h" +#import "chrome/browser/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +@interface BBDelegate : NSObject<BookmarkBubbleControllerDelegate> { + NSWindow* window_; // weak + int edits_; + int dones_; +} +@property (readonly) int edits; +@property (readonly) int dones; +@property (readonly) NSWindow* window; +@end + +@implementation BBDelegate + +@synthesize edits = edits_; +@synthesize window = window_; +@synthesize dones = dones_; + +- (NSPoint)topLeftForBubble { + return NSMakePoint(10, 300); +} + +- (void)editBookmarkNode:(const BookmarkNode*)node { + edits_++; +} + +- (void)doneWithBubbleController:(BookmarkBubbleController*)controller { + dones_++; +} + +- (void)clear { + edits_ = 0; + dones_ = 0; +} + +@end + +namespace { + +class BookmarkBubbleControllerTest : public PlatformTest { + public: + CocoaTestHelper cocoa_helper_; // Inits Cocoa, creates window, etc... + BrowserTestHelper helper_; + scoped_nsobject<BBDelegate> delegate_; + scoped_nsobject<BookmarkBubbleController> controller_; + + BookmarkBubbleControllerTest() { + delegate_.reset([[BBDelegate alloc] init]); + } + + // Returns a controller but ownership not transferred. + // Only one of these will be valid at a time. + BookmarkBubbleController* ControllerForNode(const BookmarkNode* node) { + controller_.reset([[BookmarkBubbleController alloc] + initWithDelegate:delegate_.get() + parentWindow:cocoa_helper_.window() + topLeftForBubble:[delegate_ topLeftForBubble] + model:helper_.profile()->GetBookmarkModel() + node:node + alreadyBookmarked:YES]); + [controller_ view]; // force nib load + return controller_.get(); + } + + BookmarkModel* GetBookmarkModel() { + return helper_.profile()->GetBookmarkModel(); + } +}; + +// Confirm basics about the bubble window (e.g. that it is inside the +// parent window) +TEST_F(BookmarkBubbleControllerTest, TestBubbleWindow) { + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + L"Bookie markie title", + GURL("http://www.google.com")); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + NSWindow* window = [controller createBubbleWindow]; + EXPECT_TRUE(window); + EXPECT_TRUE(NSContainsRect([cocoa_helper_.window() frame], + [window frame])); +} + +// Confirm population of folder list +TEST_F(BookmarkBubbleControllerTest, TestFillInFolder) { + // Create some folders, including a nested folder + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node1 = model->AddGroup(model->GetBookmarkBarNode(), + 0, L"one"); + const BookmarkNode* node2 = model->AddGroup(model->GetBookmarkBarNode(), + 1, L"two"); + const BookmarkNode* node3 = model->AddGroup(model->GetBookmarkBarNode(), + 2, L"three"); + const BookmarkNode* node4 = model->AddGroup(node2, + 0, L"sub"); + model->AddURL(node1, 0, L"title1", GURL("http://www.google.com")); + model->AddURL(node3, 0, L"title2", GURL("http://www.google.com")); + model->AddURL(node4, 0, L"title3", GURL("http://www.google.com/reader")); + + BookmarkBubbleController* controller = ControllerForNode(node4); + EXPECT_TRUE(controller); + + NSArray* items = [[controller folderComboBox] objectValues]; + EXPECT_TRUE([items containsObject:@"one"]); + EXPECT_TRUE([items containsObject:@"two"]); + EXPECT_TRUE([items containsObject:@"three"]); + EXPECT_TRUE([items containsObject:@"sub"]); + EXPECT_FALSE([items containsObject:@"title1"]); + EXPECT_FALSE([items containsObject:@"title2"]); +} + +// Click on edit; bubble gets closed. +TEST_F(BookmarkBubbleControllerTest, TestSimpleActions) { + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + L"Bookie markie title", + GURL("http://www.google.com")); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + + EXPECT_EQ([delegate_ edits], 0); + EXPECT_EQ([delegate_ dones], 0); + EXPECT_FALSE([controller windowHasBeenClosed]); + [controller edit:controller]; + EXPECT_EQ([delegate_ edits], 1); + EXPECT_EQ([delegate_ dones], 1); + EXPECT_TRUE([controller windowHasBeenClosed]); + + [delegate_ clear]; + EXPECT_EQ([delegate_ edits], 0); + EXPECT_EQ([delegate_ dones], 0); + + controller = ControllerForNode(node); + EXPECT_TRUE(controller); + EXPECT_FALSE([controller windowHasBeenClosed]); + [controller close:controller]; + EXPECT_EQ([delegate_ edits], 0); + EXPECT_EQ([delegate_ dones], 1); + EXPECT_TRUE([controller windowHasBeenClosed]); +} + +// User changes title and parent folder in the UI +TEST_F(BookmarkBubbleControllerTest, TestUserEdit) { + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + L"short-title", + GURL("http://www.google.com")); + model->AddGroup(model->GetBookmarkBarNode(), 0, L"grandma"); + model->AddGroup(model->GetBookmarkBarNode(), 0, L"grandpa"); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + + // simulate a user edit + [controller setTitle:@"oops" parentFolder:@"grandma"]; + [controller edit:controller]; + + // Make sure bookmark has changed + EXPECT_EQ(node->GetTitle(), L"oops"); + EXPECT_EQ(node->GetParent()->GetTitle(), L"grandma"); +} + +// Click the "remove" button +TEST_F(BookmarkBubbleControllerTest, TestRemove) { + BookmarkModel* model = GetBookmarkModel(); + GURL gurl("http://www.google.com"); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + L"Bookie markie title", + gurl); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + EXPECT_TRUE(model->IsBookmarked(gurl)); + + [controller remove:controller]; + EXPECT_FALSE(model->IsBookmarked(gurl)); + EXPECT_TRUE([controller windowHasBeenClosed]); + EXPECT_EQ([delegate_ dones], 1); +} + +// Confirm picking "choose another folder" caused edit: to be called. +TEST_F(BookmarkBubbleControllerTest, ComboSelectionChanged) { + BookmarkModel* model = GetBookmarkModel(); + GURL gurl("http://www.google.com"); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, L"super-title", + gurl); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + + NSString* chooseAnotherFolder = [controller chooseAnotherFolderString]; + EXPECT_EQ([delegate_ edits], 0); + [controller setTitle:@"DOH!" parentFolder:chooseAnotherFolder]; + EXPECT_EQ([delegate_ edits], 1); +} + + +} // namespace diff --git a/chrome/browser/cocoa/bookmark_bubble_view.h b/chrome/browser/cocoa/bookmark_bubble_view.h new file mode 100644 index 0000000..1e811a8 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_view.h @@ -0,0 +1,11 @@ +// 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 <Cocoa/Cocoa.h> + +// Content view for a Bookmark Bubble opened by clicking on the star +// toolbar button. This is where nonrectangular drawing happens. +@interface BookmarkBubbleView : NSView +@end + diff --git a/chrome/browser/cocoa/bookmark_bubble_view.mm b/chrome/browser/cocoa/bookmark_bubble_view.mm new file mode 100644 index 0000000..b4c5c7e56 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_view.mm @@ -0,0 +1,58 @@ +// 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/cocoa/bookmark_bubble_view.h" +#import "third_party/GTM/AppKit/GTMTheme.h" + +namespace { +// TODO(jrg): confirm constants with UI dudes +const CGFloat kBubbleCornerRadius = 8.0; +const CGFloat kBubbleArrowXOffset = 10.0; +const CGFloat kBubbleArrowWidth = 15.0; +const CGFloat kBubbleArrowHeight = 8.0; +const CGFloat kBubbleBorderLineWidth = 1.0; +} + +@implementation BookmarkBubbleView + +- (void)drawRect:(NSRect)rect { + // Make room for the border to be seen. + NSRect bounds = [self bounds]; + bounds.size.height -= kBubbleArrowHeight; + bounds = NSInsetRect(bounds, + kBubbleBorderLineWidth/2.0, + kBubbleBorderLineWidth/2.0); + + NSBezierPath* bezier = [NSBezierPath bezierPath]; + rect.size.height -= kBubbleArrowHeight; + + // Start with a rounded rectangle. + [bezier appendBezierPathWithRoundedRect:bounds + xRadius:kBubbleCornerRadius + yRadius:kBubbleCornerRadius]; + + // Add the bubble arrow (pointed at the star). + NSPoint arrowStart = NSMakePoint(NSMinX(bounds), NSMaxY(bounds)); + arrowStart.x += kBubbleArrowXOffset; + [bezier moveToPoint:NSMakePoint(arrowStart.x, arrowStart.y)]; + [bezier lineToPoint:NSMakePoint(arrowStart.x + kBubbleArrowWidth/2.0, + arrowStart.y + kBubbleArrowHeight)]; + [bezier lineToPoint:NSMakePoint(arrowStart.x + kBubbleArrowWidth, + arrowStart.y)]; + [bezier closePath]; + + // Draw the outline... + [[NSColor blackColor] set]; + [bezier setLineWidth:kBubbleBorderLineWidth]; + [bezier stroke]; + + // Then fill the inside. + GTMTheme *theme = [GTMTheme defaultTheme]; + NSGradient *gradient = [theme gradientForStyle:GTMThemeStyleToolBar + state:NO]; + [gradient drawInBezierPath:bezier angle:0.0]; +} + +@end + diff --git a/chrome/browser/cocoa/bookmark_bubble_view_unittest.mm b/chrome/browser/cocoa/bookmark_bubble_view_unittest.mm new file mode 100644 index 0000000..9744397 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_view_unittest.mm @@ -0,0 +1,36 @@ +// 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 <Cocoa/Cocoa.h> + +#include "base/scoped_nsobject.h" +#import "chrome/browser/cocoa/bookmark_bubble_view.h" +#import "chrome/browser/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +namespace { + +class BookmarkBubbleViewTest : public PlatformTest { + public: + BookmarkBubbleViewTest() { + NSRect frame = NSMakeRect(0, 0, 100, 30); + view_.reset([[BookmarkBubbleView alloc] initWithFrame:frame]); + [cocoa_helper_.contentView() addSubview:view_.get()]; + } + + CocoaTestHelper cocoa_helper_; // Inits Cocoa, creates window, etc... + scoped_nsobject<BookmarkBubbleView> view_; +}; + +// Test drawing and an add/remove from the view hierarchy to ensure +// nothing leaks or crashes. +TEST_F(BookmarkBubbleViewTest, AddRemoveDisplay) { + [view_ display]; + EXPECT_EQ(cocoa_helper_.contentView(), [view_ superview]); + [view_.get() removeFromSuperview]; + EXPECT_FALSE([view_ superview]); +} + +} // namespace diff --git a/chrome/browser/cocoa/bookmark_bubble_window.h b/chrome/browser/cocoa/bookmark_bubble_window.h new file mode 100644 index 0000000..45e2f3b --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_window.h @@ -0,0 +1,11 @@ +// 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 <Cocoa/Cocoa.h> + +// Window for the bookmark bubble that comes up when you click on "STAR". +@interface BookmarkBubbleWindow : NSWindow +- (id)initWithContentRect:(NSRect)contentRect; +@end + diff --git a/chrome/browser/cocoa/bookmark_bubble_window.mm b/chrome/browser/cocoa/bookmark_bubble_window.mm new file mode 100644 index 0000000..126b30e --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_window.mm @@ -0,0 +1,33 @@ +// 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/cocoa/bookmark_bubble_window.h" + +@implementation BookmarkBubbleWindow + +- (id)initWithContentRect:(NSRect)contentRect { + if ((self = [super initWithContentRect:contentRect + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:YES])) { + [self setReleasedWhenClosed:NO]; + [self setBackgroundColor:[NSColor clearColor]]; + [self setExcludedFromWindowsMenu:YES]; + [self setAlphaValue:1.0]; + [self setOpaque:NO]; + } + return self; +} + +// According to +// http://www.cocoabuilder.com/archive/message/cocoa/2006/6/19/165953, +// NSBorderlessWindowMask windows cannot become key or main. In our +// case, however, we don't want all of that behavior. (As an example, +// our bubble has buttons!) + +- (BOOL)canBecomeKeyWindow { + return YES; +} + +@end diff --git a/chrome/browser/cocoa/bookmark_bubble_window_unittest.mm b/chrome/browser/cocoa/bookmark_bubble_window_unittest.mm new file mode 100644 index 0000000..08baec4 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bubble_window_unittest.mm @@ -0,0 +1,27 @@ +// 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. + +#include "base/scoped_ptr.h" +#include "chrome/browser/cocoa/cocoa_test_helper.h" +#include "chrome/browser/cocoa/bookmark_bubble_window.h" +#include "testing/gtest/include/gtest/gtest.h" + +class BookmarkBubbleWindowTest : public testing::Test { + public: + CocoaTestHelper cocoa_helper_; +}; + +TEST_F(BookmarkBubbleWindowTest, Basics) { + scoped_nsobject<BookmarkBubbleWindow> window_; + window_.reset([[BookmarkBubbleWindow alloc] + initWithContentRect:NSMakeRect(0,0,10,10)]); + + EXPECT_TRUE([window_ canBecomeKeyWindow]); + EXPECT_FALSE([window_ canBecomeMainWindow]); + + EXPECT_TRUE([window_ isExcludedFromWindowsMenu]); + EXPECT_FALSE([window_ isReleasedWhenClosed]); +} + + diff --git a/chrome/browser/cocoa/bookmark_editor_controller.h b/chrome/browser/cocoa/bookmark_editor_controller.h index 742fbd1..4c80161 100644 --- a/chrome/browser/cocoa/bookmark_editor_controller.h +++ b/chrome/browser/cocoa/bookmark_editor_controller.h @@ -39,6 +39,9 @@ configuration:(BookmarkEditor::Configuration)configuration handler:(BookmarkEditor::Handler*)handler; +// Run the bookmark editor as a modal sheet. Does not block. +- (void)runAsModalSheet; + // Actions for the buttons at the bottom of the window. - (IBAction)newFolder:(id)sender; - (IBAction)cancel:(id)sender; diff --git a/chrome/browser/cocoa/bookmark_editor_controller.mm b/chrome/browser/cocoa/bookmark_editor_controller.mm index 4ba0ab39..79cd1dc 100644 --- a/chrome/browser/cocoa/bookmark_editor_controller.mm +++ b/chrome/browser/cocoa/bookmark_editor_controller.mm @@ -10,11 +10,6 @@ #include "chrome/browser/profile.h" #import "chrome/browser/cocoa/bookmark_editor_controller.h" -@interface BookmarkEditorController(Private) -// Run the bookmark editor as a modal sheet. Does not block. -- (void)runModal; -@end - // static; implemented for each platform. void BookmarkEditor::Show(gfx::NativeView parent_hwnd, Profile* profile, @@ -30,7 +25,7 @@ void BookmarkEditor::Show(gfx::NativeView parent_hwnd, node:node configuration:configuration handler:handler]; - [controller runModal]; + [controller runAsModalSheet]; } @@ -107,7 +102,7 @@ void BookmarkEditor::Show(gfx::NativeView parent_hwnd, */ // TODO(jrg): consider NSModalSession. -- (void)runModal { +- (void)runAsModalSheet { [NSApp beginSheet:[self window] modalForWindow:parentWindow_ modalDelegate:self diff --git a/chrome/browser/cocoa/bookmark_name_folder_controller.h b/chrome/browser/cocoa/bookmark_name_folder_controller.h index 64c3183..764a04a 100644 --- a/chrome/browser/cocoa/bookmark_name_folder_controller.h +++ b/chrome/browser/cocoa/bookmark_name_folder_controller.h @@ -26,7 +26,7 @@ - (id)initWithParentWindow:(NSWindow*)window profile:(Profile*)profile node:(const BookmarkNode*)node; -- (void)runModal; +- (void)runAsModalSheet; - (IBAction)cancel:(id)sender; - (IBAction)ok:(id)sender; @end diff --git a/chrome/browser/cocoa/bookmark_name_folder_controller.mm b/chrome/browser/cocoa/bookmark_name_folder_controller.mm index fa24ac5..859e8e7 100644 --- a/chrome/browser/cocoa/bookmark_name_folder_controller.mm +++ b/chrome/browser/cocoa/bookmark_name_folder_controller.mm @@ -39,7 +39,7 @@ } // TODO(jrg): consider NSModalSession. -- (void)runModal { +- (void)runAsModalSheet { [NSApp beginSheet:[self window] modalForWindow:parentWindow_ modalDelegate:self diff --git a/chrome/browser/cocoa/browser_window_cocoa.mm b/chrome/browser/cocoa/browser_window_cocoa.mm index 91c6a0f..ff1d813 100644 --- a/chrome/browser/cocoa/browser_window_cocoa.mm +++ b/chrome/browser/cocoa/browser_window_cocoa.mm @@ -202,7 +202,8 @@ void BrowserWindowCocoa::ShowBookmarkManager() { void BrowserWindowCocoa::ShowBookmarkBubble(const GURL& url, bool already_bookmarked) { - NOTIMPLEMENTED(); + [controller_ showBookmarkBubbleForURL:url + alreadyBookmarked:(already_bookmarked ? YES : NO)]; } bool BrowserWindowCocoa::IsDownloadShelfVisible() const { diff --git a/chrome/browser/cocoa/browser_window_controller.h b/chrome/browser/cocoa/browser_window_controller.h index 6713044..eb34d94 100644 --- a/chrome/browser/cocoa/browser_window_controller.h +++ b/chrome/browser/cocoa/browser_window_controller.h @@ -16,6 +16,7 @@ #include "base/scoped_ptr.h" #import "chrome/browser/cocoa/tab_window_controller.h" #import "chrome/browser/cocoa/bookmark_bar_controller.h" +#import "chrome/browser/cocoa/bookmark_bubble_controller.h" #import "chrome/browser/cocoa/view_resizer.h" #import "third_party/GTM/AppKit/GTMTheme.h" @@ -40,6 +41,7 @@ class TabStripModelObserverBridge; @interface BrowserWindowController : TabWindowController<NSUserInterfaceValidations, BookmarkURLOpener, + BookmarkBubbleControllerDelegate, ViewResizer, GTMThemeDelegate> { @private @@ -63,6 +65,7 @@ class TabStripModelObserverBridge; scoped_nsobject<InfoBarContainerController> infoBarContainerController_; scoped_ptr<StatusBubble> statusBubble_; scoped_nsobject<DownloadShelfController> downloadShelfController_; + scoped_nsobject<BookmarkBubbleController> bookmarkBubbleController_; scoped_nsobject<GTMTheme> theme_; BOOL ownsBrowser_; // Only ever NO when testing BOOL fullscreen_; @@ -139,6 +142,10 @@ class TabStripModelObserverBridge; // Delegate method for the status bubble to query about its vertical offset. - (float)verticalOffsetForStatusBubble; +// Show the bookmark bubble (e.g. user just clicked on the STAR) +- (void)showBookmarkBubbleForURL:(const GURL&)url + alreadyBookmarked:(BOOL)alreadyBookmarked; + // Returns the (lazily created) window sheet controller of this window. Used // for the per-tab sheets. - (GTMWindowSheetController*)sheetController; @@ -165,6 +172,9 @@ class TabStripModelObserverBridge; // Return an autoreleased NSWindow suitable for fullscreen use. - (NSWindow*)fullscreenWindow; +// Return a point suitable for the topLeft for a bookmark bubble. +- (NSPoint)topLeftForBubble; + @end // BrowserWindowController(TestingAPI) #endif // CHROME_BROWSER_COCOA_BROWSER_WINDOW_CONTROLLER_H_ diff --git a/chrome/browser/cocoa/browser_window_controller.mm b/chrome/browser/cocoa/browser_window_controller.mm index 5918cc4..01cd173 100644 --- a/chrome/browser/cocoa/browser_window_controller.mm +++ b/chrome/browser/cocoa/browser_window_controller.mm @@ -9,6 +9,7 @@ #import "base/scoped_nsobject.h" #include "base/sys_string_conversions.h" #include "chrome/app/chrome_dll_resource.h" // IDC_* +#include "chrome/browser/bookmarks/bookmark_editor.h" #include "chrome/browser/browser.h" #include "chrome/browser/browser_list.h" #include "chrome/browser/browser_process.h" @@ -20,6 +21,7 @@ #include "chrome/browser/tab_contents/tab_contents_view.h" #include "chrome/browser/tabs/tab_strip_model.h" #import "chrome/browser/cocoa/bookmark_bar_controller.h" +#import "chrome/browser/cocoa/bookmark_editor_controller.h" #import "chrome/browser/cocoa/browser_window_cocoa.h" #import "chrome/browser/cocoa/browser_window_controller.h" #import "chrome/browser/cocoa/download_shelf_controller.h" @@ -933,6 +935,53 @@ willPositionSheet:(NSWindow*)sheet return theme_ ? theme_ : [GTMTheme defaultTheme]; } +- (NSPoint)topLeftForBubble { + NSRect rect = [toolbarController_ starButtonInWindowCoordinates]; + NSPoint p = NSMakePoint(NSMinX(rect), NSMinY(rect)); // bottom left + return p; +} + +// Show the bookmark bubble (e.g. user just clicked on the STAR). +- (void)showBookmarkBubbleForURL:(const GURL&)url + alreadyBookmarked:(BOOL)alreadyBookmarked { + BookmarkModel* model = browser_->profile()->GetBookmarkModel(); + const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url); + + // Bring up the bubble. But clicking on STAR while the bubble is + // open should make it go away. + if (bookmarkBubbleController_.get()) { + [self doneWithBubbleController:bookmarkBubbleController_.get()]; + } else { + bookmarkBubbleController_.reset([[BookmarkBubbleController alloc] + initWithDelegate:self + parentWindow:[self window] + topLeftForBubble:[self topLeftForBubble] + model:model + node:node + alreadyBookmarked:alreadyBookmarked]); + [bookmarkBubbleController_ showWindow]; + } +} + +// Implement BookmarkBubbleControllerDelegate +- (void)editBookmarkNode:(const BookmarkNode*)node { + // A BookmarkEditorController is a sheet that owns itself, and + // deallocates itself when closed. + [[[BookmarkEditorController alloc] + initWithParentWindow:[self window] + profile:browser_->profile() + parent:node->GetParent() + node:node + configuration:BookmarkEditor::SHOW_TREE + handler:NULL] + runAsModalSheet]; +} + +// Implement BookmarkBubbleControllerDelegate +- (void)doneWithBubbleController:(BookmarkBubbleController*)controller { + bookmarkBubbleController_.reset(nil); +} + @end @implementation BrowserWindowController (Private) diff --git a/chrome/browser/cocoa/browser_window_controller_unittest.mm b/chrome/browser/cocoa/browser_window_controller_unittest.mm index 4b40a67..743c90c 100644 --- a/chrome/browser/cocoa/browser_window_controller_unittest.mm +++ b/chrome/browser/cocoa/browser_window_controller_unittest.mm @@ -257,4 +257,15 @@ TEST_F(BrowserWindowControllerTest, TestResizeViews) { EXPECT_TRUE(NSEqualRects([toolbar frame], NSMakeRect(0, 561, 800, 39))); } +TEST_F(BrowserWindowControllerTest, TestTopLeftForBubble) { + NSPoint p = [controller_ topLeftForBubble]; + NSRect all = [[controller_ window] frame]; + + // As a sanity check make sure the point is vaguely in the top left + // of the window. + EXPECT_GT(p.y, all.origin.y + (all.size.height/2)); + EXPECT_LT(p.x, all.origin.x + (all.size.width/2)); +} + + /* TODO(???): test other methods of BrowserWindowController */ diff --git a/chrome/browser/cocoa/toolbar_controller.h b/chrome/browser/cocoa/toolbar_controller.h index c5135d3..31aafc4 100644 --- a/chrome/browser/cocoa/toolbar_controller.h +++ b/chrome/browser/cocoa/toolbar_controller.h @@ -128,6 +128,10 @@ class ToolbarView; - (IBAction)showPageMenu:(id)sender; - (IBAction)showWrenchMenu:(id)sender; +// The bookmark bubble (when you click the star) needs to know where to go. +// Somewhere near the star button seems like a good start. +- (NSRect)starButtonInWindowCoordinates; + @end // A set of private methods used by tests, in the absence of "friends" in ObjC. diff --git a/chrome/browser/cocoa/toolbar_controller.mm b/chrome/browser/cocoa/toolbar_controller.mm index 04c7e5e0..1bcf8a4 100644 --- a/chrome/browser/cocoa/toolbar_controller.mm +++ b/chrome/browser/cocoa/toolbar_controller.mm @@ -373,4 +373,10 @@ class PrefObserverBridge : public NotificationObserver { forView:wrenchButton_]; } +- (NSRect)starButtonInWindowCoordinates { + return [[[starButton_ window] contentView] convertRect:[starButton_ bounds] + fromView:starButton_]; +} + + @end diff --git a/chrome/browser/cocoa/toolbar_controller_unittest.mm b/chrome/browser/cocoa/toolbar_controller_unittest.mm index aa0257193..091bddc 100644 --- a/chrome/browser/cocoa/toolbar_controller_unittest.mm +++ b/chrome/browser/cocoa/toolbar_controller_unittest.mm @@ -248,4 +248,13 @@ TEST_F(ToolbarControllerTest, BookmarkBarIsFullWidth) { EXPECT_TRUE([bookmarkBarView isDescendantOf:[bar_ view]]); } +TEST_F(ToolbarControllerTest, StarButtonInWindowCoordinates) { + NSRect star = [bar_ starButtonInWindowCoordinates]; + NSRect all = [[[bar_ view] window] frame]; + + // Make sure the star is completely inside the window rect + EXPECT_TRUE(NSContainsRect(all, star)); +} + + } // namespace |