diff options
author | jrg@chromium.org <jrg@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-02-24 23:26:27 +0000 |
---|---|---|
committer | jrg@chromium.org <jrg@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-02-24 23:26:27 +0000 |
commit | d27fd0e73b0620cc3af740d2dd2138c20ca4372a (patch) | |
tree | e97bc4d580ed7893b035168f2380b3b2f28908bb /chrome | |
parent | 38eb08a5fd1ea4f529b9d4651a2f2eb31b0d2192 (diff) | |
download | chromium_src-d27fd0e73b0620cc3af740d2dd2138c20ca4372a.zip chromium_src-d27fd0e73b0620cc3af740d2dd2138c20ca4372a.tar.gz chromium_src-d27fd0e73b0620cc3af740d2dd2138c20ca4372a.tar.bz2 |
Custom "menus" for the bookmark bar folders.
Full behavior: http://JRG_WRITE_FULL_DOC_AND_TEST_PLAN_TOMORROW
BUG=17608 (and a slew of others)
Brief details on how to test:
- add some bookmarks and bookmark folders.
- at a basic level, make sure bookmark folders feel like menus e.g.
-- click to open
-- can open "submenus" and sub-sub-menus
-- can open (click on) bookmarks in any of these submenus
- click-drag does NOT open a menu (different than Mac menus); it initiates a Drag
- click on folder in bookmark bar initiates "hover open"; moving mouse
over other folders will pop them open immediately (much like Mac menus)
- Bookmark bar non-drag hover-open is immediate, but bookmark folder
hover-open has a brief delay so quick "move down" a folder does not
trigger them all to open while you travel (much like Mac menus).
- DnD of bookmarks and folders on bookmark bar.
- While doing DnD of bookmark, "hover" over a folder and see it open.
- Bookmark folder menus have normal DnD "drop indicators" like the bookmark bar.
- Can "hover open" a nested subfolder.
- Can drag a bookmark from one deep sub-sub-folder to a different deep one.
- Confirm buttons and folders in submenus are themed, both with the
theme set at launch time and the theme we change to after launch.
- Empty folders have an "(empty)" item which is not selectable.
- Intentional delay in closing a sub-sub-folder when hovering over
another one. E.g. When moving to a sub-sub-menu, 'brief' travel
over a different submenu does not close the destination sub-menu.
- can use bookmark context menus in folder "menus".
- confirm DnD from "Other bookmarks" to any other random folder and
vice versa.
- While non-drag hover open is active, clicking anywhere other than
the bookmark bar or folder (e.g. the main web view) turns it off.
TODO:
- random bugs (e.g. "add folder" over a folder doesn't put it in there)
- (empty) needs to be revisited, both visually and for a drop indication
- core animations instead of drop indicators
- ...
Review URL: http://codereview.chromium.org/551226
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@39947 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
27 files changed, 3193 insertions, 129 deletions
diff --git a/chrome/app/nibs/BookmarkBarFolderWindow.xib b/chrome/app/nibs/BookmarkBarFolderWindow.xib new file mode 100644 index 0000000..13dd230 --- /dev/null +++ b/chrome/app/nibs/BookmarkBarFolderWindow.xib @@ -0,0 +1,886 @@ +<?xml version="1.0" encoding="UTF-8"?> +<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10"> + <data> + <int key="IBDocument.SystemTarget">1050</int> + <string key="IBDocument.SystemVersion">10B504</string> + <string key="IBDocument.InterfaceBuilderVersion">732</string> + <string key="IBDocument.AppKitVersion">1038.2</string> + <string key="IBDocument.HIToolboxVersion">437.00</string> + <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string> + <string key="NS.object.0">732</string> + </object> + <object class="NSMutableArray" key="IBDocument.EditedObjectIDs"> + <bool key="EncodedWithXMLCoder">YES</bool> + <integer value="2"/> + </object> + <object class="NSArray" key="IBDocument.PluginDependencies"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + </object> + <object class="NSMutableDictionary" key="IBDocument.Metadata"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSArray" key="dict.sortedKeys" id="0"> + <bool key="EncodedWithXMLCoder">YES</bool> + </object> + <object class="NSMutableArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + </object> + </object> + <object class="NSMutableArray" key="IBDocument.RootObjects" id="1000"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSCustomObject" id="1001"> + <string key="NSClassName">BookmarkBarFolderController</string> + </object> + <object class="NSCustomObject" id="1003"> + <string key="NSClassName">FirstResponder</string> + </object> + <object class="NSCustomObject" id="1004"> + <string key="NSClassName">NSApplication</string> + </object> + <object class="NSWindowTemplate" id="1005"> + <int key="NSWindowStyleMask">15</int> + <int key="NSWindowBacking">2</int> + <string key="NSWindowRect">{{196, 240}, {480, 270}}</string> + <int key="NSWTFlags">536870912</int> + <string key="NSWindowTitle">BmbPopUpWindow</string> + <string key="NSWindowClass">BookmarkBarFolderWindow</string> + <nil key="NSViewClass"/> + <string key="NSWindowContentMaxSize">{1.79769e+308, 1.79769e+308}</string> + <object class="NSView" key="NSWindowView" id="1006"> + <reference key="NSNextResponder"/> + <int key="NSvFlags">256</int> + <object class="NSMutableArray" key="NSSubviews"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSCustomView" id="762431297"> + <reference key="NSNextResponder" ref="1006"/> + <int key="NSvFlags">274</int> + <string key="NSFrameSize">{480, 270}</string> + <reference key="NSSuperview" ref="1006"/> + <string key="NSClassName">BookmarkBarFolderView</string> + </object> + </object> + <string key="NSFrameSize">{480, 270}</string> + <reference key="NSSuperview"/> + </object> + <string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string> + <string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string> + </object> + </object> + <object class="IBObjectContainer" key="IBDocument.Objects"> + <object class="NSMutableArray" key="connectionRecords"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBConnectionRecord"> + <object class="IBOutletConnection" key="connection"> + <string key="label">mainView_</string> + <reference key="source" ref="1001"/> + <reference key="destination" ref="762431297"/> + </object> + <int key="connectionID">7</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBOutletConnection" key="connection"> + <string key="label">window</string> + <reference key="source" ref="1001"/> + <reference key="destination" ref="1005"/> + </object> + <int key="connectionID">8</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBOutletConnection" key="connection"> + <string key="label">delegate</string> + <reference key="source" ref="1005"/> + <reference key="destination" ref="1001"/> + </object> + <int key="connectionID">9</int> + </object> + </object> + <object class="IBMutableOrderedSet" key="objectRecords"> + <object class="NSArray" key="orderedObjects"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBObjectRecord"> + <int key="objectID">0</int> + <reference key="object" ref="0"/> + <reference key="children" ref="1000"/> + <nil key="parent"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">-2</int> + <reference key="object" ref="1001"/> + <reference key="parent" ref="0"/> + <string key="objectName">File's Owner</string> + </object> + <object class="IBObjectRecord"> + <int key="objectID">-1</int> + <reference key="object" ref="1003"/> + <reference key="parent" ref="0"/> + <string key="objectName">First Responder</string> + </object> + <object class="IBObjectRecord"> + <int key="objectID">-3</int> + <reference key="object" ref="1004"/> + <reference key="parent" ref="0"/> + <string key="objectName">Application</string> + </object> + <object class="IBObjectRecord"> + <int key="objectID">1</int> + <reference key="object" ref="1005"/> + <object class="NSMutableArray" key="children"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference ref="1006"/> + </object> + <reference key="parent" ref="0"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">2</int> + <reference key="object" ref="1006"/> + <object class="NSMutableArray" key="children"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference ref="762431297"/> + </object> + <reference key="parent" ref="1005"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">4</int> + <reference key="object" ref="762431297"/> + <reference key="parent" ref="1006"/> + </object> + </object> + </object> + <object class="NSMutableDictionary" key="flattenedProperties"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSArray" key="dict.sortedKeys"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>-1.IBPluginDependency</string> + <string>-2.IBPluginDependency</string> + <string>-3.IBPluginDependency</string> + <string>1.IBEditorWindowLastContentRect</string> + <string>1.IBPluginDependency</string> + <string>1.IBWindowTemplateEditedContentRect</string> + <string>1.NSWindowTemplate.visibleAtLaunch</string> + <string>1.WindowOrigin</string> + <string>1.editorWindowContentRectSynchronizationRect</string> + <string>2.IBPluginDependency</string> + <string>4.IBPluginDependency</string> + </object> + <object class="NSMutableArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>{{867, 900}, {480, 270}}</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>{{867, 900}, {480, 270}}</string> + <boolean value="NO"/> + <string>{196, 240}</string> + <string>{{357, 418}, {480, 270}}</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + </object> + </object> + <object class="NSMutableDictionary" key="unlocalizedProperties"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference key="dict.sortedKeys" ref="0"/> + <object class="NSMutableArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + </object> + </object> + <nil key="activeLocalization"/> + <object class="NSMutableDictionary" key="localizations"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference key="dict.sortedKeys" ref="0"/> + <object class="NSMutableArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + </object> + </object> + <nil key="sourceID"/> + <int key="maxID">9</int> + </object> + <object class="IBClassDescriber" key="IBDocument.Classes"> + <object class="NSMutableArray" key="referencedPartialClassDescriptions"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBPartialClassDescription"> + <string key="className">BackgroundGradientView</string> + <string key="superclassName">NSView</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">browser/cocoa/background_gradient_view.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">BookmarkBarFolderController</string> + <string key="superclassName">NSWindowController</string> + <object class="NSMutableDictionary" key="outlets"> + <string key="NS.key.0">mainView_</string> + <string key="NS.object.0">BookmarkBarFolderView</string> + </object> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">browser/cocoa/bookmark_bar_folder_controller.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">BookmarkBarFolderView</string> + <string key="superclassName">BackgroundGradientView</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">browser/cocoa/bookmark_bar_folder_view.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">BookmarkBarFolderWindow</string> + <string key="superclassName">NSWindow</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">browser/cocoa/bookmark_bar_folder_window.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">browser/cocoa/status_bubble_mac.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">browser/cocoa/tab_strip_model_observer_bridge.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSWindow</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">browser/cocoa/nswindow_local_state.h</string> + </object> + </object> + </object> + <object class="NSMutableArray" key="referencedPartialClassDescriptionsV3.2+"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBPartialClassDescription"> + <string key="className">NSApplication</string> + <string key="superclassName">NSResponder</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier" id="321325912"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSApplication.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSApplication</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier" id="587048469"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSApplicationScripting.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSApplication</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier" id="448641522"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSColorPanel.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSApplication</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSHelpManager.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSApplication</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSPageLayout.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSMenu</string> + <string key="superclassName">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier" id="636743587"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSMenu.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSAccessibility.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSAlert.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSAnimation.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <reference key="sourceIdentifier" ref="321325912"/> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <reference key="sourceIdentifier" ref="587048469"/> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSBrowser.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <reference key="sourceIdentifier" ref="448641522"/> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSComboBox.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSComboBoxCell.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSControl.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSDatePickerCell.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSDictionaryController.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSDragging.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier" id="429966883"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSDrawer.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSFontManager.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSFontPanel.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSImage.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSKeyValueBinding.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <reference key="sourceIdentifier" ref="636743587"/> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSNibLoading.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSOutlineView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSPasteboard.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSRuleEditor.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSSavePanel.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSSound.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSSpeechRecognizer.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSSpeechSynthesizer.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSSplitView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSTabView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSTableView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSText.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSTextStorage.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSTextView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSTokenField.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSTokenFieldCell.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSToolbar.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSToolbarItem.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier" id="293622296"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier" id="617292003"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSWindow.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSArchiver.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSClassDescription.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSConnection.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSError.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSFileManager.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSKeyValueCoding.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSKeyValueObserving.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSKeyedArchiver.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSMetadata.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSNetServices.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSObject.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSObjectScripting.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSPort.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSPortCoder.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSRunLoop.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSScriptClassDescription.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSScriptKeyValueCoding.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSScriptObjectSpecifiers.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSScriptWhoseTests.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSSpellServer.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSStream.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSThread.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSURL.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSURLConnection.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSURLDownload.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Foundation.framework/Headers/NSXMLParser.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">Print.framework/Headers/PDEPluginInterface.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">QuartzCore.framework/Headers/CAAnimation.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">QuartzCore.framework/Headers/CALayer.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">QuartzCore.framework/Headers/CIImageProvider.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">SecurityInterface.framework/Headers/SFAuthorizationView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">SecurityInterface.framework/Headers/SFCertificatePanel.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">SecurityInterface.framework/Headers/SFChooseIdentityPanel.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSResponder</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSInterfaceStyle.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSResponder</string> + <string key="superclassName">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSResponder.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSView</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSClipView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSView</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSMenuItem.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSView</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSRulerView.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSView</string> + <string key="superclassName">NSResponder</string> + <reference key="sourceIdentifier" ref="293622296"/> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSWindow</string> + <reference key="sourceIdentifier" ref="429966883"/> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSWindow</string> + <string key="superclassName">NSResponder</string> + <reference key="sourceIdentifier" ref="617292003"/> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSWindow</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSWindowScripting.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> + <string key="className">NSWindowController</string> + <string key="superclassName">NSResponder</string> + <object class="NSMutableDictionary" key="actions"> + <string key="NS.key.0">showWindow:</string> + <string key="NS.object.0">id</string> + </object> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSWindowController.h</string> + </object> + </object> + </object> + </object> + <int key="IBDocument.localizationMode">0</int> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string> + <integer value="1050" key="NS.object.0"/> + </object> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string> + <integer value="1050" key="NS.object.0"/> + </object> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string> + <integer value="3000" key="NS.object.0"/> + </object> + <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool> + <string key="IBDocument.LastKnownRelativeProjectPath">../../chrome.xcodeproj</string> + <int key="IBDocument.defaultPropertyAccessControl">3</int> + </data> +</archive> diff --git a/chrome/browser/cocoa/bookmark_bar_controller.h b/chrome/browser/cocoa/bookmark_bar_controller.h index 874987a..f36a76e 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller.h +++ b/chrome/browser/cocoa/bookmark_bar_controller.h @@ -8,6 +8,7 @@ #import <Cocoa/Cocoa.h> #include <map> +#import "base/chrome_application_mac.h" #include "base/scoped_nsobject.h" #include "base/scoped_ptr.h" #include "chrome/browser/cocoa/bookmark_bar_bridge.h" @@ -18,6 +19,7 @@ #include "webkit/glue/window_open_disposition.h" @class BookmarkBarController; +@class BookmarkBarFolderController; @class BookmarkBarView; @class BookmarkButton; class BookmarkModel; @@ -35,6 +37,8 @@ class TabContents; namespace bookmarks { // Magic numbers from Cole +// TODO(jrg): create an objc-friendly version of bookmark_bar_constants.h? + const CGFloat kDefaultBookmarkWidth = 150.0; const CGFloat kBookmarkVerticalPadding = 2.0; const CGFloat kBookmarkHorizontalPadding = 1.0; @@ -42,6 +46,45 @@ const CGFloat kBookmarkHorizontalPadding = 1.0; const CGFloat kNoBookmarksHorizontalOffset = 5.0; const CGFloat kNoBookmarksVerticalOffset = 6.0; +// (end magic numbers from Cole) + +// Delay before opening a subfolder (and closing the previous one) +// when hovering over a folder button. +const NSTimeInterval kHoverOpenDelay = 0.3; + +// Delay on hover before a submenu opens when dragging. +// Experimentally a drag hover open delay needs to be bigger than a +// normal (non-drag) menu hover open such as used in the bookmark folder. +// TODO(jrg): confirm feel of this constant with ui-team. +// http://crbug.com/36276 +const NSTimeInterval kDragHoverOpenDelay = 0.7; + +// Notes on use of kDragHoverCloseDelay in +// -[BookmarkBarFolderController draggingEntered:]. +// +// We have an implicit delay on stop-hover-open before a submenu +// closes. This cannot be zero since it's nice to move the mouse in a +// direct line from "current position" to "position of item in +// submenu". However, by doing so, it's possible to overlap a +// different button on the current menu. Example: +// +// Folder1 +// Folder2 ---> Sub1 +// Folder3 Sub2 +// Sub3 +// +// If you hover over the F in Folder2 to open the sub, and then want to +// select Sub3, a direct line movement of the mouse may cross over +// Folder3. Without this delay, that'll cause Sub to be closed before +// you get there, since a "hover over" of Folder3 gets activated. +// It's subtle but without the delay it feels broken. +// +// This is only really a problem with vertical menu --> vertical menu +// movement; the bookmark bar (horizontal menu, sort of) seems fine, +// perhaps because mouse move direction is purely vertical so there is +// no opportunity for overlap. +const NSTimeInterval kDragHoverCloseDelay = 0.4; + } // namespace bookmarks // The interface for the bookmark bar controller's delegate. Currently, the @@ -69,7 +112,9 @@ willAnimateFromState:(bookmarks::VisualState)oldState @interface BookmarkBarController : NSViewController<BookmarkBarState, BookmarkBarToolbarViewController, - BookmarkButtonDelegate> { + BookmarkButtonDelegate, + BookmarkButtonControllerProtocol, + CrApplicationEventHookProtocol> { @private // The visual state of the bookmark bar. If an animation is running, this is // set to the "destination" and |lastVisualState_| is set to the "original" @@ -118,6 +163,13 @@ willAnimateFromState:(bookmarks::VisualState)oldState // Delegate that can resize us. id<ViewResizer> resizeDelegate_; // weak + // A controller for a pop-up bookmark folder window (custom menu). + // This is not a scoped_nsobject because it owns itself (when its + // window closes the controller gets autoreleased). + BookmarkBarFolderController* folderController_; + + BOOL watchingForClickOutside_; // Are watching for a "click outside"? + IBOutlet BookmarkBarView* buttonView_; IBOutlet MenuButton* offTheSideButton_; // aka the chevron IBOutlet NSMenu* buttonContextMenu_; @@ -128,6 +180,13 @@ willAnimateFromState:(bookmarks::VisualState)oldState // We have a special menu for folder buttons. This starts as a copy // of the bar menu. scoped_nsobject<BookmarkMenu> buttonFolderContextMenu_; + + // When doing a drag, this is folder button "hovered over" which we + // may want to open after a short delay. There are cases where a + // mouse-enter can open a folder (e.g. if the menus are "active") + // but that doesn't use this variable or need a delay so "hover" is + // the wrong term. + scoped_nsobject<BookmarkButton> hoverButton_; } @property(readonly, nonatomic) bookmarks::VisualState visualState; @@ -170,22 +229,13 @@ willAnimateFromState:(bookmarks::VisualState)oldState // need it for animations. Try not to propagate its use. - (void)layoutSubviews; -// Complete a drag of a bookmark button to the given point (given in window -// coordinates) on the main bar. -// TODO(jrg): submenu DnD. -// Returns YES on success. -- (BOOL)dragButton:(BookmarkButton*)sourceButton to:(NSPoint)point; - -// The x-coordinate of (the middle of) the indicator to draw for a drag of the -// source button to the given point (given in window coordinates). -// TODO(viettrungluu,jrg): instead of this, make buttons move around. -- (CGFloat)indicatorPosForDragOfButton:(BookmarkButton*)sourceButton - toPoint:(NSPoint)point; +// Called by our view when it is moved to a window. +- (void)viewDidMoveToWindow; // Actions for manipulating bookmarks. -// From a button, ... +// Open a normal bookmark or folder from a button, ... - (IBAction)openBookmark:(id)sender; -- (IBAction)openFolderMenuFromButton:(id)sender; +- (IBAction)openBookmarkFolderFromButton:(id)sender; // From a context menu over the button, ... - (IBAction)openBookmarkInNewForegroundTab:(id)sender; - (IBAction)openBookmarkInNewWindow:(id)sender; @@ -243,6 +293,9 @@ willAnimateFromState:(bookmarks::VisualState)oldState - (NSButton*)otherBookmarksButton; - (BookmarkNode*)nodeFromMenuItem:(id)sender; - (void)updateTheme:(ThemeProvider*)themeProvider; +- (BookmarkBarFolderController*)folderController; +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point; +- (BOOL)isEventAClickOutside:(NSEvent*)event; @end // The (internal) |NSPasteboard| type string for bookmark button drags, used for diff --git a/chrome/browser/cocoa/bookmark_bar_controller.mm b/chrome/browser/cocoa/bookmark_bar_controller.mm index adb6e13..6d376f37 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller.mm +++ b/chrome/browser/cocoa/bookmark_bar_controller.mm @@ -2,6 +2,7 @@ // 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_bar_controller.h" #include "app/l10n_util_mac.h" #include "app/resource_bundle.h" #include "base/mac_util.h" @@ -14,7 +15,8 @@ #import "chrome/browser/cocoa/background_gradient_view.h" #import "chrome/browser/cocoa/bookmark_bar_bridge.h" #import "chrome/browser/cocoa/bookmark_bar_constants.h" -#import "chrome/browser/cocoa/bookmark_bar_controller.h" +#import "chrome/browser/cocoa/bookmark_bar_folder_controller.h" +#import "chrome/browser/cocoa/bookmark_bar_folder_window.h" #import "chrome/browser/cocoa/bookmark_bar_toolbar_view.h" #import "chrome/browser/cocoa/bookmark_bar_view.h" #import "chrome/browser/cocoa/bookmark_button.h" @@ -171,6 +173,8 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; - (NSImage*)getFavIconForNode:(const BookmarkNode*)node; - (void)setNodeForBarMenu; +- (void)watchForClickOutside:(BOOL)watch; + @end @implementation BookmarkBarController @@ -200,12 +204,12 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; folderImage_.reset([rb.GetNSImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]); defaultImage_.reset([rb.GetNSImageNamed(IDR_DEFAULT_FAVICON) retain]); - // Register for theme changes. - NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; - [defaultCenter addObserver:self - selector:@selector(themeDidChangeNotification:) - name:kBrowserThemeDidChangeNotification - object:nil]; + // Register for theme changes. + NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; + [defaultCenter addObserver:self + selector:@selector(themeDidChangeNotification:) + name:kBrowserThemeDidChangeNotification + object:nil]; // This call triggers an awakeFromNib, which builds the bar, which // might uses folderImage_. So make sure it happens after @@ -224,6 +228,10 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; //TODO(dmaclach): Remove -- http://crbug.com/25845 [[self view] removeFromSuperview]; + // Be sure there is no dangling pointer. + if ([[self view] respondsToSelector:@selector(setController:)]) + [[self view] performSelector:@selector(setController:) withObject:nil]; + // For safety, make sure the buttons can no longer call us. for (BookmarkButton* button in buttons_.get()) { [button setDelegate:nil]; @@ -233,6 +241,7 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; bridge_.reset(NULL); [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self watchForClickOutside:NO]; [super dealloc]; } @@ -255,6 +264,11 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; [[otherBookmarksButton_ cell] setTextColor:color]; } +// Exposed purely for testing. +- (BookmarkBarFolderController*)folderController { + return folderController_; +} + // Called after the current theme has changed. - (void)themeDidChangeNotification:(NSNotification*)aNotification { ThemeProvider* themeProvider = @@ -288,16 +302,32 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; // no longer visible), or add/remove the "off the side" menu. [[self view] setPostsFrameChangedNotifications:YES]; [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(frameDidChange) - name:NSViewFrameDidChangeNotification - object:[self view]]; + addObserver:self + selector:@selector(frameDidChange) + name:NSViewFrameDidChangeNotification + object:[self view]]; // Don't pass ourself along (as 'self') until our init is completely // done. Thus, this call is (almost) last. bridge_.reset(new BookmarkBarBridge(self, bookmarkModel_)); } +// Called by our main view (a BookmarkBarView) when it gets moved to a +// window. We perform operations which need to know the relevant +// window (e.g. watch for a window close) so they can't be performed +// earlier (such as in awakeFromNib). +- (void)viewDidMoveToWindow { + NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; + [defaultCenter addObserver:self + selector:@selector(parentWindowWillClose:) + name:NSWindowWillCloseNotification + object:[[self view] window]]; + [defaultCenter addObserver:self + selector:@selector(parentWindowDidResignKey:) + name:NSWindowDidResignKeyNotification + object:[[self view] window]]; +} + // (Private) Method is the same as [self view], but is provided to be explicit. - (BackgroundGradientView*)backgroundGradientView { DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]); @@ -315,7 +345,8 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; NSRect frame = [offTheSideButton_ frame]; if (otherBookmarksButton_.get()) { frame.origin.x = ([otherBookmarksButton_ frame].origin.x - - (frame.size.width + bookmarks::kBookmarkHorizontalPadding)); + (frame.size.width + + bookmarks::kBookmarkHorizontalPadding)); [offTheSideButton_ setFrame:frame]; } } @@ -350,6 +381,121 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; [self centerNoItemsLabel]; } +// Close all bookmark folders. "Folder" here is the fake menu for +// bookmark folders, not a button context menu. +- (void)closeAllBookmarkFolders { + [self watchForClickOutside:NO]; + [[folderController_ window] close]; + folderController_ = nil; +} + +- (void)closeBookmarkFolder:(id)sender { + // We're the top level, so close one means close them all. + [self closeAllBookmarkFolders]; +} + +- (BookmarkModel*)bookmarkModel { + return bookmarkModel_; +} + +// NSNotificationCenter callback. +- (void)parentWindowWillClose:(NSNotification*)notification { + [self closeAllBookmarkFolders]; +} + +// NSNotificationCenter callback. +- (void)parentWindowDidResignKey:(NSNotification*)notification { + [self closeAllBookmarkFolders]; +} + +// BookmarkButtonDelegate protocol implementation. When menus are +// "active" (e.g. you clicked to open one), moving the mouse over +// another folder button should close the 1st and open the 2nd (like +// real menus). We detect and act here. +- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event { + DCHECK([sender isKindOfClass:[BookmarkButton class]]); + // If nothing is open, do nothing. Different from + // BookmarkBarFolderController since we default to NOT being enabled. + if (!folderController_) + return; + + // From here down: same logic as BookmarkBarFolderController. + // TODO(jrg): find a way to share these 4 non-comment lines? + // http://crbug.com/35966 + + // If already opened, then we exited but re-entered the button, so do nothing. + if ([folderController_ parentButton] == sender) + return; + // Else open a new one if it makes sense to do so. + if ([sender bookmarkNode]->is_folder()) + [self openBookmarkFolderFromButton:sender]; +} + +// BookmarkButtonDelegate protocol implementation. +- (void)mouseExitedButton:(id)sender event:(NSEvent*)event { + // Don't care; do nothing. + // This is different behavior that the folder menus. +} + +// Begin (or end) watching for a click outside this window. Unlike +// normal NSWindows, bookmark folder "fake menu" windows do not become +// key or main. Thus, traditional notification (e.g. WillResignKey) +// won't work. Our strategy is to watch (at the app level) for a +// "click outside" these windows to detect when they logically lose +// focus. +- (void)watchForClickOutside:(BOOL)watch { + CrApplication* app = static_cast<CrApplication*>([NSApplication + sharedApplication]); + DCHECK([app isKindOfClass:[CrApplication class]]); + if (watch) { + if (!watchingForClickOutside_) + [app addEventHook:self]; + } else { + if (watchingForClickOutside_) + [app removeEventHook:self]; + } + watchingForClickOutside_ = watch; +} + +// Implementation of CrApplicationEventHookProtocol. +// NOT an override of a standard Cocoa call made to NSViewControllers. +- (void)hookForEvent:(NSEvent*)theEvent { + if ([self isEventAClickOutside:theEvent]) { + [self watchForClickOutside:NO]; + [self closeAllBookmarkFolders]; + } +} + +// Return YES if the event represents a "click outside" of the area we +// are watching. At this time we are watching the area that includes +// all popup bookmark folder windows. +- (BOOL)isEventAClickOutside:(NSEvent*)event { + NSWindow* eventWindow = [event window]; + NSWindow* myWindow = [[self view] window]; + switch ([event type]) { + case NSLeftMouseDown: + case NSRightMouseDown: + // If a click in my window and NOT in the bookmark bar, + // then is a click outside. + if ((eventWindow == myWindow) && + ([[eventWindow contentView] + hitTest:[event locationInWindow]] != [self view])) { + return YES; + } + // If a click in a bookmark bar folder window and that isn't + // one of my bookmark bar folders, YES is click outside. + if (([eventWindow isKindOfClass:[BookmarkBarFolderWindow + class]]) && + ([eventWindow parentWindow] != myWindow)) { + return YES; + } + break; + default: + break; + } + return NO; +} + // Keep the "no items" label centered in response to a frame size change. - (void)centerNoItemsLabel { // Note that this computation is done in the parent's coordinate system, which @@ -525,13 +671,6 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; toPoint:(NSPoint)point { DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); - void* pointer = [[[sourceButton cell] representedObject] pointerValue]; - const BookmarkNode* sourceNode = static_cast<const BookmarkNode*>(pointer); - if (!sourceNode) { - NOTREACHED(); - return -1; - } - // Identify which buttons we are between. For now, assume a button // location is at the center point of its view, and that an exact // match means "place before". @@ -540,8 +679,8 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; NSPoint dropLocation = [[self view] convertPoint:point fromView:[[[self view] window] contentView]]; - NSButton* buttonToTheRightOfDraggedButton = nil; - for (NSButton* button in buttons_.get()) { + BookmarkButton* buttonToTheRightOfDraggedButton = nil; + for (BookmarkButton* button in buttons_.get()) { CGFloat midpoint = NSMidX([button frame]); if (dropLocation.x <= midpoint) { buttonToTheRightOfDraggedButton = button; @@ -549,26 +688,28 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; } } if (buttonToTheRightOfDraggedButton) { - pointer = [[[buttonToTheRightOfDraggedButton cell] - representedObject] pointerValue]; - const BookmarkNode* afterNode = static_cast<const BookmarkNode*>(pointer); + const BookmarkNode* afterNode = + [buttonToTheRightOfDraggedButton bookmarkNode]; + DCHECK(afterNode); return afterNode->GetParent()->IndexOfChild(afterNode); } // If nothing is to my right I am at the end! - return sourceNode->GetParent()->GetChildCount(); + return [buttons_ count]; } - (BOOL)dragButton:(BookmarkButton*)sourceButton to:(NSPoint)point { DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); - void* pointer = [[[sourceButton cell] representedObject] pointerValue]; - const BookmarkNode* sourceNode = static_cast<const BookmarkNode*>(pointer); + const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; DCHECK(sourceNode); int destIndex = [self indexForDragOfButton:sourceButton toPoint:point]; if (destIndex >= 0 && sourceNode) { - bookmarkModel_->Move(sourceNode, sourceNode->GetParent(), destIndex); + // Our destination parent is not sourceNode->GetParent()! + bookmarkModel_->Move(sourceNode, + bookmarkModel_->GetBookmarkBarNode(), + destIndex); } else { NOTREACHED(); } @@ -579,6 +720,121 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; return YES; } +// Find something like std::is_between<T>? I can't believe one doesn't exist. +static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { + return ((value >= low) && (value <= high)); +} + +// Return the proposed drop target for a hover open button from the +// given array, or nil if none. We use this for distinguishing +// between a hover-open candidate or drop-indicator draw. +// Helper for buttonForDroppingOnAtPoint:. +// Get UI review on "middle half" ness. +// http://crbug.com/36276 +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point + fromArray:(NSArray*)array { + for (BookmarkButton* button in array) { + // Break early if we've gone too far. + if (NSMinX([button frame]) > point.x) + return nil; + // Careful -- this only applies to the bar with horiz buttons. + // Intentionally NOT using NSPointInRect() so that scrolling into + // a submenu doesn't cause it to be closed. + if (ValueInRangeInclusive(NSMinX([button frame]), + point.x, + NSMaxX([button frame]))) { + // Over a button but let's be a little more specific (make sure + // it's over the middle half, not just over it.) + NSRect frame = [button frame]; + NSRect middleHalfOfButton = NSInsetRect(frame, frame.size.width / 4, 0); + if (ValueInRangeInclusive(NSMinX(middleHalfOfButton), + point.x, + NSMaxX(middleHalfOfButton))) { + // It makes no sense to drop on a non-folder; there is no hover. + if (![button isFolder]) + return nil; + // Got it! + return button; + } else { + // Over a button but not over the middle half. + return nil; + } + } + } + // Not hovering over a button. + return nil; +} + +// Return the proposed drop target for a hover open button, or nil if +// none. Works with both the bookmark buttons and the "Other +// Bookmarks" button. Point is in [self view] coordinates. +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point { + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point + fromArray:buttons_.get()]; + // One more chance -- try "Other Bookmarks". + // This is different than BookmarkBarFolderController. + if (!button) { + NSArray* array = [NSArray arrayWithObject:otherBookmarksButton_]; + button = [self buttonForDroppingOnAtPoint:point + fromArray:array]; + } + return button; +} + +// TODO(jrg): much of this logic is duped with +// [BookmarkBarFolderController draggingEntered:] except when noted. +// http://crbug.com/35966 +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + NSPoint point = [info draggingLocation]; + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + if ([button isFolder]) { + if (hoverButton_ == button) { + return NSDragOperationMove; // already open or timed to open + } + if (hoverButton_) { + // Oops, another one triggered or open. + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ + target]]; + // Unlike BookmarkBarFolderController, we do not delay the close + // of the previous one. Given the lack of diagonal movement, + // there is no need, and it feels awkward to do so. See + // comments about kDragHoverCloseDelay in + // bookmark_bar_folder_controller.mm for more details. + [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; + hoverButton_.reset(); + } + hoverButton_.reset([button retain]); + [[hoverButton_ target] + performSelector:@selector(openBookmarkFolderFromButton:) + withObject:hoverButton_ + afterDelay:bookmarks::kDragHoverOpenDelay]; + } + if (!button) { + if (hoverButton_) { + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; + [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; + hoverButton_.reset(); + } + } + + // Thrown away but kept to be consistent with the draggingEntered: interface. + return NSDragOperationMove; +} + +- (void)draggingExited:(id<NSDraggingInfo>)info { + // NOT the same as a cancel --> we may have moved the mouse into the submenu. + if (hoverButton_) { + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; + hoverButton_.reset(); + } +} + +// Return YES if we should show the drop indicator, else NO. +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { + return ![self buttonForDroppingOnAtPoint:point]; +} + +// Return the x position for a drop indicator. - (CGFloat)indicatorPosForDragOfButton:(BookmarkButton*)sourceButton toPoint:(NSPoint)point { CGFloat x = 0; @@ -617,6 +873,11 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; return x; } +// Return the parent window for all BookmarkBarFolderController windows. +- (NSWindow*)parentWindow { + return [[self view] window]; +} + - (int)currentTabContentsHeight { return browser_->GetSelectedTabContents() ? browser_->GetSelectedTabContents()->view()->GetContainerSize().height() : @@ -627,14 +888,6 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; return browser_->profile()->GetThemeProvider(); } -- (BookmarkNode*)nodeFromButton:(id)button { - NSCell* cell = [button cell]; - BookmarkNode* node = static_cast<BookmarkNode*>( - [[cell representedObject] pointerValue]); - DCHECK(node); - return node; -} - // Enable or disable items. We are the menu delegate for both the bar // and for bookmark folder buttons. - (BOOL)validateUserInterfaceItem:(id)item { @@ -695,7 +948,9 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; } - (IBAction)openBookmark:(id)sender { - BookmarkNode* node = [self nodeFromButton:sender]; + [self closeAllBookmarkFolders]; + DCHECK([sender respondsToSelector:@selector(bookmarkNode)]); + const BookmarkNode* node = [sender bookmarkNode]; WindowOpenDisposition disposition = event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); [self openURL:node->GetURL() disposition:disposition]; @@ -801,14 +1056,31 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; return menu; } -// Called from a Folder bookmark button. -- (IBAction)openFolderMenuFromButton:(id)sender { - NSMenu* menu = [self menuForFolderNode:[self nodeFromButton:sender]]; - if (menu) { - [NSMenu popUpContextMenu:menu - withEvent:[NSApp currentEvent] - forView:sender]; +// Heuristics: +// 1: click and hold (without moving much): opens it +// 2: click and release (normal button push): opens it. +// This is called on the second one. +// +// Called from a Folder bookmark button to open the folder. +- (IBAction)openBookmarkFolderFromButton:(id)sender { + DCHECK(sender); + if (folderController_) { + // closeAllBookmarkFolders sets folderController_ to nil + // so we need the SAME check to happen first. + BOOL same = ([folderController_ parentButton] == sender); + [self closeAllBookmarkFolders]; + // If click on same folder, close it and be done. + // Else we clicked on a different folder so more work to do. + if (same) + return; } + + // Folder controller, like many window controllers, owns itself. + folderController_ = [[BookmarkBarFolderController alloc] + initWithParentButton:sender + parentController:self]; + [folderController_ showWindow:self]; + [self watchForClickOutside:YES]; } // Rebuild the off-the-side menu, taking into account which buttons are @@ -827,9 +1099,9 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; [menu removeItemAtIndex:i]; // Add items corresponding to buttons which aren't displayed. - for (NSButton* button in buttons_.get()) { + for (BookmarkButton* button in buttons_.get()) { if (![button superview]) - [self addNode:[self nodeFromButton:button] toMenu:menu]; + [self addNode:[button bookmarkNode] toMenu:menu]; } } @@ -903,6 +1175,9 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; BookmarkNode* node = [self nodeFromMenuItem:sender]; bookmarkModel_->Remove(node->GetParent(), node->GetParent()->IndexOfChild(node)); + // TODO(jrg): don't close; rebuild. + // http://crbug.com/36614 + [self closeAllBookmarkFolders]; } // An ObjC version of bookmark_utils::OpenAllImpl(). @@ -986,19 +1261,29 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; // Return an autoreleased NSCell suitable for a bookmark button. // TODO(jrg): move much of the cell config into the BookmarkButtonCell class. - (NSCell*)cellForBookmarkNode:(const BookmarkNode*)node { - NSString* title = base::SysWideToNSString(node->GetTitle()); BookmarkButtonCell* cell = [[[BookmarkButtonCell alloc] initTextCell:nil] autorelease]; DCHECK(cell); - [cell setRepresentedObject:[NSValue valueWithPointer:node]]; + [cell setBookmarkNode:node]; + + if (node) { + NSString* title = base::SysWideToNSString(node->GetTitle()); + NSImage* image = [self getFavIconForNode:node]; + [cell setBookmarkCellText:title image:image]; + if (node->is_folder()) + [cell setMenu:buttonFolderContextMenu_]; + else + [cell setMenu:buttonContextMenu_]; + } else { + [cell setEmpty:YES]; + [cell setBookmarkCellText:l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU) + image:nil]; + } + + // Note: a quirk of setting a cell's text color is that it won't work + // until the cell is associated with a button, so we can't theme the cell yet. - NSImage* image = [self getFavIconForNode:node]; - [cell setBookmarkCellText:title image:image]; - if (node->is_folder()) - [cell setMenu:buttonFolderContextMenu_]; - else - [cell setMenu:buttonContextMenu_]; return cell; } @@ -1101,7 +1386,7 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; if (child->is_folder()) { [button setTarget:self]; - [button setAction:@selector(openFolderMenuFromButton:)]; + [button setAction:@selector(openBookmarkFolderFromButton:)]; } else { // Make the button do something [button setTarget:self]; @@ -1193,7 +1478,7 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; [button setCell:cell]; [button setDelegate:self]; [button setTarget:self]; - [button setAction:@selector(openFolderMenuFromButton:)]; + [button setAction:@selector(openBookmarkFolderFromButton:)]; [buttonView_ addSubview:button]; // Now that it's here, move the chevron over. @@ -1269,13 +1554,11 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; // favicons will eventually load. - (void)nodeFavIconLoaded:(BookmarkModel*)model node:(const BookmarkNode*)node { - for (NSButton* button in buttons_.get()) { - BookmarkButtonCell* cell = [button cell]; - void* pointer = [[cell representedObject] pointerValue]; - const BookmarkNode* cellnode = static_cast<const BookmarkNode*>(pointer); + for (BookmarkButton* button in buttons_.get()) { + const BookmarkNode* cellnode = [button bookmarkNode]; if (cellnode == node) { - [cell setBookmarkCellText:nil - image:[self getFavIconForNode:node]]; + [[button cell] setBookmarkCellText:nil + image:[self getFavIconForNode:node]]; // Adding an image means we might need more room for the // bookmark. Test for it by growing the button (if needed) // and shifting everything else over. @@ -1502,7 +1785,7 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; // (BookmarkButtonDelegate protocol) - (void)fillPasteboard:(NSPasteboard*)pboard forDragOfButton:(BookmarkButton*)button { - if (BookmarkNode* node = [self nodeFromButton:button]) { + if (const BookmarkNode* node = [button bookmarkNode]) { // Put the bookmark information into the pasteboard, and then write our own // data for |kBookmarkButtonDragType|. [self copyBookmarkNode:node diff --git a/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm b/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm index 4e1aa933..5f460a7 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm +++ b/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm @@ -10,8 +10,10 @@ #include "base/sys_string_conversions.h" #import "chrome/browser/cocoa/bookmark_bar_constants.h" #import "chrome/browser/cocoa/bookmark_bar_controller.h" +#import "chrome/browser/cocoa/bookmark_bar_folder_window.h" #import "chrome/browser/cocoa/bookmark_bar_view.h" #import "chrome/browser/cocoa/bookmark_button.h" +#import "chrome/browser/cocoa/bookmark_button_cell.h" #import "chrome/browser/cocoa/bookmark_menu.h" #include "chrome/browser/cocoa/browser_test_helper.h" #import "chrome/browser/cocoa/cocoa_test_helper.h" @@ -146,6 +148,9 @@ class BookmarkBarControllerTest : public CocoaTest { frame.origin.y = 100; [[[bar view] superview] setFrame:frame]; + // Make sure it's on in a window so viewDidMoveToWindow is called + [[test_window() contentView] addSubview:parent_view_]; + // Make sure it's open so certain things aren't no-ops. [bar updateAndShowNormalBar:YES showDetachedBar:NO @@ -434,8 +439,9 @@ TEST_F(BookmarkBarControllerTest, OpenBookmark) { GURL gurl("http://walla.walla.ding.dong.com"); scoped_ptr<BookmarkNode> node(new BookmarkNode(gurl)); - scoped_nsobject<NSButtonCell> cell([[NSButtonCell alloc] init]); - scoped_nsobject<NSButton> button([[NSButton alloc] init]); + scoped_nsobject<BookmarkButtonCell> cell([[BookmarkButtonCell alloc] init]); + [cell setBookmarkNode:node.get()]; + scoped_nsobject<BookmarkButton> button([[BookmarkButton alloc] init]); [button setCell:cell.get()]; [cell setRepresentedObject:[NSValue valueWithPointer:node.get()]]; @@ -651,6 +657,13 @@ TEST_F(BookmarkBarControllerTest, Cell) { EXPECT_TRUE(cell); EXPECT_TRUE([[cell title] isEqual:@"supertitle"]); EXPECT_EQ(node, [[cell representedObject] pointerValue]); + EXPECT_TRUE([cell menu]); + + // Empty cells have no menu. + cell = [bar_ cellForBookmarkNode:nil]; + EXPECT_FALSE([cell menu]); + // Even empty cells have a title (of "(empty)") + EXPECT_TRUE([cell title]); // cell is autoreleased; no need to release here } @@ -932,6 +945,129 @@ TEST_F(BookmarkBarControllerTest, TestClearOnDealloc) { } } +TEST_F(BookmarkBarControllerTest, TestFolders) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + + // Create some folder buttons. + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folder = model->AddGroup(parent, + parent->GetChildCount(), + L"group"); + model->AddURL(folder, folder->GetChildCount(), + L"f1", GURL("http://framma-lamma.com")); + folder = model->AddGroup(parent, parent->GetChildCount(), L"empty"); + + EXPECT_EQ([[bar_ buttons] count], 2U); + BookmarkButton* button = [[bar_ buttons] objectAtIndex:0]; // full one + + EXPECT_FALSE([bar_ folderController]); + [bar_ openBookmarkFolderFromButton:button]; + BookmarkBarFolderController* bbfc = [bar_ folderController]; + EXPECT_TRUE(bbfc); + + // Make sure a 2nd open on the same button closes things. + [bar_ openBookmarkFolderFromButton:button]; + EXPECT_FALSE([bar_ folderController]); + + // Next open is a different button. + [bar_ openBookmarkFolderFromButton:[[bar_ buttons] objectAtIndex:1]]; + EXPECT_TRUE([bar_ folderController]); + EXPECT_NE(bbfc, [bar_ folderController]); + + // Finally confirm a close removes the folder controller. + [bar_ closeBookmarkFolder:nil]; + EXPECT_FALSE([bar_ folderController]); + + // Next part of the test: similar actions but with mouseEntered/mouseExited. + + // First confirm mouseEntered does nothing if "menus" aren't active. + NSEvent* event = test_event_utils::MakeMouseEvent(NSOtherMouseUp, 0); + [bar_ mouseEnteredButton:[[bar_ buttons] objectAtIndex:0] event:event]; + EXPECT_FALSE([bar_ folderController]); + + // Make one active. Entering it is now a no-op. + [bar_ openBookmarkFolderFromButton:[[bar_ buttons] objectAtIndex:0]]; + bbfc = [bar_ folderController]; + EXPECT_TRUE(bbfc); + [bar_ mouseEnteredButton:[[bar_ buttons] objectAtIndex:0] event:event]; + EXPECT_EQ(bbfc, [bar_ folderController]); + + // Enter a different one; a new folderController is active. + [bar_ mouseEnteredButton:[[bar_ buttons] objectAtIndex:1] event:event]; + EXPECT_NE(bbfc, [bar_ folderController]); + + // Confirm exited is a no-op. + [bar_ mouseExitedButton:[[bar_ buttons] objectAtIndex:1] event:event]; + EXPECT_NE(bbfc, [bar_ folderController]); + + // Clean up. + [bar_ closeBookmarkFolder:nil]; +} + +TEST_F(BookmarkBarControllerTest, ClickOutsideCheck) { + NSEvent* event = test_event_utils::MakeMouseEvent(NSMouseMoved, 0); + EXPECT_FALSE([bar_ isEventAClickOutside:event]); + + BookmarkBarFolderWindow* folderWindow = [[[BookmarkBarFolderWindow alloc] + init] autorelease]; + [[[bar_ view] window] addChildWindow:folderWindow + ordered:NSWindowAbove]; + event = test_event_utils::LeftMouseDownAtPointInWindow(NSMakePoint(1,1), + folderWindow); + EXPECT_FALSE([bar_ isEventAClickOutside:event]); + + event = test_event_utils::LeftMouseDownAtPointInWindow(NSMakePoint(100,100), + test_window()); + EXPECT_TRUE([bar_ isEventAClickOutside:event]); + [[[bar_ view] window] removeChildWindow:folderWindow]; +} + +TEST_F(BookmarkBarControllerTest, DropDestination) { + // Make some buttons. + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + model->AddGroup(parent, parent->GetChildCount(), L"group 1"); + model->AddGroup(parent, parent->GetChildCount(), L"group 2"); + EXPECT_EQ([[bar_ buttons] count], 2U); + + // Confirm "off to left" and "off to right" match nothing. + NSPoint p = NSMakePoint(-1, 2); + EXPECT_FALSE([bar_ buttonForDroppingOnAtPoint:p]); + EXPECT_TRUE([bar_ shouldShowIndicatorShownForPoint:p]); + p = NSMakePoint(50000, 10); + EXPECT_FALSE([bar_ buttonForDroppingOnAtPoint:p]); + EXPECT_TRUE([bar_ shouldShowIndicatorShownForPoint:p]); + + // Confirm "right in the center" (give or take a pixel) is a match, + // and confirm "just barely in the button" is not. Anything more + // specific seems likely to be tweaked. + for (BookmarkButton* button in [bar_ buttons]) { + CGFloat x = NSMidX([button frame]); + // Somewhere near the center: a match + EXPECT_EQ(button, + [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x-1, 10)]); + EXPECT_EQ(button, + [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x+1, 10)]); + EXPECT_FALSE([bar_ shouldShowIndicatorShownForPoint:NSMakePoint(x, 10)]);; + + // On the very edges: NOT a match + x = NSMinX([button frame]); + EXPECT_NE(button, + [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x, 9)]); + x = NSMaxX([button frame]); + EXPECT_NE(button, + [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x, 11)]); + } +} + +// TODO(jrg): draggingEntered: and draggingExited: trigger timers so +// they are hard to test. Factor out "fire timers" into routines +// which can be overridden to fire immediately to make behavior +// confirmable. + +// TODO(jrg): add unit test to make sure "Other Bookmarks" responds +// properly to a hover open. + // TODO(viettrungluu): figure out how to test animations. } // namespace diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller.h b/chrome/browser/cocoa/bookmark_bar_folder_controller.h new file mode 100644 index 0000000..5f199d4 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_controller.h @@ -0,0 +1,92 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/scoped_nsobject.h" +#import "chrome/browser/cocoa/bookmark_button.h" + +@class BookmarkBarFolderView; + +// A controller for the pop-up windows from bookmark folder buttons +// which look sort of like menus. +@interface BookmarkBarFolderController : + NSWindowController<BookmarkButtonDelegate, + BookmarkButtonControllerProtocol> { + @private + // The button whose click opened us. + scoped_nsobject<BookmarkButton> parentButton_; + + // Bookmark bar folder controller chains are torn down in two ways: + // 1. Clicking "outside" the folder (see use of + // CrApplicationEventHookProtocol in the bookmark bar controller). + // 2. Engaging a different folder (via hover over or explicit click). + // + // In either case, the BookmarkButtonControllerProtocol method + // closeAllBookmarkFolders gets called. For bookmark bar folder + // controllers, this is passed up the chain so we begin with a top + // level "close". + // When any bookmark folder window closes, it necessarily tells + // subcontroller windows to close (down the chain), and autoreleases + // the controller. (Must autorelease since the controller can still + // get delegate events such as windowDidClose). + // + // Bookmark bar folder controllers own their buttons. When doing + // drag and drop of a button from one sub-sub-folder to a different + // sub-sub-folder, we need to make sure the button's pointers stay + // valid until we've dropped (or cancelled). Note that such a drag + // causes the source sub-sub-folder (previous parent window) to go + // away (windows close, controllers autoreleased) since you're + // hovering over a different folder chain for dropping. To keep + // things valid (like the button's target, its delegate, the parent + // cotroller that we have a pointer to below [below], etc), we heep + // strong pointers to our owning controller, so the entire chain + // stays owned. + + // Our parent controller. This may be another + // BookmarkBarFolderController (if we are a nested folder) or it may + // be the BookmarkBarController (if not). + // Strong to insure the object lives as long as we need it. + scoped_nsobject<NSObject<BookmarkButtonControllerProtocol> > + parentController_; + + // Our buttons. We do not have buttons for nested folders. + scoped_nsobject<NSMutableArray> buttons_; + + // The main view of this window (where the buttons go). + IBOutlet BookmarkBarFolderView* mainView_; + + // Like normal menus, hovering over a folder button causes it to + // open. This variable is set when a hover is initiated (but has + // not necessarily fired yet). + scoped_nsobject<BookmarkButton> hoverButton_; + + // A controller for a pop-up bookmark folder window (custom menu). + // We (self) are the parentController_ for our folderController_. + // This is not a scoped_nsobject because it owns itself (when its + // window closes the controller gets autoreleased). + BookmarkBarFolderController* folderController_; + + // Has a draggingExited been called? Only relevant for + // performSelector:after:delay: calls that get triggered in the + // middle of a drag. + BOOL draggingExited_; +} + +- (id)initWithParentButton:(BookmarkButton*)button + parentController:(NSObject<BookmarkButtonControllerProtocol>*)controller; + +// Return the parent button that owns the bookmark folder we represent. +- (BookmarkButton*)parentButton; + +@end + + +@interface BookmarkBarFolderController(TestingAPI) +- (NSView*)mainView; +- (NSPoint)windowTopLeft; +- (NSArray*)buttons; +- (BookmarkBarFolderController*)folderController; +@end + diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller.mm b/chrome/browser/cocoa/bookmark_bar_folder_controller.mm new file mode 100644 index 0000000..64488cf --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_controller.mm @@ -0,0 +1,587 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/cocoa/bookmark_bar_folder_controller.h" +#include "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/browser_theme_provider.h" +#import "chrome/browser/cocoa/bookmark_bar_constants.h" // namespace bookmarks +#import "chrome/browser/cocoa/bookmark_bar_controller.h" // namespace bookmarks +#import "chrome/browser/cocoa/bookmark_bar_folder_view.h" +#import "chrome/browser/cocoa/bookmark_button_cell.h" + + +@interface BookmarkBarFolderController(Private) +- (void)configureWindow; +- (IBAction)openBookmarkFolderFromButton:(id)sender; +@end + + +@implementation BookmarkBarFolderController + +- (id)initWithParentButton:(BookmarkButton*)button + parentController:(NSObject<BookmarkButtonControllerProtocol>*)controller { + NSString* nibPath = + [mac_util::MainAppBundle() pathForResource:@"BookmarkBarFolderWindow" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibPath owner:self])) { + parentButton_.reset([button retain]); + parentController_.reset([controller retain]); + buttons_.reset([[NSMutableArray alloc] init]); + + // Register for theme changes. + NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; + [defaultCenter addObserver:self + selector:@selector(themeDidChangeNotification:) + name:kBrowserThemeDidChangeNotification + object:nil]; + + [self configureWindow]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + // Note: we don't need to + // [NSObject cancelPreviousPerformRequestsWithTarget:self]; + // Because all of our performSelector: calls use withDelay: which + // retains us. + [super dealloc]; +} + +// Update theme information for all our buttons. +- (void)updateTheme:(ThemeProvider*)themeProvider { + if (!themeProvider) + return; + NSColor* color = + themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, + true); + for (BookmarkButton* button in buttons_.get()) { + BookmarkButtonCell* cell = [button cell]; + [cell setTextColor:color]; + } +} + +// Called after the current theme has changed. +- (void)themeDidChangeNotification:(NSNotification*)aNotification { + ThemeProvider* themeProvider = + static_cast<ThemeProvider*>([[aNotification object] pointerValue]); + [self updateTheme:themeProvider]; +} + +// Redirect bookmark button cell creation to our parent to allow a +// single implementation. +- (NSCell*)cellForBookmarkNode:(const BookmarkNode*)child { + return [parentController_ cellForBookmarkNode:child]; +} + +// Create a bookmark button for the given node using frame. +// +// If |node| is NULL this is an "(empty)" button. +// Does NOT add this button to our button list. +// Returns an autoreleased button. +// +// TODO(jrg): combine with addNodesToButtonList: code from +// bookmark_bar_controller.mm, and generalize that to use both x and y +// offsets. +// http://crbug.com/35966 +- (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node + frame:(NSRect)frame { + BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame] + autorelease]; + DCHECK(button); + NSCell* cell = [self cellForBookmarkNode:node]; + [button setCell:cell]; + [button setDelegate:self]; + if (node) { + if (node->is_folder()) { + [button setTarget:self]; + [button setAction:@selector(openBookmarkFolderFromButton:)]; + } else { + // Make the button do something. + [button setTarget:self]; + [button setAction:@selector(openBookmark:)]; + // Add a tooltip. + NSString* title = base::SysWideToNSString(node->GetTitle()); + std::string urlString = node->GetURL().possibly_invalid_spec(); + NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title, + urlString.c_str()]; + [button setToolTip:tooltip]; + } + } else { + [button setEnabled:NO]; + } + return button; +} + +// Exposed for testing. +- (NSView*)mainView { + return mainView_; +} + +// Exposed for testing. +- (BookmarkBarFolderController*)folderController { + return folderController_; +} + +// Compute and return the top left point of our window (screen +// coordinates). The top left is positioned in a manner similar to +// cascading menus. +- (NSPoint)windowTopLeft { + NSPoint newWindowTopLeft; + if (![parentController_ isKindOfClass:[self class]]) { + // If we're not popping up from one of ourselves, we must be + // popping up from the bookmark bar itself. In this case, start + // BELOW the parent button. Our left is the button left; our top + // is bottom of button's parent view. + NSPoint buttonBottomLeftInScreen = + [[parentButton_ window] + convertBaseToScreen:[parentButton_ + convertPointToBase:NSZeroPoint]]; + NSPoint bookmarkBarBottomLeftInScreen = + [[parentButton_ window] + convertBaseToScreen:[[parentButton_ superview] + convertPointToBase:NSMakePoint(0,0)]]; + newWindowTopLeft = NSMakePoint(buttonBottomLeftInScreen.x, + bookmarkBarBottomLeftInScreen.y); + } else { + // Our parent controller is another BookmarkBarFolderController. + // In this case, start ot the RIGHT of the parent button. + // Start to RIGHT of the button. + // TODO(jrg): If too far to right, pop left again. + // http://crbug.com/36225 + newWindowTopLeft.x = NSMaxX([[parentButton_ window] frame]); + NSPoint top = NSMakePoint(0, (NSMaxY([parentButton_ frame]) + + bookmarks::kBookmarkVerticalPadding)); + NSPoint topOfWindow = + [[parentButton_ window] + convertBaseToScreen:[[parentButton_ superview] + convertPointToBase:top]]; + newWindowTopLeft.y = topOfWindow.y; + } + return newWindowTopLeft; +} + +// Determine window size and position. +// Create buttons for all our nodes. +// TODO(jrg): break up into more and smaller routines for easier unit testing. +- (void)configureWindow { + NSPoint newWindowTopLeft = [self windowTopLeft]; + const BookmarkNode* node = [parentButton_ bookmarkNode]; + DCHECK(node); + int buttons = node->GetChildCount(); + if (buttons == 0) + buttons = 1; // the "empty" button + int height = buttons * bookmarks::kBookmarkButtonHeight; + // TODO(jrg): use full width for buttons, like menus? + // http://crbug.com/36487 + int width = (bookmarks::kDefaultBookmarkWidth + + 2 * bookmarks::kBookmarkVerticalPadding); + [[self window] setFrame:NSMakeRect(newWindowTopLeft.x, + newWindowTopLeft.y - height, + width, + height) + display:YES]; + + // TODO(jrg): combine with frame code in bookmark_bar_controller.mm + // http://crbug.com/35966 + NSRect frame = NSMakeRect(bookmarks::kBookmarkHorizontalPadding, + height - (bookmarks::kBookmarkBarHeight - + bookmarks::kBookmarkHorizontalPadding), + bookmarks::kDefaultBookmarkWidth, + (bookmarks::kBookmarkBarHeight - + 2 * bookmarks::kBookmarkVerticalPadding)); + + // TODO(jrg): combine with addNodesToButtonList: code from + // bookmark_bar_controller.mm (but use y offset) + // http://crbug.com/35966 + if (!node->GetChildCount()) { + // If no children we are the empty button. + BookmarkButton* button = [self makeButtonForNode:nil + frame:frame]; + [buttons_ addObject:button]; + [mainView_ addSubview:button]; + } else { + for (int i = 0; i < node->GetChildCount(); i++) { + const BookmarkNode* child = node->GetChild(i); + BookmarkButton* button = [self makeButtonForNode:child + frame:frame]; + [buttons_ addObject:button]; + [mainView_ addSubview:button]; + frame.origin.y -= bookmarks::kBookmarkBarHeight; + } + } + + [self updateTheme:[self themeProvider]]; + [[parentController_ parentWindow] addChildWindow:[self window] + ordered:NSWindowAbove]; +} + +- (ThemeProvider*)themeProvider { + return [parentController_ themeProvider]; +} + +// Recursively close all bookmark folders. +- (void)closeAllBookmarkFolders { + // Closing the top level implicitly closes all children. + [parentController_ closeAllBookmarkFolders]; +} + +// Close our bookmark folder (a sub-controller) if we have one. +- (void)closeBookmarkFolder:(id)sender { + // folderController_ may be nil but that's OK. + [[folderController_ window] close]; + folderController_ = nil; +} + +// Called after a delay to close a previously hover-opened folder. +- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button { + if (hoverButton_ || !draggingExited_) { + // If there is a new one which hover-opened, we're done. If we + // are still inside while dragging but have no hoverButton_ we + // must be hovering over something else (e.g. normal bookmark + // button), so we're still done. + [[button target] closeBookmarkFolder:button]; + hoverButton_.reset(); + } else { + // If there is no hoverOpen but we've exited our window, we may be + // in a subfolder. Restore our state so it's cleaned up later. + hoverButton_.reset([button retain]); + } +} + +// Delegate callback. +- (void)windowWillClose:(NSNotification*)notification { + [[self parentWindow] removeChildWindow:[self window]]; + [self closeBookmarkFolder:self]; + [self autorelease]; +} + +- (BookmarkButton*)parentButton { + return parentButton_.get(); +} + +// Ugh... copied from bookmark_bar_controller.mm +// Is it worth it to factor out for, essentially, 2 lines? +// TODO(jrg): answer is probably yes. +// http://crbug.com/35966 +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button { + const BookmarkNode* node = [button bookmarkNode]; + if (node) { + // Put the bookmark information into the pasteboard, and then write our own + // data for |kBookmarkButtonDragType|. + + /* // TODO(jrg): combine code + -[BookmarkBarController copyBookmarkNode:node + toPasteboard:pboard]; + */ + [pboard declareTypes:[NSArray arrayWithObject:kBookmarkButtonDragType] + owner:button]; + [pboard setData:[NSData dataWithBytes:&button length:sizeof(button)] + forType:kBookmarkButtonDragType]; + + } else { + NOTREACHED(); + } +} + +// Find something like std::is_between<T>? I can't believe one doesn't exist. +// http://crbug.com/35966 +static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { + return ((value >= low) && (value <= high)); +} + +// Return the proposed drop target for a hover open button, or nil if none. +// +// TODO(jrg): this is just like the version in +// bookmark_bar_controller.mm, but vertical instead of horizontal. +// Generalize to be axis independent then share code. +// http://crbug.com/35966 +// Get UI review on "middle half" ness. +// http://crbug.com/36276 +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point { + for (BookmarkButton* button in buttons_.get()) { + // No early break -- makes no assumption about button ordering. + + // Intentionally NOT using NSPointInRect() so that scrolling into + // a submenu doesn't cause it to be closed. + if (ValueInRangeInclusive(NSMinY([button frame]), + point.y, + NSMaxY([button frame]))) { + + // Over a button but let's be a little more specific + // (e.g. over the middle half). + NSRect frame = [button frame]; + NSRect middleHalfOfButton = NSInsetRect(frame, 0, frame.size.height / 4); + if (ValueInRangeInclusive(NSMinY(middleHalfOfButton), + point.y, + NSMaxY(middleHalfOfButton))) { + // It makes no sense to drop on a non-folder; there is no hover. + if (![button isFolder]) + return nil; + // Got it! + return button; + } else { + // Over a button but not over the middle half. + return nil; + } + } + } + // Not hovering over a button. + return nil; +} + +// Most of the work (e.g. drop indicator) is taken care of in the +// folder_view. Here we handle hover open issues for subfolders. +// Caution: there are subtle differences between this one and +// bookmark_bar_controller.mm's version. +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + draggingExited_ = NO; + NSPoint currentLocation = [info draggingLocation]; + + BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation]; + if ([button isFolder]) { + if (hoverButton_ == button) { + return NSDragOperationMove; // already open or timed to open. + } + if (hoverButton_) { + // Oops, another one triggered or open. + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ + target]]; + [self performSelector:@selector(closeBookmarkFolderOnHoverButton:) + withObject:[hoverButton_ target] + afterDelay:bookmarks::kDragHoverCloseDelay]; + } + hoverButton_.reset([button retain]); + [[hoverButton_ target] + performSelector:@selector(openBookmarkFolderFromButton:) + withObject:hoverButton_ + afterDelay:bookmarks::kDragHoverOpenDelay]; + } + + // If we get here we may no longer have a hover button. + if (!button) { + if (hoverButton_) { + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; + [self performSelector:@selector(closeBookmarkFolderOnHoverButton:) + withObject:hoverButton_ + afterDelay:bookmarks::kDragHoverCloseDelay]; + hoverButton_.reset(); + } + } + + return NSDragOperationMove; +} + +// Unlike bookmark_bar_controller, we need to keep track of dragging state. +// We also need to make sure we cancel the delayed hover close. +- (void)draggingExited:(id<NSDraggingInfo>)info { + draggingExited_ = YES; + // NOT the same as a cancel --> we may have moved the mouse into the submenu. + if (hoverButton_) { + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; + [NSObject cancelPreviousPerformRequestsWithTarget:hoverButton_]; + hoverButton_.reset(); + } +} + +// TODO(jrg): again we have code dup, sort of, with +// bookmark_bar_controller.mm, but the axis is changed. One minor +// difference is accomodation for the "empty" button (which may not +// exist in the future). +// http://crbug.com/35966 +- (int)indexForDragOfButton:(BookmarkButton*)sourceButton + toPoint:(NSPoint)point { + DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); + + // Identify which buttons we are between. For now, assume a button + // location is at the center point of its view, and that an exact + // match means "place before". + // TODO(jrg): revisit position info based on UI team feedback. + // dropLocation is in bar local coordinates. + // http://crbug.com/36276 + NSPoint dropLocation = + [mainView_ convertPoint:point + fromView:[[self window] contentView]]; + BookmarkButton* buttonToTheTopOfDraggedButton = nil; + // Buttons are laid out in this array from top to bottom (screen + // wise), which means "biggest y" --> "smallest y". + for (BookmarkButton* button in buttons_.get()) { + CGFloat midpoint = NSMidY([button frame]); + if (dropLocation.y > midpoint) { + break; + } + buttonToTheTopOfDraggedButton = button; + } + + // TODO(jrg): On Windows, dropping onto (empty) highlights the + // entire drop location and does not use an insertion point. + // http://crbug.com/35967 + if (!buttonToTheTopOfDraggedButton) { + // We are at the very top (we broke out of the loop on the first try). + return 0; + } + if ([buttonToTheTopOfDraggedButton isEmpty]) { + // There is a button but it's an empty placeholder. + // Default to inserting on top of it. + return 0; + } + const BookmarkNode* beforeNode = [buttonToTheTopOfDraggedButton + bookmarkNode]; + DCHECK(beforeNode); + return beforeNode->GetParent()->IndexOfChild(beforeNode) + 1; +} + +- (BookmarkModel*)bookmarkModel { + return [parentController_ bookmarkModel]; +} + +// TODO(jrg): ARGH more code dup. +// http://crbug.com/35966 +- (BOOL)dragButton:(BookmarkButton*)sourceButton to:(NSPoint)point { + DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); + + const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; + DCHECK(sourceNode); + + int destIndex = [self indexForDragOfButton:sourceButton toPoint:point]; + if (destIndex >= 0 && sourceNode) { + [parentController_ bookmarkModel]->Move(sourceNode, + [parentButton_ bookmarkNode], + destIndex); + } else { + NOTREACHED(); + } + + // Movement of a node triggers observers (like us) to rebuild the + // bar so we don't have to do so explicitly. + + return YES; +} + +// Return YES if we should show the drop indicator, else NO. +// TODO(jrg): ARGH code dup! +// http://crbug.com/35966 +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { + return ![self buttonForDroppingOnAtPoint:point]; +} + + +// Return the y position for a drop indicator. +// +// TODO(jrg): again we have code dup, sort of, with +// bookmark_bar_controller.mm, but the axis is changed. +// http://crbug.com/35966 +- (CGFloat)indicatorPosForDragOfButton:(BookmarkButton*)sourceButton + toPoint:(NSPoint)point { + CGFloat y = 0; + int destIndex = [self indexForDragOfButton:sourceButton toPoint:point]; + int numButtons = static_cast<int>([buttons_ count]); + + // If it's a drop strictly between existing buttons or at the very beginning + if (destIndex >= 0 && destIndex < numButtons) { + // ... put the indicator right between the buttons. + BookmarkButton* button = + [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)]; + DCHECK(button); + NSRect buttonFrame = [button frame]; + y = buttonFrame.origin.y + + buttonFrame.size.height + + 0.5 * bookmarks::kBookmarkVerticalPadding; + + // If it's a drop at the end (past the last button, if there are any) ... + } else if (destIndex == numButtons) { + // and if it's past the last button ... + if (numButtons > 0) { + // ... find the last button, and put the indicator below it. + BookmarkButton* button = + [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)]; + DCHECK(button); + NSRect buttonFrame = [button frame]; + y = buttonFrame.origin.y - + 0.5 * bookmarks::kBookmarkVerticalPadding; + + } + } else { + NOTREACHED(); + } + + return y; +} + +- (NSWindow*)parentWindow { + return [parentController_ parentWindow]; +} + +// Close the old hover-open bookmark folder, and open a new one. We +// do both in one step to allow for a delay in closing the old one. +// See comments above kDragHoverCloseDelay (bookmark_bar_controller.h) +// for more details. +- (void)openBookmarkFolderFromButtonAndCloseOldOne:(id)sender { + // If an old submenu exists, close it immediately. + [self closeBookmarkFolder:sender]; + + // Open a new one if meaningful. + if ([sender isFolder]) + [self openBookmarkFolderFromButton:sender]; +} + +// Called from BookmarkButton. +// Unlike bookmark_bar_controller's version, we DO default to being enabled. +- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event { + // Cancel a previous hover if needed. + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + + // If already opened, then we exited but re-entered the button + // (without entering another button open), do nothing. + if ([folderController_ parentButton] == sender) + return; + + [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:) + withObject:sender + afterDelay:bookmarks::kHoverOpenDelay]; +} + +// Called from the BookmarkButton +- (void)mouseExitedButton:(id)sender event:(NSEvent*)event { + // Stop any timer about opening a new hover-open folder. + + // Since a performSelector:withDelay: on self retains self, it is + // possible that a cancelPreviousPerformRequestsWithTarget: reduces + // the refcount to 0, releasing us. That's a bad thing to do while + // this object (or others it may own) is in the event chain. Thus + // we have a retain/autorelease. + [self retain]; + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + [self autorelease]; +} + +- (IBAction)openBookmark:(id)sender { + // Carent controller closes it all... + [parentController_ openBookmark:sender]; +} + +// Unlike the bookmark_bar_controller, we do not watch for click outside. +- (IBAction)openBookmarkFolderFromButton:(id)sender { + if (folderController_) { + // If the same we have nothing to do. + if ([folderController_ parentButton] == sender) + return; + + [self closeBookmarkFolder:sender]; + } + folderController_ = [[BookmarkBarFolderController alloc] + initWithParentButton:sender + parentController:self]; + [folderController_ showWindow:self]; +} + +- (NSArray*)buttons { + return buttons_.get(); +} + +@end // BookmarkBarFolderController diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm b/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm new file mode 100644 index 0000000..d523aaf --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm @@ -0,0 +1,204 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/basictypes.h" +#include "base/scoped_nsobject.h" +#import "chrome/browser/cocoa/bookmark_bar_controller.h" +#import "chrome/browser/cocoa/bookmark_bar_folder_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" + +class BookmarkBarFolderControllerTest : public CocoaTest { + public: + scoped_nsobject<BookmarkBarController> parentBarController_; + BrowserTestHelper helper_; + + BookmarkBarFolderControllerTest() { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folderA = model->AddGroup(parent, + parent->GetChildCount(), + L"group"); + model->AddGroup(parent, parent->GetChildCount(), + L"sibbling group"); + const BookmarkNode* folderB = model->AddGroup(folderA, + folderA->GetChildCount(), + L"subgroup"); + model->AddURL(folderA, folderA->GetChildCount(), L"title a", + GURL("http://www.google.com/a")); + model->AddURL(folderA, folderA->GetChildCount(), + L"title super duper long long whoa momma title you betcha", + GURL("http://www.google.com/b")); + model->AddURL(folderB, folderB->GetChildCount(), L"t", + GURL("http://www.google.com/c")); + + parentBarController_.reset( + [[BookmarkBarController alloc] + initWithBrowser:helper_.browser() + initialWidth:300 + delegate:nil + resizeDelegate:nil]); + [parentBarController_ loaded:model]; + } + + // Return a simple BookmarkBarFolderController. + BookmarkBarFolderController* SimpleBookmarkBarFolderController() { + BookmarkButton* parentButton = [[parentBarController_ buttons] + objectAtIndex:0]; + return [[BookmarkBarFolderController alloc] + initWithParentButton:parentButton + parentController:parentBarController_]; + } + +}; + +TEST_F(BookmarkBarFolderControllerTest, InitCreateAndDelete) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + + // Make sure none of the buttons overlap, and that all are inside + // the content frame. + NSArray* buttons = [bbfc buttons]; + EXPECT_TRUE([buttons count]); + for (unsigned int i = 0; i < ([buttons count]-1); i++) { + EXPECT_FALSE(NSContainsRect([[buttons objectAtIndex:i] frame], + [[buttons objectAtIndex:i+1] frame])); + } + for (BookmarkButton* button in buttons) { + NSRect r = [[bbfc mainView] convertRect:[button frame] fromView:button]; + EXPECT_TRUE(NSContainsRect([[bbfc mainView] frame], r)); + } + + // Confirm folder buttons have no tooltip. The important thing + // really is that we insure folders and non-folders are treated + // differently; not sure of any other generic way to do this. + for (BookmarkButton* button in buttons) { + if ([button isFolder]) + EXPECT_FALSE([button toolTip]); + else + EXPECT_TRUE([button toolTip]); + } +} + +// Make sure closing of the window releases the controller. +// (e.g. valgrind shouldn't complain if we do this). +TEST_F(BookmarkBarFolderControllerTest, ReleaseOnClose) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + + [bbfc retain]; // stop the scoped_nsobject from doing anything + [[bbfc window] close]; // trigger an autorelease of bbfc.get() +} + +TEST_F(BookmarkBarFolderControllerTest, Position) { + BookmarkButton* parentButton = [[parentBarController_ buttons] + objectAtIndex:0]; + EXPECT_TRUE(parentButton); + + // If parent is a BookmarkBarController, grow down. + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset([[BookmarkBarFolderController alloc] + initWithParentButton:parentButton + parentController:parentBarController_]); + NSPoint pt = [bbfc windowTopLeft]; + EXPECT_EQ(pt.y, NSMinY([[parentBarController_ view] frame])); + + // If parent is a BookmarkBarFolderController, grow right. + scoped_nsobject<BookmarkBarFolderController> bbfc2; + bbfc2.reset([[BookmarkBarFolderController alloc] + initWithParentButton:[[bbfc buttons] objectAtIndex:0] + parentController:bbfc.get()]); + pt = [bbfc2 windowTopLeft]; + EXPECT_EQ(pt.x, NSMaxX([[[bbfc.get() window] contentView] frame])); +} + +TEST_F(BookmarkBarFolderControllerTest, DropDestination) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + + // Confirm "off the top" and "off the bottom" match no buttons. + NSPoint p = NSMakePoint(NSMidX([[bbfc mainView] frame]), 10000); + EXPECT_FALSE([bbfc buttonForDroppingOnAtPoint:p]); + EXPECT_TRUE([bbfc shouldShowIndicatorShownForPoint:p]); + p = NSMakePoint(NSMidX([[bbfc mainView] frame]), -1); + EXPECT_FALSE([bbfc buttonForDroppingOnAtPoint:p]); + EXPECT_TRUE([bbfc shouldShowIndicatorShownForPoint:p]); + + // Confirm "right in the center" (give or take a pixel) is a match, + // and confirm "just barely in the button" is not. Anything more + // specific seems likely to be tweaked. + for (BookmarkButton* button in [bbfc buttons]) { + CGFloat x = NSMidX([button frame]); + CGFloat y = NSMidY([button frame]); + // Somewhere near the center: a match (but only if a folder!) + if ([button isFolder]) { + EXPECT_EQ(button, + [bbfc buttonForDroppingOnAtPoint:NSMakePoint(x-1, y+1)]); + EXPECT_EQ(button, + [bbfc buttonForDroppingOnAtPoint:NSMakePoint(x+1, y-1)]); + EXPECT_FALSE([bbfc shouldShowIndicatorShownForPoint:NSMakePoint(x, y)]);; + } else { + // If not a folder we don't drop into it. + EXPECT_FALSE([bbfc buttonForDroppingOnAtPoint:NSMakePoint(x-1, y+1)]); + EXPECT_FALSE([bbfc buttonForDroppingOnAtPoint:NSMakePoint(x+1, y-1)]); + EXPECT_TRUE([bbfc shouldShowIndicatorShownForPoint:NSMakePoint(x, y)]);; + } + + + // On some corners: NOT a match. Confirm that the indicator + // position for these two points is NOT the same. + BookmarkButton* dragButton = [[bbfc buttons] lastObject]; + x = NSMinX([button frame]); + y = NSMinY([button frame]); + CGFloat pos1 = [bbfc indicatorPosForDragOfButton:dragButton + toPoint:NSMakePoint(x, y)]; + EXPECT_NE(button, + [bbfc buttonForDroppingOnAtPoint:NSMakePoint(x, y)]); + x = NSMaxX([button frame]); + y = NSMaxY([button frame]); + CGFloat pos2 = [bbfc indicatorPosForDragOfButton:dragButton + toPoint:NSMakePoint(x, y)]; + EXPECT_NE(button, + [bbfc buttonForDroppingOnAtPoint:NSMakePoint(x, y)]); + if (dragButton != button) { + EXPECT_NE(pos1, pos2); + } + } +} + +TEST_F(BookmarkBarFolderControllerTest, OpenFolder) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + + EXPECT_FALSE([bbfc folderController]); + [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:0]]; + id controller = [bbfc folderController]; + EXPECT_TRUE(controller); + + // Open the same one --> no change. + [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:0]]; + EXPECT_EQ(controller, [bbfc folderController]); + + // Open a new one --> change. + [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:1]]; + EXPECT_NE(controller, [bbfc folderController]); + + // Close it --> all gone! + [bbfc closeBookmarkFolder:nil]; + EXPECT_FALSE([bbfc folderController]); +} + +// TODO(jrg): draggingEntered: and draggingExited: trigger timers so +// they are hard to test. Factor out "fire timers" into routines +// which can be overridden to fire immediately to make behavior +// confirmable. +// There is a similar problem with mouseEnteredButton: and +// mouseExitedButton:. diff --git a/chrome/browser/cocoa/bookmark_bar_folder_view.h b/chrome/browser/cocoa/bookmark_bar_folder_view.h new file mode 100644 index 0000000..a8fe85b --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_view.h @@ -0,0 +1,25 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> +#import "chrome/browser/cocoa/background_gradient_view.h" + +@protocol BookmarkButtonControllerProtocol; + +// Main content view for a bookmark bar folder "menu" window. This is +// logically similar to a BookmarkBarView but is oriented vertically. +@interface BookmarkBarFolderView : BackgroundGradientView { + @private + BOOL inDrag_; // Are we in the middle of a drag? + BOOL dropIndicatorShown_; + CGFloat dropIndicatorPosition_; // y position +} +// Return the controller that owns this view. +- (id<BookmarkButtonControllerProtocol>)controller; +@end + +@interface BookmarkBarFolderView(TestingAPI) +- (void)setDropIndicatorShown:(BOOL)shown; +@end + diff --git a/chrome/browser/cocoa/bookmark_bar_folder_view.mm b/chrome/browser/cocoa/bookmark_bar_folder_view.mm new file mode 100644 index 0000000..e8e91dd --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_view.mm @@ -0,0 +1,185 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/cocoa/bookmark_bar_folder_view.h" + +#import "chrome/browser/browser_theme_provider.h" +#import "chrome/browser/cocoa/bookmark_bar_controller.h" + +@implementation BookmarkBarFolderView + +- (id<BookmarkButtonControllerProtocol>)controller { + return [[self window] windowController]; +} + +- (void)awakeFromNib { + [super awakeFromNib]; + + // BackgroundGradientView's awakeFromNib does a |showsDivider_ = YES|. + // Make sure we turn it off. + [self setShowsDivider:NO]; + + NSArray* types = [NSArray arrayWithObject:kBookmarkButtonDragType]; + [self registerForDraggedTypes:types]; +} + +- (void)dealloc { + [self unregisterDraggedTypes]; + [super dealloc]; +} + +- (void)drawRect:(NSRect)rect { + [self drawBackground]; + + // TODO(jrg): copied from bookmark_bar_view but orientation changed. + // Code dup sucks but I'm not sure I can take 16 lines and make it + // generic for horiz vs vertical while keeping things simple. + // TODO(jrg): when throwing it all away and using animations, try + // hard to make a common routine for both. + // http://crbug.com/35966, http://crbug.com/35968 + + // Draw the bookmark-button-dragging drop indicator if necessary. + if (dropIndicatorShown_) { + const CGFloat kBarHeight = 1; + const CGFloat kBarHorizPad = 4; + const CGFloat kBarOpacity = 0.85; + + NSRect uglyBlackBar = + NSMakeRect(kBarHorizPad, dropIndicatorPosition_, + NSWidth([self bounds]) - 2*kBarHorizPad, + kBarHeight); + // themeProvider is nil in unit tests. + ThemeProvider* themeProvider = [[self controller] themeProvider]; + if (themeProvider) { + NSColor* uglyBlackBarColor = themeProvider-> + GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, true); + [[uglyBlackBarColor colorWithAlphaComponent:kBarOpacity] setFill]; + [[NSBezierPath bezierPathWithRect:uglyBlackBar] fill]; + } + } +} + +// Virtually identical to [BookmarkBarView draggingEntered:]. +// TODO(jrg): find a way to share code. Lack of multiple inheritance +// makes things more of a pain but there should be no excuse for laziness. +// http://crbug.com/35966 +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + inDrag_ = YES; + + NSData* data = [[info draggingPasteboard] + dataForType:kBookmarkButtonDragType]; + // [info draggingSource] is nil if not the same application. + if (data && [info draggingSource]) { + // Find the position of the drop indicator. + BookmarkButton* button = nil; + [data getBytes:&button length:sizeof(button)]; + + BOOL showIt = [[self controller] + shouldShowIndicatorShownForPoint:[info draggingLocation]]; + if (!showIt) { + if (dropIndicatorShown_) { + dropIndicatorShown_ = NO; + [self setNeedsDisplay:YES]; + } + } else { + CGFloat y = + [[self controller] + indicatorPosForDragOfButton:button + toPoint:[info draggingLocation]]; + + // Need an update if the indicator wasn't previously shown or if it has + // moved. + if (!dropIndicatorShown_ || dropIndicatorPosition_ != y) { + dropIndicatorShown_ = YES; + dropIndicatorPosition_ = y; + [self setNeedsDisplay:YES]; + } + } + + [[self controller] draggingEntered:info]; // allow hover-open to work + return NSDragOperationMove; + } + + return NSDragOperationNone; +} + +- (void)draggingExited:(id<NSDraggingInfo>)info { + [[self controller] draggingExited:info]; + + // Regardless of the type of dragging which ended, we need to get rid of the + // drop indicator if one was shown. + if (dropIndicatorShown_) { + dropIndicatorShown_ = NO; + [self setNeedsDisplay:YES]; + } +} + +- (void)draggingEnded:(id<NSDraggingInfo>)info { + // Awkwardness since views open and close out from under us. + if (inDrag_) { + inDrag_ = NO; + + // This line makes sure menus get closed when a drag isn't + // completed. + [[self controller] closeAllBookmarkFolders]; + } + + [self draggingExited:info]; +} + +- (BOOL)wantsPeriodicDraggingUpdates { + // TODO(jrg): This should probably return |YES| and the controller should + // slide the existing bookmark buttons interactively to the side to make + // room for the about-to-be-dropped bookmark. + // http://crbug.com/35968 + return NO; +} + +- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info { + // For now it's the same as draggingEntered:. + // TODO(jrg): once we return YES for wantsPeriodicDraggingUpdates, + // this should ping the [self controller] to perform animations. + // http://crbug.com/35968 + return [self draggingEntered:info]; +} + +- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)info { + return YES; +} + +// Implement NSDraggingDestination protocol method +// performDragOperation: for bookmarks. +- (BOOL)performDragOperationForBookmark:(id<NSDraggingInfo>)info { + BOOL doDrag = NO; + NSData* data = [[info draggingPasteboard] + dataForType:kBookmarkButtonDragType]; + // [info draggingSource] is nil if not the same application. + if (data && [info draggingSource]) { + BookmarkButton* button = nil; + [data getBytes:&button length:sizeof(button)]; + doDrag = [[self controller] dragButton:button to:[info draggingLocation]]; + } + return doDrag; +} + +- (BOOL)performDragOperation:(id<NSDraggingInfo>)info { + NSPasteboard* pboard = [info draggingPasteboard]; + if ([pboard dataForType:kBookmarkButtonDragType]) { + if ([self performDragOperationForBookmark:info]) + return YES; + // Fall through.... + } + return NO; +} + +@end // BookmarkBarFolderView + + +@implementation BookmarkBarFolderView(TestingAPI) + +- (void)setDropIndicatorShown:(BOOL)shown { + dropIndicatorShown_ = shown; +} + +@end diff --git a/chrome/browser/cocoa/bookmark_bar_folder_view_unittest.mm b/chrome/browser/cocoa/bookmark_bar_folder_view_unittest.mm new file mode 100644 index 0000000..514c3b9 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_view_unittest.mm @@ -0,0 +1,139 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/scoped_nsobject.h" +#import "chrome/browser/cocoa/bookmark_bar_controller.h" +#import "chrome/browser/cocoa/bookmark_bar_folder_view.h" +#import "chrome/browser/cocoa/bookmark_button.h" +#import "chrome/browser/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +// Fake NSDraggingInfo for testing +@interface FakeDraggingInfo : NSObject +@end + +@implementation FakeDraggingInfo + +- (id)draggingPasteboard { + return self; +} + +- (NSData*)dataForType:(NSString*)type { + if ([type isEqual:kBookmarkButtonDragType]) + return [NSData dataWithBytes:&self length:sizeof(self)]; + return nil; +} + +- (BOOL)draggingSource { + return YES; // pretend we're local +} + +@end + + +// We are our own controller for test convenience. +@interface BookmarkBarFolderViewFakeController : + BookmarkBarFolderView<BookmarkButtonControllerProtocol> { + @public + BOOL closedAll_; + BOOL controllerEntered_; + BOOL controllerExited_; +} +@end + +@implementation BookmarkBarFolderViewFakeController + +- (id<BookmarkButtonControllerProtocol>)controller { + return self; +} + +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)pt { + return YES; +} + +- (CGFloat)indicatorPosForDragOfButton:(BookmarkButton*)button + toPoint:(NSPoint)point { + return 101.0; // Arbitrary value. +} + +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + controllerEntered_ = YES; + return NSDragOperationMove; +} + +- (void)draggingExited:(id<NSDraggingInfo>)info { + controllerExited_ = YES; +} + +- (void)closeAllBookmarkFolders { + closedAll_ = YES; +} + +- (BOOL)dragButton:(BookmarkButton*)sourceButton to:(NSPoint)point { + return NO; +} + +- (BookmarkModel*)bookmarkModel { + NOTREACHED(); + return NULL; +} + +- (void)closeBookmarkFolder:(id)sender { +} + +- (NSWindow*)parentWindow { + return nil; +} + +- (ThemeProvider*)themeProvider { + return nil; +} + +@end + +namespace { + +class BookmarkBarFolderViewTest : public CocoaTest { + public: + virtual void SetUp() { + CocoaTest::SetUp(); + view_.reset([[BookmarkBarFolderViewFakeController alloc] init]); + } + + scoped_nsobject<BookmarkBarFolderViewFakeController> view_; +}; + +TEST_F(BookmarkBarFolderViewTest, Basics) { + [view_ awakeFromNib]; + [[test_window() contentView] addSubview:view_]; + + // Confirm an assumption made in our awakeFromNib + EXPECT_FALSE([view_ showsDivider]); + + // Make sure we're set up for DnD + NSArray* types = [view_ registeredDraggedTypes]; + EXPECT_TRUE([types containsObject:kBookmarkButtonDragType]); + + // This doesn't confirm results but it makes sure we don't crash or leak. + [view_ drawRect:NSMakeRect(0,0,10,10)]; + [view_ setDropIndicatorShown:YES]; + [view_ drawRect:NSMakeRect(0,0,10,10)]; + + [view_ removeFromSuperview]; +} + +TEST_F(BookmarkBarFolderViewTest, SimpleDragEnterExit) { + [view_ awakeFromNib]; + scoped_nsobject<FakeDraggingInfo> info([[FakeDraggingInfo alloc] init]); + + [view_ draggingEntered:(id<NSDraggingInfo>)info.get()]; + // Confirms we got a chance to hover-open. + EXPECT_TRUE(view_.get()->controllerEntered_); + + [view_ draggingEnded:(id<NSDraggingInfo>)info.get()]; + EXPECT_TRUE(view_.get()->controllerExited_); +} + +} // namespace diff --git a/chrome/browser/cocoa/bookmark_bar_folder_window.h b/chrome/browser/cocoa/bookmark_bar_folder_window.h new file mode 100644 index 0000000..41019b7 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_window.h @@ -0,0 +1,16 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_COCOA_BOOKMARK_BAR_FOLDER_WINDOW_H_ +#define CHROME_BROWSER_COCOA_BOOKMARK_BAR_FOLDER_WINDOW_H_ + +#import <Cocoa/Cocoa.h> + +// Window for a bookmark folder "menu". This menu pops up when you +// click on a bookmark button that represents a folder of bookmarks. +// This window is borderless. +@interface BookmarkBarFolderWindow : NSWindow +@end + +#endif // CHROME_BROWSER_COCOA_BOOKMARK_BAR_FOLDER_WINDOW_H_ diff --git a/chrome/browser/cocoa/bookmark_bar_folder_window.mm b/chrome/browser/cocoa/bookmark_bar_folder_window.mm new file mode 100644 index 0000000..f911777 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_window.mm @@ -0,0 +1,19 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/cocoa/bookmark_bar_folder_window.h" + +@implementation BookmarkBarFolderWindow + +- (id)initWithContentRect:(NSRect)contentRect + styleMask:(NSUInteger)windowStyle + backing:(NSBackingStoreType)bufferingType + defer:(BOOL)deferCreation { + return [super initWithContentRect:contentRect + styleMask:NSBorderlessWindowMask // override + backing:bufferingType + defer:deferCreation]; +} + +@end diff --git a/chrome/browser/cocoa/bookmark_bar_folder_window_unittest.mm b/chrome/browser/cocoa/bookmark_bar_folder_window_unittest.mm new file mode 100644 index 0000000..f86d6d4 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_folder_window_unittest.mm @@ -0,0 +1,22 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/scoped_ptr.h" +#include "chrome/browser/cocoa/bookmark_bar_folder_window.h" +#include "chrome/browser/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +class BookmarkBarFolderWindowTest : public CocoaTest { +}; + +TEST_F(BookmarkBarFolderWindowTest, Borderless) { + scoped_nsobject<BookmarkBarFolderWindow> window_; + window_.reset([[BookmarkBarFolderWindow alloc] + initWithContentRect:NSMakeRect(0,0,20,20) + styleMask:0 + backing:NSBackingStoreBuffered + defer:NO]); + EXPECT_EQ(NSBorderlessWindowMask, [window_ styleMask]); +} diff --git a/chrome/browser/cocoa/bookmark_bar_view.h b/chrome/browser/cocoa/bookmark_bar_view.h index 567544d..d6f1344 100644 --- a/chrome/browser/cocoa/bookmark_bar_view.h +++ b/chrome/browser/cocoa/bookmark_bar_view.h @@ -15,15 +15,18 @@ @interface BookmarkBarView : NSView { @private BOOL dropIndicatorShown_; - CGFloat dropIndicatorPosition_; + CGFloat dropIndicatorPosition_; // x position IBOutlet BookmarkBarController* controller_; IBOutlet NSTextField* noItemTextfield_; } - (NSTextField*)noItemTextfield; +- (BookmarkBarController*)controller; @end -@interface BookmarkBarView(TestingAPI) +@interface BookmarkBarView() // TestingOrInternalAPI +@property (readonly) BOOL dropIndicatorShown; +@property (readonly) CGFloat dropIndicatorPosition; - (void)setController:(id)controller; @end diff --git a/chrome/browser/cocoa/bookmark_bar_view.mm b/chrome/browser/cocoa/bookmark_bar_view.mm index 96b7594..b1099b2 100644 --- a/chrome/browser/cocoa/bookmark_bar_view.mm +++ b/chrome/browser/cocoa/bookmark_bar_view.mm @@ -17,11 +17,19 @@ @implementation BookmarkBarView +@synthesize dropIndicatorShown = dropIndicatorShown_; +@synthesize dropIndicatorPosition = dropIndicatorPosition_; + - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; // This probably isn't strictly necessary, but can't hurt. [self unregisterDraggedTypes]; [super dealloc]; + + // To be clear, our controller_ is an IBOutlet and owns us, so we + // don't deallocate it explicitly. It is owned by the browser + // window controller, so gets deleted with a browser window is + // closed. } - (void)awakeFromNib { @@ -50,6 +58,10 @@ [controller_ updateTheme:themeProvider]; } +- (void)viewDidMoveToWindow { + [controller_ viewDidMoveToWindow]; +} + // Called after the current theme has changed. - (void)themeDidChangeNotification:(NSNotification*)aNotification { ThemeProvider* themeProvider = @@ -79,6 +91,10 @@ return noItemTextfield_; } +- (BookmarkBarController*)controller { + return controller_; +} + -(void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; @@ -113,18 +129,31 @@ // Find the position of the drop indicator. BookmarkButton* button = nil; [data getBytes:&button length:sizeof(button)]; - CGFloat x = - [controller_ indicatorPosForDragOfButton:button - toPoint:[info draggingLocation]]; - - // Need an update if the indicator wasn't previously shown or if it has - // moved. - if (!dropIndicatorShown_ || dropIndicatorPosition_ != x) { - dropIndicatorShown_ = YES; - dropIndicatorPosition_ = x; - [self setNeedsDisplay:YES]; + + // We only show the drop indicator if we're not in a position to + // perform a hover-open since it doesn't make sense to do both. + BOOL showIt = + [controller_ shouldShowIndicatorShownForPoint: + [info draggingLocation]]; + if (!showIt) { + if (dropIndicatorShown_) { + dropIndicatorShown_ = NO; + [self setNeedsDisplay:YES]; + } + } else { + CGFloat x = + [controller_ indicatorPosForDragOfButton:button + toPoint:[info draggingLocation]]; + // Need an update if the indicator wasn't previously shown or if it has + // moved. + if (!dropIndicatorShown_ || dropIndicatorPosition_ != x) { + dropIndicatorShown_ = YES; + dropIndicatorPosition_ = x; + [self setNeedsDisplay:YES]; + } } + [controller_ draggingEntered:info]; // allow hover-open to work. return NSDragOperationMove; } // Fall through otherwise. @@ -210,13 +239,8 @@ return NO; } -@end // @implementation BookmarkBarView - - -@implementation BookmarkBarView(TestingAPI) - - (void)setController:(id)controller { controller_ = controller; } -@end // @implementation BookmarkBarView(TestingAPI) +@end // @implementation BookmarkBarView diff --git a/chrome/browser/cocoa/bookmark_bar_view_unittest.mm b/chrome/browser/cocoa/bookmark_bar_view_unittest.mm index e99dda1..600c91d 100644 --- a/chrome/browser/cocoa/bookmark_bar_view_unittest.mm +++ b/chrome/browser/cocoa/bookmark_bar_view_unittest.mm @@ -10,17 +10,31 @@ #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" +namespace { +const CGFloat kFakeIndicatorPos = 7.0; +}; + // Fake DraggingInfo, fake BookmarkBarController, fake pasteboard... @interface FakeBookmarkDraggingInfo : NSObject { + @public scoped_nsobject<NSData> data_; BOOL pong_; + BOOL dropIndicatorShown_; + BOOL draggingEnteredCalled_; } +@property (readwrite) BOOL dropIndicatorShown; +@property (readwrite) BOOL draggingEnteredCalled; @end @implementation FakeBookmarkDraggingInfo +@synthesize dropIndicatorShown = dropIndicatorShown_; +@synthesize draggingEnteredCalled = draggingEnteredCalled_; + - (id)init { if ((self = [super init])) { + dropIndicatorShown_ = YES; + draggingEnteredCalled_ = NO; data_.reset([[NSData dataWithBytes:&self length:sizeof(self)] retain]); } return self; @@ -63,7 +77,16 @@ - (CGFloat)indicatorPosForDragOfButton:(BookmarkButton*)sourceButton toPoint:(NSPoint)point { - return 0; + return kFakeIndicatorPos; +} + +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { + return dropIndicatorShown_; +} + +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + draggingEnteredCalled_ = YES; + return NSDragOperationNone; } @end @@ -94,4 +117,20 @@ TEST_F(BookmarkBarViewTest, BookmarkButtonDragAndDrop) { EXPECT_TRUE([info dragButtonToPong]); } +TEST_F(BookmarkBarViewTest, BookmarkButtonDropIndicator) { + scoped_nsobject<FakeBookmarkDraggingInfo> + info([[FakeBookmarkDraggingInfo alloc] init]); + + [view_ setController:info.get()]; + EXPECT_FALSE([info draggingEnteredCalled]); + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_TRUE([info draggingEnteredCalled]); // Ensure controller pingged. + EXPECT_TRUE([view_ dropIndicatorShown]); + EXPECT_EQ([view_ dropIndicatorPosition], kFakeIndicatorPos); + + [info setDropIndicatorShown:NO]; + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_FALSE([view_ dropIndicatorShown]); +} + } // namespace diff --git a/chrome/browser/cocoa/bookmark_button.h b/chrome/browser/cocoa/bookmark_button.h index 3034166..3144247 100644 --- a/chrome/browser/cocoa/bookmark_button.h +++ b/chrome/browser/cocoa/bookmark_button.h @@ -3,30 +3,111 @@ // found in the LICENSE file. #import <Cocoa/Cocoa.h> - #import "chrome/browser/cocoa/draggable_button.h" -@protocol BookmarkButtonDelegate; +@class BookmarkButton; +class BookmarkModel; +class BookmarkNode; +class ThemeProvider; + +// Protocol for a BookmarkButton's delegate, responsible for doing +// things on behalf of a bookmark button. +@protocol BookmarkButtonDelegate + +// Fill the given pasteboard with appropriate data when the given button is +// dragged. Since the delegate has no way of providing pasteboard data later, +// all data must actually be put into the pasteboard and not merely promised. +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button; + +// Bookmark buttons pass mouseEntered: and mouseExited: events to +// their delegate. This allows the delegate to decide (for example) +// which one, if any, should perform a hover-open. +- (void)mouseEnteredButton:(id)button event:(NSEvent*)event; +- (void)mouseExitedButton:(id)button event:(NSEvent*)event; + +@end + + +// Protocol to be implemented by controllers that logically own +// bookmark buttons. The controller may be either an NSViewController +// or NSWindowController. The BookmarkButton doesn't use this +// protocol directly; it is used when BookmarkButton controllers talk +// to each other. +// Other than the top level owner (the bookmark bar), all bookmark +// button controllers have a parent controller. +@protocol BookmarkButtonControllerProtocol + +// Close all bookmark folders, walking up the ownership chain. +- (void)closeAllBookmarkFolders; + +// Close just my bookmark folder. +- (void)closeBookmarkFolder:(id)sender; + +// Return the bookmark model for this controller. +- (BookmarkModel*)bookmarkModel; + +// Perform drag enter/exit operations, such as hover-open and hover-close. +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info; +- (void)draggingExited:(id<NSDraggingInfo>)info; + +// Perform the actual DnD of a bookmark button. + +// |point| is in the base coordinate system of the destination window; +// |it comes from an id<NSDraggingInfo>. +- (BOOL)dragButton:(BookmarkButton*)sourceButton to:(NSPoint)point; + +// Return YES if we should show the drop indicator, else NO. In some +// cases (e.g. hover open) we don't want to show the drop indicator. +// |point| is in the base coordinate system of the destination window; +// |it comes from an id<NSDraggingInfo>. +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point; + +// The x or y coordinate of (the middle of) the indicator to draw for +// a drag of the source button to the given point (given in window +// coordinates). +// |point| is in the base coordinate system of the destination window; +// |it comes from an id<NSDraggingInfo>. +// TODO(viettrungluu,jrg): instead of this, make buttons move around. +// http://crbug.com/35968 +- (CGFloat)indicatorPosForDragOfButton:(BookmarkButton*)sourceButton + toPoint:(NSPoint)point; + +// Return the parent window for all BookmarkBarFolderController windows. +- (NSWindow*)parentWindow; + +// Return the theme provider associated with this browser window. +- (ThemeProvider*)themeProvider; + +@end // @protocol BookmarkButtonControllerProtocol + // Class for bookmark bar buttons that can be drag sources. @interface BookmarkButton : DraggableButton { @private - id<BookmarkButtonDelegate> delegate_; + NSObject<BookmarkButtonDelegate>* delegate_; // weak like all delegates } -@property(assign, nonatomic) id<BookmarkButtonDelegate> delegate; +@property(assign, nonatomic) NSObject<BookmarkButtonDelegate>* delegate; + +// Return the bookmark node associated with this button, or NULL. +- (const BookmarkNode*)bookmarkNode; + +// Return YES if this is a folder button (the node has subnodes). +- (BOOL)isFolder; + +// At this time we represent an empty folder (e.g. the string +// '(empty)') as a disabled button with no associated node. +// +// TODO(jrg): improve; things work but are slightly ugly since "empty" +// and "one disabled button" are not the same thing. +// http://crbug.com/35967 +- (BOOL)isEmpty; @end // @interface BookmarkButton -// Protocol for a |BookmarkButton|'s delegate, which is responsible for doing -// things which require information about the bookmark represented by this -// button. -@protocol BookmarkButtonDelegate -// Fill the given pasteboard with appropriate data when the given button is -// dragged. Since the delegate has no way of providing pasteboard data later, -// all data must actually be put into the pasteboard and not merely promised. -- (void)fillPasteboard:(NSPasteboard*)pboard - forDragOfButton:(BookmarkButton*)button; +@interface BookmarkButton(TestingAPI) +- (void)beginDrag:(NSEvent*)event; +@end -@end // @protocol BookmarkButtonDelegate diff --git a/chrome/browser/cocoa/bookmark_button.mm b/chrome/browser/cocoa/bookmark_button.mm index b0b6ecd..5e87c09 100644 --- a/chrome/browser/cocoa/bookmark_button.mm +++ b/chrome/browser/cocoa/bookmark_button.mm @@ -2,9 +2,10 @@ // 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_button.h" #include "base/logging.h" #import "base/scoped_nsobject.h" -#import "chrome/browser/cocoa/bookmark_button.h" +#include "chrome/browser/bookmarks/bookmark_model.h" #import "chrome/browser/cocoa/bookmark_button_cell.h" // The opacity of the bookmark button drag image. @@ -17,22 +18,42 @@ static const CGFloat kDragImageOpacity = 0.7; @end // @interface BookmarkButton(Private) + @implementation BookmarkButton @synthesize delegate = delegate_; +- (const BookmarkNode*)bookmarkNode { + return [[self cell] bookmarkNode]; +} + +- (BOOL)isFolder { + const BookmarkNode* node = [self bookmarkNode]; + return (node && node->is_folder()); +} + +- (BOOL)isEmpty { + return [self bookmarkNode] ? NO : YES; +} + // By default, NSButton ignores middle-clicks. +// But we want them. - (void)otherMouseUp:(NSEvent*)event { [self performClick:self]; } // Overridden from DraggableButton. - (void)beginDrag:(NSEvent*)event { - if (delegate_) { + // Don't allow a drag of the empty node. + // The empty node is a placeholder for "(empty)", to be revisited. + if ([self isEmpty]) + return; + + if ([self delegate]) { // Ask our delegate to fill the pasteboard for us. NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; - [delegate_ fillPasteboard:pboard forDragOfButton:self]; + [[self delegate] fillPasteboard:pboard forDragOfButton:self]; // At the moment, moving bookmarks causes their buttons (like me!) // to be destroyed and rebuilt. Make sure we don't go away while on @@ -44,7 +65,7 @@ static const CGFloat kDragImageOpacity = 0.7; [self dragImage:[self dragImage] at:NSMakePoint(0, yAt) offset:dragOffset event:event pasteboard:pboard source:self slideBack:YES]; - // And we're done. + // And we're done. [self autorelease]; } else { // Avoid blowing up, but we really shouldn't get here. @@ -63,6 +84,21 @@ static const CGFloat kDragImageOpacity = 0.7; : NSDragOperationCopy; } +// mouseEntered: and mouseExited: are called from our +// BookmarkButtonCell. We redirect this information to our delegate. +// The controller can then perform menu-like actions (e.g. "hover over +// to open menu"). +- (void)mouseEntered:(NSEvent*)event { + [delegate_ mouseEnteredButton:self event:event]; + [super mouseEntered:event]; +} + +// See comments above mouseEntered:. +- (void)mouseExited:(NSEvent*)event { + [delegate_ mouseExitedButton:self event:event]; + [super mouseExited:event]; +} + @end @implementation BookmarkButton(Private) diff --git a/chrome/browser/cocoa/bookmark_button_cell.h b/chrome/browser/cocoa/bookmark_button_cell.h index 89d461b..8a0c13d 100644 --- a/chrome/browser/cocoa/bookmark_button_cell.h +++ b/chrome/browser/cocoa/bookmark_button_cell.h @@ -8,11 +8,23 @@ #import "base/cocoa_protocols_mac.h" #import "chrome/browser/cocoa/gradient_button_cell.h" -// A button cell that handles drawing/highlighting of buttons in the -// bookmark bar. +class BookmarkNode; +// A button cell that handles drawing/highlighting of buttons in the +// bookmark bar. This cell forwards mouseEntered/mouseExited events +// to its control view so that pseudo-menu operations +// (e.g. hover-over to open) can be implemented. @interface BookmarkButtonCell : GradientButtonCell<NSMenuDelegate> { + @private + BOOL empty_; // is this an "empty" button placeholder button cell? } + +@property (readwrite, assign) const BookmarkNode* bookmarkNode; + +- (id)initTextCell:(NSString*)string; // Designated initializer +- (BOOL)empty; // returns YES if empty. +- (void)setEmpty:(BOOL)empty; + // |-setBookmarkCellText:image:| is used to set the text and image of // a BookmarkButtonCell, and align the image to the left (NSImageLeft) // if there is text in the title, and centered (NSImageCenter) if diff --git a/chrome/browser/cocoa/bookmark_button_cell.mm b/chrome/browser/cocoa/bookmark_button_cell.mm index 8be166a..0f790e3 100644 --- a/chrome/browser/cocoa/bookmark_button_cell.mm +++ b/chrome/browser/cocoa/bookmark_button_cell.mm @@ -28,6 +28,15 @@ return self; } +- (BOOL)empty { + return empty_; +} + +- (void)setEmpty:(BOOL)empty { + empty_ = empty; + [self setShowsBorderOnlyWhileMouseInside:!empty]; +} + - (NSSize)cellSizeForBounds:(NSRect)aRect { NSSize size = [super cellSizeForBounds:aRect]; size.width += 2; @@ -58,10 +67,21 @@ [self setTitle:title]; } +- (void)setBookmarkNode:(const BookmarkNode*)node { + [self setRepresentedObject:[NSValue valueWithPointer:node]]; +} + +- (const BookmarkNode*)bookmarkNode { + return static_cast<const BookmarkNode*>([[self representedObject] + pointerValue]); +} + // We share the context menu among all bookmark buttons. To allow us // to disambiguate when needed (e.g. "open bookmark"), we set the // menu's associated node to be our represented object. - (NSMenu*)menu { + if (empty_) + return nil; BookmarkMenu* menu = (BookmarkMenu*)[super menu]; [menu setRepresentedObject:[self representedObject]]; return menu; @@ -70,6 +90,13 @@ // Unfortunately, NSCell doesn't already have something like this. // TODO(jrg): consider placing in GTM. - (void)setTextColor:(NSColor*)color { + + // We can't properly set the cell's text color without a control. + // In theory we could just save the next for later and wait until + // the cell is moved to a control, but there is no obvious way to + // accomplish that (e.g. no "cellDidMoveToControl" notification.) + DCHECK([self controlView]); + scoped_nsobject<NSMutableParagraphStyle> style([NSMutableParagraphStyle new]); [style setAlignment:NSCenterTextAlignment]; NSDictionary* dict = [NSDictionary @@ -88,4 +115,18 @@ } } +// To implement "hover open a bookmark button to open the folder" +// which feels like menus, we override NSButtonCell's mouseEntered: +// and mouseExited:, then and pass them along to our owning control. +- (void)mouseEntered:(NSEvent*)event { + [super mouseEntered:event]; + [[self controlView] mouseEntered:event]; +} + +// See comment above mouseEntered:, above. +- (void)mouseExited:(NSEvent*)event { + [super mouseExited:event]; + [[self controlView] mouseExited:event]; +} + @end diff --git a/chrome/browser/cocoa/bookmark_button_cell_unittest.mm b/chrome/browser/cocoa/bookmark_button_cell_unittest.mm index a1b7b40..07f9984 100644 --- a/chrome/browser/cocoa/bookmark_button_cell_unittest.mm +++ b/chrome/browser/cocoa/bookmark_button_cell_unittest.mm @@ -5,10 +5,31 @@ #include "base/scoped_nsobject.h" #import "chrome/browser/cocoa/bookmark_button_cell.h" #import "chrome/browser/cocoa/bookmark_menu.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" +// Simple class to remember how many mouseEntered: and mouseExited: +// calls it gets. Only used by BookmarkMouseForwarding but placed +// at the top of the file to keep it outside the anon namespace. +@interface ButtonRemembersMouseEnterExit : NSButton { + @public + int enters_; + int exits_; +} +@end + +@implementation ButtonRemembersMouseEnterExit +- (void)mouseEntered:(NSEvent*)event { + enters_++; +} +- (void)mouseExited:(NSEvent*)event { + exits_++; +} +@end + + namespace { class BookmarkButtonCellTest : public CocoaTest { @@ -29,11 +50,59 @@ TEST_F(BookmarkButtonCellTest, SizeForBounds) { EXPECT_TRUE(size.width < 200 && size.height < 200); } -// Make sure the default from the base class is overridden +// Make sure the default from the base class is overridden. TEST_F(BookmarkButtonCellTest, MouseEnterStuff) { scoped_nsobject<BookmarkButtonCell> cell( [[BookmarkButtonCell alloc] initTextCell:@"Testing"]); + [cell setMenu:[[[BookmarkMenu alloc] initWithTitle:@"foo"] autorelease]]; EXPECT_TRUE([cell.get() showsBorderOnlyWhileMouseInside]); + EXPECT_TRUE([cell menu]); + + [cell setEmpty:YES]; + EXPECT_FALSE([cell.get() showsBorderOnlyWhileMouseInside]); + EXPECT_FALSE([cell menu]); +} + +TEST_F(BookmarkButtonCellTest, BookmarkNode) { + BrowserTestHelper helper_; + BookmarkModel& model(*(helper_.profile()->GetBookmarkModel())); + scoped_nsobject<BookmarkButtonCell> cell( + [[BookmarkButtonCell alloc] initTextCell:@"Testing"]); + + const BookmarkNode* node = model.GetBookmarkBarNode(); + [cell setBookmarkNode:node]; + EXPECT_EQ(node, [cell bookmarkNode]); + + node = model.other_node(); + [cell setBookmarkNode:node]; + EXPECT_EQ(node, [cell bookmarkNode]); +} + +TEST_F(BookmarkButtonCellTest, BookmarkMouseForwarding) { + scoped_nsobject<BookmarkButtonCell> cell( + [[BookmarkButtonCell alloc] initTextCell:@"Testing"]); + scoped_nsobject<ButtonRemembersMouseEnterExit> + button([[ButtonRemembersMouseEnterExit alloc] + initWithFrame:NSMakeRect(0,0,50,50)]); + [button setCell:cell.get()]; + EXPECT_EQ(0, button.get()->enters_); + EXPECT_EQ(0, button.get()->exits_); + NSEvent* event = [NSEvent mouseEventWithType:NSMouseMoved + location:NSMakePoint(10,10) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + eventNumber:0 + clickCount:0 + pressure:0]; + [cell mouseEntered:event]; + EXPECT_TRUE(button.get()->enters_ && !button.get()->exits_); + + for (int i = 0; i < 3; i++) + [cell mouseExited:event]; + EXPECT_EQ(button.get()->enters_, 1); + EXPECT_EQ(button.get()->exits_, 3); } } // namespace diff --git a/chrome/browser/cocoa/bookmark_button_unittest.mm b/chrome/browser/cocoa/bookmark_button_unittest.mm index 5ec87bd..6175f43 100644 --- a/chrome/browser/cocoa/bookmark_button_unittest.mm +++ b/chrome/browser/cocoa/bookmark_button_unittest.mm @@ -4,12 +4,39 @@ #include "base/scoped_nsobject.h" #import "chrome/browser/cocoa/bookmark_button.h" +#import "chrome/browser/cocoa/bookmark_button_cell.h" +#import "chrome/browser/cocoa/browser_test_helper.h" #import "chrome/browser/cocoa/cocoa_test_helper.h" +#import "chrome/browser/cocoa/test_event_utils.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" +// Fake BookmarkButton delegate to get a pong on mouse entered/exited +@interface FakeButtonDelegate : NSObject<BookmarkButtonDelegate> { + @public + int entered_; + int exited_; +} +@end + +@implementation FakeButtonDelegate + +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button { +} + +- (void)mouseEnteredButton:(id)buton event:(NSEvent*)event { + entered_++; +} + +- (void)mouseExitedButton:(id)buton event:(NSEvent*)event { + exited_++; +} +@end + +namespace { + class BookmarkButtonTest : public CocoaTest { - public: }; // Make sure nothing leaks @@ -17,3 +44,63 @@ TEST_F(BookmarkButtonTest, Create) { scoped_nsobject<BookmarkButton> button; button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]); } + +// Test folder and empty node queries. +TEST_F(BookmarkButtonTest, FolderAndEmptyOrNot) { + BrowserTestHelper helper_; + scoped_nsobject<BookmarkButton> button; + scoped_nsobject<BookmarkButtonCell> cell; + + button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]); + cell.reset([[BookmarkButtonCell alloc] initTextCell:@"hi mom"]); + [button setCell:cell]; + + EXPECT_TRUE([button isEmpty]); + EXPECT_FALSE([button isFolder]); + EXPECT_FALSE([button bookmarkNode]); + + NSEvent* downEvent = + test_event_utils::LeftMouseDownAtPoint(NSMakePoint(10,10)); + // Since this returns (does not actually begin a modal drag), success! + [button beginDrag:downEvent]; + + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* node = model->GetBookmarkBarNode(); + [cell setBookmarkNode:node]; + EXPECT_FALSE([button isEmpty]); + EXPECT_TRUE([button isFolder]); + EXPECT_EQ([button bookmarkNode], node); + + node = model->AddURL(node, 0, L"hi mom", GURL("http://www.google.com")); + [cell setBookmarkNode:node]; + EXPECT_FALSE([button isEmpty]); + EXPECT_FALSE([button isFolder]); + EXPECT_EQ([button bookmarkNode], node); +} + +TEST_F(BookmarkButtonTest, MouseEnterExitRedirect) { + NSEvent* moveEvent = + test_event_utils::MouseEventAtPoint(NSMakePoint(10,10), NSMouseMoved, 0); + scoped_nsobject<BookmarkButton> button; + scoped_nsobject<BookmarkButtonCell> cell; + scoped_nsobject<FakeButtonDelegate> + delegate([[FakeButtonDelegate alloc] init]); + button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]); + cell.reset([[BookmarkButtonCell alloc] initTextCell:@"hi mom"]); + [button setCell:cell]; + [button setDelegate:delegate]; + + EXPECT_EQ(0, delegate.get()->entered_); + EXPECT_EQ(0, delegate.get()->exited_); + + [button mouseEntered:moveEvent]; + EXPECT_EQ(1, delegate.get()->entered_); + EXPECT_EQ(0, delegate.get()->exited_); + + [button mouseExited:moveEvent]; + [button mouseExited:moveEvent]; + EXPECT_EQ(1, delegate.get()->entered_); + EXPECT_EQ(2, delegate.get()->exited_); +} + +} diff --git a/chrome/browser/cocoa/test_event_utils.h b/chrome/browser/cocoa/test_event_utils.h index ac4b0f5..6b2eba5 100644 --- a/chrome/browser/cocoa/test_event_utils.h +++ b/chrome/browser/cocoa/test_event_utils.h @@ -25,10 +25,15 @@ class ScopedClassSwizzler { namespace test_event_utils { -// Create synthetic mouse events for testing. Currently these are very basic, -// flesh out as needed. +// Create synthetic mouse events for testing. Currently these are very +// basic, flesh out as needed. Points are all in window coordinates; +// where the window is not specified, coordinate system is undefined +// (but will be repeated when the event is queried). NSEvent* MakeMouseEvent(NSEventType type, NSUInteger modifiers); +NSEvent* MouseEventAtPoint(NSPoint point, NSEventType type, + NSUInteger modifiers); NSEvent* LeftMouseDownAtPoint(NSPoint point); +NSEvent* LeftMouseDownAtPointInWindow(NSPoint point, NSWindow* window); } // namespace test_event_utils diff --git a/chrome/browser/cocoa/test_event_utils.mm b/chrome/browser/cocoa/test_event_utils.mm index f0c4fc7..23b0535 100644 --- a/chrome/browser/cocoa/test_event_utils.mm +++ b/chrome/browser/cocoa/test_event_utils.mm @@ -19,22 +19,23 @@ ScopedClassSwizzler::~ScopedClassSwizzler() { namespace test_event_utils { -NSEvent* MakeMouseEvent(NSEventType type, NSUInteger modifiers) { +NSEvent* MouseEventAtPoint(NSPoint point, NSEventType type, + NSUInteger modifiers) { if (type == NSOtherMouseUp) { // To synthesize middle clicks we need to create a CGEvent with the // "center" button flags so that our resulting NSEvent will have the // appropriate buttonNumber field. NSEvent provides no way to create a // mouse event with a buttonNumber directly. - CGPoint location = { 0, 0 }; + CGPoint location = { point.x, point.y }; CGEventRef cg_event = CGEventCreateMouseEvent(NULL, kCGEventOtherMouseUp, - location, - kCGMouseButtonCenter); + location, + kCGMouseButtonCenter); NSEvent* event = [NSEvent eventWithCGEvent:cg_event]; CFRelease(cg_event); return event; } return [NSEvent mouseEventWithType:type - location:NSMakePoint(0, 0) + location:point modifierFlags:modifiers timestamp:0 windowNumber:0 @@ -44,16 +45,24 @@ NSEvent* MakeMouseEvent(NSEventType type, NSUInteger modifiers) { pressure:1.0]; } -NSEvent* LeftMouseDownAtPoint(NSPoint point) { +NSEvent* MakeMouseEvent(NSEventType type, NSUInteger modifiers) { + return MouseEventAtPoint(NSMakePoint(0, 0), type, modifiers); +} + +NSEvent* LeftMouseDownAtPointInWindow(NSPoint point, NSWindow* window) { return [NSEvent mouseEventWithType:NSLeftMouseDown location:point modifierFlags:0 timestamp:0 - windowNumber:0 + windowNumber:[window windowNumber] context:nil eventNumber:0 clickCount:1 pressure:1.0]; } +NSEvent* LeftMouseDownAtPoint(NSPoint point) { + return LeftMouseDownAtPointInWindow(point, nil); +} + } // namespace test_event_utils diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 7d43fc1..d74a627 100755 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -427,6 +427,12 @@ 'browser/cocoa/bookmark_bar_constants.h', 'browser/cocoa/bookmark_bar_controller.h', 'browser/cocoa/bookmark_bar_controller.mm', + 'browser/cocoa/bookmark_bar_folder_controller.h', + 'browser/cocoa/bookmark_bar_folder_controller.mm', + 'browser/cocoa/bookmark_bar_folder_view.h', + 'browser/cocoa/bookmark_bar_folder_view.mm', + 'browser/cocoa/bookmark_bar_folder_window.h', + 'browser/cocoa/bookmark_bar_folder_window.mm', 'browser/cocoa/bookmark_bar_state.h', 'browser/cocoa/bookmark_bar_toolbar_view.h', 'browser/cocoa/bookmark_bar_toolbar_view.mm', @@ -2350,6 +2356,7 @@ 'app/nibs/AutoFillDialog.xib', 'app/nibs/BookmarkAllTabs.xib', 'app/nibs/BookmarkBar.xib', + 'app/nibs/BookmarkBarFolderWindow.xib', 'app/nibs/BookmarkBubble.xib', 'app/nibs/BookmarkEditor.xib', 'app/nibs/BookmarkManager.xib', diff --git a/chrome/chrome_dll.gypi b/chrome/chrome_dll.gypi index d900130..c99fc08 100644 --- a/chrome/chrome_dll.gypi +++ b/chrome/chrome_dll.gypi @@ -184,6 +184,7 @@ 'app/nibs/AutoFillDialog.xib', 'app/nibs/BookmarkAllTabs.xib', 'app/nibs/BookmarkBar.xib', + 'app/nibs/BookmarkBarFolderWindow.xib', 'app/nibs/BookmarkBubble.xib', 'app/nibs/BookmarkEditor.xib', 'app/nibs/BookmarkManager.xib', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index c1ba1c6..0390631 100755 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -581,6 +581,9 @@ 'browser/cocoa/bookmark_all_tabs_controller_unittest.mm', 'browser/cocoa/bookmark_bar_bridge_unittest.mm', 'browser/cocoa/bookmark_bar_controller_unittest.mm', + 'browser/cocoa/bookmark_bar_folder_controller_unittest.mm', + 'browser/cocoa/bookmark_bar_folder_view_unittest.mm', + 'browser/cocoa/bookmark_bar_folder_window_unittest.mm', 'browser/cocoa/bookmark_bar_toolbar_view_unittest.mm', 'browser/cocoa/bookmark_bar_view_unittest.mm', 'browser/cocoa/bookmark_bubble_controller_unittest.mm', |