diff options
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 |